commit 63a27d2fb8df6403a0348969bafece301361cb14 Author: Simon Sarasova Date: Thu Apr 11 13:51:56 2024 +0000 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. diff --git a/Changelog.md b/Changelog.md new file mode 100644 index 0000000..3ea0468 --- /dev/null +++ b/Changelog.md @@ -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` + diff --git a/Contributors.md b/Contributors.md new file mode 100644 index 0000000..5089fcd --- /dev/null +++ b/Contributors.md @@ -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 \ No newline at end of file diff --git a/Imports.md b/Imports.md new file mode 100644 index 0000000..9f5ee09 --- /dev/null +++ b/Imports.md @@ -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) + + diff --git a/ReadMe.md b/ReadMe.md new file mode 100644 index 0000000..8ccd650 --- /dev/null +++ b/ReadMe.md @@ -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) + + diff --git a/documentation/Contributing.md b/documentation/Contributing.md new file mode 100644 index 0000000..51ea918 --- /dev/null +++ b/documentation/Contributing.md @@ -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) + diff --git a/documentation/Documentation.md b/documentation/Documentation.md new file mode 100644 index 0000000..773d0f7 --- /dev/null +++ b/documentation/Documentation.md @@ -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. diff --git a/documentation/Future Plans.md b/documentation/Future Plans.md new file mode 100644 index 0000000..a6317c0 --- /dev/null +++ b/documentation/Future Plans.md @@ -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. + diff --git a/documentation/Specification.md b/documentation/Specification.md new file mode 100644 index 0000000..66c3e0d --- /dev/null +++ b/documentation/Specification.md @@ -0,0 +1,2034 @@ +# Specification + +This document describes the Seekia specification. + +Some of the information here may not match what is in the code, and some of it might have to be changed. + +As of version 0.1, whatever exists in the code is probably more up-to-date, and this file should be updated to reflect it. + +We can still easily change things because the network has not been launched yet. + +## Value Lengths + +Name | Length In Bytes | String Encoding +--- | --- | --- +Credit Account Identifier | 14 | Hex +Credit Account Key | 32 | Hex +Device Identifier | 11 | Hex +Request Identifier | 16 | None +Identity Key | 32 | Hex +Identity Hash | 16 | Custom +Secret Inbox Seed | 22 | None +Message Inbox | 10 | Base32 +Message Cipher Key Hash | 25 | None +Message Cipher Key | 32 | Hex +Message Hash | 26 | Hex +Attribute Hash | 27 | Hex +Profile Hash | 28 | Hex +Review Hash | 29 | Hex +Report Hash | 30 | Hex +Network Parameters Hash | 31 | Hex +Memo Hash | 32 | Hex + +## Parameters + +Parameters are how Network Parameters are referred to in the code. + +Parameters are encoded in MessagePack. + +### Parameters Hash + +A parameters hash is a 30 bytes long Blake3 hash of the parameters bytes, with a 1 byte metadata suffix + +*TODO: Use the metadata suffix to encode something useful* + +### Parameters Encoding + +A profile is a MessagePack encoded list of {[]Signature, Content} + +The list of admin signatures are encoded in MessagePack. + +* **Signature**: + * 64 byte long Ed25519 signature of Blake3_256(Content), signed by the parameters admin signer identity public key +* **Content**: + * MessagePack encoded map of `map[int][]byte` + * Map structure: Field Identifier -> Field value bytes + +### Parameters Fields + +These are a work in progress. Most of these parameters have not been implemented in the code yet. + +The Format section describes the parameters map entries for each parameters type. + +The Value Bytes section describes the map entry values in bytes, which are used when encoding the values in MessagePack. + +The Value String section describes the map entry values encoded as String, which are used when encoding the parameters map as a `map[string]string` + +* **AdminPermissions** + * Description: + * Describes which admins can sign each parametersType, and how many admin signatures are needed. + * Format: + * **(ParametersType)_AuthorizedAdmins** + * Description: List of admins (ed25519 public keys) who can sign the parameters + * Value Bytes: `[][32]byte` + * Value String: List of admin public keys encoded as hex, separated by "+" + * **(ParametersType)_MinimumSigners** + * Description: Minimum number of admins needed to sign the parameters file + * Value Bytes: `int` + * Value String: `int` encoded as String + * **(ParametersType)_MinimumBroadcastTime** + * Description: + * The minimum broadcast time required for the parametersType. + * This is useful if old parameters become the newest valid parameters again, after admins are changed. + * This allows for any maliciously authored parameters to be rejected, by setting this time after they were authored. + * Value Bytes: `int64` + * Value String: `int64` encoded as String +* **GeneralParameters** + * Description: + * Miscellaneous parameters + * Format: + * **SeekiaMinimumSafeVersion** + * Description: + * Version float of the latest seekia client that is safe to run. + * This is useful if a vulnerability is found in a version of Seekia. + * It will require clients to update to at least the minimum safe version to operate (connect to the internet). + * For example, imagine a bug is found in version 2.4 that allows remote code execution on a requestor's machine. + * This value can be set to 2.5, and any <=2.4 clients will not be able to connect to any hosts after downloading the updated parameters. + * Value Bytes: `float32` + * Value String: `float32` encoded as String + * **SeekiaNewestVersion** + * Description: + * Version float of the newest Seekia application, so the user knows when an update is available. + * This information should be shown in the GUI as "Update Available". + * Value Bytes: `float32` + * Value String: `float32` encoded as String + * **SeekiaClearnetWebsite** + * Description: URL of the current Seekia clearnet website (shown in the GUI) + * Value Bytes: Unicode bytes + * Value String: Unicode string + * Example: `https://seekia.net` + * **SeekiaTorWebsite** + * Description: .Onion address of the current Seekia website hidden service (shown in the GUI) + * Value Bytes: Unicode bytes + * Value String: Unicode string + * **SeekiaEthWebsite** + * Description: .eth address of the current Seekia website Ethereum Name Service IPFS site (shown in the GUI) + * Changing this is only needed if the seekia.eth site was seized by a nefarious entity. + * Value Bytes: Unicode bytes + * Value String: Unicode string + * Example: `seekia.eth` + * **MateInactivityExpirationTime** + * Description: + * Number of seconds until a Mate profile is dropped from the network + * The profile's BroadcastTime must be younger than this time. + * Value Bytes: `int` + * Value String: `int` encoded as String + * **HostInactivityExpirationTime** + * Description: + * Number of seconds until a Host profile is dropped from the network + * The profile's BroadcastTime must be younger than this time. + * Value Bytes: `int` + * Value String: `int` encoded as String + +* **MinimumIdentityDaysToFund** + * Description: + * A file describing the minimum number of days required to fund a mate/host identity + * The historic rates are needed so that updates to the durations will have time to be downloaded by the account credit servers and users of the network before the new durations take effect + * Format: + * **(IdentityType)_(TimeUnix)** + * Description: The minimum number of days required to fund for identities of the identityType + * Value Bytes: TODO + * Value String: TODO (big.Int as String?) + * Examples: + * `Mate_1690000000 -> 60` + * `Host_1690000000 -> 80` + +* **MaximumIdentityDaysToFund** + * Description: + * A file describing the maximum number of days required to fund a mate/host identity + * The historic rates are needed so that updates to the durations will have time to be downloaded by the account credit servers and users of the network before the new durations take effect + * Format: + * **(IdentityType)_(TimeUnix)** + * Description: The maximum number of days required to fund for identities of the identityType + * Value Bytes: TODO + * Value String: TODO (big.Int as String?) + * Examples: + * `Mate_1690000000 -> 300` + * `Host_1690000000 -> 600` + +* **SeekiaApplicationHashes** + * Description: + * A file describing the official application hashes + * The GUI should have a tool to show this information, as well as verify application hashes + * TODO: Create Verify Application page, which has button which brings up dialog to select file + * Format: + * **(Filename)** + * Description: + * Blake3 hash of the official application file + * Value Bytes: `[32]byte` + * Value String: `[32]byte` encoded in Hex + * Example: + * `Seekia_v0.1.zip -> 12a34e43b2c112f34e43a21a341958a4841ab12abbba192384ab3804ab (Not A Real Hash)` + * `Seekia_v1.2.dmg -> 12a34e43b2c112f34e43a21a341958a4841ab12abbba192384ab3804ab (Not A Real Hash)` + * `Seekia_v1.2.exe -> 12a34e43b2c112f34e43a21a341958a4841ab12abbba192384ab3804ab (Not A Real Hash)` +* **MessageCosts** + * Description: + * These are the gold/kilobytes/day rates for messages to be hosted on the network + * The historical cost rates must be kept for 1 month (the longest time a message can be funded *2) + * Format: + * **(TimeUnix)** + * Description: Milligrams of gold per day rate to store 1 kilobyte of message data for 1 day (24 hours) at timeUnix + * Value Bytes: TODO + * Value String: TODO + * Example: + * `1690000000 -> 1000` +* **FundingIdentityCosts** + * Description: + * These are the milligrams of gold/day rates for Mate/Host identities to be funded and hosted on the network + * The historical cost rates are needed so updates to the rates have time to be downloaded by the account credit servers and users before the new rates go into effect + * Format: + * **(IdentityType)_(TimeUnix)** + * Description: Milligrams of gold rate per day at TimeUnix to fund an IdentityType identity + * Value Bytes: TODO + * Value String: TODO + * Examples: + * `Mate_1690000000 -> 1000` + * `Host_1690000000 -> 2000` +* **ReportCosts** + * Description: + * The cost to fund a report + * Historical times are needed so that all users and account credits servers have time to download the parameters before a price change takes place + * Format: + * **(TimeUnix)** + * Description: Cost in milligrams of gold to fund a report for the standard report duration at timeUnix + * Value Bytes: TODO + * Value String: TODO + * Example: + * `1690000000 -> 1000` +* **MateProfileCosts** + * Description: + * The cost to fund a mate profile + * Historical times are needed so that all users and account credits servers have time to download the parameters before a price change takes place + * Format: + * **(TimeUnix)** + * Description: Cost in milligrams of gold to fund a mate profile at timeUnix + * Value Bytes: TODO + * Value String: TODO + * Example: + * `1690000000 -> 1000` +* **ModerationParameters** + * Description: + * Various moderation parameters + * Format: + * **StickyStatusIntervalDuration** + * Description: The amount of time in seconds to wait between recording each historical viewable status + * Value Bytes: `int` + * Value String: `int` encoded as String + * **StickyStatusEstablishingTime** + * Description: The minimum number of seconds required for a host to be online and downloading the relevant reviews before they start sharing the sticky status to requestors + * Value Bytes: `int` + * Value String: `int` encoded as String + * **StickyStatusHistoricalExpirationTime_(IdentityType)Identity** + * Description: The duration of time in seconds to keep historical identity sticky statuses. Any statuses older than this time will be discarded. + * Value Bytes: `int` + * Value String: `int` encoded as String + * Examples: + * `StickyStatusHistoricalExpirationTime_MateIdentity -> 50000` + * `StickyStatusHistoricalExpirationTime_HostIdentity -> 50000` + * `StickyStatusHistoricalExpirationTime_ModeratorIdentity -> 50000` + * **StickyStatusHistoricalExpirationTime_(IdentityType)Profile** + * Description: The duration of time in seconds to keep historical profile sticky statuses. Any statuses older than this time will be discarded. + * Value Bytes: `int` + * Value Bytes: `int` + * Value String: `int` encoded as String + * Examples: + * `StickyStatusHistoricalExpirationTime_MateProfile -> 50000` + * `StickyStatusHistoricalExpirationTime_HostProfile -> 50000` + * `StickyStatusHistoricalExpirationTime_ModeratorProfile -> 50000` + * **StickyStatusHistoricalExpirationTime_Message** + * Description: The duration of time in seconds to keep historical message sticky statuses. Any statuses older than this time will be discarded. + * Value Bytes: `int` + * Value Bytes: `int` + * Value String: `int` encoded as String + * **StickyStatusMinimumViewablePercentage_(IdentityType)Identity** + * Description: The minimum viewable verdict percentage required for an identity's sticky status to be viewable + * Value Bytes: `int` between 0-100 + * Value String: `int` encoded as String + * Examples: + * `StickyStatusMinimumViewablePercentage_MateIdentity -> 70` + * `StickyStatusMinimumViewablePercentage_HostIdentity -> 60` + * `StickyStatusMinimumViewablePercentage_ModeratorIdentity -> 50` + * **StickyStatusMinimumViewablePercentage_(IdentityType)Profile** + * Description: The minimum viewable verdict percentage required for a profile's sticky status to be viewable + * Value Bytes: `int` between 0-100 + * Value String: `int` encoded as String + * Examples: + * `StickyStatusMinimumViewablePercentage_MateProfile -> 70` + * `StickyStatusMinimumViewablePercentage_HostProfile -> 60` + * `StickyStatusMinimumViewablePercentage_ModeratorProfile -> 50` + * **StickyStatusMinimumViewablePercentage_Message** + * Description: The minimum viewable verdict percentage required for a message's sticky status to be viewable + * Value Bytes: `int` between 0-100 + * Value String: `int` encoded as String + * **ReportDuration** + * Description: The number of seconds that a funded report should exist on the network before expiring + * Value Bytes: `int` + * Value String: `int` encoded as String + +* **Supermoderators** + * Description: + * A list of supermoderators + * They are ranked, so supermoderators can ban each other + * Any banning/banned supermoderator should eventually be removed from the parameters list, either the banner or the banned + * Format: + * **(Supermoderator Rank)** + * Description: Supermoderator identity hash + * Value Bytes: `[16]byte` + * Value String: `[16]byte` encoded as identity hash string + * Examples: + * `1 -> wgkxplfvju7yhpoadvxju4l2r` + * `2 -> fbeqvukltaw73yzbrx34vg4wr` +* **ExchangeRates** + * Description: + * Exchange rates for different currencies. + * These are approximate values, and are not used for anything network consensus-critical + * They are used to convert the cryptocurrency storage costs to fiat so users know what they are paying + * They are also used to convert each user's wealth to a user's preferred fiat currency + * No historical data is needed. + * Format: + * **(CurrencyCode)**: + * Description: Amount of CurrencyCode units required to buy 1 kilogram of gold + * Value Bytes: TODO + * Value String: TODO (big.Int as String?) + * Examples: + * `ETH -> 10000000 (wei)` + * `USD -> 5000` + * `EUR -> 5100` +* **GoldRates** + * Description: + * Exchange rates for gold to crypto + * This is used to calculate a moderator's identity score, so all hosts and moderators must agree on the rates + * The historical rates must be kept forever, because all deposits to an identity's address must be converted + * Format: + * **(TimeUnix)_(Crypto)** + * Description: Number of crypto atomic units to buy 1 kilogram of gold at time unix + * Value Bytes: `int64` + * Value String: `int64` encoded as String + * Examples: + * `1690000000_Ethereum -> 12332123123123` + * `1690000000_Cardano -> 123213123123123123123` +* **AccountCreditServersStatus** + * Description: + * Used to tell users if the account credit server(s) were hacked/broken for any period of time + * The nodes that relied upon this inaccurate data would disregard any data received from the account credit servers during these time periods + * The Seekia application must keep track of when it learned information from the servers + * This is necessary because Hosts usually never check the servers for updates after receiving certain kinds of information + * For example, if a host learned that a message was funded for 14 days, then they would host the message for 14 days and never ask the server about the message's funded status again + * An attack on the servers could result in hosts hosting unfunded messages for a while + * Format: + * TODO: Historical information describing periods that the servers were hacked/broken +* **AccountCreditServersList** + * Description: + * A list of all of the account credit server URLS + * These may be clearnet or Tor + * Format: + * **(URL)** + * Description: The read-only/writeable status of the server. Some servers are read-only. + * Value Bytes: `bool` + * Value String: "Unwriteable"/"Writeable" + * Examples: + * `https://server1.seekia.net/ -> Unwriteable` + * `https://server2.seekia.net/ -> Writeable` + +*TODO: Add parameters to ban specific messages (they could be malformed and undecryptable with the cipherkeyhash, but still contain unruleful content within them)* + +## Profiles + +Profile creation is implemented in `/internal/profiles/createProfiles/createProfiles.go` + +Profile reading is implemented in `/internal/profiles/readProfiles/readProfiles.go` + +Profiles are encoded in MessagePack. + +### Profile Hashes + +A profile hash is a 28 bytes long Blake3 hash of the profile bytes, with a 1 byte metadata suffix + +The metadata byte is described below: +* 1 = Mate profile, disabled +* 2 = Mate profile, not disabled +* 3 = Host profile, disabled +* 4 = Host profile, not disabled +* 5 = Moderator profile, disabled +* 6 = Moderator profile, not disabled + +### Profile Encoding + +A profile is a MessagePack encoded list of {Signature, Content} + +* **Signature**: + * 64 byte long Ed25519 signature of Blake3_256(Content), signed by the profile author's identity public key +* **Content**: + * MessagePack encoded map of `map[int][]byte` + * Map structure: Attribute Identifier -> Attribute value bytes + +### Profile attributes: + +Each attribute has a bytes and string encoding. + +The Bytes encoding is used when encoding the attributes in MessagePack. + +The String encoding is used when reading a profile's attributes. + +Many of the more space-efficient Bytes encodings have not been implemented yet. + +For example, we should encode values such as "Yes"/"No" as `bool` rather than `string` (1 byte rather than 3/2 bytes). + +Whenever a string value is described for the bytes value, we are encoding the string as unicode bytes `[]byte("Example")` + +* **NetworkType** + * Attribute Identifier: TODO (see profileFormat.go) + * Profile Types: Mate/Host/Moderator + * Description: Network Type (1 == Mainnet, 2 == Testnet1) + * Value Bytes: 1 byte + * Value String: 1 byte (Example: "1"/"2") + * Is Required: Yes + * Mandatory Attributes: All other required attributes + * Is Canonical: Always +* **IdentityKey** + * Attribute Identifier: TODO (see profileFormat.go) + * Profile Types: Mate/Host/Moderator + * Description: Profile author Ed25519 Identity Public Key + * Value Bytes: 32 bytes + * Value String: 32 bytes encoded in Hex + * Is Required: Yes + * Mandatory Attributes: All other required attributes + * Is Canonical: Always +* **BroadcastTime** + * Attribute Identifier: TODO (see profileFormat.go) + * Profile Types: Mate/Host/Moderator + * Description: Time that profile was broadcast + * Value Bytes: Integer number of seconds after unix origin time that profile was broadcasted + * Value String: Int64 encoded as string + * Is Required: Yes + * Mandatory Attributes: All other required attributes + * Is Canonical: Always +* **ProfileType** + * Attribute Identifier: TODO (see profileFormat.go) + * Profile Types: Mate/Host/Moderator + * Description: Identity type of profile author + * Value Bytes: "Mate"/"Host"/"Moderator" + * Value String: "Mate"/"Host"/"Moderator" + * Is Required: Yes + * Mandatory Attributes: All other required attributes + * Is Canonical: Always +* **ChatKeysLatestUpdateTime** + * Attribute Identifier: TODO (see profileFormat.go) + * Profile Types: Mate/Moderator + * Description: Time that user last updated their chat keys + * Value Bytes: Int64 (Unix time) + * Value String: Int64 encoded as string + * Is Required: Yes + * Mandatory Attributes: All other required attributes + * Is Canonical: Always +* **DeviceIdentifier** + * Attribute Identifier: TODO (see profileFormat.go) + * Profile Types: Mate/Moderator + * Description: User's device identifier + * Value Bytes: 11 bytes + * Value String: 11 bytes encoded in Hex + * Is Required: Yes + * Mandatory Attributes: All other required attributes + * Is Canonical: Always +* **NaclKey** + * Attribute Identifier: TODO (see profileFormat.go) + * Profile Types: Mate/Moderator + * Description: Used to encrypt all messages sent to user + * Value Bytes: 32 bytes + * Value String: 32 bytes encoded in Base64 + * Is Required: Yes + * Mandatory Attributes: All other required attributes + * Is Canonical: Always +* **KyberKey** + * Attribute Identifier: TODO (see profileFormat.go) + * Profile Types: Mate/Moderator + * Description: Used to encrypt all messages sent to user + * Value Bytes: 1568 bytes + * Value String: 1568 bytes encoded in Base64 + * Is Required: Yes + * Mandatory Attributes: All other required attributes + * Is Canonical: Always +* **ProfileLanguage** + * Attribute Identifier: TODO (see profileFormat.go) + * Profile Types: Mate/Host/Moderator + * Description: Language that the profile is written in + * Value Bytes: Integer (TODO in code) + * Value String: A language identifier integer + * Is Required: No + * Mandatory Attributes: None + * Is Canonical: Always +* **Height** + * Attribute Identifier: TODO (see profileFormat.go) + * Profile Types: Mate + * Description: User's height in centimeters + * Value Bytes: Float64 (TODO in code) + * Value String: Float64 between 30 and 400 representing centimeters encoded as string + * Is Required: No + * Mandatory Attributes: None + * Is Canonical: Always +* **Sex** + * Attribute Identifier: TODO (see profileFormat.go) + * Profile Types: Mate + * Description: User's biological sex + * Value Bytes: "Male"/"Female"/"Intersex Male"/"Intersex Female"/"Intersex" + * Value String: "Male"/"Female"/"Intersex Male"/"Intersex Female"/"Intersex" + * Is Required: No + * Mandatory Attributes: None + * Is Canonical: Always +* **Age** + * Attribute Identifier: TODO (see profileFormat.go) + * Profile Types: Mate + * Description: User's age in years + * Value Bytes: Integer (TODO in code) + * Value String: Int between 18 and 150 encoded as string + * Is Required: No + * Mandatory Attributes: None + * Is Canonical: Always +* **Description** + * Attribute Identifier: TODO (see profileFormat.go) + * Profile Types: Mate/Host/Moderator + * Description: A user's description + * Value Bytes: Unicode UTF-8 Bytes + * Must be <= 3000 bytes (Mate), <= 300 bytes (Host), <= 500 bytes (Moderator) + * Cannot be the empty string "" + * Value String: Unicode UTF-8 String + * Is Required: No + * Mandatory Attributes: None + * Is Canonical: Never +* **Username** + * Attribute Identifier: TODO (see profileFormat.go) + * Profile Types: Mate/Host/Moderator + * Description: A user's username + * Value Bytes: Unicode UTF-8 Bytes that is <= 25 bytes, containing no tabs or newlines. Cannot be empty. + * Value String: Unicode UTF-8 String + * Is Required: No + * Mandatory Attributes: None + * Is Canonical: Never +* **PrimaryLocationLatitude** + * Attribute Identifier: TODO (see profileFormat.go) + * Profile Types: Mate + * Description: Latitude of user's primary location (Number between -90 and 90) + * Value Bytes: Float64 (TODO in code) + * Value String: Float64 as String + * Is Required: No + * Mandatory Attributes: PrimaryLocationLongitude + * Is Canonical: Always +* **PrimaryLocationLongitude** + * Attribute Identifier: TODO (see profileFormat.go) + * Profile Types: Mate + * Description: Longitude of user's primary location (Number between -180 and 180) + * Value Bytes: Float64 (TODO in code) + * Value String: Float64 as String + * Is Required: No + * Mandatory Attributes: PrimaryLocationLatitude + * Is Canonical: Always +* **PrimaryLocationCountry** + * Attribute Identifier: TODO (see profileFormat.go) + * Profile Types: Mate + * Description: Country where user's primary location is located, represented as an integer from the worldLocations countries list. + * Value Bytes: Integer (TODO in code) + * Value String: Integer as String + * Is Required: No + * Mandatory Attributes: PrimaryLocationLatitude, PrimaryLocationLongitude + * Is Canonical: Always +* **SecondaryLocationLatitude** + * Attribute Identifier: TODO (see profileFormat.go) + * Profile Types: Mate + * Description: Latitude of user's secondary location (Number between -90 and 90) + * Value Bytes: Float64 (TODO in code) + * Value String: Float64 as String + * Is Required: No + * Mandatory Attributes: SecondaryLocationLongitude + * Is Canonical: Always +* **SecondaryLocationLongitude** + * Attribute Identifier: TODO (see profileFormat.go) + * Profile Types: Mate + * Description: Longitude of user's secondary location (Number between -180 and 180) + * Value Bytes: Float64 (TODO in code) + * Value String: Float64 as String + * Is Required: No + * Mandatory Attributes: SecondaryLocationLatitude + * Is Canonical: Always +* **SecondaryLocationCountry** + * Attribute Identifier: TODO (see profileFormat.go) + * Profile Types: Mate + * Description: Country where the user's secondary location is located, represented as an integer from the worldLocations countries list. + * Value Bytes: Integer (TODO in code) + * Value String: Integer as String + * Is Required: No + * Mandatory Attributes: SecondaryLocationLatitude, SecondaryLocationLongitude + * Is Canonical: Always +* **Tags** + * Attribute Identifier: TODO (see profileFormat.go) + * Profile Types: Mate + * Description: User's profile tags + * Value Bytes: TODO (Encode as MessagePack list) + * Value String: List of tags, separated by "+&". + * Each tag cannot contain "+&" + * Each tag cannot be longer than 40 bytes. + * All tags cannot exceed 500 bytes. + * Number of tags cannot exceed 30. + * Tag cannot be the empty string "" + * Each tag cannot contain tab or newline + * Is Required: No + * Mandatory Attributes: None + * Is Canonical: Never +* **Photos** + * Attribute Identifier: TODO (see profileFormat.go) + * Profile Types: Mate + * Description: User's profile photos. + * Value Bytes: Webp image bytes list `[][]byte` + * Value String: List of base64 encoded webp images, separated by "+". + * Maximum of 5 photos. + * Each photo cannot surpass 20,000 bytes in size + * The full Standard image requirements are described in imagery/images.go + * Is Required: No + * Mandatory Attributes: None + * Is Canonical: Never +* **Questionnaire** + * Attribute Identifier: TODO (see profileFormat.go) + * Profile Types: Mate + * Description: A questionnaire which users can fill out and send their responses to the user. + * Value Bytes: TODO + * Value String: A questionnaire, encoded in the proper format. + * The format is described later in this document, and implemented in mateQuestionnaire.go + * Is Required: No + * Mandatory Attributes: None + * Is Canonical: Never +* **Avatar** + * Attribute Identifier: TODO (see profileFormat.go) + * Profile Types: Mate/Host/Moderator + * Description: An avatar, which is selected from 3630 available options in the application. + * Value Bytes: Integer (TODO in code) + * Value String: An integer between 1 and 3630, encoded as String + * Is Required: No + * Mandatory Attributes: None + * Is Canonical: Always +* **Sexuality** + * Attribute Identifier: TODO (see profileFormat.go) + * Profile Types: Mate + * Description: The sex(es) that the user is interested in. + * Value Bytes: "Male"/"Female"/"Male And Female" + * Value String: "Male"/"Female"/"Male And Female" + * Is Required: No + * Mandatory Attributes: None + * Is Canonical: Always +* **23andMe_AncestryComposition** + * Attribute Identifier: TODO (see profileFormat.go) + * Profile Types: Mate + * Description: The user's 23andMe ancestry composition. + * Value Bytes: A 23andMe ancestry location composition, encoded as bytes + * Value String: A 23andMe ancestry location composition. + * Format is described in /genetics/companyAnalysis/23andMe.go + * Is Required: No + * Mandatory Attributes: None + * Is Canonical: Always +* **23andMe_NeanderthalVariants** + * Attribute Identifier: TODO (see profileFormat.go) + * Profile Types: Mate + * Description: The user's neanderthal variants count, as reported by 23andMe. Is an integer between 0 and 7462. + * Value Bytes: Integer (TODO in code) + * Value String: Integer as String + * Is Required: No + * Mandatory Attributes: None + * Is Canonical: Always +* **23andMe_MaternalHaplogroup** + * Attribute Identifier: TODO (see profileFormat.go) + * Profile Types: Mate + * Description: The user's maternal haplogroup, as reported by 23andMe. + * Value Bytes: Unicode UTF-8 bytes + * Value String: A string no greater than 25 bytes in length, containing no tabs or newlines. Cannot be an empty string. + * Is Required: No + * Mandatory Attributes: None + * Is Canonical: Sometimes + * Canonical values are listed in `/genetics/companyAnalysis/23andMe.go` +* **23andMe_PaternalHaplogroup** + * Attribute Identifier: TODO (see profileFormat.go) + * Profile Types: Mate + * Description: The user's paternal haplogroup, as reported by 23andMe. + * Value Bytes: Unicode UTF-8 bytes + * Value String: A string no greater than 25 bytes in length, containing no tabs or newlines. Cannot be an empty string. + * Is Required: No + * Mandatory Attributes: None + * Is Canonical: Sometimes + * Canonical values are listed in `/genetics/companyAnalysis/23andMe.go` +* **BodyFat** + * Attribute Identifier: TODO (see profileFormat.go) + * Profile Types: Mate + * Description: The amount of body fat a user has, on a scale of 1-4 (least-most) + * Value Bytes: Integer (TODO in code) + * Value String: An integer between 1 and 4 + * Is Required: No + * Mandatory Attributes: None + * Is Canonical: Always +* **BodyMuscle** + * Attribute Identifier: TODO (see profileFormat.go) + * Profile Types: Mate + * Description: The amount of body muscle a user has, on a scale of 1-4 (least-most) + * Value Bytes: Integer (TODO in code) + * Value String: An integer between 1 and 4 + * Is Required: No + * Mandatory Attributes: None + * Is Canonical: Always +* **EyeColor** + * Attribute Identifier: TODO (see profileFormat.go) + * Profile Types: Mate + * Description: The user's eye color. + * Value Bytes: Unicode UTF-8 bytes + * Value String: A "+" delimited string consisting of: "Blue", "Green", "Hazel", Brown" + * Must contain at least 1 color, cannot contain more than 4 colors. Repeats are not allowed. + * Example: "Blue+Green", "Blue" + * Is Required: No + * Mandatory Attributes: None + * Is Canonical: Always +* **HairColor** + * Attribute Identifier: TODO (see profileFormat.go) + * Profile Types: Mate + * Description: The user's natural hair color. + * Value Bytes: Unicode UTF-8 bytes + * Value String: A "+" delimited string consisting of "Brown", "Black", "Blonde", "Orange" + * Must contain at least 1 color, cannot contain more than 2 colors. Repeats are not allowed. + * Is Required: No + * Mandatory Attributes: None + * Is Canonical: Always +* **HairTexture** + * Attribute Identifier: TODO (see profileFormat.go) + * Profile Types: Mate + * Description: The user's natural hair texture. + * Value Bytes: Integer + * Value String: An integer between 1-6 + * 1 == Straight, 2 == Slightly Wavy, 3 == Wavy, 4 == Big Curls, 5 == Small Curls, 6 == Very Tight Curls + * Is Required: No + * Mandatory Attributes: None + * Is Canonical: Always +* **SkinColor** + * Attribute Identifier: TODO (see profileFormat.go) + * Profile Types: Mate + * Description: The user's natural skin color. + * Value Bytes: Integer between 1-6 (1 = lightest skin, 6 = darkest skin) + * Value String: Unicode String + * Is Required: No + * Mandatory Attributes: None + * Is Canonical: Always +* **HasHIV** + * Attribute Identifier: TODO (see profileFormat.go) + * Profile Types: Mate + * Description: The user's HIV infection status. + * Value Bytes: Bool (TODO in code) + * Value String: "Yes"/"No" + * Is Required: No + * Mandatory Attributes: None + * Is Canonical: Always +* **HasGenitalHerpes** + * Attribute Identifier: TODO (see profileFormat.go) + * Profile Types: Mate + * Description: The user's genital herpes infection status. + * Value Bytes: Bool (TODO in code) + * Value String: "Yes"/"No" + * Is Required: No + * Mandatory Attributes: None + * Is Canonical: Always +* **Wealth** + * Attribute Identifier: TODO (see profileFormat.go) + * Profile Types: Mate + * Description: The user's wealth, described in WealthCurrency units. + * Value Bytes: Integer (TODO in code) + * Value String: An integer between 0 and 9223372036854775807 + * Is Required: No + * Mandatory Attributes: WealthIsLowerBound, WealthCurrency + * Is Canonical: Always +* **WealthIsLowerBound** + * Attribute Identifier: TODO (see profileFormat.go) + * Profile Types: Mate + * Description: Describes if the user's wealth is a lower bound or an exact value. + * Value Bytes: Bool (TODO in code) + * Value String: "Yes"/"No" + * Is Required: No + * Mandatory Attributes: Wealth, WealthCurrency + * Is Canonical: Always +* **WealthCurrency** + * Attribute Identifier: TODO (see profileFormat.go) + * Profile Types: Mate + * Description: The currency that the user's Wealth attribute is represented in. + * Value Bytes: Unicode UTF-8 bytes + * Value String: A valid currency code from `currencies.go` + * Is Required: No + * Mandatory Attributes: Wealth, WealthIsLowerBound + * Is Canonical: Always +* **Hobbies** + * Attribute Identifier: TODO (see profileFormat.go) + * Profile Types: Mate + * Description: The user's hobbies. + * Value Bytes: Unicode UTF-8 bytes + * Value String: A string that is no longer than 1000 bytes + * Is Required: No + * Mandatory Attributes: None + * Is Canonical: Never +* **FruitRating** + * Attribute Identifier: TODO (see profileFormat.go) + * Profile Types: Mate + * Description: The user's rating of Fruit, on a scale of 1-10 (least liked - most liked) + * Value Bytes: Integer (TODO in code) + * Value String: An int between 1 and 10 + * Is Required: No + * Mandatory Attributes: None + * Is Canonical: Always +* **VegetablesRating** + * Attribute Identifier: TODO (see profileFormat.go) + * Profile Types: Mate + * Description: The user's rating of Vegetables, on a scale of 1-10 (least liked - most liked) + * Value Bytes: Integer (TODO in code) + * Value String: An int between 1 and 10 + * Is Required: No + * Mandatory Attributes: None + * Is Canonical: Always +* **NutsRating** + * Attribute Identifier: TODO (see profileFormat.go) + * Profile Types: Mate + * Description: The user's rating of Nuts, on a scale of 1-10 (least liked - most liked) + * Value Bytes: Integer (TODO in code) + * Value String: An int between 1 and 10 + * Is Required: No + * Mandatory Attributes: None + * Is Canonical: Always +* **GrainsRating** + * Attribute Identifier: TODO (see profileFormat.go) + * Profile Types: Mate + * Description: The user's rating of Grains, on a scale of 1-10 (least liked - most liked) + * Value Bytes: Integer (TODO in code) + * Value String: An int between 1 and 10 + * Is Required: No + * Mandatory Attributes: None + * Is Canonical: Always +* **DairyRating** + * Attribute Identifier: TODO (see profileFormat.go) + * Profile Types: Mate + * Description: The user's rating of Dairy, on a scale of 1-10 (least liked - most liked) + * Value Bytes: Integer (TODO in code) + * Value String: An int between 1 and 10 + * Is Required: No + * Mandatory Attributes: None + * Is Canonical: Always +* **SeafoodRating** + * Attribute Identifier: TODO (see profileFormat.go) + * Profile Types: Mate + * Description: The user's rating of Seafood, on a scale of 1-10 (least liked - most liked) + * Value Bytes: Integer (TODO in code) + * Value String: An int between 1 and 10 + * Is Required: No + * Mandatory Attributes: None + * Is Canonical: Always +* **BeefRating** + * Attribute Identifier: TODO (see profileFormat.go) + * Profile Types: Mate + * Description: The user's rating of Beef, on a scale of 1-10 (least liked - most liked) + * Value Bytes: Integer (TODO in code) + * Value String: An int between 1 and 10 + * Is Required: No + * Mandatory Attributes: None + * Is Canonical: Always +* **PorkRating** + * Attribute Identifier: TODO (see profileFormat.go) + * Profile Types: Mate + * Description: The user's rating of Pork, on a scale of 1-10 (least liked - most liked) + * Value Bytes: Integer (TODO in code) + * Value String: An int between 1 and 10 + * Is Required: No + * Mandatory Attributes: None + * Is Canonical: Always +* **PoultryRating** + * Attribute Identifier: TODO (see profileFormat.go) + * Profile Types: Mate + * Description: The user's rating of Poultry, on a scale of 1-10 (least liked - most liked) + * Value Bytes: Integer (TODO in code) + * Value String: An int between 1 and 10 + * Is Required: No + * Mandatory Attributes: None + * Is Canonical: Always +* **EggsRating** + * Attribute Identifier: TODO (see profileFormat.go) + * Profile Types: Mate + * Description: The user's rating of Eggs, on a scale of 1-10 (least liked - most liked) + * Value Bytes: Integer (TODO in code) + * Value String: An int between 1 and 10 + * Is Required: No + * Mandatory Attributes: None + * Is Canonical: Always +* **BeansRating** + * Attribute Identifier: TODO (see profileFormat.go) + * Profile Types: Mate + * Description: The user's rating of Beans, on a scale of 1-10 (least liked - most liked) + * Value Bytes: Integer (TODO in code) + * Value String: An int between 1 and 10 + * Is Required: No + * Mandatory Attributes: None + * Is Canonical: Always +* **Fame** + * Attribute Identifier: TODO (see profileFormat.go) + * Profile Types: Mate + * Description: The user's fame, on a scale of 1-10 (least famous - most famous) + * Value Bytes: Integer (TODO in code) + * Value String: An integer between 1 and 10 + * Is Required: No + * Mandatory Attributes: None + * Is Canonical: Always +* **AlcoholFrequency** + * Attribute Identifier: TODO (see profileFormat.go) + * Profile Types: Mate + * Description: The user's Alcohol consumption frequency, on a scale of 1-10 (never - constantly) + * Value Bytes: Integer (TODO in code) + * Value String: An int between 1 and 10 + * Is Required: No + * Mandatory Attributes: None + * Is Canonical: Always +* **TobaccoFrequency** + * Attribute Identifier: TODO (see profileFormat.go) + * Profile Types: Mate + * Description: The user's Tobacco consumption frequency, on a scale of 1-10 (never - constantly) + * Value Bytes: Integer (TODO in code) + * Value String: An int between 1 and 10 + * Is Required: No + * Mandatory Attributes: None + * Is Canonical: Always +* **CannabisFrequency** + * Attribute Identifier: TODO (see profileFormat.go) + * Profile Types: Mate + * Description: The user's Cannabis consumption frequency, on a scale of 1-10 (never - constantly) + * Value Bytes: Integer (TODO in code) + * Value String: An int between 1 and 10 + * Is Required: No + * Mandatory Attributes: None + * Is Canonical: Always +* **Language** + * Attribute Identifier: TODO (see profileFormat.go) + * Profile Types: Mate + * Description: The language(s) that the user can speak, along with each language's fluency + * Value Bytes: Unicode UTF-8 bytes + * Value String: List of language items, separated by "+&". + * Each language item is LanguageName + "$" + Language Rating + * Language rating is an int between 1 and 5 + * Each language name cannot contain a tab or newline + * Each language name cannot contain "+&" or "$" + * Each language cannot be longer than 30 bytes. + * Number of languages cannot exceed 100. + * Language cannot be the empty string "" + * Is Required: No + * Mandatory Attributes: None + * Is Canonical: Sometimes +* **Beliefs** + * Attribute Identifier: TODO (see profileFormat.go) + * Profile Types: Mate + * Description: The user's beliefs. This may include worldview, religion, and philosophies. + * Value Bytes: Unicode bytes that is no longer than 1000 bytes long + * Value String: Unicode UTF-8 string + * Is Required: No + * Mandatory Attributes: None + * Is Canonical: Never +* **GenderIdentity** + * Attribute Identifier: TODO (see profileFormat.go) + * Profile Types: Mate + * Description: The gender that the user mentally identifies as. + * Value Bytes: Unicode UTF-8 bytes + * Value String: Either "Man", "Woman", or a string that is no longer than 50 bytes. Cannot contain tabs or newlines. + * Is Required: No + * Mandatory Attributes: None + * Is Canonical: Sometimes + * Is Canonical for "Man"/"Woman" +* **PetsRating** + * Attribute Identifier: TODO (see profileFormat.go) + * Profile Types: Mate + * Description: The user's rating of pet ownership in general, on a scale of 1-10 (hate-love) + * Value Bytes: Integer (TODO in code) + * Value String: An int between 1 and 10 + * Is Required: No + * Mandatory Attributes: None + * Is Canonical: Always +* **CatsRating** + * Attribute Identifier: TODO (see profileFormat.go) + * Profile Types: Mate + * Description: The user's rating of cat ownership, on a scale of 1-10 (hate-love) + * Value Bytes: Integer (TODO in code) + * Value String: An int between 1 and 10 + * Is Required: No + * Mandatory Attributes: None + * Is Canonical: Always +* **DogsRating** + * Attribute Identifier: TODO (see profileFormat.go) + * Profile Types: Mate + * Description: The user's rating of dog ownership, on a scale of 1-10 (hate-love) + * Value Bytes: Integer (TODO in code) + * Value String: An int between 1 and 10 + * Is Required: No + * Mandatory Attributes: None + * Is Canonical: Always +* **SeekiaVersion** + * Attribute Identifier: TODO (see profileFormat.go) + * Profile Types: Host + * Description: The version of Seekia that the host is running (Examples: "0.1", "1.2") + * Value Bytes: Float32 (TODO in code) + * Value String: Float32 + * Is Required: Yes + * Mandatory Attributes: All other required attributes + * Is Canonical: Always +* **TorAddress** + * Attribute Identifier: TODO (see profileFormat.go) + * Profile Types: Host + * Description: The host's Tor address + * Value Bytes: Hidden service address bytes without the .onion suffix and decoded from base32. + * Value String: Hidden service .onion address + * Is Required: No + * Mandatory Attributes: None + * Is Canonical: Always +* **ClearnetAddress** + * Attribute Identifier: TODO (see profileFormat.go) + * Profile Types: Host + * Description: The host's clearnet address + * Value Bytes: Unicode UTF-8 bytes + * Value String: Clearnet address (can be domain or IP address) (encoded in Unicode) + * Is Required: No + * Mandatory Attributes: ClearnetPort + * Is Canonical: Never +* **ClearnetPort** + * Attribute Identifier: TODO (see profileFormat.go) + * Profile Types: Host + * Description: The host's clearnet address port. + * Value Bytes: Positive Integer + * Value String: Positive integer as String + * Is Required: No + * Mandatory Attributes: ClearnetAddress + * Is Canonical: Always +* **HostingParameters** + * Attribute Identifier: TODO (see profileFormat.go) + * Profile Types: Host + * Description: Describes if the host is hosting the network parameters. + * Value Bytes: Bool + * Value String: "Yes"/"No" + * Is Required: Yes + * Mandatory Attributes: All other required attributes + * Is Canonical: Always +* **HostingHostContent** + * Attribute Identifier: TODO (see profileFormat.go) + * Profile Types: Host + * Description: Describes if the host is hosting Host profiles and reviews/reports of host identities, profiles, and attributes. + * Value Bytes: Bool + * Value String: "Yes"/"No" + * Is Required: Yes + * Mandatory Attributes: All other required attributes + * Is Canonical: Always +* **HostIdentitiesQuantity** + * Attribute Identifier: TODO (see profileFormat.go) + * Profile Types: Host + * Description: The number of hosted Host identities whom have a profile. + * Value Bytes: Positive integer + * Value String: Positive integer as String + * Is Required: Only if HostingHostContent is true. + * Mandatory Attributes: HostProfilesQuantity, HostReviewsQuantity, HostReportsQuantity + * Is Canonical: Always +* **HostProfilesQuantity** + * Attribute Identifier: TODO (see profileFormat.go) + * Profile Types: Host + * Description: The number of hosted Host profiles + * Value Bytes: Positive integer + * Value String: Positive integer as String + * Is Required: Only if HostingHostContent is true. + * Mandatory Attributes: HostIdentitiesQuantity, HostReviewsQuantity, HostReportsQuantity + * Is Canonical: Always +* **HostReviewsQuantity** + * Attribute Identifier: TODO (see profileFormat.go) + * Profile Types: Host + * Description: The number of hosted reviews of host identities/profiles/attributes. + * Value Bytes: Positive integer + * Value String: Positive integer as String + * Is Required: Only if HostingHostContent is true. + * Mandatory Attributes: HostIdentitiesQuantity, HostProfilesQuantity, HostReportsQuantity + * Is Canonical: Always +* **HostReportsQuantity** + * Attribute Identifier: TODO (see profileFormat.go) + * Profile Types: Host + * Description: The number of hosted reports of host identities/profiles/attributes + * Value Bytes: Positive integer + * Value String: Positive integer as String + * Is Required: Only if HostingHostContent is true. + * Mandatory Attributes: HostIdentitiesQuantity, HostProfilesQuantity, HostReviewsQuantity + * Is Canonical: Always +* **HostingModeratorContent** + * Attribute Identifier: TODO (see profileFormat.go) + * Profile Types: Host + * Description: Describes if the host is hosting moderator profiles and reviews/reports of moderator identities, profiles, and attributes. + * Value Bytes: Bool + * Value String: "Yes"/"No" + * Is Required: Yes + * Mandatory Attributes: All other required attributes + * Is Canonical: Always +* **ModeratorIdentitiesQuantity** + * Attribute Identifier: TODO (see profileFormat.go) + * Profile Types: Host + * Description: The number of hosted moderator identities who have a profile. + * Value Bytes: Positive integer + * Value String: Positive integer as String + * Is Required: Only if HostingModeratorContent is true. + * Mandatory Attributes: ModeratorProfilesQuantity, ModeratorReviewsQuantity, ModeratorReportsQuantity + * Is Canonical: Always +* **ModeratorProfilesQuantity** + * Attribute Identifier: TODO (see profileFormat.go) + * Profile Types: Host + * Description: The number of hosted Moderator profiles + * Value Bytes: Positive integer + * Value String: Positive integer as String + * Is Required: Only if HostingModeratorContent is true. + * Mandatory Attributes: ModeratorIdentitiesQuantity, ModeratorReviewsQuantity, ModeratorReportsQuantity + * Is Canonical: Always +* **ModeratorReviewsQuantity** + * Attribute Identifier: TODO (see profileFormat.go) + * Profile Types: Host + * Description: The number of hosted reviews for Moderator identities/profiles/attributes + * Value Bytes: Positive integer + * Value String: Positive integer as String + * Is Required: Only if HostingModeratorContent is true. + * Mandatory Attributes: ModeratorIdentitiesQuantity, ModeratorProfilesQuantity, ModeratorReportsQuantity + * Is Canonical: Always +* **ModeratorReportsQuantity** + * Attribute Identifier: TODO (see profileFormat.go) + * Profile Types: Host + * Description: Number of hosted reports for Moderator identities/profiles/attributes + * Value Bytes: Positive integer + * Value String: Positive integer as String + * Is Required: Only if HostingModeratorContent is true. + * Mandatory Attributes: ModeratorIdentitiesQuantity, ModeratorProfilesQuantity, ModeratorReviewsQuantity + * Is Canonical: Always +* **HostingMateContent** + * Attribute Identifier: TODO (see profileFormat.go) + * Profile Types: Host + * Description: Describes if the host is hosting Mate profiles/attributes, and reviews/reports of Mate identities, profiles and attributes + * Value Bytes: Bool + * Value String: "Yes"/"No" + * Is Required: Yes + * Mandatory Attributes: All other required attributes + * Is Canonical: Always +* **MateIdentitiesRangeStart** + * Attribute Identifier: TODO (see profileFormat.go) + * Profile Types: Host + * Description: Identity hash range start for Mate identities that the host is hosting. + * Value Bytes: `[16]byte` + * Value String: `[16]byte` Encoded Hex, because identity ranges are usually not valid identity hashes. + * Is Required: Only if HostingMateContent is true. + * Mandatory Attributes: MateIdentitiesRangeEnd, MateIdentitiesQuantity, MateProfilesQuantity, MateReviewsQuantity, MateReportsQuantity + * Is Canonical: Always +* **MateIdentitiesRangeEnd** + * Attribute Identifier: TODO (see profileFormat.go) + * Profile Types: Host + * Description: Identity hash range end for Mate identities that the host is hosting. + * Value Bytes: `[16]byte` + * Value String: `[16]byte` Encoded Hex, because identity ranges are usually not valid identity hashes. + * Is Required: Only if HostingMateContent is true. + * Mandatory Attributes: MateIdentitiesRangeStart, MateIdentitiesQuantity, MateProfilesQuantity, MateReviewsQuantity, MateReportsQuantity + * Is Canonical: Always +* **MateIdentitiesQuantity** + * Attribute Identifier: TODO (see profileFormat.go) + * Profile Types: Host + * Description: Number of mate identities within hosted range who have a profile + * Value Bytes: Positive integer + * Value String: Positive Integer as String + * Is Required: Only if HostingMateContent is true. + * Mandatory Attributes: MateIdentitiesRangeStart, MateIdentitiesRangeEnd, MateProfilesQuantity, MateReviewsQuantity, MateReportsQuantity + * Is Canonical: Always +* **MateProfilesQuantity** + * Attribute Identifier: TODO (see profileFormat.go) + * Profile Types: Host + * Description: Number of mate profiles within hosted range + * Value Bytes: Positive integer + * Value String: Positive integer as String + * Is Required: Only if HostingMateContent is true. + * Mandatory Attributes: MateIdentitiesRangeStart, MateIdentitiesRangeEnd, MateIdentitiesQuantity, MateReviewsQuantity, MateReportsQuantity + * Is Canonical: Always +* **MateReviewsQuantity** + * Attribute Identifier: TODO (see profileFormat.go) + * Profile Types: Host + * Description: Number of reviews for mate identities/profiles/attributes within hosted range + * Value Bytes: Positive integer + * Value String: Positive integer as String + * Is Required: Only if HostingMateContent is true. + * Mandatory Attributes: MateIdentitiesRangeStart, MateIdentitiesRangeEnd, MateIdentitiesQuantity, MateProfilesQuantity, MateReportsQuantity + * Is Canonical: Always +* **MateReportsQuantity** + * Attribute Identifier: TODO (see profileFormat.go) + * Profile Types: Host + * Description: Number of reports for mate identities/profiles/attributes within hosted range + * Value Bytes: Positive integer + * Value String: Positive integer as String + * Is Required: Only if HostingMateContent is true. + * Mandatory Attributes: MateIdentitiesRangeStart, MateIdentitiesRangeEnd, MateIdentitiesQuantity, MateProfilesQuantity, MateReviewsQuantity + * Is Canonical: Always +* **HostingUnviewableProfiles** + * Attribute Identifier: TODO (see profileFormat.go) + * Profile Types: Host + * Description: Describes if the Host is hosting unviewable profiles (profiles that have been banned (or not approved (if Mate))) + * Value Bytes: Bool + * Value String: "Yes"/"No" + * Is Required: Yes + * Mandatory Attributes: All other required attributes + * Is Canonical: Always +* **HostingMessages** + * Attribute Identifier: TODO (see profileFormat.go) + * Profile Types: Host + * Description: Describes if Host is hosting messages + * Value Bytes: Bool + * Value String: "Yes"/"No" + * Is Required: Yes + * Mandatory Attributes: All other required attributes + * Is Canonical: Always +* **MessageInboxesRangeStart** + * Attribute Identifier: TODO (see profileFormat.go) + * Profile Types: Host + * Description: The start of the message inbox range the host is hosting + * Value Bytes: `[10]byte` + * Value String: `[10]byte` encoded in Base32 + * Is Required: Only if HostingMessages is true. + * Mandatory Attributes: MessageInboxesRangeEnd, InboxesQuantity, MessagesQuantity, MessageReviewsQuantity, MessageReportsQuantity + * Is Canonical: Always +* **MessageInboxesRangeEnd** + * Attribute Identifier: TODO (see profileFormat.go) + * Profile Types: Host + * Description: The end of the message inbox range the host is hosting + * Value Bytes: `[10]byte` + * Value String: `[10]byte` encoded in Base32 + * Is Required: Only if HostingMessages is true. + * Mandatory Attributes: MessageInboxesRangeStart, InboxesQuantity, MessagesQuantity, MessageReviewsQuantity, MessageReportsQuantity + * Is Canonical: Always +* **InboxesQuantity** + * Attribute Identifier: TODO (see profileFormat.go) + * Profile Types: Host + * Description: Number of inboxes within hosted range + * Value Bytes: Positive integer + * Value String: Positive integer encoded as string + * Is Required: Only if HostingMessages is true. + * Mandatory Attributes: MessageInboxesRangeStart, MessageInboxesRangeEnd, MessagesQuantity, MessageReviewsQuantity, MessageReportsQuantity + * Is Canonical: Always +* **MessagesQuantity** + * Attribute Identifier: TODO (see profileFormat.go) + * Profile Types: Host + * Description: Number of hosted messages within range + * Value Bytes: Positive integer + * Value String: Positive integer encoded as string + * Is Required: Only if HostingMessages is true. + * Mandatory Attributes: MessageInboxesRangeStart, MessageInboxesRangeEnd, InboxesQuantity, MessageReviewsQuantity, MessageReportsQuantity + * Is Canonical: Always +* **MessageReviewsQuantity** + * Attribute Identifier: TODO (see profileFormat.go) + * Profile Types: Host + * Description: Number of message reviews within hosted range + * Value Bytes: Positive integer + * Value String: Positive integer encoded as string + * Is Required: Only if HostingMessages is true. + * Mandatory Attributes: MessageInboxesRangeStart, MessageInboxesRangeEnd, InboxesQuantity, MessagesQuantity, MessageReportsQuantity + * Is Canonical: Always +* **MessageReportsQuantity** + * Attribute Identifier: TODO (see profileFormat.go) + * Profile Types: Host + * Description: Number of message reports within hosted range + * Value Bytes: Positive integer + * Value String: Positive integer encoded as string + * Is Required: Only if HostingMessages is true. + * Mandatory Attributes: MessageInboxesRangeStart, MessageInboxesRangeEnd, InboxesQuantity, MessagesQuantity, MessageReviewsQuantity + * Is Canonical: Always +* **HostingEthereumBlockchain** + * Attribute Identifier: TODO (see profileFormat.go) + * Profile Types: Host + * Description: Describes if the user is hosting the Ethereum blockchain and can be queried for Ethereum address deposits. + * Value Bytes: Bool + * Value String: "Yes"/"No" + * Is Required: Yes + * Mandatory Attributes: All other required attributes + * Is Canonical: Always +* **HostingCardanoBlockchain** + * Attribute Identifier: TODO (see profileFormat.go) + * Profile Types: Host + * Description: Describes if the user is hosting the Cardano blockchain and can be queried for Cardano address deposits. + * Value Bytes: Bool + * Value String: "Yes"/"No" + * Is Required: Yes + * Mandatory Attributes: All other required attributes + * Is Canonical: Always + +## Messages + +Message creation is implemented in `/internal/messaging/chatMessage/createMessages/createMessages.go` + +Message reading is implemented in `/internal/messaging/chatMessage/readMessages/readMessages.go` + +Messages are encoded in MessagePack. + +*TODO: Describe the encoding in this document.* + +## Reviews + +Review creation is implemented in `/internal/moderation/createReviews/createReviews.go` + +Reviews reading is implemented in `/internal/moderation/readReviews/readReviews.go` + +Reviews are encoded in MessagePack. + +### Review Hashes + +A review hash is a 28 byte Blake3 hash of the review bytes, with a 1 byte metadata suffix + +The Metadata byte is described below: +* 1 = Identity review +* 2 = Profile review +* 3 = Attribute review +* 4 = Message review + +### Review Encoding + +A review is a MessagePack encoded list of {Signature, Content} + +* **Signature**: + * 64 byte long Ed25519 signature of Blake3_256(Content), signed by the review author's identity public key +* **Content**: + * MessagePack encoded map of `map[int][]byte` + * Map structure: Field Identifier -> Field value (encoded MessagePack) + +### Review Fields: + +Each field value has a bytes and string representation. + +The bytes representation is used to encode the values in MessagePack. + +The string representation is used to read the review into a `map[string]string`. + +* **ReviewVersion** + * Field Identifier: TODO (see readReviews.go) + * Description: The version of the review + * New versions are created when the format/specification for encoding reviews is changed + * Value Bytes: `int` + * Value String: `int` encoded as String + * Is Required: Yes +* **NetworkType** + * Field Identifier: TODO (see readReviews.go) + * Description: The network type of the review + * 1 == Mainnet, 2 == Testnet1 + * Value Bytes: `byte` + * Value String: `byte` encoded as String + * Is Required: Yes +* **IdentityKey** + * Field Identifier: TODO (see readReviews.go) + * Description: Ed25519 Identity key of moderator review author + * Value Bytes: `[32]byte` + * Value string: `[32]byte` encoded in Hex + * Is Required: Yes +* **BroadcastTime** + * Field Identifier: TODO (see readReviews.go) + * Description: Unix Broadcast Time (Number of seconds after unix origin time that the review was broadcast) + * Value Bytes: `int64` + * Value String: `int64` encoded as String + * Is Required: Yes +* **ReviewedHash** + * Field Identifier: TODO (see readReviews.go) + * Description: Identity Hash/Message Hash/Profile Hash/Attribute Hash being reviewed + * Value Bytes: `[]byte` + * Value String: Reviewed hash encoded (Identity hashes use custom encoding, message/profile/attribute hashes are encoded in Hex) + * Is Required: Yes +* **Verdict** + * Field Identifier: TODO (see readReviews.go) + * Description: The verdict of the review (Approve not allowed if ReviewType == "Identity") + * Value Bytes: "Ban"/"Approve"/"None" (TODO: Encode as a single byte to save space) + * Value String: "Ban"/"Approve"/"None" + * Is Required: Yes +* **Reason** + * Field Identifier: TODO (see readReviews.go) + * Description: Reason for Approve/Ban + * Value Bytes: Unicode UTF-8 bytes (TODO: Length limit) + * Value String: Unicode UTF-8 String + * Is Required: No +* **ErrantProfiles** + * Field Identifier: TODO (see readReviews.go) + * Description: List of profile hashes that serve as evidence for why the ban happened (if Verdict == Ban and ReviewedType == "Identity") + * Value Bytes: `[][28]byte` (List of profile hashes) + * Value String: TODO + * Is Required: No +* **ErrantMessages** + * Field Identifier: TODO (see readReviews.go) + * Description: Same as ErrantProfiles but with message hashes + * Value Bytes: `[][26]byte` (List of message hashes) + * Value String: TODO + * Is Required: No +* **ErrantReviews** + * Field Identifier: TODO (see readReviews.go) + * Description: Same as ErrantProfiles but with review hashes (if reviewed identity hash profileType == "Moderator") + * Value Bytes: `[][29]byte` (List of review hashes) + * Value String: TODO + * Is Required: No +* **ErrantAttributes** + * Field Identifier: TODO (see readReviews.go) + * Description: Same as ErrantProfiles but with attribute hashes + * Value Bytes: `[][27]byte` (List of attribute hashes) + * Value String: TODO + * Is Required: No +* **MessageCipherKey** + * Field Identifier: TODO (see readReviews.go) + * Description: Cipher key used to decrypt reviewed message + * Value Bytes: `[32]byte` + * Value String: `[32]byte` encoded as Hex + * Is Required: Yes (If reviewedType == "Message") + +## Reports + +Report creation is implemented in `/internal/moderation/createReports/createReports.go` + +Report reading is implemented in `/internal/moderation/readReports/readReports.go` + +Reports are encoded in MessagePack. + +### Report Hashes + +A report hash is a 29 byte Blake3 hash of the report bytes with 1 byte metadata suffix. + +The Metadata byte is described below: +* 1 = Identity report +* 2 = Profile report +* 3 = Attribute report +* 4 = Message report + +### Report Encoding + +A report is a MessagePack encoded map of `map[int][]byte` + * Map structure: Field Identifier -> Field value (encoded MessagePack) + +### Report Fields: + +Each field value has a bytes and string representation. + +The bytes representation is used to encode the values in MessagePack. + +The string representation is used to read the report into a `map[string]string`. + +* **ReportVersion** + * Field Identifier: TODO (see readReports.go) + * Description: The version of the report. + * New versions are created when the format/specification for encoding reports is changed + * Value Bytes: `int` + * Value String: `int` encoded as String + * Is Required: Yes +* **NetworkType** + * Field Identifier: TODO (see readReports.go) + * Description: The network type of the report. + * 1 == Mainnet, 2 == Testnet1 + * Value Bytes: `byte` + * Value String: `byte` encoded as String + * Is Required: Yes +* **BroadcastTime** + * Field Identifier: TODO (see readReports.go) + * Description: Unix Broadcast time of report (number of seconds after unix origin time representing broadcast time of report) + * Value Bytes: `int64` + * Value String: `int64` encoded as String + * Is Required: Yes +* **ReportedHash** + * Field Identifier: TODO (see readReports.go) + * Description: Identity Hash or Profile Hash or Attribute Hash or Message Hash being reported + * Value Bytes: `[]byte` + * Value String: Reported hash encoded as String (Identity hashes use custom encoding, message/profile/attribute hashes are encoded in Hex) + * Is Required: Yes +* **Reason** + * Field Identifier: TODO (see readReports.go) + * Description: Explanation of unruleful behavior + * Value Bytes: Unicode UTF-8 bytes + * Value String: Unicode UTF-8 String + * Is Required: No +* **MessageCipherKey** + * Field Identifier: TODO (see readReports.go) + * Description: Cipher key of message that is being reported (If ReportedType == "Message") + * Value Bytes: `[32]byte` Cipher key bytes + * Value String: `[32]byte` encoded as Hex + * Is Required: Yes (if reportedType == "Message") + +## Global Settings + +These are the settings that are stored within the `globalSettings` package. + +Each setting is an entry in a `map[string]string`. + +* **AppTheme** + * Description: The color scheme to display the app in. + * Value: "Light"/"Dark"/"Love" +* **MetricOrImperial** + * Description: The measurement system to display length values in + * Value: "Metric"/"Imperial" +* **Currency** + * Description: The currency to use when displaying monetary values to the user + * Value: 3 letter ISO currency code to display all amounts in (Example: USD, EUR) +* **AppNetworkType** + * Description: The network type byte the application should interface with. + * Value: "1" or "2" + * 1 == Mainnet, 2 == Testnet1 + +## User Settings + +These are the settings that are stored with the `mySettings` package. + +Each setting is an entry in a `map[string]string`. + +*TODO: Some of these have an "OnOff" suffix, some don't. Some also have a "Status" suffix, some don't.* + +*We should either remove these suffixes for all or add them for all, to standardize the names.* + +*Some of the setting names could also be improved in other ways.* + +* **MemoDecoration** + * Description: The decoration to use when creating Seekia memos. + * Value: "«« Seekia Memo »»"/"⁕ Seekia Memo ⁕"/"⁂ Seekia Memo ⁂"... +* **NavigationBarLocation** + * Description: The location of the navigation bar within the GUI. + * Value: "Top"/"Bottom"/"Left"/"Right" +* **ShowHostButtonNavigation** + * Description: Describes if the Host button should be shown in the navigation bar. + * Value: "Yes/"No" +* **ShowModerateButtonNavigation** + * Description: Describes if the Moderate button should be shown in the navigation bar. + * Value: "Yes"/"No" +* **AllowedStorageSpace** + * Description: Describes the amount of allowed storage space for the database to use + * Value: Float64 value that describes the gigabytes amount. +* **DeviceSeed** + * Description: The entropy used to derive the device identifier for the user's Mate/Moderator identities + * Value: 64 bytes, encoded in Hex +* **ChatPageIdentityType** + * Description: The identity type the user is currently viewing on the Chat page. + * Value: "Mate"/"Moderator" +* **MyMateContactsPageViewedCategory** + * Description: The category the user is viewing on the Mate contacts page + * Value: Category Name +* **MyHostContactsPageViewedCategory** + * Description: The category the user is viewing on the Host contacts page + * Value: Category name +* **MyModeratorContactsPageViewedCategory** + * Description: The category the user is viewing on the Moderator contacts page + * Value: Category name +* **HostModeOnOffStatus** + * Description: The user's Host mode status. + * Value: "On"/"Off" +* **HostingHostContent** + * Description: The user's HostingHostContent status. + * Value: "Yes"/"No" +* **HostingMateContent** + * Description: The user's HostingMateContent status. + * Value: "Yes"/"No" +* **HostingModeratorContent** + * Description: The user's HostingModeratorContent status. + * Value: "Yes"/"No" +* **HostedMateContentRangeStart** + * Description: The start of the user's hosted Mate identities range. + * Value: `[16]byte`, encoded in Hex +* **HostedMateContentRangeEnd** + * Description: The end of the user's hosted Mate identities range + * Value: `[16]byte`, encoded in Hex +* **HostMessagesOnOffStatus** + * Description: The user's HostMessages status. + * Value: "On"/"Off" +* **HostedInboxRangeStart** + * Description: The start of the user's hosted message inboxes range. + * Value: `[10]byte`, encoded in base32 +* **HostedInboxRangeEnd** + * Description: The end of the user's hosted message inboxes range. + * Value: `[10]byte`, encoded in base32 +* **HostOverClearnetOnOffStatus** + * Description: The user's HostOverClearnet status + * Value: "Yes"/"No" +* **HostOverTorOnOffStatus** + * Description: The user's HostOverTor status + * Value: "Yes"/"No" +* **HostUnviewableProfilesOnOffStatus** + * Description: The user's HostUnviewableProfiles status + * Value: "Yes"/"No" +* **HostUnviewableMessagesOnOffStatus** + * Description: The user's HostUnviewableMessages status + * Value: "Yes"/"No" +* **HostOverClearnetPort** + * Description: The user's HostOverClearnet port. + * Value: Positive integer value, encoded as String +* **ModeratorModeOnOffStatus** + * Description: The user's moderator mode status + * Value: "On"/"Off" +* **ModerateMessagesOnOffStatus** + * Description: The user's ModerateMessages status + * Value: "On"/"Off" +* **ModeratedInboxRangeStart** + * Description: The user's moderated inbox range start + * Value: `[10]byte`, encoded as base32 +* **ModeratedInboxRangeEnd** + * Description: The user's moderated inbox range end + * Value: `[10]byte`, encoded as base32 +* **ModerateMateContentOnOffStatus** + * Description: The user's ModerateMateContent status + * Value: "On"/"Off" +* **ModerateHostContentOnOffStatus** + * Description: The user's ModerateHostContent status + * Value: "On"/"Off" +* **ModerateModeratorContentOnOffStatus** + * Description: The user's ModerateModeratorContent status + * Value: "On"/"Off" +* **ModeratedMateContentRangeStart** + * Description: The user's moderated Mate content identity range start + * Value: `[16]byte`, encoded as Hex +* **ModeratedMateContentRangeEnd** + * Description: The user's moderated Mate content identity range end + * Value: `[16]byte`, encoded as Hex +* **ModeratedHostContentRangeStart** + * Description: The user's moderated Host content identity range start + * Value: `[16]byte`, encoded as Hex +* **ModeratedHostContentRangeEnd** + * Description: The user's moderated Host content identity range end + * Value: `[16]byte`, encoded as Hex +* **ModeratedModeratorContentRangeStart** + * Description: The user's moderated Moderator content identity range start + * Value: `[16]byte`, encoded as Hex +* **ModeratedModeratorContentRangeEnd** + * Description: The user's moderated Moderator content identity range end + * Value: `[16]byte`, encoded as Hex +* **MatchesNeedRefreshYesNo** + * Description: The matchesNeedRefresh status + * This is set to "Yes" when new mate profiles are downloaded + * Value: "Yes"/"No" +* **MatchesGeneratedStatus** + * Description: The user's MatchesGeneratedStatus + * This is set to "No" whenever the user changes their desires + * Value: "Yes"/"No" +* **MatchesSortedStatus** + * Description: The user's MatchesSortedStatus + * Value: "Yes"/"No" +* **MatchesSortDirection** + * Description: The user's MatchesSortDirection + * Value: "Ascending"/"Descending" +* **MatchesSortByAttribute** + * Description: The user's MatchesSortByAttribute + * Value: Sort by Attribute ( Examples: "Height", "Wealth", "MatchScore") +* **MatchesViewIndex** + * Description: The user's MatchesViewIndex + * This describes the match the user is currently viewing + * Value: Integer between (0-2147483647) +* **MateChatMessagesUpdatedStatus** + * Description: The user's Mate ChatMessagesUpdatedStatus + * Is true if all of the user's Mate chat messages have been imported + * Value: "Yes"/"No" +* **ModeratorChatMessagesUpdatedStatus** + * Description: The user's Moderator ChatMessagesUpdatedStatus + * Is true if all of the user's Moderator chat messages have been imported + * Value: "Yes"/"No" +* **MateChatConversationsGeneratedStatus** + * Description: The user's Mate chat conversations generated status + * Value: "Yes"/"No" +* **ModeratorChatConversationsGeneratedStatus** + * Description: The user's Moderator chat conversations generated status + * Value: "Yes"/"No" +* **MateChatConversationsSortedStatus** + * Description: The user's Mate chat conversations sorted status + * Value: "Yes"/"No" +* **ModeratorChatConversationsSortedStatus** + * Description: The user's Moderator chat conversations sorted status + * Value: "Yes"/"No" +* **MateChatConversationsNeedRefreshYesNo** + * Description: The user's Mate chat conversations need refresh status + * It is set to "Yes" whenever a new message has been downloaded that belongs to one of the user's Mate inboxes + * Value: "Yes"/"No" +* **ModeratorChatConversationsNeedRefreshYesNo** + * Description: The user's Moderator chat conversations need refresh status + * It is set to "Yes" whenever a new message has been downloaded that belongs to one of the user's Moderator inboxes + * Value: "Yes"/"No" +* **MateChatConversations_SortByAttribute** + * Description: The user's Mate chat conversations sort by attribute + * Value: Sort by attribute (Example: "Height", "Age") +* **ModeratorChatConversations_SortByAttribute** + * Description: The user's Moderator chat conversations sort by attribute + * Value: Sort by attribute (Example: "IdentityScore") +* **MateChatConversations_SortDirection** + * Description: The user's Mate chat conversations sort direction + * Value: "Ascending"/"Descending" +* **ModeratorChatConversations_SortDirection** + * Description: The user's Moderator chat conversations sort direction + * Value: "Ascending"/"Descending" +* **MateChatConversations_ViewIndex** + * Description: The user's Mate chat conversations view index. + * This describes the page of chat conversations the user is viewing. + * Value: Integer between (0-2147483647) +* **ModeratorChatConversations_ViewIndex** + * Description: The user's Moderator chat conversations view index. + * This describes the page of chat conversations the user is viewing. + * Value: Integer between (0-2147483647) +* **ViewedContentGeneratedStatus** + * Description: The user's ViewedContent generated status + * Value: "Yes"/"No" +* **ViewedContentSortedStatus** + * Description: The user's ViewedContent sorted status + * Value: "Yes"/"No" +* **ViewedContentSortByAttribute** + * Description: The user's ViewedContent sort by attribute + * Value: Sort by attribute (Example: "Controversy") +* **ViewedContentSortDirection** + * Description: The user's ViewedContent sort direction + * Value: "Ascending"/"Descending" +* **ViewedContentNeedsRefreshYesNo** + * Description: The user's ViewedContentNeedsRefresh status + * Value: "Yes"/"No" +* **ViewedContentViewIndex** + * Description: The user's ViewedContent view index + * This represents the page of viewedContents they are viewing + * Value: Integer between (0-2147483647) +* **ViewedModeratorsGeneratedStatus** + * Description: The user's ViewedModeratorsGeneratedStatus + * Value: "Yes"/"No" +* **ViewedModeratorsSortedStatus** + * Description: The user's ViewedModeratorsSorted status + * Value: "Yes"/"No" +* **ViewedModeratorsNeedsRefreshYesNo** + * Description: The user's ViewedModeratorsNeedsRefresh status + * Value: "Yes"/"No" +* **ViewedModeratorsSortByAttribute** + * Description: The user's ViewedModerators sort by attribute + * Value: Sort by attribute (Example: "IdentityScore", "Controversy") +* **ViewedModeratorsSortDirection** + * Description: The user's ViewedModeratorsSortDirection + * Value: "Ascending"/"Descending" +* **ViewedModeratorsViewIndex** + * Description: The user's ViewedModeratorsViewIndex + * Value: Integer between (0-2147483647) +* **ViewedHostsGeneratedStatus** + * Description: The user's ViewedHostsGenerated status + * Value: "Yes"/"No" +* **ViewedHostsSortedStatus** + * Description: The user's ViewedHostsSorted status + * Value: "Yes"/"No" +* **ViewedHostsNeedsRefreshYesNo** + * Description: The user's ViewedHostsNeedsRefresh status + * Value: "Yes"/"No" +* **ViewedHostsSortByAttribute** + * Description: The user's ViewedHostsSortByAttribute + * Value: Sort by attribute (Example: "BanAdvocates") +* **ViewedHostsSortDirection** + * Description: The user's ViewedHostsSortDirection + * Value: "Ascending"/"Descending" +* **ViewedHostsViewIndex** + * Description: The user's ViewedHosts view index. This describes the page of viewedHosts they are viewing. + * Value: Integer between (0-2147483647) + +# Questionnaire + +*TODO: Encode Questionnaire and responses using MessagePack rather than this current encoding* + +Questionnaires and responses are implemented in `/internal/network/mateQuestionnaire/mateQuestionnaire.go` + +A questionnaire can contain a maximum of 25 questions. + +Each question has: + +* Identifier +* Type +* Content +* Options + +Each question is separated by "+&" + +{Question1} + "+&" + {Question2} + "+&" + {Question3} + ... + +Question information is separated by "%¢" + +{Identifier} + "%¢" + {Type} + "%¢" + {Content} + "%¢" + {Options} + +*TODO: Swap order of Content and Options* + +*TODO: Change length of question identifiers to 12/13 to avoid collisions. 9 bytes is too short, 10 is already being used for inboxes, 11 is already being used for device identifiers.* + +There are 2 questions types: **Choice** and **Entry**. + +* **Choice**: + * Identifier: 9 bytes encoded Hex + * Type: "Choice" + * Options: + * {Maximum Answers Allowed} + "#" + {Choices delimited by "$¥"} + * Each choice can be a maximum of 100 bytes in length + * Each choice cannot contain "+&" or "%¢" or "$¥" + * Content: + * Value: The question text (Example: "Select the music genres you enjoy.") + * Maximum of 500 bytes in length + * Text cannot contain "+&" or "%¢" + * Maximum Answers allowed: 1, 2, 3, 4, 5, or 6 +* **Entry**: + * Identifier: 10 bytes encoded Hex + * Type: "Entry" + * Options: "Numeric"/"Any" + * Content: + * Value: The question text (Example: "What is your favourite movie?") + * Maximum of 500 bytes in length + * Cannot contain "+&" or "%¢" + +## Questionnaire Response + +A questionnaire response is encoded as follows: + +Each question response is delimited with "+&" + +{Question1} + "+&" + {Question2} + "+&" + {Question3} + ... + +Question: {Identifier} + "%" + {Response} + +* **Entry**: + * Identifier: + * The question identifier + * Response: + * Response (Maximum of 2000 bytes) + * Response cannot contain "+&" +* **Choice**: + * Identifier: + * The question identifier + * Response: + * The number of each choice, separated by "$" (Choice 1 = "1", choice 2 = "2"...) + +## Requests and Responses + +Requests and responses are used to communicate with Seekia hosts. + +Requests are implemented in `internal/network/serverRequest/serverRequest.go` + +Responses are implemented in `internal/network/serverResponse/serverResponse.go` + +All requests and responses are encoded in MessagePack. + +The `RecipientHost`, `RequestType`, `RequestIdentifier`, and `NetworkType` fields exist for all requests but are not included here. + +* **GetParametersInfo** + * Description: + * Used to retrieve info on the parameters a host has + * Request: + * *Nothing other than the standard fields.* + * Response: + * ParametersInfo: `map[string]int64` + * {ParametersType -> Parameters Broadcast Time} for each stored parameters (Empty if none exist) +* **GetParameters** + * Description: + * Used to retrieve parameters from a host + * Request: + * ParametersTypesList: `[]string` + * List of parameters types to retrieve + * Response: + * ParametersList: `[]RawMessagePack` + * List of parameters (each encoded in MessagePack) + +* **GetProfilesInfo** + * Description: + * Used to get information about profiles + * Can be used by to get profiles authored by specified authors + * Request: + * AcceptableVersionsList: `[]int` + * Acceptable profile versions the requestor can accept + * ProfileType: "Mate"/"Host"/"Moderator" + * IdentityHashesList: `[][16]byte` + * Limit request to profiles authored by specific identity hashes + * RangeStart: `[16]byte` + * Identity hash range start + * RangeEnd: `[16]byte` + * Identity hash range end + * GetNewestOnly: `bool` + * Only retrieve newest profiles (maximum 1 profile for each identity) + * GetViewableOnly: `bool` + * Only retrieve viewable profiles + * Criteria: Criteria of the profiles to retrieve. Profiles must fulfill the criteria. + * Encoded MessagePack, see `mateCriteria.go` and `myMateCriteria.go` to see how it is formatted + * Response: + * ProfilesInfo: `[]ProfileInfoStruct` (empty if none exist) + * ProfileInfoStruct: + * ProfileHash: Profile hash `[28]byte` + * ProfileAuthor: Profile author identity hash `[16]byte` + * ProfileBroadcastTime: Profile Broadcast Time `int64` + +* **GetProfiles** + * Description: + * Used to retrieve profiles + * Request: + * ProfileType: "Mate"/"Host"/"Moderator" + * Profile type of profile hashes to retrieve + * ProfileHashesList: `[][28]byte` + * List of profile hashes of profiles to retrieve + * Response: + * ProfilesList: `[]RawMessagePack` + * List of profiles (empty if none exist) + +* **GetMessageHashesList** + * Description: + * Used to get message hashes of messages + * Request: + * AcceptableVersions: `[]int` + * Message versions the requestor can accept + * RangeStart: `[10]byte` + * Start of message inbox range to retrieve + * RangeEnd: `[10]byte` + * End of message inbox range to retrieve + * InboxesList: `[][10]byte` + * List of inboxes to retrieve (Empty if none provided) + * All messages in response must be sent to the inboxes in the list (unless list is empty) + * GetViewableOnly: `bool` + * If true, only retrieve messages that are not banned + * GetDecryptableOnly: `bool` + * If true, only retrieve messages which have been reviewed or reported with a valid message cipher key + * Response: + * MessageHashesList: `[][26]byte` + * List of message hashes (Empty if none exist) +* **GetMessages** + * Description: + * Get messages + * Request: + * MessageHashesList: `[][26]byte` + * List of message hashes of messages to retrieve + * Response: + * MessagesList: `[]RawMessagePack` + * List of messages (Empty if none exist) + +* **GetIdentityReviewsInfo** + * Description: + * Used to retrieve info about identity, profile, and attribute reviews that a host has. + * Request: + * AcceptableVersions: `[]int` + * Review versions the requestor can accept + * IdentityType: "Mate"/"Host"/"Moderator" + * Identity type of reviewed identity/profile author/attribute hashes to retrieve + * ReviewedIdentitiesList: `[][16]byte` + * Only retrieve reviews reviewing these identity hashes, or profiles/attributes authored by these identities + * If list is empty, allow all identities/profiles/attributes within requested range. + * ReviewersList: `[][16]byte` + * List of reviewer identity hashes (to only retrieve reviews authored by these reviewers) + * If list is empty, allow all reviewers + * RangeStart: `[16]byte` + * Start of reviewed identity hash range + * RangeEnd: `[16]byte` + * End of reviewed identity hash range + * Response: + * ReviewsInfo: `map[[29]byte][]byte` + * Map Structure: Review Hash -> Reviewed Hash (Empty if none exist) + * Reviewed hash is either an identity hash, profile hash, or attribute hash +* **GetMessageReviewsInfo** + * Description: + * Used to retrieve info about message reviews the host has. + * Request: + * AcceptableVersions: `[]int` + * Review versions the requestor can accept + * ReviewedMessagesList: `[][26]byte` + * Only retrieve reviews reviewing these message hashes. + * If list is empty, allow all reviewed message within requested range. + * ReviewersList: `[][16]byte` + * List of reviewer identity hashes (to only retrieve reviews authored by these reviewers) + * If list is empty, allow all reviewers + * RangeStart: `[10]byte` + * Start of reviewed message inbox range + * Reviewed messages must be sent to an inbox within this range + * RangeEnd: `[10]byte` + * End of reviewed message inbox range + * Response: + * ReviewsInfo: `map[[29]byte][26]byte` + * Map Structure: Review Hash -> Reviewed Message Hash + * Map is empty if no reviews exist + +* **GetReviews** + * Description: + * Used to retrieve reviews + * Request: + * ReviewHashesList: `[][29]byte` + * List of review hashes of reviews to retrieve + * Response: + * ReviewsList: `[]RawMessagePack` + * List of reviews (Empty if none exist) + +* **GetIdentityReportsInfo** + * Description: + * Used to retrieve info about identity, profile, and attribute reports that the host has. + * Request: + * AcceptableVersions `[]int` + * Report versions the requestor can accept + * IdentityType: "Mate"/"Host"/"Moderator" + * Identity type of reported identity hashes to retrieve (or authors of reported profiles/attributes) + * RangeStart: `[16]byte` + * Start of reported identity hash range to retrieve + * RangeEnd: `[16]byte` + * End of reported identity hash range to retrieve + * ReportedIdentitiesList: `[][16]byte` + * List of reported identity hashes to retrieve reports for (reported identity hashes, or authors of reported profiles/attributes) + * Response: + * ReportsInfo: `map[[30]byte][]byte` + * Map Structure: Report hash -> Reported Hash (Empty if none exist) + * Reported hash is either an identity hash, profile hash, or attribute hash +* **GetMessageReportsInfo** + * Description: + * Used to retrieve info about message reports the host has. + * Request: + * AcceptableVersions `[]int` + * Report versions the requestor can accept + * RangeStart: `[10]byte` + * Start of reported message inbox range + * Reports must be reporting messages that were sent to an inbox within this range + * RangeEnd: `[10]byte` + * End of reported message inbox range + * ReportedMessagesList: `[][26]byte` + * List of reported message hashes to retrieve reports for + * Response: + * ReportsInfo: `map[[30]byte][26]byte` + * Map Structure: Report hash -> Reported Message Hash + * Map is empty if no reports exist. +* **GetReports** + * Description: + * Used to retrieve reports + * Request: + * ReportHashesList: `[][30]byte` + * List of report hashes of reports to retrieve + * Response: + * ReportsList: `[]RawMessagePack` + * List of reports (Empty if none exist) + +* **GetAddressDeposits** + * Description: + * Used to retrieve info about deposits made to cryptocurrency addresses + * Request: + * Cryptocurrency: "Ethereum", "Cardano" + * AddressesList: `[]string` + * List of cryptocurrency addresses to retrieve deposit information about + * Response: + * AddressDepositsList: `[]DepositStruct` + * DepositStruct: This object type is used to represent a deposit to an address + * Address: `string` + * The cryptocurrency address where funds were deposited + * DepositTime: `int64` + * The unix time of the block when deposit(s) were made + * DepositAmount: `big.Int` + * The sum of all deposit amounts in the block to the specified address, in crypto atomic units (example: wei) + +* **GetViewableStatuses** + * Description: + * Used to retrieve verified moderator viewable consensus statuses + * Request: + * IdentityHashesList: `[][16]byte` + * Identity hashes to retrieve viewable statuses of + * ProfileHashesList: `[][28]byte` + * Profile hashes to retrieve viewable statuses of + * Response: + * IdentityHashStatuses: `map[[16]byte]bool` + * Map Structure: Identity Hash -> true/false (Viewable/Unviewable) + * ProfileHashStatuses: `map[[28]byte]bool` + * Map Structure: Profile Hash -> true/false (Viewable/Unviewable) + +* **BroadcastContent** + * Description: + * Used to broadcast profiles, messages, reviews, reports, and parameters + * Request: + * ContentType: "Profile", "Message", "Review", "Report", "Parameters" + * ContentList: `[]RawMessagePack` + * List of each piece of content to broadcast + * Response: + * ContentAcceptedInfo: `map[string]bool` + * Map Structure: Content Hash -> true/false + * Host uses this to verify that the content is within their hosted range, and they will host the content + diff --git a/documentation/User Guide.md b/documentation/User Guide.md new file mode 100644 index 0000000..480b12e --- /dev/null +++ b/documentation/User Guide.md @@ -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* diff --git a/documentation/Whitepaper.md b/documentation/Whitepaper.md new file mode 100644 index 0000000..cc85083 --- /dev/null +++ b/documentation/Whitepaper.md @@ -0,0 +1,391 @@ +# Whitepaper + +This document is used to edit the whitepaper. + +New releases will created from this document and exported as PDFs. + +## Seekia + +Be Race Aware + +A race aware mate discovery network. + +## Introduction + +The human species is a fascinating biological phenomenon with a complex and mysterious origin story. Geographically isolated groups of humans evolved separately over long periods of time, each genetically adapting to their unique social and physical environments. Many instances of interbreeding between members of different population groups occurred, infusing each group with new genetic variation. + +What has resulted is a species which possesses much beauty and biodiversity. + +Humans can be classified by their geographic ancestry by describing the locations where each person's ancestors lived at different times in history. Ancestry can be measured by analyzing the percentage of DNA that a person shares with past human populations. Geographic distance was a significant impediment to gene flow between population groups for most of humanity's history. The humans within each isolated population group bred among themselves, resulting in the loss of genetic variation through the process of genetic drift. Consequently, members of these populations tend to possess similar genetic patterns and physical traits. + +We can classify humans into different races. Races are defined by grouping humans by genetic attributes such as skin, eye and hair color; skin and hair texture; facial structure, and genetic ancestry. If a human is sufficiently different from any other human, they are considered the only member of a unique race. Classifications of racial groups are fuzzy and not fully discrete, because every human is genetically and physically unique. + +## Racial Loneliness + +Primarily over the past several centuries, advances in transportation technologies have enabled all of the world's races to spread throughout the earth. As a result of a larger global population and the increased prevalence of interracial breeding, I posit that there now exists a higher quantity of human races than ever before. + +These circumstances have drastically increased the prevalence of racial loneliness. + +Racial loneliness is the condition of being unable to find members of one's own race to mate with and befriend. A sufferer may be a member of a rare race or may live in an area where members of their own race are rare. Even if they are able to meet someone belonging to their own race, they must also be socially compatible with them, further decreasing the odds of a successful relationship. + +Racial loneliness has become an epidemic. It is the cause of significant despair, and is a contributing factor to global fertility collapse. + +Global fertility collapse can be partially explained by the trend of marriage and reproduction delayment, a phenomenon which has been worsened by racial loneliness. Many humans have a biological and psychological desire to breed with humans who resemble them and to produce offspring who resemble them. People who desire for their mate and children to look like them are spending more time searching and waiting for a mate who resembles them. The goal of finding a mate who belongs to a person's own race is a practical impossibility for many. + +## Beauty + +Human beauty is defined as the ability of a person's physical appearance to evoke feelings of sexual attraction, arousal, and pleasure in other humans. Beauty is subjective for each individual, but trends and patterns emerge when surveying large quantities of people. Human beauty ranking is calculated by comparing the sentiments expressed by large populations of humans. Some people are more beautiful than other people, and some races are more beautiful than other races. Human beauty inequality is an inevitable consequence of human appearance diversity. + +## The Beauty Crisis + +The beauty crisis, also known as the beauty scarcity crisis, the beauty shortage, and the ugliness crisis, is one of the most dire and widespread issues plaguing humanity. + +The beauty crisis is defined as the modern scarcity of beautiful humans and the scarcity's negative effects on humanity's happiness and flourishing. The modern human species has collectively become much less beautiful than it was throughout most of human history. + +There are many causes to the modern beauty crisis. One primary cause is the increase in humanity's collective beauty standards. Humanity's modern exposure to the world's most beautiful people and races has caused our beauty standards to increase. I posit that modern humans collectively rate their peers as being much less beautiful than ancient humans rated their own peers. When humans were hunter gatherers living in small tribes, we typically had visual exposure to a much lower quantity of races. Ancient human beauty standards were typically formed by a person's exposure to members of their own race and races which were more similar to their own. Modern humans now have visual exposure to the world's most rare and beautiful people and races via photos, videos, and in-person interactions. Digital beautification technologies and artificially generated humans have also exposed us to gorgeous humans that are more beautiful than any real-life human beings. Frequent exposure to highly beautiful human specimens has desensitized the parts of our brains which respond to beauty, causing us to perceive people as being uglier. Humanity's modern widened exposure to a larger diversity of humans has also expanded our collective beauty ranking bounds to include all of the world's ugliest and most beautiful races and people. + +Beauty exposure's effect of increasing humanity's collective beauty standards is very difficult to undo. The best treatment for people living in modern times is to cease consumption of media which depicts beautiful people to attempt to reset one's beauty standards. Unfortunately, this will never reset humanity's beauty standards to the state they existed in for ancient humans. Firstly, most humans will still be exposed to a larger diversity of races in their daily lives than ancient humans were exposed to due to advances in transportation technology, which have enabled the spread all of the world's races to all regions of the world. Secondly, it is very difficult to unsee and forget beautiful people and races. Forgetting someone's face can take decades, and it is usually impossible to forget the beauty of a race or particular anatomical structures. People should avoid consuming content which depicts beautiful people as early in life as possible, especially pornography. One possible way to decrease a person's beauty standards is a novel technology which could involve showing a person photos of ugly people while giving them some form of therapeutic stimulation. Some ideas include a cocktail of drugs, electricity to the brain, brain-machine interfaces, and magnetism to the brain. + +The second major cause of the modern beauty crisis is the obesity crisis. Modern technology has caused and enabled people to become more obese than ever before. Rates of obesity have skyrocketed over the past century, causing humans to become much uglier. Some of the heaviest humans of all time are alive today. We are living in an age of unprecedented ugliness. + +We are also living in an age of unprecedented beauty. Teeth straightening braces, supplements, skin creams, and various other technologies have helped to produce some of the most beautiful humans of all time. Modern beautification technologies have unfortunately not been powerful enough to offset the uglifying effects of our modern world. + +The beauty crisis is one of the most significant issues plaguing humankind. Humans have become less attracted to each other, exacerbating many societal ills. + +The beauty crisis is contributing to the global fertility crisis. Many people lament that they cannot find a mate who they feel is beautiful enough to engage in a relationship with who also feels the same way about them. Many people are choosing not to have children because they view themselves as ugly and do not want to create ugly offspring. These factors are increasing the prevalence of singledom and childlessness, which are both increasing the global rates of loneliness, depression, meaninglessness, and lassitude. + +Modern unprecedented beauty exposure, beautification technologies, and modern society's uglifying effects on our species have all contributed to the emergence of the beauty inequality crisis. The beauty inequality crisis is defined as the modern widening of the gap between the beautifulest and the ugliest humans and the gap's negative effects on humanity. The size disparity between the most and least sexually desirable portions of humanity has grown substantially. A larger portion of humanity now wants to mate and breed with a smaller portion of humanity. Competition for access to the increasingly-scarce resource of beautiful people has grown fiercer than ever. The beauty disparity crisis has increased the prevalence of people who are deeply envious of beautiful people and their superior social, romantic, and career success. + +Human beauty is also becoming more important for human happiness and wellbeing. If humanity's story is like a video game, we are running out of quests to complete. Robots are replacing humans for labor. Artificial intelligence will soon provide the answers to most solvable questions. Humanity will eventually solve most of the world's problems such as curing most diseases and giving most people access to food and shelter. We are reaching the end of philosophy where most ideas have been thought of and most quandaries have been solved. As a result of these factors, modern humans have fewer sources of meaning. Humanity's modern shortage of meaning is known as the meaning crisis. The creation and worship of beauty are becoming some of the only sources of meaning in people's lives. Humans are now pursuing indulgent lives of hedonism, romance with other humans, and immersion into artificial and natural beauty. Human beauty worship and sexual relationships have become even more important to a happy human life. Robots cannot solve the beauty crisis because sexual and emotional human contact are some of the only things robots will not be able to replace or replicate. + +The optimal solution to the beauty crisis is to beautify the human species. The ideal way to make humans more beautiful is by improving their genetics. + +## Seekia + +*App home page image* + +To help cure racial loneliness and beautify the human species, I present Seekia: a race aware mate discovery network. + +Seekia is a mate discovery network where users can find a mate while having a deep awareness of each potential partner's race. Users can share racial information in their profiles such as their eye, skin, and hair color; hair texture; genetic ancestry; haplogroups; and the alleles in their genome which effect physical traits. + +Seekia enables users to browse and filter potential mates by their racial attributes. Seekia can also calculate the racial characteristics for prospective offspring between users. Seekia allows for users to predict and choose the race of their offspring by selecting a mate who is the most capable and likely to produce offspring of their desired race. + +Seekia helps users to find members of their own race to mate with, curing racial loneliness. Seekia aims to beautify the human species by enabling people to predict what their offspring will look like with each potential mate, helping to encourage breeding between people who will produce the most beautiful offspring who belong to the most beautiful races and possess the most beautiful traits. Seekia aims to help members of the most beautiful races to meet and have offspring, helping to increase the populations of the world's most beautiful races. + +I will now describe an overview of the features and advantages of Seekia. The technicals of Seekia are described in greater detail in the Seekia documentation and code implementation. + +## Eugenics + +Eugenics is the practice of improving humanity's genetic quality. Genetic quality is defined by three main attributes: beauty, health, and intelligence. Seekia aims to improve humanity's genetic quality by making humans more beautiful, healthy, and intelligent. + +Seekia aims to improve humanity's genetics by facilitating the eugenic technique of selective breeding. Selective breeding is the practice of breeding specific human pairs to produce humans of a higher genetic quality. By encouraging breeding between certain people, it is possible to increase humanity's overall beauty, health, and intelligence. This technique is akin to combining the same set of foods together to create either 5 delicious meals or 5 revolting meals. + +Seekia provides users with the ability to mate in a genetics aware manner. Users can choose their mate in such a way to reduce the probability of their offspring having genetic diseases and increase the probability of their offspring having certain traits. + +### Beauty + +Seekia aims to beautify the human species by encouraging human mate pairings which will create the most beautiful offspring and increase the proportion of beautiful people and races on Earth. Seekia users will be able to choose their mate with a greater knowledge of what their offspring will look like, helping them to produce the most beautiful offspring belonging to the most beautiful races and possessing the most beautiful traits. + +### Health + +Seekia aims to make humanity more healthy by encouraging relationships between people whose offspring will have a lower likelihood of having genetic diseases. Users can sort their matches by their offspring's total monogenic disease probability and total polygenic disease risk score. Seekia measures a prospective offspring's disease risk by combining both user's genetic information to predict the offspring's genome alleles and disease risks. + +### Intelligence + +Seekia aims to make humanity more intelligent by encouraging breeding between specific human pairs who are more likely to produce intelligent offspring. Seekia users will be able to sort their potential mates by the average intelligence of their prospective offspring. Seekia users will choose mates with whom their offspring is likely to have a higher intelligence. This process will cause humanity's intelligence to increase because humans who possess a more compatible set of genes for intelligence will be more likely to breed with each other. Seekia aims to produce an average offspring intelligence prediction for each potential mate pairing by combining both user's genome alleles to create many potential offspring genomes and measuring each offspring's predicted intelligence. + +## Open Source + +The genetic future of the human species should be steered by open source technologies. Freely available source code will help race and genetics aware mate discovery technologies to be impartial, auditable, decentralized, and rapidly improvable. + +The Seekia application is open source software. It is released into the public domain under the Unlicense. It is written in Golang, an open source programming language. + +I encourage others to replicate and improve upon Seekia's technology. I want alternative mate discovery services to incorporate race and genetics aware features, even if they are closed-source and for-profit. No attribution is necessary. + +Seekia is not reliant on proprietary mobile app stores. The Seekia application can be compiled for use on mobile platforms, but users are recommended to use the desktop app on an open source operating system. + +## Decentralization + +The genetic destiny of the human species should not be controlled by a small number of entities. Centralized mate discovery services can attempt to encourage certain kinds of relationships to form. For example, a nefarious mate discovery service could try to increase the prevalence of genetic disorders by encouraging relationships between people who have a higher probability of producing diseased offspring. + +The Seekia network strives to be open and decentralized. The Seekia network aims to be resilient in the event that any host suddenly stops participating or is compromised by bad actors. Seekia is not fully decentralized, because it relies on a central credit database to perform scalable private accounting. Seekia still reaps many benefits from its decentralized architecture. + +Anyone can participate as a network host, which involves serving profiles and messages to other network peers. It is impossible for a single host to prevent specific profiles and messages from reaching the rest of the network. Users broadcast and download content to and from multiple network hosts. + +The decentralized architecture of Seekia helps to sustain network reliability. User data exists on many computers around the world, so events such as solar flares and hosting provider bans are less likely to result in a loss of user data or network downtime. Each user's application periodically rebroadcasts content to help prevent user data from disappearing from the network. + +## Dark Web + +Seekia utilizes the Tor mixnet anonymity network to provide users with privacy. + +User requests are sent through the Tor network to prevent sensitive data such as user mate desires and conversation partners from being linked to a user's identity. Hosts can choose to host over the Tor network to shield their IP address and protect themselves against potential risks. Hosts and moderators can choose to host or moderate over clearnet for a faster experience. + +The Seekia website is also hosted on the dark web, enabling the Seekia application to be distributed in a private and uncensorable manner. + +## Cryptographic Identity + +Each user has an Identity Key, which is a cryptographic signing key. A hash of this key is called a user's Identity Hash, which is their unique identifier on the Seekia network. User profiles and messages are digitally signed with the author's identity key. + +Centralized mate discovery services can sabotage their user's mating efforts by editing profiles and messages. Content on the Seekia network is cryptographically signed, making it impossible to impersonate users without their private keys. + +A user's identity key is derived from a 15 word mnemonic seed phrase. A seed phrase can be used to recover a user's identity on any device. + +There are three identity types: Mate, Host, and Moderator. + +## Profiles + +Each Seekia user has a profile. Users must broadcast a profile to be able to chat with other users. Profiles which are broadcasted to the network are viewable by anyone. + +User profiles can contain information about a variety of topics such as age, location, biological sex, gender identity, sexuality, genetics, race, height, body type, language, fame, wealth, infectious diseases, drug use, hobbies, job, beliefs, diet, and pets. + +Users can browse the network and find matches for free without creating an identity or broadcasting a profile. This freedom allows many more people to search for matches, which should significantly increase the quantity of users who eventually broadcast a profile. A web explorer should be built that allows anyone to view user profiles without having to download the Seekia app. + +Users should only share information in their profiles which they are comfortable being fully public and searchable. Sharing less will possibly result in more matches and messages for a user, because others will fill the gaps of knowledge about the user with their fantasies of a perfect mate. + +## Questionnaires + +A questionnaire is a set of questions that users can create and share on their profile. There are 2 kinds of questions: Choice and Entry. Choice questions offer a selection of predefined options. Entry questions allow users to respond with any text, and can also be constrained to only allow numerical responses. + +Users can create questionnaire responses and send them to other users in encrypted messages. Users can filter and sort their matches and conversators by the responses that they provided to their questionnaire. For example, a user could create a numeric Entry question asking how many countries other users have visited. The user could then sort their matches by their responses to this question. + +## Desires + +Desires represent a user's mate preferences, and are used to generate a user's matches. Users can choose their desires within the Seekia app. Seekia aims to give users total control of the algorithmic curation of their matches. + +### Match Scores + +Each desire has an Importance, which is a number the user can adjust. Each desire's Importance is added to a user's Match Score if the user fulfills the desire. Users can sort their matches by their Match Score. + +### Desire Options + +Each desire has 1 or 2 options: Filter All and Require Response. + +Filter All, when enabled, will filter all users who do not fulfill the desire. Without this enabled, a desire only represents a preference rather than a requirement. The desire will still influence user match scores. + +Require Response, when enabled, requires users to have provided a response to the attribute. For example, if a user enables Require Response for Age, then only users who have provided their Age will qualify as a match. + +A user's desires are stored locally on their machine. A user's desires do not need to be uploaded anywhere or shared to the network. + +### Download Desires + +Users can choose their Download Desires, which are the desires that users are comfortable sharing with hosts. The more desires they share, the fewer profiles they will need to download. Most users should share desires such as Age and Distance, because these are usually not too private or embarrassing to risk being publicly revealed. If a user does not select any download desires, the Seekia app will download all of the newest mate profiles on the network, allowing the user to privately generate their matches without having to share any of their desires to hosts. + +## Greet, Reject, Like, and Ignore + +Users can send Greet and Reject messages to other users. Greet messages signal interest, and Reject messages signal disinterest. Users can filter their matches and conversations to only show users who have greeted them, and to hide users who have rejected them. + +Users can also designate other users as being Liked or Ignored. A user's Liked and Ignored users are stored on their machine and are never shared or uploaded anywhere. Users can filter their matches and conversations to only show users who they have liked, and hide users who they have ignored. + +## Race + +Seekia is a race aware mate discovery network. Users can browse potential mates while having a deep awareness of each user's race and the predicted race of their offspring. Seekia aims to allow users to choose a mate who is the most likely and capable of producing offspring who belong to their desired race. + +The racial description of users should be highly precise and detailed. The more accurately a user's profile can describe their race, the more effective Seekia will be at helping users to meet and mate with members of their desired races. + +Users can share detailed information about their race such as their physical traits, trait genes, genetic ancestry, parent haplogroups, and neanderthal variant count. Users can filter and sort other users by these attributes and the calculated attributes of their offspring. + +Users profiles can contain a user's skin, eye and hair color; hair texture; and genome allele values for genes which effect these traits. Users can also share the allele values for genes which effect facial structure. + +User profiles can include ancestral analyses from multiple providers and computational methods. The Seekia app should also eventually provide the ability to perform ancestral analyses from raw genome data files. + +### Racial Similarity + +For a person to have offspring who look as similar as possible to them, they should breed with someone who is the most racially similar to them, without being so similar that the negative effects of inbreeding occur. Users are able to sort other users based on their Racial Similarity, a calculation which measures trait similarity, trait gene similarity, ancestral similarity, and haplogroup similarity. + +Trait similarity compares user traits such as eye color, skin color, hair color, and hair texture. Trait gene similarity compares the alleles responsible for physical traits from each user's genome. Ancestral similarity compares the geographic distance between each user's ancestry composition locations. + +Facial similarity detection technology is another planned feature for Seekia. The Seekia app could compare user profile photos to help users to find potential mates whom have similar facial structures, helping to cure racial loneliness. Users could also import photos of people they are strongly attracted to for the purpose of finding a mate who looks similar to them. + +## Genetics Aware + +Seekia is also a genetics aware mate discovery network. Seekia gives users the ability to choose their mate in a way that maximizes the health of their offspring and increases the probability of their offspring possessing their desired traits. + +The Seekia application is capable of producing genetic analyses on raw genome files. Users and couples can perform offline analyses of their genomes within the app. Genetic analyses are computed privately on user machines without uploading any data anywhere. + +There are two analysis types: Person and Couple. A person analysis contains a person's monogenic disease probabilities, polygenic disease risk scores, and traits scores. A Couple analysis is performed for two people, and contains the monogenic disease probabilities, polygenic disease risk scores, and trait scores for offspring produced from both people. + +Seekia plans to add more genetic attribute analyses and genetic compatibility testing features. Kinship analysis technology should be built into Seekia to help users avoid accidental inbreeding. + +### Raw Genome Files + +Users must first import their raw genome file(s) from sequencing companies. The sequences obtained from these companies usually contain some inaccurate reported gene values. To remedy these errors, users can import as many raw genome files as they desire to find and root out conflicts. Seekia will combine any number of raw genomes into two genomes to be analyzed: Only Include Shared and Only Exclude Conflicts. + +The Only Include Shared genome is the most accurate, and will only include genome locations where at least 2 files have agreed on the locus value. If conflicts are found, then the most attested value is chosen. If a tie exists between all files, then the location is not included. + +The Only Exclude Conflicts genome is less accurate, but includes more data. It is created identically to Only Include Shared, except it will include locations which only 1 genome file has recorded. + +Each analysis reports the results from each of the component genomes and the combined genomes, so users can see where conflicts exist and how those conflicts effect the analysis results. + +### Monogenic Diseases + +Genetic disorders are a significant issue for humanity, causing severe suffering for millions of people. The prevalence of genetic disorders among humans is increasing due to the weakening of the natural selection pressures for health. As a result of lifesaving medical technologies, people with genetic disorders are living longer and healthier lives, and are more likely to produce diseased offspring who possess their defective genes. + +Seekia aims to drastically reduce the prevalence of recessive monogenic diseases within the human species. There are thousands of genes which, if defective, cause recessive monogenic disease in humans. All humans have 2 copies of these genes. A recessive monogenic disease is a disease which only causes symptoms if both copies of a person's gene are defective. Most people are carriers for many recessive monogenic diseases. A carrier has 1 defective and 1 healthy copy of the disease causing gene. Few people have defects in both copies of the same gene, which is required to cause disease symptoms. + +If two people who have the same recessive monogenic disease breed, their offspring has a ~100% probability of having the disease. If someone with a recessive monogenic disease breeds with a carrier, their offspring has a ~50% probability of having the disease. If two carriers of a recessive monogenic disease breed, their offspring has a ~25% probability of having the disease. If neither people are carriers, or only one person is a carrier and the other is not, or only one person has the disease and the other is not a carrier, their offspring has a ~0% probability of having the disease. + +In order to prevent people with recessive monogenic diseases from being conceived, we must prevent people who have any defects in genes for the same diseases from breeding with each other. This practice only requires reducing each person's pool of potential mates by a small amount (~5% in 2024), but will result in a drastic reduction in the prevalence of recessive monogenic disorders within the human species. + +A Person analysis describes a person’s probability of having each monogenic disease and their probability of passing a disease variant for each disease. A Couple analysis will report on the offspring's probability of having each monogenic disease. Users can share their monogenic disease probabilities on their profiles, and users can filter and sort users based on their offspring's probability of having a monogenic disease. + +Users have 2 options for filtering their offspring's monogenic disease probability: 0% and <100%. + +Selecting 0% will only show the user potential mates with whom the user's offspring has a 0% probability of having any monogenic diseases. This option will filter all potential mates who have defects in the same recessive monogenic disease-causing genes as the user. The 0% option will also filter all users with dominant monogenic diseases, because those users always have a ~50% or greater probability of passing their dominant monogenic disease to their offspring. The 0% option should be selected by users who do not want to use embryo screening for reproduction. + +Selecting <100% will only show the user potential mates with whom the user's offspring has a <100% probability of having any monogenic diseases. This option will filter potential mates who have the same recessive monogenic diseases as the user. This option will also filter any users who have a double dominant monogenic disease, because all offspring produced by these individuals have a ~100% probability of being diseased. The <100% filter could be useful for users who plan to use embryo screening, and only need to have the capability of producing disease-free offspring with their mate. It is still better to avoid these kinds of relationships, because both people could accidentally conceive diseased offspring without using embryo screening. + +### Polygenic Diseases + +Polygenic diseases are diseases whose risk is influenced by many genes. + +A Person analysis describes a person's risk score for each polygenic disease. A Couple analysis describes a prospective offspring's average risk score for each polygenic disease. Users can share their allele values for genes which influence each disease's risk on their profile. The Seekia app is able to calculate genetic outcome probabilities for each user's offspring. Users can sort potential mates by their offspring's disease risk scores. Seekia enables users to mate with other users with whom their offspring has a lower probability of having polygenic diseases. + +Seekia allows for a user's polygenic disease risk to influence their sexual market value. For users who share their polygenic disease allele values, their disease risk is calculable from their profile. Users can sort their matches by each match's total polygenic disease risk score. Users who are more likely to be healthy will be more sought after. Users with a higher risk of dying from various diseases may choose to mate with each other. Users with a higher risk of cognitive decline in their old age may choose to mate with users who do not have an elevated risk, increasing the probability that the user's mate will be able to care for them in their old age. + +### Traits + +A Person analysis contains a person's trait outcome scores, and a Couple analysis contains the offspring's trait outcome scores. Users can share the allele values for locations in their genome which influence each trait in their profiles. The Seekia app can calculate the offspring outcomes for each user. Seekia allows users to filter and sort other users based on their offspring's trait outcome probabilities. + +A user could sort users based on the probability of their offspring being able to tolerate lactose. A user who enjoys cooking lactose-based meals could use this technology to maximize the probability that their offspring will be able to tolerate those foods in adulthood. A user could also try to maximize the probability of their offspring having a certain hair texture or eye color. + +In summary, the genetic matchmaking technology within Seekia is a major improvement to the human mating experience. These features can be used in conjunction with the genetic screening of embryos to maximize each user's ability to increase the health of their offspring and to choose the traits of their offspring which they desire. + +## Non-Profit + +A common criticism of for-profit mate discovery services is that they have a perverse incentive to extract money from their users. Critics claim that a profit incentive may motivate these services to keep users as customers by preventing them from finding a long term mate and instead encouraging users to go on many fruitless dates with incompatible people. + +No entities profit directly from Seekia users. The only reason why participating in the Seekia network costs money is to prevent spam and bad behavior. All spent cryptocurrency funds are destroyed. Requiring spent funds to be burned discourages bad actors from attacking the network, because it is impossible for them to recover any funds used in their attacks. Burning funds also reduces any incentive to keep costs high because there are no direct financial beneficiaries. + +Hosts and moderators are not financially rewarded by the network protocol. Most hosts will hopefully be volunteers and non-profit institutions who are altruistically motivated. Moderators could be funded by donations, companies, non-profit institutions, or decentralized autonomous organizations. + +## Spam Prevention + +Without any form of spam prevention, a single malicious actor could spam the Seekia network with billions of fake profiles and messages, rendering the network inoperative. + +Seekia requires users to fund their identities before broadcasting content to the network. Users must also fund each message, report, and mate profile. + +In a fully decentralized model, users would use a cryptocurrency to fund each identity and piece of content. Cryptocurrency addresses would be derived from identity and content hashes. This approach requires using a decentralized cryptocurrency which can support tens of thousands of privacy-preserving transactions per second. I am not aware of any cryptocurrency which can support the necessary throughput, so a centralized accounting model is used instead. + +### Account Credit + +A centralized account credit database is used to facilitate the funding of content and identities on the Seekia network. + +Each account has a credit balance. An account is represented by a public/private key pair. Users must possess an account's private key to view and spend its balance. Credit can be purchased with cryptocurrency. Users can send credit from one account to another. + +The database is trusted to not log user behavior. If a snapshot of the database were ever leaked, sensitive information such as the senders of messages would not be revealed. + +Using a central database allows for admins to freely create and distribute credit. Admins are able to onboard users to Seekia for free by sending them credit. A website could be created that allows users to receive credit by verifying ownership of a phone number. Phone numbers are costly for attackers to obtain. + +The account credit database is a single point of failure which the network relies upon. Creating backups of the database is prudent. If the database ever goes offline, hosts will continue to serve any content which has already been funded until the content expires from the network. + +## Messaging + +Seekia provides a messaging system for users to communicate privately. + +Each message must be funded. The cost of funding a message is determined by its size and desired duration. + +Users can filter and sort the users who they are communicating with. For example, a user can filter their conversations to only show users who fulfill their desires, and sort their conversations by the distance of the conversator. + +### Message Inboxes + +Every Seekia message contains a publicly viewable 10 byte value called an Inbox. All other sensitive elements of each message such as the sender, recipient, and communication are encrypted. + +There are two types of message inboxes: Public and Secret. + +A user's public inbox is created by hashing their identity hash. Everyone can see the quantity and size of messages in each user's public inbox. Public inboxes sacrifice privacy but increase sync speed. A more private method would require users to download information about messages which were not sent to them, increasing bandwidth and message latency. A way to mitigate the public inbox privacy flaw is to create services which send fake messages to users in an attempt to equalize the quantity of messages in each user's public inbox. + +If Seekia only used public inboxes, it would be possible to determine which users were chatting with each other by analyzing message sent times and metadata about users. For example, if two users who live near each other are both receiving messages around the same time, it is possible to guess that they are communicating. + +Secret Inboxes are used to resist this kind of analysis. Each message contains 2 secret inboxes which belong to the sender: Current and Next. The recipient sends future messages to the sender's secret inboxes rather than their public inbox. Each secret inbox corresponds to a secret inbox epoch. Users send each message to the secret inbox which corresponds to the epoch of the message's sent time. Identical epoch start and end times are used by all message senders. Even if an attacker were able to correlate pairs of communicating secret inboxes, all active secret inboxes change at the same time, facilitating a mixing effect for all secret inbox pairs. + +## Encryption + +All chat messages and network communications are encrypted with Nacl and Kyber. Kyber is a quantum-resistant encryption method. Seekia plans to utilize more encryption methods in the future. Using many encryption methods protects against future data decryption in the event that utilized cryptography is broken. If all encryption methods used to seal messages were broken, previously broadcasted messages would be decryptable. Expired messages may be stored by malicious entities who are preparing for this possibility. + +Seekia users share their chat encryption keys on their profiles. A user's chat keys are generated locally on their machine, and are periodically replaced with new keys. Old keys are automatically deleted, and users can choose to delete old messages. After deleting a broadcasted message's private chat keys and unencrypted contents, the message cannot be decrypted if the sender or recipient's machines or seed phrases are compromised. + +## Statistics + +Seekia provides users with the ability to view their Desire Statistics. Desire statistics describe the number and percentage of users who are being filtered by a user’s desires. For example, if a user’s total match percentage is 5%, it means that 5% of the newest Mate user profiles they have downloaded pass all of their desires. + +Desire statistics also describe each desire’s filtration statistics. For each desire, the user can see how many users the desire is filtering, what percentage of users pass the desire, the number of matches a user would have if they disabled the desire, and what percentage of users would be a match if they disabled the desire. + +Seekia provides graphing functionality which gives everyone the ability to view statistics about users. For example, the app can display a chart plotting Age on the X-axis and Average Wealth on the Y-axis. User statistics graphing enables anyone to learn more about the demographics of Seekia users, and can inform users about how they should alter their desires to increase their match percentage. + +The Seekia network can act as a public census resource. Seekia's userbase can help humanity to track the geographic locations of the world's races, helping to inform people about where they should move to be closer to members of their favorite races. User profiles will be available to all researchers around the world, enabling interesting and valuable new insights about humanity to be discovered. Harvesting and analyzing user data is not the primary purpose of Seekia. People are more likely to lie on a mating profile than a research survey. Researchers should conduct their own studies to collect higher quality data. + +## Moderation + +Seekia has a transparent and decentralized moderation system. + +Fair moderation systems are vital to ensure that mate discovery technologies are genetically impartial. Mate discovery services have historically banned users to harm the happiness and reproductive capability of their ideological enemies. + +Anyone can participate as a moderator. Moderators create reviews of identities, profiles, and messages. + +Each identity, profile, and message has a consensus verdict. Profiles and messages can be Approved, Banned, or Undecided. Identities can be Banned or Not Banned. Identities cannot be approved, because their behavior can always become unruleful. + +The moderators who have approved or banned each identity, profile, and message are publicly viewable, along with their reasons for doing so. + +### Identity Scores + +Each moderator has an identity score which determines their power. An identity score is the sum of all cryptocurrency sent to a moderator's identity score cryptocurrency addresses, valued in gold at time sent. Each address is derived from the moderator's identity hash and thus has no known private key. Funds sent to these addresses are destroyed forever. Anyone can destroy cryptocurrency to increase the identity score of moderators who they trust. + +A moderator's rank is calculated by sorting all moderators in the order of their identity scores. Moderators can ban moderators who are below them in rank. The verdict of a piece of content is calculated by summing and comparing the identity scores of the content's approve and ban advocates. + +Identity scores provide many advantages. All moderators must spend funds to participate, increasing the barrier to entry for malicious moderators. Moderators are less able to increase their power by creating many moderator identities. Malicious moderators will suffer financially because they must fund new identities after being banned. + +Relying on identity scores to settle disputes encourages cooperation between moderators. When moderators disagree, rather than leapfrogging each other by funding their own identity scores and banning each other, they are incentivized to resolve their differences or to recruit other higher ranked moderators to ban their opponents. + +### Supermoderators + +Supermoderators are a set of moderators chosen by the network admins. Supermoderators have the absolute authority to ban non-supermoderators. Supermoderators are ranked and possess the ability to ban supermoderators below them in rank. + +Supermoderators are a safeguard against attacks on the moderation system by malicious moderators. In the scenario where a moderator funded their identity enough to become the top ranked moderator and banned all other moderators, another moderator would have to fund their identity enough to gain the ability to ban this malicious moderator. Supermoderators are able to ban these malicious moderators without spending any funds. + +### Sticky Viewable Statuses + +Each identity, profile, and message consensus verdict is always able to change. A verdict may be unjust for a period of time. For example, a malicious moderator could fund their identity to a high rank, ban all moderators below them, and ban all content on the network. This moderator could cause ruleful content on the network to have a Banned verdict. To undo the damage, a higher-ranked moderator or supermoderator must ban this malicious moderator to restore order to the network. This could take a while, depending on how highly ranked the malicious moderator is. + +Sticky viewable statuses are used to solve the problem of verdict variability. Each identity, profile, and message has a sticky viewable status which is determined by the percentage of its viewable verdicts for a defined time period. A viewable verdict is a verdict which an identity or piece of content must possess to be visible to regular users. To be considered viewable, Mate profiles must be Approved, whereas Host and Moderator profiles can be Undecided or Approved. + +Sticky viewable statuses become stuck and impervious to temporary changes to real-time verdicts. Sticky statuses allow content to remain viewable and served to users during attacks on the network by malicious moderators. + +### Attribute Reviews + +When reviewing profiles, moderators can submit Profile reviews or Attribute reviews. An attribute review is a review of a specific attribute within a profile. + +Attribute reviews provide several advantages. Moderators can specify the attribute which motivated them to ban a profile. Moderators do not have to approve all attributes of a profile again if the user resubmits their profile with 1 attribute changed. Moderators would only have to approve the single changed attribute if they had already approved all of the profile's other attributes. + +Moderators have the flexibility to choose which attributes they want to review. A moderator can choose to only review images and still contribute to the network. Within the Seekia app, moderators choose the attribute they want to review and cycle through the attribute value for each user profile. This functionality reduces the cognitive load of context switching and increases moderator efficiency. + +### Reporting Content + +Users can report identities, profiles, and messages. Reports are created anonymously and have no public author. Each report must be funded to prevent spam. + +To report a message, a user includes the decryption key for the message in the report. Moderators use this key to decrypt and view the message. The decryption key is included in each message review, which functions as a proof that the reviewer has seen the contents of the message. + +## Conclusion + +The goal of Seekia is to accelerate humanity's adoption of race and genetics aware mate discovery technology. + +Informing human mating choices with racial and genetic information will have a major positive impact on the human species. Seekia aims to usher in a new era of human breeding strategies. Seekia aims to bring genetic order to humanity's breeding patterns. + +Seekia aims to cure racial loneliness, beautify the human species, reduce the prevalence of genetic diseases, increase humanity's intelligence, and boost global fertility rates. + +Seekia has the potential to create families, facilitate the conception of beautiful and healthy offspring, and increase the amount of love and happiness in the world. + +Join me in my effort to change the world and build a better future for all. + +The genetic future of our species is at stake. + +## Learn More + +Access Seekia's website from these domains: **Seekia.eth** or **Seekia.net** + +Access Simon Sarasova's website: SimonSarasova.eth + +Research online for instructions on how to access .eth IPFS websites. + +These domains may have been seized or lost by the time you are reading this. You can only trust that content is authored by me if it contains my digital signature. You can verify that Seekia memos are signed with my identity hash by using the Seekia application. + +Simon's Sarasova's identity hash is: simonx5yudleks5jhwhnck5s28m diff --git a/documentation/Whitepaper.pdf b/documentation/Whitepaper.pdf new file mode 100644 index 0000000..84a9ab7 Binary files /dev/null and b/documentation/Whitepaper.pdf differ diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..c98e656 --- /dev/null +++ b/go.mod @@ -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 +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..3d93129 --- /dev/null +++ b/go.sum @@ -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= diff --git a/gui/accountCreditGui.go b/gui/accountCreditGui.go new file mode 100644 index 0000000..0164ad5 --- /dev/null +++ b/gui/accountCreditGui.go @@ -0,0 +1,1092 @@ +package gui + +// accountCreditGui.go implements pages to view and increase a user's account credit, and to spend credit to increase their identity balance + +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/dialog" + +import "seekia/resources/currencies" + +import "seekia/internal/appMemory" +import "seekia/internal/convertCurrencies" +import "seekia/internal/globalSettings" +import "seekia/internal/helpers" +import "seekia/internal/network/appNetworkType/getAppNetworkType" +import "seekia/internal/network/myAccountCredit" +import "seekia/internal/parameters/getParameters" + +import "errors" +import "time" + +func setViewMyAccountCreditPage(window fyne.Window, myIdentityType string, previousPage func()){ + + currentPage := func(){setViewMyAccountCreditPage(window, myIdentityType, previousPage)} + + appMemory.SetMemoryEntry("CurrentViewedPage", "AccountCredit") + + title := getPageTitleCentered("My " + myIdentityType + " Account Credit") + + backButton := getBackButtonCentered(previousPage) + + description1 := getLabelCentered("Broadcasting to the Seekia network requires credit.") + description2 := getLabelCentered("You can be gifted credit to your account by sharing an account identifier.") + description3 := getLabelCentered("You can also send Ethereum or Cardano to add to your account credit.") + + getMyCreditBalanceSection := func()(*fyne.Container, error){ + + appNetworkType, err := getAppNetworkType.GetAppNetworkType() + if (err != nil) { return nil, err } + + myCreditBalanceLabel := getItalicLabelCentered("My Credit Balance:") + + getCurrentAppCurrency := func()(string, error){ + exists, currentAppCurrency, err := globalSettings.GetSetting("Currency") + if (err != nil) { return "", err } + if (exists == false){ + return "USD", nil + } + return currentAppCurrency, nil + } + + currentAppCurrencyCode, err := getCurrentAppCurrency() + if (err != nil){ return nil, err } + + _, appCurrencySymbol, err := currencies.GetCurrencyInfoFromCurrencyCode(currentAppCurrencyCode) + if (err != nil) { return nil, err } + + appCurrencySymbolButton := widget.NewButton(appCurrencySymbol, func(){ + setChangeAppCurrencyPage(window, currentPage) + }) + + parametersExist, appCurrencyAccountCreditBalance, err := myAccountCredit.GetMyCreditAccountBalanceInAnyCurrency(myIdentityType, appNetworkType, currentAppCurrencyCode) + if (err != nil) { return nil, err } + + getAccountBalanceText := func()(string, error){ + + if (parametersExist == false){ + return "Unknown", nil + } + + appCurrencyAccountCreditBalanceString := helpers.ConvertFloat64ToStringRounded(appCurrencyAccountCreditBalance, 3) + + return appCurrencyAccountCreditBalanceString, nil + } + + accountBalanceText, err := getAccountBalanceText() + if (err != nil) { return nil, err } + + appCurrencyAccountCreditBalanceLabel := getBoldLabel(accountBalanceText + " " + currentAppCurrencyCode) + + myCreditBalanceRow := container.NewHBox(layout.NewSpacer(), appCurrencySymbolButton, appCurrencyAccountCreditBalanceLabel, layout.NewSpacer()) + + currentBalanceRefreshButton := getWidgetCentered(widget.NewButtonWithIcon("Refresh", theme.ViewRefreshIcon(), func(){ + + if (parametersExist == false){ + + dialogTitle := translate("Parameters Missing.") + dialogMessageA := getLabelCentered(translate("The network parameters are not downloaded.")) + dialogMessageB := getLabelCentered(translate("You must wait for them to download to view your balance.")) + + //TODO: View progress button + + dialogContent := container.NewVBox(dialogMessageA, dialogMessageB) + dialog.ShowCustom(dialogTitle, translate("Close"), dialogContent, window) + return + } + + //TODO + + showUnderConstructionDialog(window) + })) + + myCreditBalanceSection := container.NewVBox(myCreditBalanceLabel, myCreditBalanceRow, currentBalanceRefreshButton) + + return myCreditBalanceSection, nil + } + + myCreditBalanceSection, err := getMyCreditBalanceSection() + if (err != nil) { + setErrorEncounteredPage(window, err, previousPage) + return + } + + viewAccountIdentifierButton := getWidgetCentered(widget.NewButtonWithIcon("View Account Identifier", theme.VisibilityIcon(), func(){ + setAddAccountCreditWithAccountIdentifierPage(window, myIdentityType, currentPage) + })) + + ethereumIcon, err := getFyneImageIcon("Ethereum") + if (err != nil){ + setErrorEncounteredPage(window, err, previousPage) + return + } + iconSize := getCustomFyneSize(0) + ethereumIcon.SetMinSize(iconSize) + + addFundsButton_Ethereum := widget.NewButton("Send Ethereum", func(){ + nextPage := func(){setAddAccountCreditWithCryptocurrencyPage(window, myIdentityType, "Ethereum", currentPage)} + + setBuyAccountCreditCryptoPrivacyWarningPage(window, currentPage, nextPage) + }) + + addFundsButtonWithIcon_Ethereum := container.NewGridWithColumns(1, ethereumIcon, addFundsButton_Ethereum) + + cardanoIcon, err := getFyneImageIcon("Cardano") + if (err != nil){ + setErrorEncounteredPage(window, err, previousPage) + return + } + cardanoIcon.SetMinSize(iconSize) + + addFundsButton_Cardano := widget.NewButton("Send Cardano", func(){ + nextPage := func(){setAddAccountCreditWithCryptocurrencyPage(window, myIdentityType, "Cardano", currentPage)} + + setBuyAccountCreditCryptoPrivacyWarningPage(window, currentPage, nextPage) + }) + + addFundsButtonWithIcon_Cardano := container.NewGridWithColumns(1, cardanoIcon, addFundsButton_Cardano) + + viewPricingButton := getWidgetCentered(widget.NewButtonWithIcon("View Pricing", theme.VisibilityIcon(), func(){ + setViewSeekiaPricingPage(window, currentPage) + })) + + addFundsButtonsGrid := getContainerCentered(container.NewGridWithColumns(2, addFundsButtonWithIcon_Ethereum, addFundsButtonWithIcon_Cardano)) + + page := container.NewVBox(title, backButton, widget.NewSeparator(), description1, description2, description3, widget.NewSeparator(), myCreditBalanceSection, widget.NewSeparator(), viewAccountIdentifierButton, widget.NewSeparator(), addFundsButtonsGrid, widget.NewSeparator(), viewPricingButton) + + setPageContent(page, window) +} + +func setAddAccountCreditWithAccountIdentifierPage(window fyne.Window, myIdentityType string, previousPage func()){ + + title := getPageTitleCentered("Add Account Credit") + + backButton := getBackButtonCentered(previousPage) + + description1 := getLabelCentered("Use this identifier to receive account credit.") + + appNetworkType, err := getAppNetworkType.GetAppNetworkType() + if (err != nil) { + setErrorEncounteredPage(window, err, previousPage) + return + } + + viewUsedIdentifiersButton := getWidgetCentered(widget.NewButtonWithIcon("View Used Identifiers", theme.HistoryIcon(), func(){ + //TODO + showUnderConstructionDialog(window) + })) + + identityExists, unusedAccountIdentifier, err := myAccountCredit.GetAnUnusedCreditAccountIdentifier(myIdentityType, appNetworkType) + if (err != nil){ + setErrorEncounteredPage(window, err, previousPage) + return + } + if (identityExists == false){ + setErrorEncounteredPage(window, errors.New("setAddAccountCreditWithAccountIdentifierPage called when identity is missing"), previousPage) + return + } + + myIdentifierLabel := getBoldLabelCentered("My Account Identifier:") + + identifierEntry := widget.NewEntry() + identifierEntry.SetText(unusedAccountIdentifier) + identifierEntry.OnChanged = func(_ string){ + identifierEntry.SetText(unusedAccountIdentifier) + } + identifierEntryBoxed := getWidgetBoxed(identifierEntry) + + entryWidener := widget.NewLabel(" ") + + entryWidened := getContainerCentered(container.NewGridWithColumns(1, identifierEntryBoxed, entryWidener)) + + page := container.NewVBox(title, backButton, widget.NewSeparator(), description1, widget.NewSeparator(), viewUsedIdentifiersButton, widget.NewSeparator(), myIdentifierLabel, entryWidened) + + setPageContent(page, window) +} + + +func setBuyAccountCreditCryptoPrivacyWarningPage(window fyne.Window, previousPage func(), nextPage func()){ + + title := getPageTitleCentered("Privacy Warning") + + backButton := getBackButtonCentered(previousPage) + + description1 := getLabelCentered("Be aware that your privacy is at risk when using cryptocurrencies.") + description2 := getLabelCentered("When you send money from your wallet, it is possible to guess that you are a user of Seekia.") + description3 := getLabelCentered("There is also a risk of someone linking your wallet to your Seekia identity.") + description4 := getBoldLabelCentered("To protect yourself, wait before funding your Seekia identity.") + description5 := getLabelCentered("For example, after funding your account, wait a few days before funding your identity.") + description6 := getLabelCentered("This will break the link between your transaction and your Seekia profile/identity.") + description7 := getLabelCentered("If you have a large amount of cryptocurrency, you should be wary of sending your funds from that wallet.") + description8 := getLabelCentered("For privacy, you should send funds directly from an exchange.") + description9 := getLabelCentered("You can also use a privacy tool such as a zero knowledge accumulator for stronger protection.") + + description10 := getBoldLabelCentered("Do you understand the privacy risks?") + + iUnderstandButton := getWidgetCentered(widget.NewButtonWithIcon("I Understand", theme.ConfirmIcon(), nextPage)) + + page := container.NewVBox(title, backButton, widget.NewSeparator(), description1, description2, description3, widget.NewSeparator(), description4, description5, description6, widget.NewSeparator(), description7, description8, description9, widget.NewSeparator(), description10, iUnderstandButton) + + setPageContent(page, window) +} + +func setAddAccountCreditWithCryptocurrencyPage(window fyne.Window, myIdentityType string, cryptocurrency string, previousPage func()){ + + setLoadingScreen(window, "Add " + myIdentityType + " Account Credit", "Loading Add Account Credit page...") + + if (cryptocurrency != "Ethereum" && cryptocurrency != "Cardano"){ + setErrorEncounteredPage(window, errors.New("Invalid cryptocurrency: " + cryptocurrency), previousPage) + return + } + + currentPage := func(){setAddAccountCreditWithCryptocurrencyPage(window, myIdentityType, cryptocurrency, previousPage)} + + title := getPageTitleCentered("Add " + myIdentityType + " Account Credit") + + backButton := getBackButtonCentered(previousPage) + + viewUsedAddressesButton := getWidgetCentered(widget.NewButtonWithIcon("View Used Addresses", theme.HistoryIcon(), func(){ + //TODO + showUnderConstructionDialog(window) + })) + + appNetworkType, err := getAppNetworkType.GetAppNetworkType() + if (err != nil) { + setErrorEncounteredPage(window, err, previousPage) + return + } + + myIdentityExists, myFundsAddress, err := myAccountCredit.GetAnUnusedCreditAccountCryptoAddress(myIdentityType, appNetworkType, cryptocurrency) + if (err != nil){ + setErrorEncounteredPage(window, err, previousPage) + return + } + if (myIdentityExists == false){ + + description1 := getBoldLabelCentered("Your " + myIdentityType + " identity does not exist.") + descriptionB := getLabelCentered("Create your identity?.") + + createIdentityButton := getWidgetCentered(widget.NewButtonWithIcon("Create Identity", theme.NavigateNextIcon(), func(){ + setChooseNewIdentityHashPage(window, myIdentityType, currentPage, currentPage) + })) + + page := container.NewVBox(title, backButton, widget.NewSeparator(), description1, descriptionB, createIdentityButton) + + setPageContent(page, window) + return + } + + description1 := getLabelCentered("Send " + cryptocurrency + " to increase your account credit.") + descriptionB := getBoldLabelCentered("All funds sent will be destroyed forever.") + + cryptoAddressLabelRow, err := getCryptocurrencyAddressLabelWithCopyAndQRButtons(window, cryptocurrency, myFundsAddress, currentPage) + if (err != nil) { + setErrorEncounteredPage(window, err, previousPage) + return + } + + estimateCostsDescription := getLabelCentered("Use the page below to determine how much you should send.") + + viewPricingButton := getWidgetCentered(widget.NewButtonWithIcon("View Pricing", theme.VisibilityIcon(), func(){ + setViewSeekiaPricingPage(window, currentPage) + })) + + page := container.NewVBox(title, backButton, widget.NewSeparator(), description1, descriptionB, widget.NewSeparator(), viewUsedAddressesButton, widget.NewSeparator(), cryptoAddressLabelRow, widget.NewSeparator(), estimateCostsDescription, viewPricingButton) + + setPageContent(page, window) + return +} + +// This page is used to show how much it costs to fund broadcasts on the Seekia network +func setViewSeekiaPricingPage(window fyne.Window, previousPage func()){ + + currentPage := func(){setViewSeekiaPricingPage(window, previousPage)} + + title := getPageTitleCentered("Seekia Pricing (Under Construction)") + + backButton := getBackButtonCentered(previousPage) + + description := getLabelCentered("Below are the estimated prices to broadcast content on the Seekia network.") + + getAppCurrency := func()(string, error){ + + exists, appCurrencyCode, err := globalSettings.GetSetting("Currency") + if (err != nil) { return "", err } + if (exists == false){ + return "USD", nil + } + return appCurrencyCode, nil + } + appCurrencyCode, err := getAppCurrency() + if (err != nil) { + setErrorEncounteredPage(window, err, previousPage) + return + } + + currentCurrencyLabel := getBoldLabelCentered("Current Currency:") + + _, appCurrencySymbol, err := currencies.GetCurrencyInfoFromCurrencyCode(appCurrencyCode) + if (err != nil) { + setErrorEncounteredPage(window, err, previousPage) + return + } + + chooseCurrencyButton := widget.NewButton(appCurrencySymbol + appCurrencyCode, func(){ + setChangeAppCurrencyPage(window, currentPage) + }) + + currentCurrencyRow := container.NewHBox(layout.NewSpacer(), currentCurrencyLabel, chooseCurrencyButton, layout.NewSpacer()) + + actionNameLabel := getItalicLabelCentered("Action Name") + costLabel := getItalicLabelCentered("Cost") + + actionNameColumn := container.NewVBox(actionNameLabel, widget.NewSeparator()) + costColumn := container.NewVBox(costLabel, widget.NewSeparator()) + + appNetworkType, err := getAppNetworkType.GetAppNetworkType() + if (err != nil) { + setErrorEncounteredPage(window, err, previousPage) + return + } + + //Outputs: + // -bool: Parameters exist + // -error + addActionRows := func()(bool, error){ + + exchangeRatesAreDownloaded, err := convertCurrencies.CheckIfExchangeRatesAreDownloaded(appNetworkType) + if (err != nil){ return false, err } + if (exchangeRatesAreDownloaded == false){ + return false, nil + } + + //Outputs: + // -bool: Exchange rates exist + // -error + addActionRow := func(actionName string, costInGramsOfGold float64)(bool, error){ + + costInKilogramsOfGold := costInGramsOfGold/1000 + + exchangeRateExists, appCurrencyCostFloat64, err := convertCurrencies.ConvertKilogramsOfGoldToAnyCurrency(appNetworkType, costInKilogramsOfGold, appCurrencyCode) + if (err != nil) { return false, err } + if (exchangeRateExists == false) { + return false, nil + } + + appCurrencyCostString := helpers.ConvertFloat64ToStringRounded(appCurrencyCostFloat64, 3) + + appCurrencyCostFormatted := appCurrencySymbol + " " + appCurrencyCostString + " " + appCurrencyCode + + actionNameText := getBoldLabelCentered(actionName) + + currencyCostLabel := getBoldLabelCentered(appCurrencyCostFormatted) + + actionNameColumn.Add(actionNameText) + costColumn.Add(currencyCostLabel) + + actionNameColumn.Add(widget.NewSeparator()) + costColumn.Add(widget.NewSeparator()) + + return true, nil + } + + currentTime := time.Now().Unix() + + parametersExist, messageKilobyteGoldCostPerDay, err := getParameters.GetMessageKilobyteGoldCostPerDay(appNetworkType, currentTime) + if (err != nil) { return false, err } + if (parametersExist == false){ + return false, nil + } + + //TODO: Fix below numbers + textMessageSizeKilobytes := float64(2) + imageMessageSizeKilobytes := float64(22) + + textMessageGramsOfGoldCost := messageKilobyteGoldCostPerDay * textMessageSizeKilobytes * 14 + imageMessageGramsOfGoldCost := messageKilobyteGoldCostPerDay * imageMessageSizeKilobytes * 14 + + parametersExist, err = addActionRow("Send 100 Text Messages", textMessageGramsOfGoldCost * 100) + if (err != nil) { return false, err } + if (parametersExist == false){ + return false, nil + } + + parametersExist, err = addActionRow("Send 100 Image Messages", imageMessageGramsOfGoldCost * 100) + if (err != nil) { return false, err } + if (parametersExist == false){ + return false, nil + } + + parametersExist, mateIdentityGoldCostPerDay, err := getParameters.GetIdentityBalanceGoldCostPerDay(appNetworkType, "Mate", currentTime) + if (err != nil) { return false, err } + if (parametersExist == false){ + return false, nil + } + + parametersExist, err = addActionRow("Fund Mate Identity for 3 Months", mateIdentityGoldCostPerDay * 90) + if (err != nil) { return false, err } + if (parametersExist == false){ + return false, nil + } + + parametersExist, hostIdentityGoldCostPerDay, err := getParameters.GetIdentityBalanceGoldCostPerDay(appNetworkType, "Host", currentTime) + if (err != nil) { return false, err } + if (parametersExist == false){ + return false, nil + } + + parametersExist, err = addActionRow("Fund Host Identity for 3 Months", hostIdentityGoldCostPerDay * 90) + if (err != nil) { return false, err } + if (parametersExist == false){ + return false, nil + } + + parametersExist, fundMateProfileCost, err := getParameters.GetFundMateProfileCostInGold(appNetworkType, currentTime) + if (err != nil) { return false, err } + if (parametersExist == false){ + return false, nil + } + parametersExist, err = addActionRow("Broadcast Mate Profile 30 Times", fundMateProfileCost * 30) + if (err != nil) { return false, err } + if (parametersExist == false){ + return false, nil + } + + parametersExist, fundReportCost, err := getParameters.GetFundReportCostInGold(appNetworkType, currentTime) + if (err != nil) { return false, err } + if (parametersExist == false){ + return false, nil + } + + parametersExist, err = addActionRow("Make 10 Reports", fundReportCost * 10) + if (err != nil) { return false, err } + if (parametersExist == false){ + return false, nil + } + + return true, nil + } + + parametersExist, err := addActionRows() + if (err != nil){ + setErrorEncounteredPage(window, err, previousPage) + return + } + if (parametersExist == false){ + description1 := getBoldLabelCentered("Currency exchange rates are not downloaded.") + descriptionB := getLabelCentered("Please wait for them to download to view pricing.") + + //TODO: Add monitor button + refreshButton := getWidgetCentered(widget.NewButtonWithIcon("Refresh", theme.ViewRefreshIcon(), currentPage)) + + page := container.NewVBox(title, backButton, widget.NewSeparator(), description1, descriptionB, refreshButton) + + setPageContent(page, window) + return + } + + pricingGrid := container.NewHBox(layout.NewSpacer(), actionNameColumn, costColumn, layout.NewSpacer()) + + calculateDescription := getLabelCentered("Use the pages below to calculate costs.") + + identityIcon, err := getFyneImageIcon("Profile") + if (err != nil) { + setErrorEncounteredPage(window, err, previousPage) + return + } + + chatIcon, err := getFyneImageIcon("Chat") + if (err != nil) { + setErrorEncounteredPage(window, err, previousPage) + return + } + + identityCostsButton := widget.NewButton("Identity Costs", func(){ + setViewIdentityBalancePriceCalculatorPage(window, "Mate", 30, currentPage) + }) + + chatCostsButton := widget.NewButton("Chat Costs", func(){ + setViewMessagePricingCalculatorPage(window, "Image", 14, 100, currentPage) + }) + + identityCostsButtonWithIcon := container.NewGridWithColumns(1, identityIcon, identityCostsButton) + chatCostsButtonWithIcon := container.NewGridWithColumns(1, chatIcon, chatCostsButton) + + buttonsGrid := getContainerCentered(container.NewGridWithRows(1, identityCostsButtonWithIcon, chatCostsButtonWithIcon)) + + page := container.NewVBox(title, backButton, widget.NewSeparator(), description, widget.NewSeparator(), currentCurrencyRow, widget.NewSeparator(), pricingGrid, widget.NewSeparator(), calculateDescription, buttonsGrid) + + setPageContent(page, window) +} + +// This page is used to show how much it costs to fund a user on the Seekia network +// Users use this to determine how much money to send to their account +func setViewIdentityBalancePriceCalculatorPage(window fyne.Window, identityType string, daysToFund int, previousPage func()){ + + currentPage := func(){setViewIdentityBalancePriceCalculatorPage(window, identityType, daysToFund, previousPage)} + + if (identityType != "Mate" && identityType != "Host"){ + setErrorEncounteredPage(window, errors.New("setViewIdentityBalancePriceCalculatorPage called with invalid identityType: " + identityType), previousPage) + return + } + + title := getPageTitleCentered("Identity Balance Pricing") + + backButton := getBackButtonCentered(previousPage) + + subtitle := getPageSubtitleCentered("Calculate Identity Balance Pricing") + + appNetworkType, err := getAppNetworkType.GetAppNetworkType() + if (err != nil) { + setErrorEncounteredPage(window, err, previousPage) + return + } + + //Outputs: + // -bool: Parameters exist + // -*fyne.Container: Calculator section + // -error + getCalculatorContainer := func()(bool, *fyne.Container, error){ + + currentTime := time.Now().Unix() + + exchangeRatesAreDownloaded, err := convertCurrencies.CheckIfExchangeRatesAreDownloaded(appNetworkType) + if (err != nil){ return false, nil, err } + if (exchangeRatesAreDownloaded == false){ + return false, nil, nil + } + + identityTypeLabel := getBoldLabelCentered("Identity Type:") + + identityTypesList := []string{"Mate", "Host"} + + identityTypeSelector := widget.NewSelect(identityTypesList, func(newIdentityType string){ + + if (newIdentityType == identityType){ + return + } + + setViewIdentityBalancePriceCalculatorPage(window, newIdentityType, daysToFund, previousPage) + }) + identityTypeSelector.Selected = identityType + + identityTypeSelectorCentered := getWidgetCentered(identityTypeSelector) + + daysToFundSelectorFunction := func(entry string){ + if (entry == "1 month"){ + if (daysToFund == 30){ + return + } + setViewIdentityBalancePriceCalculatorPage(window, identityType, 30, previousPage) + } + if (entry == "3 months"){ + if (daysToFund == 90){ + return + } + setViewIdentityBalancePriceCalculatorPage(window, identityType, 90, previousPage) + } + if (entry == "6 months"){ + if (daysToFund == 180){ + return + } + setViewIdentityBalancePriceCalculatorPage(window, identityType, 180, previousPage) + } + } + + desiredDurationLabel := getBoldLabelCentered("Desired Duration:") + + daysToFundSelectorOptions := []string{"1 month", "3 months", "6 months"} + + daysToFundSelector := widget.NewSelect(daysToFundSelectorOptions, daysToFundSelectorFunction) + if (daysToFund == 30){ + daysToFundSelector.Selected = "1 month" + } else if (daysToFund == 90){ + daysToFundSelector.Selected = "3 months" + } else if (daysToFund == 180){ + daysToFundSelector.Selected = "6 months" + } + + daysToFundSelectorCentered := getWidgetCentered(daysToFundSelector) + + costLabel := getBoldLabelCentered("Cost:") + + parametersExist, gramsOfGoldCostPerDay, err := getParameters.GetIdentityBalanceGoldCostPerDay(appNetworkType, identityType, currentTime) + if (err != nil) { return false, nil, err } + if (parametersExist == false){ + return false, nil, nil + } + + gramsOfGoldCost := gramsOfGoldCostPerDay * float64(daysToFund) + kilogramsOfGoldCost := gramsOfGoldCost/1000 + + getAppCurrency := func()(string, error){ + + exists, appCurrencyCode, err := globalSettings.GetSetting("Currency") + if (err != nil) { return "", err } + if (exists == false){ + return "USD", nil + } + return appCurrencyCode, nil + } + appCurrencyCode, err := getAppCurrency() + if (err != nil) { return false, nil, err } + + exchangeRateExists, appCurrencyCost, err := convertCurrencies.ConvertKilogramsOfGoldToAnyCurrency(appNetworkType, kilogramsOfGoldCost, appCurrencyCode) + if (err != nil) { return false, nil, err } + if (exchangeRateExists == false) { + return false, nil, nil + } + + appCurrencyCostString := helpers.ConvertFloat64ToStringRounded(appCurrencyCost, 2) + + _, appCurrencySymbol, err := currencies.GetCurrencyInfoFromCurrencyCode(appCurrencyCode) + if (err != nil) { return false, nil, err } + + appCurrencyIconButton := widget.NewButton(appCurrencySymbol, func(){ + setChangeAppCurrencyPage(window, currentPage) + }) + appCurrencyCostLabel := getBoldLabel(appCurrencyCostString + " " + appCurrencyCode) + + appCurrencyRow := container.NewHBox(layout.NewSpacer(), appCurrencyIconButton, appCurrencyCostLabel, layout.NewSpacer()) + + calculatorContainer := container.NewVBox(identityTypeLabel, identityTypeSelectorCentered, widget.NewSeparator(), desiredDurationLabel, daysToFundSelectorCentered, widget.NewSeparator(), costLabel, appCurrencyRow) + + return true, calculatorContainer, nil + } + + parametersExist, calculatorContainer, err := getCalculatorContainer() + if (err != nil) { + setErrorEncounteredPage(window, err, previousPage) + return + } + if (parametersExist == false){ + + description1 := getBoldLabelCentered("Network parameters are not downloaded.") + description2 := getLabelCentered("Please wait for them to download.") + + //TODO: Add page to monitor download progress. + + refreshButton := getWidgetCentered(widget.NewButtonWithIcon("Refresh", theme.ViewRefreshIcon(), currentPage)) + + page := container.NewVBox(title, backButton, widget.NewSeparator(), subtitle, widget.NewSeparator(), description1, description2, refreshButton) + + setPageContent(page, window) + return + } + + description1 := getLabelCentered("Calculate identity balance pricing below.") + description2 := getLabelCentered("All mate and host identities must be funded before broadcasting profiles.") + description3 := getLabelCentered("You can always increase your identity balance later.") + + page := container.NewVBox(title, backButton, widget.NewSeparator(), subtitle, widget.NewSeparator(), description1, description2, description3, widget.NewSeparator(), calculatorContainer) + + setPageContent(page, window) +} + +// This page is used to show how much it costs to fund messages +// Users can use this to determine how much credit to send to their account +func setViewMessagePricingCalculatorPage(window fyne.Window, messageType string, messageDuration int64, numberOfMessages int, previousPage func()){ + + if (messageType != "Image" && messageType != "Text"){ + setErrorEncounteredPage(window, errors.New("setViewMessagePricingCalculatorPage called with invalid messageType: " + messageType), previousPage) + return + } + if (messageDuration != 2 && messageDuration != 14){ + setErrorEncounteredPage(window, errors.New("setViewMessagePricingCalculatorPage called with invalid messageDuration."), previousPage) + return + } + if (numberOfMessages != 10 && numberOfMessages != 100 && numberOfMessages != 1000){ + setErrorEncounteredPage(window, errors.New("setViewMessagePricingCalculatorPage called with invalid numberOfMessages."), previousPage) + return + } + + currentPage := func(){setViewMessagePricingCalculatorPage(window, messageType, messageDuration, numberOfMessages, previousPage)} + + title := getPageTitleCentered("Message Pricing Calculator") + + backButton := getBackButtonCentered(previousPage) + + subtitle := getPageSubtitleCentered("Calculate Message Costs") + + appNetworkType, err := getAppNetworkType.GetAppNetworkType() + if (err != nil) { + setErrorEncounteredPage(window, err, previousPage) + return + } + + //Outputs: + // -bool: Parameters exist + // -*fyne.Container: Calculator section + // -error + getPricingCalculatorSection := func()(bool, *fyne.Container, error){ + + messageTypeLabel := getBoldLabelCentered("Message Type:") + + messageTypesList := []string{"Image", "Text"} + + messageTypeSelector := widget.NewSelect(messageTypesList, func(newMessageType string){ + if (newMessageType == messageType){ + return + } + setViewMessagePricingCalculatorPage(window, newMessageType, messageDuration, numberOfMessages, previousPage) + }) + + messageTypeSelector.Selected = messageType + + messageTypeSelectorCentered := getWidgetCentered(messageTypeSelector) + + messageDurationLabel := getBoldLabelCentered("Message Duration:") + + durationOptionsList := []string{"2 days", "2 weeks"} + + durationSelectFunction := func(newDuration string){ + + if (newDuration == "2 days"){ + if (messageDuration == 2){ + return + } + setViewMessagePricingCalculatorPage(window, messageType, 2, numberOfMessages, previousPage) + return + } + if (newDuration == "2 weeks"){ + if (messageDuration == 14){ + return + } + setViewMessagePricingCalculatorPage(window, messageType, 14, numberOfMessages, previousPage) + return + } + } + + durationSelector := widget.NewSelect(durationOptionsList, durationSelectFunction) + + if (messageDuration == 2){ + durationSelector.Selected = "2 days" + } else if (messageDuration == 14){ + durationSelector.Selected = "2 weeks" + } + + durationSelectorCentered := getWidgetCentered(durationSelector) + + numberOfMessagesLabel := getBoldLabelCentered("Number Of Messages:") + + numberOfMessagesOptionsList := []string{"10 Messages", "100 Messages", "1000 Messages"} + + numberOfMessagesSelector := widget.NewSelect(numberOfMessagesOptionsList, func(newNumberOfMessages string){ + + if (newNumberOfMessages == "10 Messages"){ + + if (numberOfMessages == 10){ + return + } + + setViewMessagePricingCalculatorPage(window, messageType, messageDuration, 10, previousPage) + return + } + if (newNumberOfMessages == "100 Messages"){ + + if (numberOfMessages == 100){ + return + } + setViewMessagePricingCalculatorPage(window, messageType, messageDuration, 100, previousPage) + return + } + if (newNumberOfMessages == "1000 Messages"){ + + if (numberOfMessages == 1000){ + return + } + setViewMessagePricingCalculatorPage(window, messageType, messageDuration, 1000, previousPage) + return + } + }) + + if (numberOfMessages == 10){ + numberOfMessagesSelector.Selected = "10 Messages" + } else if (numberOfMessages == 100){ + numberOfMessagesSelector.Selected = "100 Messages" + } else if (numberOfMessages == 1000){ + numberOfMessagesSelector.Selected = "1000 Messages" + } + + numberOfMessagesSelectorCentered := getWidgetCentered(numberOfMessagesSelector) + + costLabel := getBoldLabelCentered("Cost:") + + currentTime := time.Now().Unix() + + parametersExist, messageKilobyteGoldCostPerDay, err := getParameters.GetMessageKilobyteGoldCostPerDay(appNetworkType, currentTime) + if (err != nil) { return false, nil, err } + if (parametersExist == false){ + return false, nil, nil + } + + getMessageSizeKilobytes := func()int{ + //TODO: Fix below numbers + if (messageType == "Text"){ + return 2 + } + // messageType == "Image" + return 22 + } + + messageSizeKilobytes := getMessageSizeKilobytes() + + gramsOfGoldCost := messageKilobyteGoldCostPerDay * float64(messageSizeKilobytes) * float64(messageDuration) * float64(numberOfMessages) + kilogramsOfGoldCost := gramsOfGoldCost/1000 + + exchangeRatesExist, err := convertCurrencies.CheckIfExchangeRatesAreDownloaded(appNetworkType) + if (err != nil) { return false, nil, err } + if (exchangeRatesExist == false){ + return false, nil, nil + } + + getAppCurrencyCode := func()(string, error){ + + exists, appCurrencyCode, err := globalSettings.GetSetting("Currency") + if (err != nil) { return "", err } + if (exists == false){ + return "USD", nil + } + return appCurrencyCode, nil + } + + appCurrencyCode, err := getAppCurrencyCode() + if (err != nil) { return false, nil, err } + + parametersExist, appCurrencyCost, err := convertCurrencies.ConvertKilogramsOfGoldToAnyCurrency(appNetworkType, kilogramsOfGoldCost, appCurrencyCode) + if (err != nil) { return false, nil, err } + if (parametersExist == false){ + return false, nil, nil + } + + _, appCurrencyUnitsSymbol, err := currencies.GetCurrencyInfoFromCurrencyCode(appCurrencyCode) + if (err != nil) { return false, nil, err } + + appCurrencyCostString := helpers.ConvertFloat64ToStringRounded(appCurrencyCost, 2) + + appCurrencySymbolButton := widget.NewButton(appCurrencyUnitsSymbol, func(){ + setChangeAppCurrencyPage(window, currentPage) + }) + + appCurrencyLabel := getBoldLabel(appCurrencyCostString + " " + appCurrencyCode) + + appCurrencyRow := container.NewHBox(layout.NewSpacer(), appCurrencySymbolButton, appCurrencyLabel, layout.NewSpacer()) + + pricingCalculatorSection := container.NewVBox(messageTypeLabel, messageTypeSelectorCentered, widget.NewSeparator(), messageDurationLabel, durationSelectorCentered, widget.NewSeparator(), numberOfMessagesLabel, numberOfMessagesSelectorCentered, widget.NewSeparator(), costLabel, appCurrencyRow) + + return true, pricingCalculatorSection, nil + } + + parametersExist, pricingCalculatorSection, err := getPricingCalculatorSection() + if (err != nil) { + setErrorEncounteredPage(window, err, previousPage) + return + } + if (parametersExist == false){ + + description1 := getBoldLabelCentered("Network parameters are not downloaded.") + description2 := getLabelCentered("Please wait for them to download.") + + //TODO: Add page to monitor download progress. + + refreshButton := getWidgetCentered(widget.NewButtonWithIcon("Refresh", theme.ViewRefreshIcon(), currentPage)) + + page := container.NewVBox(title, backButton, widget.NewSeparator(), subtitle, widget.NewSeparator(), description1, description2, refreshButton) + + setPageContent(page, window) + return + } + + description := getLabelCentered("Use this calculator to estimate message costs.") + + page := container.NewVBox(title, backButton, widget.NewSeparator(), subtitle, widget.NewSeparator(), description, widget.NewSeparator(), pricingCalculatorSection) + + setPageContent(page, window) +} + +func setIncreaseMyIdentityBalancePage(window fyne.Window, myIdentityType string, daysToFund int, previousPage func()){ + + if (myIdentityType != "Mate" && myIdentityType != "Host"){ + setErrorEncounteredPage(window, errors.New("setIncreaseMyIdentityBalancePage called with invalid myIdentityType: " + myIdentityType), previousPage) + } + + currentPage := func(){setIncreaseMyIdentityBalancePage(window, myIdentityType, daysToFund, previousPage)} + + title := getPageTitleCentered("Increase Identity Balance") + + backButton := getBackButtonCentered(previousPage) + + appNetworkType, err := getAppNetworkType.GetAppNetworkType() + if (err != nil) { + setErrorEncounteredPage(window, err, previousPage) + return + } + + //Outputs: + // -bool: Parameters exist + // -*fyne.Container + // -error + getPageContent := func()(bool, *fyne.Container, error){ + + description := getLabelCentered("Choose the amount of time to add to your identity balance.") + + myCreditBalanceLabel := getItalicLabelCentered("My Credit Balance:") + + getCurrentAppCurrency := func()(string, error){ + exists, currentAppCurrency, err := globalSettings.GetSetting("Currency") + if (err != nil) { return "", err } + if (exists == false){ + return "USD", nil + } + return currentAppCurrency, nil + } + + currentAppCurrencyCode, err := getCurrentAppCurrency() + if (err != nil){ return false, nil, err } + + _, appCurrencySymbol, err := currencies.GetCurrencyInfoFromCurrencyCode(currentAppCurrencyCode) + if (err != nil){ return false, nil, err } + + appCurrencySymbolButton1 := widget.NewButton(appCurrencySymbol, func(){ + setChangeAppCurrencyPage(window, currentPage) + }) + + parametersExist, appCurrencyAccountCreditBalance, err := myAccountCredit.GetMyCreditAccountBalanceInAnyCurrency(myIdentityType, appNetworkType, currentAppCurrencyCode) + if (err != nil){ return false, nil, err } + if (parametersExist == false){ + return false, nil, nil + } + + getAccountBalanceText := func()(string, error){ + + if (parametersExist == false){ + return "Unknown", nil + } + + appCurrencyAccountCreditBalanceString := helpers.ConvertFloat64ToStringRounded(appCurrencyAccountCreditBalance, 3) + + return appCurrencyAccountCreditBalanceString, nil + } + + accountBalanceText, err := getAccountBalanceText() + if (err != nil){ return false, nil, err } + + appCurrencyAccountCreditBalanceLabel := getBoldLabel(accountBalanceText + " " + currentAppCurrencyCode) + + myCreditBalanceRow := container.NewHBox(layout.NewSpacer(), appCurrencySymbolButton1, appCurrencyAccountCreditBalanceLabel, layout.NewSpacer()) + + manageMyBalanceButton := getWidgetCentered(widget.NewButtonWithIcon("Manage", theme.VisibilityIcon(), func(){ + setViewMyAccountCreditPage(window, myIdentityType, currentPage) + })) + + currentTime := time.Now().Unix() + + selectTimeDescription := getLabelCentered("Select the amount of time to fund.") + + //TODO: Make clear that there is a minimum amount that must be initially sent and show that value in the GUI + + timeOptionsList := []string{"1 Day", "1 Week", "1 Month", "3 Months", "6 Months"} + + timeToFundSelector := widget.NewSelect(timeOptionsList, func(response string){ + getNewDaysToFund := func()int{ + + if (response == "1 Day"){ + return 1 + } + if (response == "1 Week"){ + return 7 + } + if (response == "1 Month"){ + return 30 + } + if (response == "3 Months"){ + return 90 + } + // response == "6 Months" + return 180 + } + + newDaysToFund := getNewDaysToFund() + if (newDaysToFund == daysToFund){ + return + } + + setIncreaseMyIdentityBalancePage(window, myIdentityType, newDaysToFund, previousPage) + }) + + if (daysToFund == 1){ + timeToFundSelector.Selected = "1 Day" + } else if (daysToFund == 7){ + timeToFundSelector.Selected = "1 Week" + } else if (daysToFund == 30){ + timeToFundSelector.Selected = "1 Month" + } else if (daysToFund == 90){ + timeToFundSelector.Selected = "3 Months" + } else if (daysToFund == 180){ + timeToFundSelector.Selected = "6 Months" + } + + timeToFundSelectorCentered := getWidgetCentered(timeToFundSelector) + + costLabel := getLabelCentered("Cost:") + + parametersExist, gramsOfGoldCostPerDay, err := getParameters.GetIdentityBalanceGoldCostPerDay(appNetworkType, myIdentityType, currentTime) + if (err != nil){ return false, nil, err } + if (parametersExist == false){ + return false, nil, nil + } + + appCurrencySymbolButton2 := widget.NewButton(appCurrencySymbol, func(){ + setChangeAppCurrencyPage(window, currentPage) + }) + + costInKilogramsOfGold := (gramsOfGoldCostPerDay/1000) * float64(daysToFund) + + parametersExist, appCurrencyCost, err := convertCurrencies.ConvertKilogramsOfGoldToAnyCurrency(appNetworkType, costInKilogramsOfGold, currentAppCurrencyCode) + if (err != nil){ return false, nil, err } + + appCurrencyCostString := helpers.ConvertFloat64ToStringRounded(appCurrencyCost, 3) + + appCurrencyLabel := getBoldLabel(appCurrencyCostString + " " + currentAppCurrencyCode) + + costRow := container.NewHBox(layout.NewSpacer(), appCurrencySymbolButton2, appCurrencyLabel, layout.NewSpacer()) + + confirmButton := getWidgetCentered(widget.NewButtonWithIcon("Confirm Payment", theme.ConfirmIcon(), func(){ + //TODO + showUnderConstructionDialog(window) + })) + + pageContent := container.NewVBox(description, widget.NewSeparator(), myCreditBalanceLabel, myCreditBalanceRow, manageMyBalanceButton, widget.NewSeparator(), selectTimeDescription, timeToFundSelectorCentered, widget.NewSeparator(), costLabel, costRow, widget.NewSeparator(), confirmButton) + + return true, pageContent, nil + } + + parametersExist, pageContent, err := getPageContent() + if (err != nil){ + setErrorEncounteredPage(window, err, previousPage) + return + } + + if (parametersExist == false){ + description1 := getBoldLabelCentered("Seekia is missing the network parameters.") + description2 := getLabelCentered("You must wait for them to download.") + + //TODO: View progress page + + refreshButton := getWidgetCentered(widget.NewButtonWithIcon("Refresh", theme.ViewRefreshIcon(), currentPage)) + + page := container.NewVBox(title, backButton, widget.NewSeparator(), description1, description2, refreshButton) + + setPageContent(page, window) + return + } + + page := container.NewVBox(title, backButton, widget.NewSeparator(), pageContent) + + setPageContent(page, window) +} + + diff --git a/gui/adminGui.go b/gui/adminGui.go new file mode 100644 index 0000000..158e52c --- /dev/null +++ b/gui/adminGui.go @@ -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) +} + + + diff --git a/gui/broadcastGui.go b/gui/broadcastGui.go new file mode 100644 index 0000000..edc7a3a --- /dev/null +++ b/gui/broadcastGui.go @@ -0,0 +1,1827 @@ +package gui + +//broadcastGui.go implements pages for a user to broadcast, disable, and enable their profiles, and to monitor manual broadcasts + +//TODO: Add review replacement functionality +// When broadcasting a review, check if the existing conflicting review exists for the same reviewedhash +// If the review is reviewing an identity that the user has already reviewed and broadcast, we should show this in the GUI +// The user should confirm to overwrite their previous verdict +// Once they agree, the application should delete their old broadcasted review +// We should also add the ability to update existing identity reviews with more errant profiles, if more unruleful profiles are discovered + +import "fyne.io/fyne/v2" +import "fyne.io/fyne/v2/canvas" +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/resources/currencies" + +import "seekia/internal/appMemory" +import "seekia/internal/encoding" +import "seekia/internal/genetics/myPeople" +import "seekia/internal/globalSettings" +import "seekia/internal/helpers" +import "seekia/internal/imagery" +import "seekia/internal/mateQuestionnaire" +import "seekia/internal/moderation/myIdentityScore" +import "seekia/internal/myIdentity" +import "seekia/internal/network/appNetworkType/getAppNetworkType" +import "seekia/internal/network/manualBroadcasts" +import "seekia/internal/network/myBroadcasts" +import "seekia/internal/network/myIdentityBalance" +import "seekia/internal/parameters/getParameters" +import "seekia/internal/profiles/attributeDisplay" +import "seekia/internal/profiles/myLocalProfiles" +import "seekia/internal/profiles/myProfileExports" +import "seekia/internal/profiles/myProfileStatus" +import "seekia/internal/profiles/profileFormat" +import "seekia/internal/profiles/readProfiles" + +import "strings" +import "image" +import "time" +import "errors" + +func setBroadcastPage(window fyne.Window, profileType string, previousPage func()){ + + setLoadingScreen(window, "Broadcast " + profileType + " Profile", "Loading Broadcast Page...") + + currentPage := func(){setBroadcastPage(window, profileType, previousPage)} + + appMemory.SetMemoryEntry("CurrentViewedPage", "Broadcast") + + if (profileType != "Mate" && profileType != "Moderator"){ + setErrorEncounteredPage(window, errors.New("setBroadcastPage called with invalid profile type: " + profileType), previousPage) + return + } + + title := getPageTitleCentered("Broadcast " + profileType + " Profile") + + backButton := getBackButtonCentered(previousPage) + + myIdentityExists, myIdentityHash, err := myIdentity.GetMyIdentityHash(profileType) + if (err != nil) { + setErrorEncounteredPage(window, err, previousPage) + return + } + if (myIdentityExists == false){ + + profileIcon, err := getFyneImageIcon("Profile") + if (err != nil) { + setErrorEncounteredPage(window, err, previousPage) + return + } + iconSize := getCustomFyneSize(10) + profileIcon.SetMinSize(iconSize) + profileIconCentered := container.NewHBox(layout.NewSpacer(), profileIcon, layout.NewSpacer()) + + description1 := getBoldLabelCentered("Your " + profileType + " identity does not exist.") + description2 := getLabelCentered("You must create your user identity hash.") + description3 := getLabelCentered("This hash is how other users will identify you.") + + createButton := getWidgetCentered(widget.NewButtonWithIcon("Create Identity", theme.NavigateNextIcon(), func(){ + setChooseNewIdentityHashPage(window, profileType, currentPage, currentPage) + })) + + page := container.NewVBox(title, backButton, widget.NewSeparator(), profileIconCentered, description1, description2, description3, createButton) + + setPageContent(page, window) + return + } + + description := getLabelCentered("Broadcast your " + profileType + " profile to the world.") + + appNetworkType, err := getAppNetworkType.GetAppNetworkType() + if (err != nil) { + setErrorEncounteredPage(window, err, previousPage) + return + } + + identityExists, myProfileIsActiveStatus, err := myProfileStatus.GetMyProfileIsActiveStatus(myIdentityHash, appNetworkType) + if (err != nil) { + setErrorEncounteredPage(window, err, previousPage) + return + } + if (identityExists == false) { + setErrorEncounteredPage(window, errors.New("My identity not found after being found already."), previousPage) + return + } + + myProfileStatusLabel := getBoldLabelCentered("My Profile Status:") + + getProfileStatusSection := func()(*fyne.Container, error){ + + if (myProfileIsActiveStatus == true){ + + activeIcon, err := getFyneImageIcon("ToggleOn") + if (err != nil) { return nil, err } + + activeLabel := getBoldLabel("Active") + activeLabelWithIcon := container.NewGridWithRows(2, activeIcon, activeLabel) + + updateProfileButton := widget.NewButtonWithIcon("Update", theme.ViewRefreshIcon(), func(){ + setBroadcastMyProfilePage(window, profileType, currentPage, currentPage) + }) + + disableButton := widget.NewButtonWithIcon("Disable", theme.CancelIcon(), func(){ + setDisableMyProfilePage(window, profileType, currentPage) + }) + + actionButtonsGrid := getContainerCentered(container.NewGridWithColumns(1, updateProfileButton, disableButton)) + + profileStatusSection := container.NewVBox(activeLabelWithIcon, widget.NewSeparator(), actionButtonsGrid) + + return profileStatusSection, nil + } + + // myProfileIsActiveStatus == false + + // Profile is not active. + // Possible reasons: + // -Profile was never broadcast + // -Identity is not funded + // -Profile is disabled + // -Profile has expired from network (For Mate profiles only) + + inactiveIcon, err := getFyneImageIcon("ToggleOff") + if (err != nil) { return nil, err } + + inactiveLabel := getBoldLabel("Inactive") + inactiveLabelWithIcon := container.NewGridWithRows(2, inactiveIcon, inactiveLabel) + + exists, localIsDisabled, err := myLocalProfiles.GetProfileData(profileType, "Disabled") + if (err != nil) { return nil, err } + if (exists == true && localIsDisabled == "Yes"){ + + enableProfileButton := getWidgetCentered(widget.NewButton("Enable", func(){ + setEnableMyProfilePage(window, profileType, currentPage, currentPage) + })) + + profileStatusSection := container.NewVBox(inactiveLabelWithIcon, widget.NewSeparator(), enableProfileButton) + + return profileStatusSection, nil + } + + broadcastProfileFunction := func()error{ + + if (profileType == "Mate"){ + + myIdentityFound, myIdentityIsActivated, myBalanceIsSufficient, _, _, err := myIdentityBalance.GetMyIdentityBalanceStatus(myIdentityHash, appNetworkType) + if (err != nil){ return err } + if (myIdentityFound == false){ + return errors.New("My identity not found after being found already.") + } + + if (myIdentityIsActivated == false || myBalanceIsSufficient == false) { + dialogTitle := translate("Identity Balance Is Insufficient.") + dialogMessageA := getLabelCentered(translate("Your identity balance is insufficient.")) + dialogMessageB := getLabelCentered(translate("You must spend credit to fund your identity and broadcast a profile.")) + dialogMessageC := getLabelCentered(translate("Visit the Manage Identity Balance page to fund your identity.")) + dialogContent := container.NewVBox(dialogMessageA, dialogMessageB, dialogMessageC) + dialog.ShowCustom(dialogTitle, translate("Close"), dialogContent, window) + return nil + } + + } else if (profileType == "Moderator"){ + + myIdentityFound, _, scoreIsSufficient, _, _, err := myIdentityScore.GetMyIdentityScore() + if (err != nil) { return err } + if (myIdentityFound == false){ + return errors.New("My moderator identity not found after being found already.") + } + if (scoreIsSufficient == false){ + + dialogTitle := translate("Identity Score Is Insufficient.") + dialogMessageA := getLabelCentered(translate("Your identity score is insufficient.")) + dialogMessageB := getLabelCentered(translate("You must send cryptocurrency to fund your identity.")) + dialogMessageC := getLabelCentered(translate("Visit the Manage Identity Score page to fund your identity.")) + dialogContent := container.NewVBox(dialogMessageA, dialogMessageB, dialogMessageC) + dialog.ShowCustom(dialogTitle, translate("Close"), dialogContent, window) + return nil + } + } + + setBroadcastMyProfilePage(window, profileType, currentPage, currentPage) + return nil + } + + broadcastButton := getWidgetCentered(widget.NewButtonWithIcon("Broadcast", theme.RadioButtonCheckedIcon(), func(){ + err := broadcastProfileFunction() + if (err != nil){ + setErrorEncounteredPage(window, err, previousPage) + return + } + })) + + profileStatusSection := container.NewVBox(inactiveLabelWithIcon, broadcastButton) + + return profileStatusSection, nil + } + + profileStatusSection, err := getProfileStatusSection() + if (err != nil) { + setErrorEncounteredPage(window, err, previousPage) + return + } + + page := container.NewVBox(title, backButton, widget.NewSeparator(), description, widget.NewSeparator(), myProfileStatusLabel, widget.NewSeparator(), profileStatusSection, widget.NewSeparator()) + + if (myProfileIsActiveStatus == true && profileType == "Mate"){ + + addProfileExpirationTimeSection := func()error{ + + profileExpirationTimeLabel := getItalicLabelCentered("My Profile Expiration Time:") + + page.Add(profileExpirationTimeLabel) + + // All mate profiles will expire after an expiration duration, which is defined in the network parameters + // They must be updated, or else they will be dropped by the network + // We will determine how long this current profile has until it is expired + // The user has to broadcast a profile again to reset this countdown + + myIdentityExists, myProfileExists, _, myProfileAttributeExists, myProfileBroadcastTime, err := myBroadcasts.GetAnyAttributeFromMyBroadcastProfile(myIdentityHash, appNetworkType, "BroadcastTime") + if (err != nil) { return err } + if (myIdentityExists == false) { + return errors.New("My identity not found after being found already.") + } + if (myProfileExists == false){ + return errors.New("My broadcast profile not found after myProfileStatus has already been determined to be active.") + } + if (myProfileAttributeExists == false){ + return errors.New("My Broadcast profile malformed: Missing BroadcastTime") + } + + myProfileBroadcastTimeInt64, err := helpers.ConvertBroadcastTimeStringToInt64(myProfileBroadcastTime) + if (err != nil){ + return errors.New("My Broadcast profile malformed: Contains invalid broadcastTime: " + myProfileBroadcastTime) + } + + _, mateProfileMaximumExistenceDuration, err := getParameters.GetMateProfileMaximumExistenceDuration(appNetworkType) + if (err != nil) { return err } + + currentTime := time.Now().Unix() + profileExistenceTime := currentTime - myProfileBroadcastTimeInt64 + + if (profileExistenceTime >= mateProfileMaximumExistenceDuration){ + + return errors.New("My Broadcast profile is expired, but myProfileStatus says it is active") + } + + timeUntilExpiration := mateProfileMaximumExistenceDuration - profileExistenceTime + + timeUntilExpirationString, err := helpers.ConvertUnixTimeDurationToUnitsTimeTranslated(timeUntilExpiration, true) + if (err != nil) { return err } + + timeUntilExpirationLabel := getBoldLabel("Expires in " + timeUntilExpirationString) + + profileExpirationTimeHelpButton := widget.NewButtonWithIcon("", theme.QuestionIcon(), func(){ + setMateProfileExpirationExplainerPage(window, currentPage) + }) + + timeUntilExpirationRow := container.NewHBox(layout.NewSpacer(), timeUntilExpirationLabel, profileExpirationTimeHelpButton, layout.NewSpacer()) + + page.Add(timeUntilExpirationRow) + page.Add(widget.NewSeparator()) + + return nil + } + + err := addProfileExpirationTimeSection() + if (err != nil) { + setErrorEncounteredPage(window, err, previousPage) + return + } + } + + if (profileType == "Mate"){ + + getMyIdentityBalanceSection := func()(*fyne.Container, error){ + + myIdentityBalanceLabel := getItalicLabelCentered("My Identity Balance:") + + getMyIdentityBalanceStatus := func()(string, error){ + + myIdentityFound, myIdentityIsActivated, myBalanceIsSufficient, _, myBalanceExpirationTime, err := myIdentityBalance.GetMyIdentityBalanceStatus(myIdentityHash, appNetworkType) + if (err != nil){ return "", err } + if (myIdentityFound == false){ + return "", errors.New("My identity not found after being found already.") + } + + if (myIdentityIsActivated == false || myBalanceIsSufficient == false){ + result := translate("Insufficient") + return result, nil + } + + currentTime := time.Now().Unix() + + timeLeft := myBalanceExpirationTime - currentTime + + timeTranslated, err := helpers.ConvertUnixTimeDurationToUnitsTimeTranslated(timeLeft, true) + if (err != nil) { return "", err } + + identityBalanceExpirationTime := "Expires in " + timeTranslated + + return identityBalanceExpirationTime, nil + } + + myIdentityBalanceStatus, err := getMyIdentityBalanceStatus() + if (err != nil){ return nil, err } + + myIdentityBalanceStatusLabel := getBoldLabelCentered(myIdentityBalanceStatus) + + manageBalanceButton := getWidgetCentered(widget.NewButtonWithIcon("Manage", theme.VisibilityIcon(), func(){ + setViewMyIdentityBalancePage(window, profileType, currentPage) + })) + + identityBalanceSection := container.NewVBox(myIdentityBalanceLabel, myIdentityBalanceStatusLabel, manageBalanceButton) + + return identityBalanceSection, nil + } + + myIdentityBalanceSection, err := getMyIdentityBalanceSection() + if (err != nil) { + setErrorEncounteredPage(window, err, previousPage) + return + } + + page.Add(myIdentityBalanceSection) + + } else if (profileType == "Moderator"){ + + getIdentityScoreSection := func()(*fyne.Container, error){ + + myIdentityScoreLabel := getItalicLabelCentered("My Identity Score:") + + myIdentityFound, _, scoreIsSufficient, _, _, err := myIdentityScore.GetMyIdentityScore() + if (err != nil) { return nil, err } + if (myIdentityFound == false){ + return nil, errors.New("My identity not found after being found already.") + } + + getIdentityScoreStatus := func()string{ + if (scoreIsSufficient == false){ + result := translate("Insufficient") + return result + } + + result := translate("Sufficient") + return result + } + + identityScoreStatus := getIdentityScoreStatus() + + identityScoreStatusLabel := getBoldLabelCentered(identityScoreStatus) + + manageIdentityScoreButton := getWidgetCentered(widget.NewButtonWithIcon("View Identity Score", theme.VisibilityIcon(), func(){ + setViewMyModeratorScorePage(window, currentPage) + })) + + identityScoreSection := container.NewVBox(myIdentityScoreLabel, identityScoreStatusLabel, manageIdentityScoreButton) + + return identityScoreSection, nil + } + + identityScoreSection, err := getIdentityScoreSection() + if (err != nil) { + setErrorEncounteredPage(window, err, previousPage) + return + } + + page.Add(identityScoreSection) + } + + setPageContent(page, window) +} + +func setBroadcastMyProfilePage(window fyne.Window, profileType string, previousPage func(), pageToVisitAfter func()) { + + currentPage := func(){setBroadcastMyProfilePage(window, profileType, previousPage, pageToVisitAfter)} + + title := getPageTitleCentered(translate("Broadcast " + profileType + " Profile")) + + backButton := getBackButtonCentered(previousPage) + + if (profileType == "Mate"){ + + myGenomePersonIdentifierExists, myGenomePersonIdentifier, err := myLocalProfiles.GetProfileData("Mate", "GenomePersonIdentifier") + if (err != nil){ + setErrorEncounteredPage(window, err, previousPage) + return + } + if (myGenomePersonIdentifierExists == true){ + + anyGenomesExist, personAnalysisIsReady, _, err := myPeople.CheckIfPersonAnalysisIsReady(myGenomePersonIdentifier) + if (err != nil){ + setErrorEncounteredPage(window, err, previousPage) + return + } + if (anyGenomesExist == true && personAnalysisIsReady == false){ + + description1 := getBoldLabelCentered(translate("Your profile contains a linked genome person.")) + description2 := getLabelCentered(translate("You need to perform your genetic analysis.")) + description3 := getLabelCentered(translate("Only the information you choose will be shared in your profile.")) + + performAnalysisButton := getWidgetCentered(widget.NewButtonWithIcon(translate("Perform Analysis"), theme.NavigateNextIcon(), func(){ + setConfirmPerformPersonAnalysisPage(window, myGenomePersonIdentifier, currentPage, currentPage) + })) + + page := container.NewVBox(title, backButton, widget.NewSeparator(), description1, description2, description3, performAnalysisButton) + + setPageContent(page, window) + return + } + } + } + + appNetworkType, err := getAppNetworkType.GetAppNetworkType() + if (err != nil) { + setErrorEncounteredPage(window, err, previousPage) + return + } + + err = myProfileExports.UpdateMyExportedProfile(profileType, appNetworkType) + if (err != nil){ + setErrorEncounteredPage(window, err, previousPage) + return + } + + myIdentityExists, myIdentityHash, err := myIdentity.GetMyIdentityHash(profileType) + if (err != nil){ + setErrorEncounteredPage(window, err, previousPage) + return + } + if (myIdentityExists == false){ + setErrorEncounteredPage(window, errors.New("My identity not found after being found already."), previousPage) + return + } + + existingBroadcastProfileExists, _, _, _, existingBroadcastProfileRawMap, err := myBroadcasts.GetMyNewestBroadcastProfile(myIdentityHash, appNetworkType) + if (err != nil) { + setErrorEncounteredPage(window, err, previousPage) + return + } + + page := container.NewVBox(title, backButton, widget.NewSeparator()) + + if (existingBroadcastProfileExists == true){ + description1 := getBoldLabelCentered("Are you sure you want to update your " + profileType + " profile?") + page.Add(description1) + } else { + description1 := getBoldLabelCentered("Are you sure you want to broadcast your " + profileType + " profile?") + page.Add(description1) + } + + viewProfileButton := getWidgetCentered(widget.NewButtonWithIcon("View Profile", theme.VisibilityIcon(), func(){ + setViewMyProfilePage(window, profileType, "Local", currentPage) + })) + page.Add(viewProfileButton) + + page.Add(widget.NewSeparator()) + + if (existingBroadcastProfileExists == true){ + + getLastUpdatedTimeAgo := func()(string, error){ + + exists, lastBroadcastTimeString, err := readProfiles.GetFormattedProfileAttributeFromRawProfileMap(existingBroadcastProfileRawMap, "BroadcastTime") + if (err != nil){ return "", err } + if (exists == false){ + return "", errors.New("My broadcast profile missing BroadcastTime") + } + + lastBroadcastTimeInt64, err := helpers.ConvertStringToInt64(lastBroadcastTimeString) + if (err != nil){ + return "", errors.New("My broadcast profile contains invalid BroadcastTime: " + lastBroadcastTimeString) + } + + lastUpdatedTimeAgo, err := helpers.ConvertUnixTimeToTimeAgoTranslated(lastBroadcastTimeInt64, true) + if (err != nil){ return "", err } + + return lastUpdatedTimeAgo, nil + } + + lastUpdatedTimeAgo, err := getLastUpdatedTimeAgo() + if (err != nil){ + setErrorEncounteredPage(window, err, previousPage) + return + } + + lastUpdatedTitle := widget.NewLabel("Last Updated:") + lastUpdatedLabel := getBoldLabel(lastUpdatedTimeAgo) + + lastUpdatedRow := container.NewHBox(layout.NewSpacer(), lastUpdatedTitle, lastUpdatedLabel, layout.NewSpacer()) + + page.Add(lastUpdatedRow) + + getNumberOfChangesMade := func()(int, error){ + + profileFound, _, _, exportProfileRawMap, err := myProfileExports.GetMyExportedProfile(profileType, appNetworkType) + if (err != nil) { return 0, err } + if (profileFound == false){ + return 0, errors.New("My exported profile not found after profile was exported.") + } + + allAttributesMap := make(map[int]struct{}) + + for attributeIdentifier, _ := range existingBroadcastProfileRawMap{ + allAttributesMap[attributeIdentifier] = struct{}{} + } + + for attributeIdentifier, _ := range exportProfileRawMap{ + allAttributesMap[attributeIdentifier] = struct{}{} + } + + changesMade := 0 + + // This will add the number of changed fields + for attributeIdentifier, _ := range allAttributesMap{ + + attributeName, err := profileFormat.GetAttributeNameFromAttributeIdentifier(attributeIdentifier) + if (err != nil) { return 0, err } + + if (attributeName == "BroadcastTime" || attributeName == "ChatKeysLatestUpdateTime" || attributeName == "NaclKey" || attributeName == "KyberKey"){ + continue + } + + existingValueExists, existingValue, err := readProfiles.GetFormattedProfileAttributeFromRawProfileMap(existingBroadcastProfileRawMap, attributeName) + if (err != nil) { return 0, err } + + exportValueExists, exportValue, err := readProfiles.GetFormattedProfileAttributeFromRawProfileMap(exportProfileRawMap, attributeName) + if (err != nil) { return 0, err } + + if (existingValueExists != exportValueExists || existingValue != exportValue){ + + // The value has changed between the old and new profile + + changesMade += 1 + } + } + + return changesMade, nil + } + + numberOfChangesMade, err := getNumberOfChangesMade() + if (err != nil){ + setErrorEncounteredPage(window, err, previousPage) + return + } + + numberOfChangesMadeString := helpers.ConvertIntToString(numberOfChangesMade) + + changesMadeTitle := getItalicLabel("Changes Made:") + + numberOfChangesMadeLabel := getBoldLabel(numberOfChangesMadeString) + + numberOfChangesMadeRow := container.NewHBox(layout.NewSpacer(), changesMadeTitle, numberOfChangesMadeLabel, layout.NewSpacer()) + page.Add(numberOfChangesMadeRow) + + if (numberOfChangesMade != 0){ + + viewChangesButton := getWidgetCentered(widget.NewButtonWithIcon("View Changes", theme.VisibilityIcon(), func(){ + setViewNewProfileChangesPage(window, profileType, currentPage) + })) + + page.Add(viewChangesButton) + } + + page.Add(widget.NewSeparator()) + } + + if (profileType == "Mate"){ + + // Mate profiles must be funded with each broadcast + + getCurrentAppCurrency := func()(string, error){ + exists, currentAppCurrency, err := globalSettings.GetSetting("Currency") + if (err != nil) { return "", err } + if (exists == false){ + return "USD", nil + } + return currentAppCurrency, nil + } + + currentAppCurrencyCode, err := getCurrentAppCurrency() + if (err != nil){ + setErrorEncounteredPage(window, err, previousPage) + return + } + + _, appCurrencySymbol, err := currencies.GetCurrencyInfoFromCurrencyCode(currentAppCurrencyCode) + if (err != nil){ + setErrorEncounteredPage(window, err, previousPage) + return + } + + costTitle := widget.NewLabel("Cost:") + + appCurrencySymbolButton := widget.NewButton(appCurrencySymbol, func(){ + setChangeAppCurrencyPage(window, currentPage) + }) + + //TODO: Fix this to actually calculate cost (get cost from parameters) + costLabel := getBoldLabel("0.1 " + currentAppCurrencyCode) + + costRow := container.NewHBox(layout.NewSpacer(), costTitle, appCurrencySymbolButton, costLabel, layout.NewSpacer()) + page.Add(costRow) + + page.Add(widget.NewSeparator()) + } + + broadcastIcon, err := getFyneImageIcon("Broadcast") + if (err != nil){ + setErrorEncounteredPage(window, err, previousPage) + return + } + + confirmButton := widget.NewButtonWithIcon("Broadcast", theme.ConfirmIcon(), func(){ + + //TODO: Make sure credit is available + + setConfirmBroadcastMyProfilePage(window, profileType, currentPage) + }) + + confirmButtonWithIcon := getContainerCentered(container.NewGridWithRows(2, broadcastIcon, confirmButton)) + + page.Add(confirmButtonWithIcon) + page.Add(widget.NewSeparator()) + + setPageContent(page, window) +} + +func setConfirmBroadcastMyProfilePage(window fyne.Window, profileType string, previousPage func()){ + + if (profileType != "Mate" && profileType != "Moderator"){ + setErrorEncounteredPage(window, errors.New("setConfirmBroadcastMyProfilePage called with invalid profileType: " + profileType), previousPage) + return + } + + title := getPageTitleCentered("Confirm Broadcast Profile") + + backButton := getBackButtonCentered(previousPage) + + //TODO: Improve wording of below: + + description1 := getBoldLabelCentered("Confirm broadcast your " + profileType + " profile?") + description2 := getLabelCentered("This will upload your profile to the Seekia network.") + description3 := getLabelCentered("The whole world will be able to see it.") + description4 := getLabelCentered("Make sure you are comfortable sharing your profile with the world.") + + confirmButton := getWidgetCentered(widget.NewButtonWithIcon("Confirm", theme.ConfirmIcon(), func(){ + + profilePage := func(){setProfilePage(window, true, profileType, false, nil)} + afterCompletionPage := func(){setBroadcastPage(window, profileType, profilePage)} + + if (profileType == "Mate"){ + + setStartAndMonitorMateProfileFundingAndBroadcastPage(window, afterCompletionPage) + + } else if (profileType == "Moderator"){ + + setStartAndMonitorMyModeratorProfileBroadcastPage(window, afterCompletionPage) + } + })) + + page := container.NewVBox(title, backButton, widget.NewSeparator(), description1, description2, description3, description4, confirmButton) + + setPageContent(page, window) +} + +// This function will show the changes between the existing broadcast profile and the new exported profile +func setViewNewProfileChangesPage(window fyne.Window, myProfileType string, previousPage func()){ + + setLoadingScreen(window, "View Profile Changes", "Loading profile changes...") + + currentPage := func(){setViewNewProfileChangesPage(window, myProfileType, previousPage)} + + title := getPageTitleCentered("View Profile Changes") + + backButton := getBackButtonCentered(previousPage) + + description := getLabelCentered("Below are the changes between your old and new profile.") + + getChangesGrid := func()(*fyne.Container, error){ + + myIdentityExists, myIdentityHash, err := myIdentity.GetMyIdentityHash(myProfileType) + if (err != nil){ return nil, err } + if (myIdentityExists == false){ + return nil, errors.New("setViewNewProfileChangesPage called with missing identity.") + } + + appNetworkType, err := getAppNetworkType.GetAppNetworkType() + if (err != nil) { return nil, err } + + oldProfileExists, _, _, _, oldProfileRawMap, err := myBroadcasts.GetMyNewestBroadcastProfile(myIdentityHash, appNetworkType) + if (err != nil) { return nil, err } + if (oldProfileExists == false){ + return nil, errors.New("setViewNewProfileChangesPage called when no broadcast profile exists.") + } + + profileFound, _, _, newProfileRawMap, err := myProfileExports.GetMyExportedProfile(myProfileType, appNetworkType) + if (err != nil) { return nil, err } + if (profileFound == false){ + return nil, errors.New("setViewNewProfileChangesPage called when exportedProfile is missing.") + } + + // We use this map to avoid duplicates + allAttributesIdentifiersMap := make(map[int]struct{}) + + for attributeIdentifier, _ := range oldProfileRawMap{ + allAttributesIdentifiersMap[attributeIdentifier] = struct{}{} + } + + for attributeIdentifier, _ := range newProfileRawMap{ + allAttributesIdentifiersMap[attributeIdentifier] = struct{}{} + } + + allAttributeNamesList := make([]string, 0, len(allAttributesIdentifiersMap)) + + for attributeIdentifier, _ := range allAttributesIdentifiersMap{ + + attributeName, err := profileFormat.GetAttributeNameFromAttributeIdentifier(attributeIdentifier) + if (err != nil) { return nil, err } + + allAttributeNamesList = append(allAttributeNamesList, attributeName) + } + + // We sort attributes so they show up in the same order each time + helpers.SortStringListToUnicodeOrder(allAttributeNamesList) + + attributeLabel := getItalicLabelCentered("Attribute") + oldProfileTitle := getItalicLabelCentered("Old Profile") + newProfileTitle := getItalicLabelCentered("New Profile") + + attributeTitleColumn := container.NewVBox(attributeLabel, widget.NewSeparator()) + oldProfileColumn := container.NewVBox(oldProfileTitle, widget.NewSeparator()) + newProfileColumn := container.NewVBox(newProfileTitle, widget.NewSeparator()) + + for _, attributeName := range allAttributeNamesList{ + + if (attributeName == "BroadcastTime" || attributeName == "Disabled" || attributeName == "ChatKeysLatestUpdateTime" || attributeName == "NaclKey" || attributeName == "KyberKey"){ + continue + } + + oldValueExists, oldValue, err := readProfiles.GetFormattedProfileAttributeFromRawProfileMap(oldProfileRawMap, attributeName) + if (err != nil) { return nil, err } + + newValueExists, newValue, err := readProfiles.GetFormattedProfileAttributeFromRawProfileMap(newProfileRawMap, attributeName) + if (err != nil) { return nil, err } + + if (oldValueExists == false && newValueExists == false){ + // This should never happen. Probably cosmic ray bit flip or faulty hardware. + return nil, errors.New("oldProfileRawMap and newProfileRawMap are missing the attribute.") + } + if (oldValueExists == true && newValueExists == true && oldValue == newValue){ + // The value is the same between both profiles + continue + } + + attributeTitle, _, formatValueFunction, _, _, err := attributeDisplay.GetProfileAttributeDisplayInfo(attributeName) + if (err != nil){ return nil, err } + + attributeTitleLabel := getBoldLabelCentered(attributeTitle) + + getProfileValueCell := func(valueExists bool, profileValue string)(*fyne.Container, error){ + + if (valueExists == false){ + noneLabel := getItalicLabelCentered(translate("None")) + return noneLabel, nil + } + + switch attributeName{ + + case "Photos":{ + + photosBase64List := strings.Split(profileValue, "+") + + photosList := make([]image.Image, 0, len(photosBase64List)) + + for _, photoBase64 := range photosBase64List{ + + goImage, err := imagery.ConvertWEBPBase64StringToCroppedDownsizedImageObject(photoBase64) + if (err != nil){ + return nil, errors.New("setViewNewProfileChangesPage called with profile containing invalid Photos attribute: " + profileValue) + } + + photosList = append(photosList, goImage) + } + + if (len(photosList) > 5){ + return nil, errors.New("setViewNewProfileChangesPage called with profile containing invalid Photos attribute: Too many photos: " + profileValue) + } + + viewAttributeButton := getWidgetCentered(widget.NewButtonWithIcon("", theme.VisibilityIcon(), func(){ + setViewFullpageImagesWithNavigationPage(window, photosList, 0, currentPage) + })) + + return viewAttributeButton, nil + } + + case "Avatar":{ + + avatarIdentifier, err := helpers.ConvertStringToInt(profileValue) + if (err != nil) { + return nil, errors.New("setViewNewProfileChangesPage called with profile containing invalid Avatar attribute: " + profileValue) + } + + emojiImage, err := getEmojiImageObject(avatarIdentifier) + if (err != nil){ + return nil, errors.New("setViewNewProfileChangesPage called with profile containing invalid Avatar attribute: " + profileValue) + } + + viewAttributeButton := getWidgetCentered(widget.NewButtonWithIcon("", theme.VisibilityIcon(), func(){ + setViewFullpageImagePage(window, emojiImage, currentPage) + })) + + return viewAttributeButton, nil + } + + case "23andMe_AncestryComposition":{ + + viewAttributeButton := getWidgetCentered(widget.NewButtonWithIcon("", theme.VisibilityIcon(), func(){ + setViewUser23andMeAncestryCompositionPage(window, profileValue, currentPage) + })) + + return viewAttributeButton, nil + } + + case "Questionnaire":{ + + questionnaireObject, err := mateQuestionnaire.ReadQuestionnaireString(profileValue) + if (err != nil) { + return nil, errors.New("setViewNewProfileChangesPage called with profile containing invalid Questionnaire attribute: " + profileValue + ". Reason: " + err.Error()) + } + + myResponsesMap := make(map[string]string) + + submitQuestionnairePage := func(_ string, _ func()){ + currentPage() + } + + viewAttributeButton := getWidgetCentered(widget.NewButtonWithIcon("", theme.VisibilityIcon(), func(){ + setTakeQuestionnairePage(window, questionnaireObject, 0, myResponsesMap, currentPage, submitQuestionnairePage) + })) + + return viewAttributeButton, nil + } + + //TODO: Format more values that cannot be displayed as a string in their raw form (Tags, Location, Language, etc...) + } + + valueFormatted, err := formatValueFunction(profileValue) + if (err != nil) { return nil, err } + + valueTrimmed, anyChangesOccurred, err := helpers.TrimAndFlattenString(valueFormatted, 20) + if (err != nil) { return nil, err } + if (anyChangesOccurred == false){ + + valueLabel := getBoldLabelCentered(valueFormatted) + + return valueLabel, nil + } + + trimmedValueLabel := getBoldLabel(valueTrimmed) + + viewFullValueButton := widget.NewButtonWithIcon("", theme.VisibilityIcon(), func(){ + setViewTextPage(window, "Viewing Attribute", valueFormatted, false, currentPage) + }) + + profileValueCell := container.NewHBox(layout.NewSpacer(), trimmedValueLabel, viewFullValueButton, layout.NewSpacer()) + + return profileValueCell, nil + } + + oldValueCell, err := getProfileValueCell(oldValueExists, oldValue) + if (err != nil) { return nil, err } + + newValueCell, err := getProfileValueCell(newValueExists, newValue) + if (err != nil) { return nil, err } + + attributeTitleColumn.Add(attributeTitleLabel) + oldProfileColumn.Add(oldValueCell) + newProfileColumn.Add(newValueCell) + + attributeTitleColumn.Add(widget.NewSeparator()) + oldProfileColumn.Add(widget.NewSeparator()) + newProfileColumn.Add(widget.NewSeparator()) + } + + displayGrid := container.NewHBox(layout.NewSpacer(), attributeTitleColumn, oldProfileColumn, newProfileColumn, layout.NewSpacer()) + + return displayGrid, nil + } + + displayGrid, err := getChangesGrid() + if (err != nil){ + setErrorEncounteredPage(window, err, previousPage) + return + } + + page := container.NewVBox(title, backButton, widget.NewSeparator(), description, widget.NewSeparator(), displayGrid) + + setPageContent(page, window) +} + + +func setEnableMyProfilePage(window fyne.Window, myProfileType string, previousPage func(), pageToVisitAfter func()){ + + if (myProfileType != "Mate" && myProfileType != "Moderator"){ + setErrorEncounteredPage(window, errors.New("setEnableMyProfilePage called with invalid profileType: " + myProfileType), previousPage) + return + } + + myIdentityExists, myIdentityHash, err := myIdentity.GetMyIdentityHash(myProfileType) + if (err != nil) { + setErrorEncounteredPage(window, err, previousPage) + return + } + if (myIdentityExists == false){ + // This should not happen, this page should only be reached if identity exists + setErrorEncounteredPage(window, errors.New("My identity not found after being found already."), previousPage) + return + } + + attributeExists, localProfileIsDisabled, err := myLocalProfiles.GetProfileData(myProfileType, "Disabled") + if (err != nil){ + setErrorEncounteredPage(window, err, previousPage) + return + } + if (attributeExists == false || localProfileIsDisabled != "Yes"){ + // This should not happen, as this page should only be viewed if profile is disabled + setErrorEncounteredPage(window, errors.New("setEnableMyProfilePage called when your profile is not disabled."), previousPage) + return + } + + title := getPageTitleCentered("Enable " + myProfileType + " Profile") + + backButton := getBackButtonCentered(previousPage) + + description1 := getBoldLabelCentered("Are you sure you want to re-enable your profile?") + description2 := getLabelCentered("You must broadcast your profile after this step.") + + confirmFunction := func(){ + + err = myBroadcasts.DeleteMyBroadcastProfiles(myIdentityHash) + if (err != nil){ + setErrorEncounteredPage(window, err, previousPage) + return + } + + err := myLocalProfiles.SetProfileData(myProfileType, "Disabled", "No") + if (err != nil){ + setErrorEncounteredPage(window, err, previousPage) + return + } + + pageToVisitAfter() + } + + confirmButton := getWidgetCentered(widget.NewButtonWithIcon("Confirm", theme.ConfirmIcon(), confirmFunction)) + + page := container.NewVBox(title, backButton, widget.NewSeparator(), description1, description2, confirmButton) + + setPageContent(page, window) +} + +func setDisableMyProfilePage(window fyne.Window, myProfileType string, previousPage func()){ + + if (myProfileType != "Mate" && myProfileType != "Moderator"){ + setErrorEncounteredPage(window, errors.New("setDisableMyProfilePage called with invalid profile type: " + myProfileType), previousPage) + return + } + + myIdentityExists, myIdentityHash, err := myIdentity.GetMyIdentityHash(myProfileType) + if (err != nil) { + setErrorEncounteredPage(window, err, previousPage) + return + } + if (myIdentityExists == false){ + // This should not happen, this page should only be reached if identity exists + setErrorEncounteredPage(window, errors.New("My identity not found on setDisableMyProfilePage."), previousPage) + return + } + + currentPage := func(){setDisableMyProfilePage(window, myProfileType, previousPage)} + + title := getPageTitleCentered("Disable My " + myProfileType + " Profile") + + backButton := getBackButtonCentered(previousPage) + + getLocalProfileIsDisabledStatus := func()(bool, error){ + + attributeExists, localProfileIsDisabled, err := myLocalProfiles.GetProfileData(myProfileType, "Disabled") + if (err != nil){ return false, err } + if (attributeExists == true && localProfileIsDisabled == "Yes"){ + return true, nil + } + + return false, nil + } + + appNetworkType, err := getAppNetworkType.GetAppNetworkType() + if (err != nil) { + setErrorEncounteredPage(window, err, previousPage) + return + } + + getBroadcastProfileIsDisabledStatus := func()(bool, error){ + + identityExists, profileExists, _, attributeExists, broadcastProfileIsDisabled, err := myBroadcasts.GetAnyAttributeFromMyBroadcastProfile(myIdentityHash, appNetworkType, "Disabled") + if (err != nil) { return false, err } + if (identityExists == false) { + return false, errors.New("My identity not found after being found already.") + } + if (profileExists == false){ + return false, nil + } + if (attributeExists == true && broadcastProfileIsDisabled == "Yes"){ + return true, nil + } + return false, nil + } + + localProfileIsDisabled, err := getLocalProfileIsDisabledStatus() + if (err != nil){ + setErrorEncounteredPage(window, err, previousPage) + return + } + + broadcastProfileIsDisabled, err := getBroadcastProfileIsDisabledStatus() + if (err != nil){ + setErrorEncounteredPage(window, err, previousPage) + return + } + + if (localProfileIsDisabled == true && broadcastProfileIsDisabled == true){ + // This should not happen, as this page should only be viewed if profile is enabled + setErrorEncounteredPage(window, errors.New("setDisableMyProfilePage accessed with enabled/missing profile."), previousPage) + return + } + + description1 := getBoldLabelCentered("Confirm to disable your " + myProfileType + " Profile?") + description2 := getLabelCentered("Other users will see your profile as disabled.") + description3 := getLabelCentered("You will stop receiving " + myProfileType + " messages.") + description4 := getLabelCentered("You can always enable your profile later.") + + page := container.NewVBox(title, backButton, widget.NewSeparator(), description1, description2, description3, description4) + + if (myProfileType == "Mate"){ + description5 := getLabelCentered("Your identity balance will be retained, but will continue to expire.") + page.Add(description5) + } else if (myProfileType == "Moderator"){ + description5 := getLabelCentered("Your identity score will be retained.") + page.Add(description5) + } + + if (myProfileType == "Mate"){ + + // Mate profiles must be funded with each broadcast + + page.Add(widget.NewSeparator()) + + getCurrentAppCurrency := func()(string, error){ + exists, currentAppCurrency, err := globalSettings.GetSetting("Currency") + if (err != nil) { return "", err } + if (exists == false){ + return "USD", nil + } + return currentAppCurrency, nil + } + + currentAppCurrencyCode, err := getCurrentAppCurrency() + if (err != nil){ + setErrorEncounteredPage(window, err, previousPage) + return + } + + _, appCurrencySymbol, err := currencies.GetCurrencyInfoFromCurrencyCode(currentAppCurrencyCode) + if (err != nil){ + setErrorEncounteredPage(window, err, previousPage) + return + } + + costTitle := widget.NewLabel("Cost:") + + appCurrencySymbolButton := widget.NewButton(appCurrencySymbol, func(){ + setChangeAppCurrencyPage(window, currentPage) + }) + + //TODO: Fix this to actually calculate cost (get cost from parameters) + costLabel := getBoldLabel("0.10 " + currentAppCurrencyCode) + + costRow := container.NewHBox(layout.NewSpacer(), costTitle, appCurrencySymbolButton, costLabel, layout.NewSpacer()) + page.Add(costRow) + + page.Add(widget.NewSeparator()) + } + + confirmButton := getWidgetCentered(widget.NewButtonWithIcon("Confirm", theme.ConfirmIcon(), func(){ + + //TODO: Make sure credit is sufficient (if profileType == "Mate") + + err := myLocalProfiles.SetProfileData(myProfileType, "Disabled", "Yes") + if (err != nil) { + setErrorEncounteredPage(window, err, previousPage) + return + } + err = myProfileExports.UpdateMyExportedProfile(myProfileType, appNetworkType) + if (err != nil){ + setErrorEncounteredPage(window, err, previousPage) + return + } + if (myProfileType == "Mate"){ + + setStartAndMonitorMateProfileFundingAndBroadcastPage(window, previousPage) + + } else if (myProfileType == "Moderator"){ + + setStartAndMonitorMyModeratorProfileBroadcastPage(window, previousPage) + } + })) + + // TODO: Add a warning that you should not disable multiple identity's profiles at the same time + // Correlation between the identities is possible + + page.Add(confirmButton) + + setPageContent(page, window) +} + +// This page will first attempt to fund a mate profile, and if it succeeds, it will initiate a manual profile broadcast +func setStartAndMonitorMateProfileFundingAndBroadcastPage(window fyne.Window, afterCompletionPage func()){ + + pageIdentifier, err := helpers.GetNewRandomHexString(16) + if (err != nil) { + setErrorEncounteredPage(window, err, afterCompletionPage) + return + } + + appMemory.SetMemoryEntry("CurrentViewedPage", pageIdentifier) + + checkIfPageHasChangedFunction := func()bool{ + exists, currentViewedPage := appMemory.GetMemoryEntry("CurrentViewedPage") + if (exists == true && currentViewedPage == pageIdentifier){ + return false + } + return true + } + + title := getPageTitleCentered("Funding Mate Profile") + + progressBinding := binding.NewString() + + updateProgressBindings := func(){ + + startTime := time.Now().Unix() + + for { + + //TODO: Add details that describe the steps (Example: contacting server, making transaction) + + currentTime := time.Now().Unix() + secondsElapsed := currentTime - startTime + if (secondsElapsed % 3 == 0){ + progressBinding.Set("Funding profile.") + } else if (secondsElapsed % 3 == 1){ + progressBinding.Set("Funding profile..") + } else { + progressBinding.Set("Funding profile...") + } + + pageHasChanged := checkIfPageHasChangedFunction() + if (pageHasChanged == true){ + return + } + + time.Sleep(time.Second/2) + } + } + + appNetworkType, err := getAppNetworkType.GetAppNetworkType() + if (err != nil) { + setErrorEncounteredPage(window, err, afterCompletionPage) + return + } + + fundProfileFunction := func(){ + + // We fund the export profile before we broadcast it + newProfileFound, newProfileHash, _, _, err := myProfileExports.GetMyExportedProfile("Mate", appNetworkType) + if (err != nil) { + setErrorEncounteredPage(window, err, afterCompletionPage) + return + } + if (newProfileFound == false){ + setErrorEncounteredPage(window, errors.New("setStartAndMonitorMateProfileFundingAndBroadcastPage called when export profile is missing."), afterCompletionPage) + return + } + + //Outputs: + // -bool: Fund successful + // -error + fundProfile := func(profileHashToFund [28]byte)(bool, error){ + //TODO: Add function to fund profile + time.Sleep(time.Second * 3) + + return true, nil + } + + fundSuccessful, err := fundProfile(newProfileHash) + if (err != nil){ + pageHasChanged := checkIfPageHasChangedFunction() + if (pageHasChanged == true){ + return + } + + setErrorEncounteredPage(window, errors.New("Profile fund encountered error: " + err.Error()), afterCompletionPage) + return + } + if (fundSuccessful == false){ + + pageHasChanged := checkIfPageHasChangedFunction() + if (pageHasChanged == true){ + return + } + + description1 := getBoldLabelCentered("Seekia failed to fund the profile.") + description2 := getLabelCentered("The account credit server we contacted may be down.") + description3 := getLabelCentered("Your internet connection may also be broken.") + + retryFunction := func(){setStartAndMonitorMateProfileFundingAndBroadcastPage(window, afterCompletionPage)} + + retryButton := getWidgetCentered(widget.NewButtonWithIcon("Retry", theme.ViewRefreshIcon(), retryFunction)) + exitButton := getWidgetCentered(widget.NewButtonWithIcon("Exit", theme.CancelIcon(), afterCompletionPage)) + + manageConnectionDescription := getLabelCentered("Check if your internet connection is working below.") + + manageConnectionButton := getWidgetCentered(widget.NewButtonWithIcon("Manage Connection", theme.DownloadIcon(), func(){ + setManageNetworkConnectionPage(window, retryFunction) + })) + + page := container.NewVBox(title, widget.NewSeparator(), description1, description2, description3, widget.NewSeparator(), retryButton, exitButton, widget.NewSeparator(), manageConnectionDescription, manageConnectionButton) + + setPageContent(page, window) + return + } + + // Fund is complete. We update the broadcast profile + + myIdentityExists, newBroadcastProfileHash, err := myBroadcasts.UpdateMyBroadcastProfile("Mate", appNetworkType) + if (err != nil) { + + pageHasChanged := checkIfPageHasChangedFunction() + if (pageHasChanged == true){ + return + } + + setErrorEncounteredPage(window, err, afterCompletionPage) + return + } + if (myIdentityExists == false){ + pageHasChanged := checkIfPageHasChangedFunction() + if (pageHasChanged == true){ + return + } + + setErrorEncounteredPage(window, errors.New("My identity not found after being found already."), afterCompletionPage) + return + } + if (newBroadcastProfileHash != newProfileHash){ + pageHasChanged := checkIfPageHasChangedFunction() + if (pageHasChanged == true){ + return + } + + // This should not happen. This means we funded the wrong profile. + // MyBroadcasts broadcasts the newest exported profile + setErrorEncounteredPage(window, errors.New("Exported profile is not the same as broadcasted profile"), afterCompletionPage) + return + } + + // Now we start a new broadcast + + //Outputs: + // -bool: Any hosts found + // -[22]byte: New process identifier + // -error + startNewBroadcastFunction := func()(bool, [22]byte, error){ + + myIdentityExists, myIdentityHash, err := myIdentity.GetMyIdentityHash("Mate") + if (err != nil){ return false, [22]byte{}, err } + if (myIdentityExists == false){ + return false, [22]byte{}, errors.New("My identity not found after being found already.") + } + + // We get the user's newest profile each time + // It is unlikely, but the user could update their broadcast profile before this manual broadcast process completes + // In this case, we will just be broadcasting the user's newest profile in multiple manual processes + + profileExists, _, profileHash, profileBytes, _, err := myBroadcasts.GetMyNewestBroadcastProfile(myIdentityHash, appNetworkType) + if (err != nil) { return false, [22]byte{}, err } + if (profileExists == false){ + return false, [22]byte{}, errors.New("Cannot get profile to broadcast on setStartAndMonitorMateProfileFundingAndBroadcastPage") + } + if (profileHash != newProfileHash){ + return false, [22]byte{}, errors.New("GetMyNewestBroadcastProfile returning different profile during Mate profile broadcast") + } + + profileToBroadcastList := [][]byte{profileBytes} + + anyHostsFound, processIdentifier, err := manualBroadcasts.StartContentBroadcast("Profile", appNetworkType, profileToBroadcastList, 3) + if (err != nil) { return false, [22]byte{}, err } + if (anyHostsFound == false){ + return false, [22]byte{}, nil + } + + return true, processIdentifier, nil + } + + anyHostsFound, newProcessIdentifier, err := startNewBroadcastFunction() + if (err != nil){ + pageHasChanged := checkIfPageHasChangedFunction() + if (pageHasChanged == true){ + return + } + setErrorEncounteredPage(window, err, afterCompletionPage) + return + } + pageHasChanged := checkIfPageHasChangedFunction() + if (pageHasChanged == true){ + + // Whether the process started or not, we will not show the user in the GUI + // If no hosts were found, their profile should still be broadcast in the background + // Seekia will automatically download enough hosts to contact + return + } + + noHostsFound := !anyHostsFound + + nextPageTitle := "Broadcasting Mate Profile" + nextPageDescription := "Seekia is broadcasting your Mate profile." + + setMonitorManualBroadcastPage(window, nextPageTitle, "Profile", nextPageDescription, noHostsFound, newProcessIdentifier, startNewBroadcastFunction, afterCompletionPage) + } + + description1 := getBoldLabelCentered("Seekia is funding your new Mate profile.") + description2 := getLabelCentered("You can leave this page.") + + progressLabel := widget.NewLabelWithData(progressBinding) + progressLabel.TextStyle = getFyneTextStyle_Bold() + + progressLabelCentered := getWidgetCentered(progressLabel) + + exitButton := getWidgetCentered(widget.NewButtonWithIcon("Exit", theme.CancelIcon(), afterCompletionPage)) + + page := container.NewVBox(title, widget.NewSeparator(), description1, description2, widget.NewSeparator(), progressLabelCentered, widget.NewSeparator(), exitButton) + + setPageContent(page, window) + + go updateProgressBindings() + go fundProfileFunction() +} + +// Starts broadcast and allows user to monitor progress +func setStartAndMonitorMyModeratorProfileBroadcastPage(window fyne.Window, afterCompletionPage func()){ + + myIdentityExists, myIdentityHash, err := myIdentity.GetMyIdentityHash("Moderator") + if (err != nil){ + setErrorEncounteredPage(window, err, afterCompletionPage) + return + } + if (myIdentityExists == false){ + setErrorEncounteredPage(window, errors.New("Identity does not exist on setStartAndMonitorMyModeratorProfileBroadcastPage"), afterCompletionPage) + return + } + + // We have to update the broadcast profile + + appNetworkType, err := getAppNetworkType.GetAppNetworkType() + if (err != nil) { + setErrorEncounteredPage(window, err, afterCompletionPage) + return + } + + identityExists, _, err := myBroadcasts.UpdateMyBroadcastProfile("Moderator", appNetworkType) + if (err != nil) { + setErrorEncounteredPage(window, err, afterCompletionPage) + return + } + if (identityExists == false){ + setErrorEncounteredPage(window, errors.New("Identity not found after being found already."), afterCompletionPage) + return + } + + //Outputs: + // -bool: Any hosts found + // -[22]byte: New process identifier + // -error + startNewBroadcastFunction := func()(bool, [22]byte, error){ + + // We get the user's newest profile each time + // It is unlikely, but the user could update their broadcast profile before this manual broadcast process completes + // In this case, we will just be broadcasting the user's newest profile in multiple manual processes + + profileExists, _, _, profileBytes, _, err := myBroadcasts.GetMyNewestBroadcastProfile(myIdentityHash, appNetworkType) + if (err != nil) { return false, [22]byte{}, err } + if (profileExists == false){ + return false, [22]byte{}, errors.New("Cannot get profile to broadcast on setStartAndMonitorMyModeratorProfileBroadcastPage") + } + + profileToBroadcastList := [][]byte{profileBytes} + + anyHostsFound, processIdentifier, err := manualBroadcasts.StartContentBroadcast("Profile", appNetworkType, profileToBroadcastList, 3) + if (err != nil) { return false, [22]byte{}, err } + if (anyHostsFound == false){ + return false, [22]byte{}, nil + } + + return true, processIdentifier, nil + } + + anyHostsFound, newProcessIdentifier, err := startNewBroadcastFunction() + if (err != nil){ + setErrorEncounteredPage(window, err, afterCompletionPage) + return + } + + noHostsFound := !anyHostsFound + + nextPageTitle := "Broadcasting Moderator Profile" + nextPageDescription := "Seekia is broadcasting your Moderator profile." + + setMonitorManualBroadcastPage(window, nextPageTitle, "Profile", nextPageDescription, noHostsFound, newProcessIdentifier, startNewBroadcastFunction, afterCompletionPage) +} + + +func setMonitorManualBroadcastPage(window fyne.Window, pageTitleText string, broadcastType string, description1Text string, noHostsFound bool, processIdentifier [22]byte, startNewBroadcastFunction func()(bool, [22]byte, error), afterCompletionPage func()){ + + currentPage := func(){setMonitorManualBroadcastPage(window, pageTitleText, broadcastType, description1Text, noHostsFound, processIdentifier, startNewBroadcastFunction, afterCompletionPage)} + + pageIdentifier, err := helpers.GetNewRandomHexString(16) + if (err != nil) { + setErrorEncounteredPage(window, err, afterCompletionPage) + return + } + + appMemory.SetMemoryEntry("CurrentViewedPage", pageIdentifier) + + checkIfPageHasChangedFunction := func()bool{ + exists, currentViewedPage := appMemory.GetMemoryEntry("CurrentViewedPage") + if (exists == true && currentViewedPage == pageIdentifier){ + return false + } + return true + } + + title := getPageTitleCentered(pageTitleText) + + retryFunction := func(){ + newBroadcastAnyHostsFound, newProcessIdentifier, err := startNewBroadcastFunction() + if (err != nil){ + setErrorEncounteredPage(window, err, afterCompletionPage) + return + } + + newBroadcastNoHostsFound := !newBroadcastAnyHostsFound + + setMonitorManualBroadcastPage(window, pageTitleText, broadcastType, description1Text, newBroadcastNoHostsFound, newProcessIdentifier, startNewBroadcastFunction, afterCompletionPage) + } + + if (noHostsFound == true){ + + description1 := getLabelCentered("No available hosts were found.") + description2 := getLabelCentered("Please wait for Seekia to find more hosts.") + description3 := getLabelCentered("This should take less than 1 minute.") + description4 := getLabelCentered("You can leave this page and the broadcast will still happen automatically.") + + 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(), afterCompletionPage)) + + descriptionD := 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, widget.NewSeparator(), description1, description2, description3, description4, widget.NewSeparator(), retryingInLabelCentered, widget.NewSeparator(), retryButton, exitButton, widget.NewSeparator(), descriptionD, manageConnectionButton) + + setPageContent(page, window) + + go startRetryCountdownFunction() + + return + } + + broadcastProgressStatusBinding := binding.NewString() + broadcastProgressDetailsBinding := binding.NewString() + + updateBindingsFunction := func(){ + + startTime := time.Now().Unix() + + setBroadcastProgressStatus := 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() + broadcastProgressStatusBinding.Set(newStatus + progressEllipsis) + } + + for { + + processFound, processIsCompleteBool, processEncounteredError, processError, numberOfCompletedBroadcasts, processProgressDetails := manualBroadcasts.GetProcessInfo(processIdentifier) + if (processFound == false){ + // This should not happen + processIdentifierHex := encoding.EncodeBytesToHexString(processIdentifier[:]) + setBroadcastProgressStatus(true, "ERROR: manualBroadcasts process not found: " + processIdentifierHex) + broadcastProgressDetailsBinding.Set("Report this error to the Seekia developers.") + + return + } + + numberOfCompletedBroadcastsString := helpers.ConvertIntToString(numberOfCompletedBroadcasts) + + if (processIsCompleteBool == true){ + + if (processEncounteredError == true){ + setBroadcastProgressStatus(true, "ERROR:" + processError.Error()) + broadcastProgressDetailsBinding.Set("Report this error to the Seekia developers.") + return + } + + pageHasChanged := checkIfPageHasChangedFunction() + if (pageHasChanged == true){ + return + } + + if (numberOfCompletedBroadcasts == 3){ + // We broadcasted to all 3 hosts. Broadcast is complete. + afterCompletionPage() + return + } + + // Broadcast did not complete all required hosts. + // We will show user option to retry. + + retryButton := getWidgetCentered(widget.NewButtonWithIcon("Retry", theme.ViewRefreshIcon(), retryFunction)) + + exitButton := getWidgetCentered(widget.NewButtonWithIcon("Exit", theme.CancelIcon(), afterCompletionPage)) + + if (numberOfCompletedBroadcasts != 0){ + // Process is complete, and at least 1 host was broadcasted to, but not all hosts. + // This means that we ran out of hosts. + // Seekia will keep broadcasting the content(s) in the background, so nothing needs to be done by the user. + + description1 := getBoldLabelCentered("The broadcast was successful to " + numberOfCompletedBroadcastsString + "/3 hosts.") + description2 := getLabelCentered("We ran out of hosts to contact.") + description3 := getLabelCentered("You can exit or wait for more hosts to be found and retry.") + description4 := getLabelCentered("Seekia will broadcast the " + broadcastType + " in the background either way.") + + page := container.NewVBox(title, widget.NewSeparator(), description1, description2, description3, description4, retryButton, exitButton) + setPageContent(page, window) + return + } + + // Broadcast completed, but 0 hosts were successfully contacted + // Now we will show them a "Broadcast Failed" page and an option to retry. + + description1 := getBoldLabelCentered("The broadcast was unsuccessful.") + description2 := getLabelCentered("Retry the broadcast?") + + descriptionC := 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, widget.NewSeparator(), description1, description2, retryButton, exitButton, widget.NewSeparator(), descriptionC, manageConnectionButton) + + setPageContent(page, window) + return + } + + // Broadcast is not complete + + progressProgressStatusString := "Broadcasted to " + numberOfCompletedBroadcastsString + "/3 hosts." + setBroadcastProgressStatus(processIsCompleteBool, progressProgressStatusString) + broadcastProgressDetailsBinding.Set(processProgressDetails) + + time.Sleep(100 * time.Millisecond) + } + } + + description1 := getBoldLabelCentered(description1Text) + description2 := getLabelCentered("This process will run in the background.") + description3 := getLabelCentered("You can leave this page.") + + broadcastProgressStatusLabel := widget.NewLabelWithData(broadcastProgressStatusBinding) + broadcastProgressStatusLabel.TextStyle = getFyneTextStyle_Bold() + broadcastProgressStatusLabelCentered := getWidgetCentered(broadcastProgressStatusLabel) + + broadcastProgressDetailsLabel := getWidgetCentered(widget.NewLabelWithData(broadcastProgressDetailsBinding)) + + exitPageButton := getWidgetCentered(widget.NewButtonWithIcon("Exit", theme.MediaSkipNextIcon(), func(){ + appMemory.DeleteMemoryEntry("CurrentViewedPage") + afterCompletionPage() + })) + + page := container.NewVBox(title, widget.NewSeparator(), description1, description2, description3, widget.NewSeparator(), broadcastProgressStatusLabelCentered, broadcastProgressDetailsLabel, widget.NewSeparator(), exitPageButton) + + setPageContent(page, window) + + go updateBindingsFunction() +} + +func setViewMyIdentityBalancePage(window fyne.Window, myIdentityType string, previousPage func()){ + + if (myIdentityType != "Mate" && myIdentityType != "Host"){ + setErrorEncounteredPage(window, errors.New("setViewMyIdentityBalancePage called with invalid identity type: " + myIdentityType), previousPage) + return + } + + currentPage := func(){setViewMyIdentityBalancePage(window, myIdentityType, previousPage)} + + title := getPageTitleCentered("My " + myIdentityType + " Identity Balance") + backButton := getBackButtonCentered(previousPage) + + myIdentityExists, myIdentityHash, err := myIdentity.GetMyIdentityHash(myIdentityType) + if (err != nil) { + setErrorEncounteredPage(window, err, previousPage) + return + } + if (myIdentityExists == false){ + // This should not occur, this page should only be reached if identity exists. + setErrorEncounteredPage(window, errors.New("Identity not found."), previousPage) + return + } + + getDescriptionSection := func()*fyne.Container{ + + if (myIdentityType == "Mate"){ + description1 := getLabelCentered("Below is your Mate identity balance.") + description2 := getLabelCentered("It must be sufficient for your profile to be broadcast.") + description3 := getLabelCentered("Spend credit to increase your identity balance.") + description4 := getLabelCentered("You can be gifted credit or buy some with cryptocurrency.") + descriptionSection := container.NewVBox(description1, description2, description3, description4) + return descriptionSection + } + + // myIdentityType == "Host" + description1 := getLabelCentered("Below is your Host identity balance.") + description2 := getLabelCentered("It must be sufficient for you to be a Seekia host.") + description3 := getLabelCentered("Spend credit to increase your identity balance.") + description4 := getLabelCentered("You can be gifted credit or buy some with cryptocurrency.") + + descriptionSection := container.NewVBox(description1, description2, description3, description4) + return descriptionSection + } + + descriptionSection := getDescriptionSection() + + getMyIdentityExpirationTimeDisplaySection := func()(*fyne.Container, error){ + + appNetworkType, err := getAppNetworkType.GetAppNetworkType() + if (err != nil) { return nil, err } + + identityFound, identityIsActivated, balanceIsSufficient, _, balanceExpirationTime, err := myIdentityBalance.GetMyIdentityBalanceStatus(myIdentityHash, appNetworkType) + if (err != nil) { return nil, err } + if (identityFound == false) { + return nil, errors.New("Identity found not found after being found already.") + } + + currentBalanceStatusText := getBoldLabelCentered("My Balance Status:") + + getBalanceStatusIcon := func()(*canvas.Image, error){ + + iconSize := getCustomFyneSize(0) + + if (identityIsActivated == false || balanceIsSufficient == false){ + + insufficientIcon, err := getFyneImageIcon("Insufficient") + if (err != nil) { return nil, err } + + insufficientIcon.SetMinSize(iconSize) + return insufficientIcon, nil + } + sufficientIcon, err := getFyneImageIcon("Sufficient") + if (err != nil) { return nil, err } + sufficientIcon.SetMinSize(iconSize) + + return sufficientIcon, nil + } + + balanceStatusIcon, err := getBalanceStatusIcon() + if (err != nil){ return nil, err } + + balanceStatusIconCentered := getFyneImageCentered(balanceStatusIcon) + + getBalanceStatusText := func()string{ + if (balanceIsSufficient == true){ + return "Sufficient" + } + return "Insufficient" + } + + balanceStatusText := getBalanceStatusText() + balanceStatusLabel := getBoldLabelCentered(balanceStatusText) + + displaySection := container.NewVBox(currentBalanceStatusText, balanceStatusIconCentered, balanceStatusLabel, widget.NewSeparator()) + + if (balanceIsSufficient == true){ + + timeRemainingTitle := getLabelCentered("Time Until Expiration:") + + currentTime := time.Now().Unix() + + if (currentTime > balanceExpirationTime){ + return nil, errors.New("Balance expiration time is less than current time while Balance is sufficient = true") + } + + timeLeft := balanceExpirationTime - currentTime + + timeTranslated, err := helpers.ConvertUnixTimeDurationToUnitsTimeTranslated(timeLeft, true) + if (err != nil) { return nil, err } + + timeRemainingLabel := getBoldLabelCentered(timeTranslated) + + displaySection.Add(timeRemainingTitle) + displaySection.Add(timeRemainingLabel) + } + + refreshBalanceButton := widget.NewButtonWithIcon(translate("Refresh"), theme.ViewRefreshIcon(), func(){ + //TODO: Add manualDownloads download and page to monitor it + showUnderConstructionDialog(window) + }) + + addTimeButton := widget.NewButtonWithIcon(translate("Add Time"), theme.MoveUpIcon(), func(){ + setIncreaseMyIdentityBalancePage(window, myIdentityType, 30, currentPage) + }) + + buttonsGrid := getContainerCentered(container.NewGridWithColumns(1, refreshBalanceButton, addTimeButton)) + + displaySection.Add(buttonsGrid) + + return displaySection, nil + } + + identityExpirationTimeSection, err := getMyIdentityExpirationTimeDisplaySection() + if (err != nil) { + setErrorEncounteredPage(window, err, previousPage) + return + } + + page := container.NewVBox(title, backButton, widget.NewSeparator(), descriptionSection, widget.NewSeparator(), identityExpirationTimeSection) + + setPageContent(page, window) +} + + diff --git a/gui/buildProfileGui_General.go b/gui/buildProfileGui_General.go new file mode 100644 index 0000000..1de186e --- /dev/null +++ b/gui/buildProfileGui_General.go @@ -0,0 +1,3622 @@ +package gui + +// buildProfileGui_General.go impements pages to build the general portion of a user profile +// Some attributes support multiple profileTypes, most attributes are only used for Mate profiles + +import "fyne.io/fyne/v2" +import "fyne.io/fyne/v2/canvas" +import "fyne.io/fyne/v2/container" +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/resources/worldLanguages" +import "seekia/resources/worldLocations" +import "seekia/resources/imageFiles" + +import "seekia/internal/allowedText" +import "seekia/internal/globalSettings" +import "seekia/internal/helpers" +import "seekia/internal/imagery" +import "seekia/internal/mateQuestionnaire" +import "seekia/internal/profiles/myLocalProfiles" + +import "strings" +import "errors" +import "image" +import "reflect" +import "slices" + + +func setBuildMyMateProfilePage(window fyne.Window, previousPage func()){ + + currentPage := func(){setBuildMyMateProfilePage(window, previousPage)} + + title := getPageTitleCentered(translate("Build Mate Profile")) + + backButton := getBackButtonCentered(previousPage) + + description1 := getLabelCentered("Build your Mate profile.") + description2 := getLabelCentered("All information is optional.") + + generalIcon, err := getFyneImageIcon("General") + if (err != nil){ + setErrorEncounteredPage(window, err, previousPage) + return + } + + physicalIcon, err := getFyneImageIcon("Person") + if (err != nil){ + setErrorEncounteredPage(window, err, previousPage) + return + } + + lifestyleIcon, err := getFyneImageIcon("Lifestyle") + if (err != nil){ + setErrorEncounteredPage(window, err, previousPage) + return + } + + mentalIcon, err := getFyneImageIcon("Mental") + if (err != nil){ + setErrorEncounteredPage(window, err, previousPage) + return + } + + generalButton := widget.NewButton(translate("General"), func(){ + setBuildMateProfileCategoryPage_General(window, currentPage) + }) + generalButtonWithIcon := container.NewGridWithRows(2, generalIcon, generalButton) + + physicalButton := widget.NewButton(translate("Physical"), func(){ + setBuildMateProfileCategoryPage_Physical(window, currentPage) + }) + physicalButtonWithIcon := container.NewGridWithRows(2, physicalIcon, physicalButton) + + lifestyleButton := widget.NewButton(translate("Lifestyle"), func(){ + setBuildMateProfileCategoryPage_Lifestyle(window, currentPage) + }) + lifestyleButtonWithIcon := container.NewGridWithRows(2, lifestyleIcon, lifestyleButton) + + mentalButton := widget.NewButton(translate("Mental"), func(){ + setBuildMateProfileCategoryPage_Mental(window, currentPage) + }) + mentalButtonWithIcon := container.NewGridWithRows(2, mentalIcon, mentalButton) + + categoriesRow := container.NewGridWithRows(1, generalButtonWithIcon, physicalButtonWithIcon, lifestyleButtonWithIcon, mentalButtonWithIcon) + + categoriesRowCentered := getContainerCentered(categoriesRow) + + categoriesRowPadded := container.NewPadded(categoriesRowCentered) + + page := container.NewVBox(title, backButton, widget.NewSeparator(), description1, description2, widget.NewSeparator(), categoriesRowPadded) + + setPageContent(page, window) +} + + +func setBuildMateProfileCategoryPage_General(window fyne.Window, previousPage func()){ + + currentPage := func(){setBuildMateProfileCategoryPage_General(window, previousPage)} + + title := getPageTitleCentered(translate("Build Mate Profile - General")) + + backButton := getBackButtonCentered(previousPage) + + profileLanguageButton := widget.NewButton(translate("Profile Language"), func(){ + setBuildProfilePage_ProfileLanguage(window, "Mate", currentPage) + }) + + usernameButton := widget.NewButton(translate("Username"), func(){ + setBuildProfilePage_Username(window, "Mate", currentPage) + }) + + locationButton := widget.NewButton(translate("Location"), func(){ + setBuildMateProfilePage_Location(window, currentPage) + }) + + descriptionButton := widget.NewButton(translate("Description"), func(){ + setBuildProfilePage_Description(window, "Mate", currentPage) + }) + + photosButton := widget.NewButton(translate("Photos"), func(){ + setBuildMateProfilePage_Photos(window, 0, currentPage) + }) + + avatarButton := widget.NewButton(translate("Avatar"), func(){ + setBuildProfilePage_Avatar(window, "Mate", currentPage) + }) + + sexualityButton := widget.NewButton(translate("Sexuality"), func(){ + setBuildMateProfilePage_Sexuality(window, currentPage) + }) + + tagsButton := widget.NewButton(translate("Tags"), func(){ + setBuildMateProfilePage_Tags(window, currentPage) + }) + + questionnaireButton := widget.NewButton(translate("Questionnaire"), func(){ + setBuildMateProfilePage_Questionnaire(window, currentPage) + }) + + buttonsGrid := container.NewGridWithColumns(1, profileLanguageButton, usernameButton, locationButton, descriptionButton, photosButton, avatarButton, sexualityButton, tagsButton, questionnaireButton) + + buttonsGridCentered := getContainerCentered(buttonsGrid) + + buttonsGridPadded := container.NewPadded(buttonsGridCentered) + + page := container.NewVBox(title, backButton, widget.NewSeparator(), buttonsGridPadded) + + setPageContent(page, window) +} + +func setBuildProfilePage_ProfileLanguage(window fyne.Window, profileType string, previousPage func()){ + + currentPage := func(){setBuildProfilePage_ProfileLanguage(window, profileType, previousPage)} + + title := getPageTitleCentered(translate("Build " + profileType + " Profile - General")) + + backButton := getBackButtonCentered(previousPage) + + subtitle := getPageSubtitleCentered(translate("Profile Language")) + + description1 := getLabelCentered("Choose your profile language.") + description2 := getLabelCentered("This is the language that your profile is written in.") + description3 := getLabelCentered("For example, select English if your description/hobbies/... are written in English.") + + currentLanguageExists, currentLanguageIdentifier, err := myLocalProfiles.GetProfileData(profileType, "ProfileLanguage") + if (err != nil) { + setErrorEncounteredPage(window, err, previousPage) + return + } + + getCurrentLanguageLabel := func()(fyne.Widget, error){ + + if (currentLanguageExists == false){ + + noResponseLabel := getBoldItalicLabel("No Response") + + return noResponseLabel, nil + } + + currentLanguageIdentifierInt, err := helpers.ConvertStringToInt(currentLanguageIdentifier) + if (err != nil){ + return nil, errors.New("MyLocalProfile is malformed: Contains invalid ProfileLanguage: " + currentLanguageIdentifier) + } + + currentLanguageObject, err := worldLanguages.GetLanguageObjectFromLanguageIdentifier(currentLanguageIdentifierInt) + if (err != nil) { return nil, err } + + currentLanguageNamesList := currentLanguageObject.NamesList + + languageDescription := helpers.TranslateAndJoinStringListItems(currentLanguageNamesList, "/") + + languageNamesLabel := getBoldLabel(languageDescription) + + return languageNamesLabel, nil + } + + currentLanguageLabel, err := getCurrentLanguageLabel() + if (err != nil){ + setErrorEncounteredPage(window, err, previousPage) + return + } + + myProfileLanguageLabel := widget.NewLabel("My Profile Language:") + + myProfileLanguageRow := container.NewHBox(layout.NewSpacer(), myProfileLanguageLabel, currentLanguageLabel, layout.NewSpacer()) + + getChooseOrEditButtonText := func()string{ + if (currentLanguageExists == false){ + result := translate("Choose Language") + + return result + } + result := translate("Edit Language") + + return result + } + + chooseOrEditButtonText := getChooseOrEditButtonText() + + chooseOrEditLanguageButton := widget.NewButtonWithIcon(chooseOrEditButtonText, theme.DocumentCreateIcon(), func(){ + setBuildProfilePage_ChooseProfileLanguage(window, profileType, currentPage, currentPage) + }) + + noResponseButton := widget.NewButtonWithIcon("No Response", theme.CancelIcon(), func(){ + err := myLocalProfiles.DeleteProfileData(profileType, "ProfileLanguage") + if (err != nil){ + setErrorEncounteredPage(window, err, currentPage) + return + } + currentPage() + }) + + buttonsGrid := getContainerCentered(container.NewGridWithColumns(1, chooseOrEditLanguageButton, noResponseButton)) + + page := container.NewVBox(title, backButton, widget.NewSeparator(), subtitle, widget.NewSeparator(), description1, description2, description3, widget.NewSeparator(), myProfileLanguageRow, widget.NewSeparator(), buttonsGrid) + + setPageContent(page, window) +} + +func setBuildProfilePage_ChooseProfileLanguage(window fyne.Window, profileType string, previousPage func(), nextPage func()){ + + currentPage := func(){setBuildProfilePage_ChooseProfileLanguage(window, profileType, previousPage, nextPage)} + + title := getPageTitleCentered(translate("Build Mate Profile - General")) + + backButton := getBackButtonCentered(previousPage) + + subtitle := getPageSubtitleCentered("Choose Language") + + description := getLabelCentered("Choose your profile 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 the language identifiers + //Map Structure: Language Description -> Language identifier + worldLanguageIdentifiersMap := make(map[string]int) + + for _, languageObject := range worldLanguageObjectsList{ + + languageIdentifier := languageObject.Identifier + languageNamesList := languageObject.NamesList + + languageDescription := helpers.TranslateAndJoinStringListItems(languageNamesList, "/") + + worldLanguageDescriptionsList = append(worldLanguageDescriptionsList, languageDescription) + + worldLanguageIdentifiersMap[languageDescription] = languageIdentifier + } + + helpers.SortStringListToUnicodeOrder(worldLanguageDescriptionsList) + + onSelectedFunction := func(itemIndex int){ + + languageDescription := worldLanguageDescriptionsList[itemIndex] + + languageIdentifier, exists := worldLanguageIdentifiersMap[languageDescription] + if (exists == false){ + setErrorEncounteredPage(window, errors.New("worldLanguageIdentifiersMap missing languageDescription: " + languageDescription), currentPage) + return + } + + languageIdentifierString := helpers.ConvertIntToString(languageIdentifier) + + err := myLocalProfiles.SetProfileData(profileType, "ProfileLanguage", languageIdentifierString) + if (err != nil){ + setErrorEncounteredPage(window, err, currentPage) + return + } + + nextPage() + } + + languagesWidgetList, err := getFyneWidgetListFromStringList(worldLanguageDescriptionsList, onSelectedFunction) + if (err != nil){ + setErrorEncounteredPage(window, err, previousPage) + return + } + + header := container.NewVBox(title, backButton, widget.NewSeparator(), subtitle, widget.NewSeparator(), description, widget.NewSeparator()) + + page := container.NewBorder(header, nil, nil, nil, languagesWidgetList) + + setPageContent(page, window) +} + +func setBuildProfilePage_Username(window fyne.Window, profileType string, previousPage func()){ + + currentPage := func(){setBuildProfilePage_Username(window, profileType, previousPage)} + + pageTitle := getPageTitleCentered(translate("Build " + profileType + " Profile - General")) + backButton := getBackButtonCentered(previousPage) + + pageSubtitle := getPageSubtitleCentered(translate("Username")) + + description1 := getLabelCentered(translate("Create your username.")) + description2 := getLabelCentered(translate("You do not have to enter your real name.")) + description3 := getLabelCentered(translate("If you want to protect your privacy, don't share your surname.")) + + getMyCurrentUsernameRow := func()(*fyne.Container, error){ + + myUsernameLabel := widget.NewLabel("My Username:") + + exists, currentUsername, err := myLocalProfiles.GetProfileData(profileType, "Username") + if (err != nil) { return nil, err } + if (exists == false){ + + noResponseLabel := getBoldItalicLabel("No Response") + currentUsernameRow := container.NewHBox(layout.NewSpacer(), myUsernameLabel, noResponseLabel, layout.NewSpacer()) + + return currentUsernameRow, nil + } + + isAllowed := allowedText.VerifyStringIsAllowed(currentUsername) + if (isAllowed == false){ + return nil, errors.New("My current " + profileType + " username is not allowed: Not valid utf8: " + currentUsername) + } + + containsTabsOrNewlines := helpers.CheckIfStringContainsTabsOrNewlines(currentUsername) + if (containsTabsOrNewlines == true){ + return nil, errors.New("My current " + profileType + " username is not allowed: Contains newline/tab: " + currentUsername) + } + + if( len(currentUsername) > 25){ + return nil, errors.New("Current " + profileType + " username is too long: " + currentUsername) + } + + currentUsernameLabel := getBoldLabel(currentUsername) + + currentUsernameRow := container.NewHBox(layout.NewSpacer(), myUsernameLabel, currentUsernameLabel, layout.NewSpacer()) + + return currentUsernameRow, nil + } + + currentUsernameRow, err := getMyCurrentUsernameRow() + if (err != nil){ + setErrorEncounteredPage(window, err, previousPage) + return + } + + editButton := widget.NewButtonWithIcon("Edit", theme.DocumentCreateIcon(), func(){ + setBuildProfilePage_EditUsername(window, profileType, currentPage, currentPage) + }) + + noResponseButton := widget.NewButtonWithIcon(translate("No Response"), theme.CancelIcon(), func(){ + myLocalProfiles.DeleteProfileData(profileType, "Username") + currentPage() + }) + + buttonsGrid := getContainerCentered(container.NewGridWithColumns(1, editButton, noResponseButton)) + + page := container.NewVBox(pageTitle, backButton, widget.NewSeparator(), pageSubtitle, widget.NewSeparator(), description1, description2, description3, widget.NewSeparator(), currentUsernameRow, widget.NewSeparator(), buttonsGrid) + + setPageContent(page, window) +} + +func setBuildProfilePage_EditUsername(window fyne.Window, profileType string, previousPage func(), nextPage func()){ + + title := getPageTitleCentered("Edit " + profileType + " Username") + + backButton := getBackButtonCentered(previousPage) + + usernameEntry := widget.NewEntry() + + // Outputs: + // -bool: Current username exists + // -string: Current username + // -error + getCurrentUsername := func()(bool, string, error){ + + exists, currentUsername, err := myLocalProfiles.GetProfileData(profileType, "Username") + if (err != nil) { return false, "", err } + if (exists == false){ + return false, "", nil + } + + isAllowed := allowedText.VerifyStringIsAllowed(currentUsername) + if (isAllowed == false){ + return false, "", errors.New("My current " + profileType + " username is not allowed: " + currentUsername) + } + + containsTabsOrNewlines := helpers.CheckIfStringContainsTabsOrNewlines(currentUsername) + if (containsTabsOrNewlines == true){ + return false, "", errors.New("My current " + profileType + " username is not allowed: " + currentUsername) + } + + if( len(currentUsername) > 25){ + return false, "", errors.New("My current " + profileType + " username is too long: " + currentUsername) + } + + return true, currentUsername, nil + } + + currentUsernameExists, currentUsername, err := getCurrentUsername() + if (err != nil){ + setErrorEncounteredPage(window, err, previousPage) + return + } + if (currentUsernameExists == false){ + usernameEntry.SetPlaceHolder("Enter a username...") + } else { + usernameEntry.SetText(currentUsername) + } + + submitButton := getWidgetCentered(widget.NewButtonWithIcon(translate("Save"), theme.ConfirmIcon(), func(){ + + newUsername := usernameEntry.Text + + isAllowed := allowedText.VerifyStringIsAllowed(newUsername) + if (isAllowed == false){ + title := translate("Invalid Username") + dialogMessageA := getLabelCentered(translate("Your username contains 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 + } + + containsTabsOrNewlines := helpers.CheckIfStringContainsTabsOrNewlines(newUsername) + if (containsTabsOrNewlines == true){ + title := translate("Invalid Username") + dialogMessageA := getLabelCentered(translate("Your username contains a tab or a newline character.")) + dialogMessageB := getLabelCentered(translate("Remove this character and resubmit.")) + dialogContent := container.NewVBox(dialogMessageA, dialogMessageB) + dialog.ShowCustom(title, translate("Close"), dialogContent, window) + return + } + + currentBytesLength := len(newUsername) + if (currentBytesLength > 25){ + + currentLengthString := helpers.ConvertIntToString(currentBytesLength) + + title := translate("Invalid Username") + dialogMessageA := getLabelCentered(translate("Username is too long.")) + dialogMessageB := getLabelCentered(translate("Username cannot be longer than 25 bytes.")) + dialogMessageC := getLabelCentered(translate("Your submission length: ") + currentLengthString + " bytes.") + dialogContent := container.NewVBox(dialogMessageA, dialogMessageB, dialogMessageC) + dialog.ShowCustom(title, translate("Close"), dialogContent, window) + return + } + + if (newUsername == ""){ + myLocalProfiles.DeleteProfileData(profileType, "Username") + } else { + myLocalProfiles.SetProfileData(profileType, "Username", newUsername) + } + nextPage() + })) + + emptyLabelA := widget.NewLabel("") + emptyLabelB := widget.NewLabel("") + emptyLabelC := widget.NewLabel("") + + submitButtonWithSpacers := container.NewVBox(submitButton, emptyLabelA, emptyLabelB, emptyLabelC) + + usernameEntryBoxed := getWidgetBoxed(usernameEntry) + + usernameEntryWithSubmitButton := container.NewBorder(nil, submitButtonWithSpacers, nil, nil, usernameEntryBoxed) + + header := container.NewVBox(title, backButton, widget.NewSeparator()) + + page := container.NewBorder(header, nil, nil, nil, usernameEntryWithSubmitButton) + + setPageContent(page, window) +} + + +func setBuildProfilePage_Description(window fyne.Window, profileType string, previousPage func()){ + + currentPage := func(){setBuildProfilePage_Description(window, profileType, previousPage)} + + pageTitle := getPageTitleCentered(translate("Build " + profileType + " Profile - General")) + + backButton := getBackButtonCentered(previousPage) + + pageSubtitle := getPageSubtitleCentered(translate("Description")) + + pageDescription := getLabelCentered(translate("Create your profile description.")) + + getMyCurrentDescriptionRow := func()(*fyne.Container, error){ + + myDescriptionLabel := widget.NewLabel("My Description:") + + exists, currentDescription, err := myLocalProfiles.GetProfileData(profileType, "Description") + if (err != nil) { return nil, err } + if (exists == false){ + + noResponseLabel := getBoldItalicLabel("No Response") + currentDescriptionRow := container.NewHBox(layout.NewSpacer(), myDescriptionLabel, noResponseLabel, layout.NewSpacer()) + + return currentDescriptionRow, nil + } + + isAllowed := allowedText.VerifyStringIsAllowed(currentDescription) + if (isAllowed == false){ + return nil, errors.New("My current " + profileType + "description is not allowed: Not utf8.") + } + + getSizeLimit := func()int{ + if (profileType == "Moderator"){ + return 500 + } + if (profileType == "Host"){ + return 300 + } + return 3000 + } + + sizeLimit := getSizeLimit() + + if (len(currentDescription) > sizeLimit){ + return nil, errors.New("My current " + profileType + " description is too long.") + } + + currentDescriptionTrimmed, _, err := helpers.TrimAndFlattenString(currentDescription, 15) + if (err != nil) { return nil, err } + + currentDescriptionLabel := getBoldLabel(currentDescriptionTrimmed) + + viewMyDescriptionButton := widget.NewButtonWithIcon("", theme.VisibilityIcon(), func(){ + setViewTextPage(window, "Viewing Description", currentDescription, false, currentPage) + }) + + currentDescriptionRow := container.NewHBox(layout.NewSpacer(), myDescriptionLabel, currentDescriptionLabel, viewMyDescriptionButton, layout.NewSpacer()) + + return currentDescriptionRow, nil + } + + currentDescriptionRow, err := getMyCurrentDescriptionRow() + if (err != nil){ + setErrorEncounteredPage(window, err, previousPage) + return + } + + editButton := widget.NewButtonWithIcon("Edit", theme.DocumentCreateIcon(), func(){ + setBuildProfilePage_EditDescription(window, profileType, currentPage, currentPage) + }) + + noResponseButton := widget.NewButtonWithIcon(translate("No Response"), theme.CancelIcon(), func(){ + setBuildProfilePage_DeleteDescription(window, profileType, currentPage, currentPage) + }) + + buttonsGrid := getContainerCentered(container.NewGridWithColumns(1, editButton, noResponseButton)) + + page := container.NewVBox(pageTitle, backButton, widget.NewSeparator(), pageSubtitle, widget.NewSeparator(), pageDescription, widget.NewSeparator(), currentDescriptionRow, widget.NewSeparator(), buttonsGrid) + + setPageContent(page, window) +} + +func setBuildProfilePage_DeleteDescription(window fyne.Window, profileType string, previousPage func(), nextPage func()){ + + title := getPageTitleCentered("Delete " + profileType + " Description") + + backButton := getBackButtonCentered(previousPage) + + description1 := getBoldLabelCentered("Delete " + profileType + " Description?") + + description2 := getLabelCentered("Confirm to delete your description?") + + confirmButton := getWidgetCentered(widget.NewButtonWithIcon("Delete", theme.DeleteIcon(), func(){ + myLocalProfiles.DeleteProfileData(profileType, "Description") + nextPage() + })) + + page := container.NewVBox(title, backButton, widget.NewSeparator(), description1, description2, confirmButton) + + setPageContent(page, window) +} + + +func setBuildProfilePage_EditDescription(window fyne.Window, profileType string, previousPage func(), nextPage func()){ + + title := getPageTitleCentered("Edit " + profileType + " Description") + + backButton := getBackButtonCentered(previousPage) + + // Returns byte length limit for description length + getSizeLimit := func()int{ + if (profileType == "Moderator"){ + return 500 + } + if (profileType == "Host"){ + return 300 + } + return 3000 + } + + sizeLimit := getSizeLimit() + + descriptionEntry := widget.NewMultiLineEntry() + descriptionEntry.Wrapping = 3 + + //Outputs: + // -bool: Current description exists + // -string: Current description + // -error + getCurrentDescription := func()(bool, string, error){ + + exists, currentDescription, err := myLocalProfiles.GetProfileData(profileType, "Description") + if (err != nil) { return false, "", err } + if (exists == false){ + return false, "", nil + } + + isAllowed := allowedText.VerifyStringIsAllowed(currentDescription) + if (isAllowed == false){ + return false, "", errors.New("My current " + profileType + "description is not allowed.") + } + + if (len(currentDescription) > sizeLimit){ + return false, "", errors.New("My current " + profileType + " description is too long.") + } + + return true, currentDescription, nil + } + + currentDescriptionExists, currentDescription, err := getCurrentDescription() + if (err != nil){ + setErrorEncounteredPage(window, err, previousPage) + return + } + if (currentDescriptionExists == false){ + descriptionEntry.SetPlaceHolder("Enter a description...") + } else { + descriptionEntry.SetText(currentDescription) + } + + submitButton := getWidgetCentered(widget.NewButtonWithIcon(translate("Save"), theme.ConfirmIcon(), func(){ + + newDescription := descriptionEntry.Text + + isAllowed := allowedText.VerifyStringIsAllowed(newDescription) + if (isAllowed == false){ + title := translate("Invalid Description") + dialogMessageA := getLabelCentered(translate("Your description contains an invalid character.")) + dialogMessageB := getLabelCentered(translate("Your description must be encoded in UTF-8.")) + dialogContent := container.NewVBox(dialogMessageA, dialogMessageB) + dialog.ShowCustom(title, translate("Close"), dialogContent, window) + return + } + + newDescriptionLength := len(newDescription) + if (newDescriptionLength > sizeLimit){ + + sizeLimitString := helpers.ConvertIntToStringWithCommas(sizeLimit) + currentLengthString := helpers.ConvertIntToString(newDescriptionLength) + + title := translate("Invalid Description") + dialogMessageA := getLabelCentered(translate("Description is too long.")) + dialogMessageB := getLabelCentered(translate("Description cannot be longer than " + sizeLimitString + " bytes.")) + dialogMessageC := getLabelCentered(translate("Your submission is " + currentLengthString + " bytes.")) + dialogContent := container.NewVBox(dialogMessageA, dialogMessageB, dialogMessageC) + dialog.ShowCustom(title, translate("Close"), dialogContent, window) + return + } + + if (newDescription == ""){ + myLocalProfiles.DeleteProfileData(profileType, "Description") + } else { + myLocalProfiles.SetProfileData(profileType, "Description", newDescription) + } + + nextPage() + })) + + emptyLabel := widget.NewLabel("") + + submitButtonWithSpacer := container.NewVBox(submitButton, emptyLabel) + + descriptionEntryBoxed := getWidgetBoxed(descriptionEntry) + + descriptionEntryWithSubmitButton := container.NewBorder(nil, submitButtonWithSpacer, nil, nil, descriptionEntryBoxed) + + header := container.NewVBox(title, backButton, widget.NewSeparator()) + + page := container.NewBorder(header, nil, nil, nil, descriptionEntryWithSubmitButton) + + setPageContent(page, window) +} + +func setBuildMateProfilePage_Location(window fyne.Window, previousPage func()){ + + setLoadingScreen(window, "Build Mate Profile - General", "Loading...") + + currentPage := func(){setBuildMateProfilePage_Location(window, previousPage)} + + title := getPageTitleCentered(translate("Build Mate Profile - General")) + + backButton := getBackButtonCentered(previousPage) + + subtitle := getPageSubtitleCentered(translate("Location")) + + description1 := getLabelCentered("Add your location to your profile.") + description2 := getLabelCentered("You can add a primary and a secondary location.") + + //Outputs: + // -bool: Location exists + // -float64: Location latitude + // -float64: Location longitude + // -bool: Location country exists + // -int: Location country identifier + // -error + getMyLocationInfo := func(rank string)(bool, float64, float64, bool, int, error){ + + if (rank != "Primary" && rank != "Secondary"){ + return false, 0, 0, false, 0, errors.New("getMyLocationInfo called with invalid rank: " + rank) + } + + exists, locationLatitude, err := myLocalProfiles.GetProfileData("Mate", rank + "LocationLatitude") + if (err != nil) { return false, 0, 0, false, 0, err } + if (exists == false){ + + return false, 0, 0, false, 0, nil + + } + exists, locationLongitude, err := myLocalProfiles.GetProfileData("Mate", rank + "LocationLongitude") + if (err != nil) { return false, 0, 0, false, 0, err } + if (exists == false){ + return false, 0, 0, false, 0, errors.New("MyLocalProfiles contains " + rank + "LocationLatitude but missing " + rank + "LocationLongitude") + } + + locationLatitudeFloat64, err := helpers.ConvertStringToFloat64(locationLatitude) + if (err != nil){ + return false, 0, 0, false, 0, errors.New("MyLocalProfiles contains invalid " + rank + "LocationLatitude: " + locationLatitude) + } + + locationLongitudeFloat64, err := helpers.ConvertStringToFloat64(locationLongitude) + if (err != nil){ + return false, 0, 0, false, 0, errors.New("MyLocalProfiles contains invalid " + rank + "LocationLongitude: " + locationLongitude) + } + + exists, locationCountryIdentifier, err := myLocalProfiles.GetProfileData("Mate", rank + "LocationCountry") + if (err != nil) { return false, 0, 0, false, 0, err } + if (exists == false){ + return true, locationLatitudeFloat64, locationLongitudeFloat64, false, 0, nil + } + + locationCountryIdentifierInt, err := helpers.ConvertStringToInt(locationCountryIdentifier) + if (err != nil){ + return false, 0, 0, false, 0, errors.New("MyLocalProfiles contains invalid " + rank + "LocationCountry: " + locationCountryIdentifier) + } + + return true, locationLatitudeFloat64, locationLongitudeFloat64, true, locationCountryIdentifierInt, nil + } + + primaryLocationExists, primaryLocationLatitude, primaryLocationLongitude, primaryLocationCountryExists, primaryLocationCountryIdentifier, err := getMyLocationInfo("Primary") + if (err != nil){ + setErrorEncounteredPage(window, err, previousPage) + return + } + + secondaryLocationExists, secondaryLocationLatitude, secondaryLocationLongitude, secondaryLocationCountryExists, secondaryLocationCountryIdentifier, err := getMyLocationInfo("Secondary") + if (err != nil){ + setErrorEncounteredPage(window, err, previousPage) + return + } + + if (primaryLocationExists == false && secondaryLocationExists == true){ + setErrorEncounteredPage(window, errors.New("Primary location does not exist, secondary location exists."), previousPage) + return + } + + addLocationButton := getWidgetCentered(widget.NewButtonWithIcon("Add Location", theme.ContentAddIcon(), func(){ + if (primaryLocationExists == true && secondaryLocationExists == true){ + title := translate("Cannot Add Location.") + dialogMessageA := getLabelCentered(translate("You can only have 2 locations.")) + dialogMessageB := getLabelCentered(translate("Delete a location first.")) + dialogContent := container.NewVBox(dialogMessageA, dialogMessageB) + dialog.ShowCustom(title, translate("Close"), dialogContent, window) + return + } + + setBuildMateProfilePage_AddLocation_ChooseCountry(window, currentPage, currentPage) + })) + + if (primaryLocationExists == false){ + + noLocationsExistLabel := getBoldLabelCentered("No Locations Exist.") + + page := container.NewVBox(title, backButton, widget.NewSeparator(), subtitle, widget.NewSeparator(), description1, description2, widget.NewSeparator(), noLocationsExistLabel, addLocationButton) + + setPageContent(page, window) + return + } + + visibilityLabel := getBoldLabel("Visibility:") + + visibilityOptions := []string{translate("Show On Profile"), translate("Hide From Profile")} + visibilitySelector := widget.NewSelect(visibilityOptions, func(newVisibility string){ + + getNewVisibilityStatus := func()string{ + + if (newVisibility == translate("Show On Profile")){ + return "Yes" + } + + return "No" + } + + newVisibilityStatus := getNewVisibilityStatus() + + err := myLocalProfiles.SetProfileData("Mate", "VisibilityStatus_Location", newVisibilityStatus) + if (err != nil){ + setErrorEncounteredPage(window, err, currentPage) + return + } + }) + + exists, visibilityStatus, err := myLocalProfiles.GetProfileData("Mate", "VisibilityStatus_Location") + if (err != nil){ + setErrorEncounteredPage(window, err, previousPage) + return + } + if (exists == true && visibilityStatus == "No"){ + visibilitySelector.Selected = translate("Hide From Profile") + } else { + visibilitySelector.Selected = translate("Show On Profile") + } + + profileVisibilityHelpButton := widget.NewButtonWithIcon("", theme.QuestionIcon(), func(){ + setMateProfileAttributeVisibilityExplainerPage(window, currentPage) + }) + + visibilitySelectorRow := container.NewHBox(layout.NewSpacer(), visibilityLabel, visibilitySelector, profileVisibilityHelpButton, layout.NewSpacer()) + + getLocationsGrid := func()(*fyne.Container, error){ + + rankLabel := getItalicLabelCentered("Rank") + + countryLabel := getItalicLabelCentered("Country") + + locationLabel := getItalicLabelCentered("Location") + + emptyLabel := widget.NewLabel("") + + rankColumn := container.NewVBox(rankLabel, widget.NewSeparator()) + countryColumn := container.NewVBox(countryLabel, widget.NewSeparator()) + locationColumn := container.NewVBox(locationLabel, widget.NewSeparator()) + manageButtonsColumn := container.NewVBox(emptyLabel, widget.NewSeparator()) + + addLocationRow := func(locationRank string, locationLatitude float64, locationLongitude float64, locationCountryExists bool, locationCountryIdentifier int)error{ + + getCountryText := func()(string, error){ + if (locationCountryExists == false){ + noneText := translate("None") + return noneText, nil + } + + locationObject, err := worldLocations.GetCountryObjectFromCountryIdentifier(locationCountryIdentifier) + if (err != nil) { return "", err } + + locationNamesList := locationObject.NamesList + + locationDescription := helpers.TranslateAndJoinStringListItems(locationNamesList, "/") + + locationCountryTranslatedTrimmed, _, err := helpers.TrimAndFlattenString(locationDescription, 20) + if (err != nil) { return "", err } + + return locationCountryTranslatedTrimmed, nil + } + + countryText, err := getCountryText() + if (err != nil) { return err } + + getLocationText := func()(string, error){ + + locationCityFound, locationCity, locationState, _, err := worldLocations.GetCityFromCoordinates(locationLatitude, locationLongitude) + if (err != nil) { return "", err } + if (locationCityFound == false){ + + locationLatitudeString := helpers.ConvertFloat64ToStringRounded(locationLatitude, 5) + locationLongitudeString := helpers.ConvertFloat64ToStringRounded(locationLongitude, 5) + + formattedCoordinates := locationLatitudeString + "°, " + locationLongitudeString + "°" + + return formattedCoordinates, nil + } + + locationCityFormatted := locationCity + ", " + locationState + + locationCityTrimmed, _, err := helpers.TrimAndFlattenString(locationCityFormatted, 25) + if (err != nil) { return "", err } + + return locationCityTrimmed, nil + } + + locationText, err := getLocationText() + if (err != nil) { return err } + + locationRankLabel := getBoldLabelCentered(locationRank) + locationCountryLabel := getBoldLabelCentered(countryText) + locationTextLabel := getBoldLabelCentered(locationText) + + manageLocationButton := widget.NewButtonWithIcon("", theme.VisibilityIcon(), func(){ + setBuildMateProfilePage_ManageLocation(window, locationRank, currentPage, currentPage) + }) + + rankColumn.Add(locationRankLabel) + countryColumn.Add(locationCountryLabel) + locationColumn.Add(locationTextLabel) + manageButtonsColumn.Add(manageLocationButton) + + rankColumn.Add(widget.NewSeparator()) + countryColumn.Add(widget.NewSeparator()) + locationColumn.Add(widget.NewSeparator()) + manageButtonsColumn.Add(widget.NewSeparator()) + + return nil + } + + err = addLocationRow("Primary", primaryLocationLatitude, primaryLocationLongitude, primaryLocationCountryExists, primaryLocationCountryIdentifier) + if (err != nil) { return nil, err } + + if (secondaryLocationExists == true){ + err := addLocationRow("Secondary", secondaryLocationLatitude, secondaryLocationLongitude, secondaryLocationCountryExists, secondaryLocationCountryIdentifier) + if (err != nil) { return nil, err } + } + + locationsGrid := container.NewHBox(layout.NewSpacer(), rankColumn, countryColumn, locationColumn, manageButtonsColumn, layout.NewSpacer()) + + if (secondaryLocationExists == false){ + + locationGridWithAddLocationButton := container.NewVBox(locationsGrid, addLocationButton) + + return locationGridWithAddLocationButton, nil + } + + swapLocationsFunction := func()error{ + + primaryLocationLatitudeString := helpers.ConvertFloat64ToString(primaryLocationLatitude) + primaryLocationLongitudeString := helpers.ConvertFloat64ToString(primaryLocationLongitude) + + secondaryLocationLatitudeString := helpers.ConvertFloat64ToString(secondaryLocationLatitude) + secondaryLocationLongitudeString := helpers.ConvertFloat64ToString(secondaryLocationLongitude) + + err = myLocalProfiles.SetProfileData("Mate", "PrimaryLocationLatitude", secondaryLocationLatitudeString) + if (err != nil) { return err } + + err = myLocalProfiles.SetProfileData("Mate", "PrimaryLocationLongitude", secondaryLocationLongitudeString) + if (err != nil) { return err } + + if (secondaryLocationCountryExists == true){ + + secondaryLocationCountryIdentifierString := helpers.ConvertIntToString(secondaryLocationCountryIdentifier) + + err := myLocalProfiles.SetProfileData("Mate", "PrimaryLocationCountry", secondaryLocationCountryIdentifierString) + if (err != nil) { return err } + } else { + err := myLocalProfiles.DeleteProfileData("Mate", "PrimaryLocationCountry") + if (err != nil) { return err } + } + + err = myLocalProfiles.SetProfileData("Mate", "SecondaryLocationLatitude", primaryLocationLatitudeString) + if (err != nil) { return err } + + err = myLocalProfiles.SetProfileData("Mate", "SecondaryLocationLongitude", primaryLocationLongitudeString) + if (err != nil) { return err } + + if (primaryLocationCountryExists == true){ + + primaryLocationCountryIdentifierString := helpers.ConvertIntToString(primaryLocationCountryIdentifier) + + err := myLocalProfiles.SetProfileData("Mate", "SecondaryLocationCountry", primaryLocationCountryIdentifierString) + if (err != nil) { return err } + } else { + err := myLocalProfiles.DeleteProfileData("Mate", "SecondaryLocationCountry") + if (err != nil) { return err } + } + + return nil + } + + swapLocationsButton := widget.NewButtonWithIcon("Swap Locations", theme.ContentRedoIcon(), func(){ + err := swapLocationsFunction() + if (err != nil){ + setErrorEncounteredPage(window, err, currentPage) + return + } + currentPage() + }) + + buttonsGrid := getContainerCentered(container.NewGridWithColumns(1, swapLocationsButton, addLocationButton)) + + locationGridWithButtons := container.NewVBox(locationsGrid, buttonsGrid) + + return locationGridWithButtons, nil + } + + locationsGrid, err := getLocationsGrid() + if (err != nil){ + setErrorEncounteredPage(window, err, previousPage) + return + } + + page := container.NewVBox(title, backButton, widget.NewSeparator(), subtitle, widget.NewSeparator(), description1, description2, widget.NewSeparator(), visibilitySelectorRow, widget.NewSeparator(), locationsGrid) + + setPageContent(page, window) +} + +func setBuildMateProfilePage_ManageLocation(window fyne.Window, locationRank string, previousPage func(), afterDeletePage func()){ + + if (locationRank != "Primary" && locationRank != "Secondary"){ + setErrorEncounteredPage(window, errors.New("setBuildMateProfilePage_ManageLocation called with invalid locationRank: " + locationRank), previousPage) + return + } + + setLoadingScreen(window, "Build Mate Profile - General", "Loading...") + + currentPage := func(){setBuildMateProfilePage_ManageLocation(window, locationRank, previousPage, afterDeletePage)} + + title := getPageTitleCentered(translate("Build Mate Profile - General")) + + backButton := getBackButtonCentered(previousPage) + + subtitle := getPageSubtitleCentered(translate("Manage Location")) + + description1 := getLabelCentered("This is your " + locationRank + " location.") + + //Outputs: + // -bool: Location exists + // -float64: Location latitude + // -float64: Location longitude + // -bool: Location country exists + // -int: Location country identifier + // -error + getMyLocationInfo := func(rank string)(bool, float64, float64, bool, int, error){ + + if (rank != "Primary" && rank != "Secondary"){ + return false, 0, 0, false, 0, errors.New("getMyLocationInfo called with invalid rank: " + rank) + } + + exists, locationLatitude, err := myLocalProfiles.GetProfileData("Mate", rank + "LocationLatitude") + if (err != nil) { return false, 0, 0, false, 0, err } + if (exists == false){ + + return false, 0, 0, false, 0, nil + } + + exists, locationLongitude, err := myLocalProfiles.GetProfileData("Mate", rank + "LocationLongitude") + if (err != nil) { return false, 0, 0, false, 0, err } + if (exists == false){ + return false, 0, 0, false, 0, errors.New("MyLocalProfiles contains " + rank + "LocationLatitude but missing " + rank + "LocationLongitude") + } + + locationLatitudeFloat64, err := helpers.ConvertStringToFloat64(locationLatitude) + if (err != nil){ + return false, 0, 0, false, 0, errors.New("MyLocalProfiles contains invalid " + rank + "LocationLatitude: " + locationLatitude) + } + + locationLongitudeFloat64, err := helpers.ConvertStringToFloat64(locationLongitude) + if (err != nil){ + return false, 0, 0, false, 0, errors.New("MyLocalProfiles contains invalid " + rank + "LocationLongitude: " + locationLongitude) + } + + exists, locationCountryIdentifier, err := myLocalProfiles.GetProfileData("Mate", rank + "LocationCountry") + if (err != nil) { return false, 0, 0, false, 0, err } + if (exists == false){ + return true, locationLatitudeFloat64, locationLongitudeFloat64, false, 0, nil + } + + locationCountryIdentifierInt, err := helpers.ConvertStringToInt(locationCountryIdentifier) + if (err != nil){ + return false, 0, 0, false, 0, errors.New("MyLocalProfiles contains invalid locationCountryIdentifier: " + locationCountryIdentifier) + } + + return true, locationLatitudeFloat64, locationLongitudeFloat64, true, locationCountryIdentifierInt, nil + } + + locationExists, locationLatitude, locationLongitude, locationCountryExists, locationCountryIdentifier, err := getMyLocationInfo(locationRank) + if (err != nil){ + setErrorEncounteredPage(window, err, previousPage) + return + } + if (locationExists == false){ + setErrorEncounteredPage(window, errors.New("setBuildMateProfilePage_ManageLocation called with missing location"), previousPage) + return + } + + getCountryText := func()(string, error){ + if (locationCountryExists == false){ + noneText := translate("None") + return noneText, nil + } + + locationObject, err := worldLocations.GetCountryObjectFromCountryIdentifier(locationCountryIdentifier) + if (err != nil) { return "", err } + + locationNamesList := locationObject.NamesList + + locationDescription := helpers.TranslateAndJoinStringListItems(locationNamesList, "/") + + return locationDescription, nil + } + + countryText, err := getCountryText() + if (err != nil){ + setErrorEncounteredPage(window, err, previousPage) + return + } + + countryLabel := getLabelCentered("Country:") + locationCountryLabel := getBoldLabel(countryText) + + coordinatesLabel := getLabelCentered("Coordinates:") + + locationLatitudeString := helpers.ConvertFloat64ToString(locationLatitude) + locationLongitudeString := helpers.ConvertFloat64ToString(locationLongitude) + + locationCoordinatesLabel := getBoldLabel(locationLatitudeString + "°, " + locationLongitudeString + "°") + + cityLabel := getLabelCentered("City:") + + getCityContent := func()(*fyne.Container, error){ + + // We will either find the exact city, or find the closest city + + cityName, cityState, cityCountryIdentifier, cityDistanceKilometers, err := worldLocations.GetClosestCityFromCoordinates(locationLatitude, locationLongitude) + if (err != nil) { return nil, err } + + if (cityDistanceKilometers == 0){ + + locationCityFormatted := cityName + ", " + cityState + + locationCityLabel := getBoldLabelCentered(locationCityFormatted) + + return locationCityLabel, nil + } + + getNearbyCityDistanceFormattedString := func()(string, error){ + + currentUnitsExist, currentUnits, err := globalSettings.GetSetting("MetricOrImperial") + if (err != nil){ return "", err } + + if (currentUnitsExist == true && currentUnits == "Imperial"){ + + distanceMiles, err := helpers.ConvertKilometersToMiles(cityDistanceKilometers) + if (err != nil){ return "", err } + + distanceMilesString := helpers.ConvertFloat64ToStringRounded(distanceMiles, 1) + result := distanceMilesString + " miles" + return result, nil + } + + distanceKilometersString := helpers.ConvertFloat64ToStringRounded(cityDistanceKilometers, 1) + + result := distanceKilometersString + " kilometers" + + return result, nil + } + + nearbyCityDistanceFormattedString, err := getNearbyCityDistanceFormattedString() + if (err != nil){ return nil, err } + + getNearbyCityNameFormatted := func()(string, error){ + + if (locationCountryExists == true && cityCountryIdentifier == locationCountryIdentifier){ + result := cityName + ", " + cityState + return result, nil + } + + locationObject, err := worldLocations.GetCountryObjectFromCountryIdentifier(cityCountryIdentifier) + if (err != nil) { return "", err } + + locationPrimaryName := locationObject.NamesList[0] + + locationPrimaryNameTranslated := translate(locationPrimaryName) + + result := cityName + ", " + cityState + ", " + locationPrimaryNameTranslated + return result, nil + } + + nearbyCityNameFormatted, err := getNearbyCityNameFormatted() + if (err != nil) { return nil, err } + + nearbyCityNameFormattedAndTrimmed, _, err := helpers.TrimAndFlattenString(nearbyCityNameFormatted, 40) + if (err != nil) { return nil, err } + + locationDistanceLabel := getBoldLabelCentered(nearbyCityDistanceFormattedString + " from") + + locationCityNameLabel := getBoldLabelCentered(nearbyCityNameFormattedAndTrimmed) + + cityInfoContainer := container.NewVBox(locationDistanceLabel, locationCityNameLabel) + + return cityInfoContainer, nil + } + + cityContent, err := getCityContent() + if (err != nil){ + setErrorEncounteredPage(window, err, previousPage) + return + } + + deleteFunction := func()error{ + + if (locationRank == "Secondary"){ + + err := myLocalProfiles.DeleteProfileData("Mate", "SecondaryLocationLatitude") + if (err != nil) { return err } + + err = myLocalProfiles.DeleteProfileData("Mate", "SecondaryLocationLongitude") + if (err != nil) { return err } + + err = myLocalProfiles.DeleteProfileData("Mate", "SecondaryLocationCountry") + if (err != nil) { return err } + + return nil + } + + // We want to delete the primary location + // If there is a secondary location, we set it as the primary after deleting the primary + + primaryLocationExists, _, _, _, _, err := getMyLocationInfo("Primary") + if (err != nil){ return err } + if (primaryLocationExists == false){ + return errors.New("Trying to delete the primary location, but the location does not exist.") + } + + err = myLocalProfiles.DeleteProfileData("Mate", "PrimaryLocationLatitude") + if (err != nil) { return err } + + err = myLocalProfiles.DeleteProfileData("Mate", "PrimaryLocationLongitude") + if (err != nil) { return err } + + err = myLocalProfiles.DeleteProfileData("Mate", "PrimaryLocationCountry") + if (err != nil) { return err } + + secondaryLocationExists, secondaryLocationLatitude, secondaryLocationLongitude, secondaryLocationCountryExists, secondaryLocationCountryIdentifier, err := getMyLocationInfo("Secondary") + if (err != nil){ return err } + if (secondaryLocationExists == false){ + + return nil + } + + err = myLocalProfiles.DeleteProfileData("Mate", "SecondaryLocationLatitude") + if (err != nil) { return err } + + err = myLocalProfiles.DeleteProfileData("Mate", "SecondaryLocationLongitude") + if (err != nil) { return err } + + err = myLocalProfiles.DeleteProfileData("Mate", "SecondaryLocationCountry") + if (err != nil) { return err } + + secondaryLocationLatitudeString := helpers.ConvertFloat64ToString(secondaryLocationLatitude) + secondaryLocationLongitudeString := helpers.ConvertFloat64ToString(secondaryLocationLongitude) + + err = myLocalProfiles.SetProfileData("Mate", "PrimaryLocationLatitude", secondaryLocationLatitudeString) + if (err != nil) { return err } + + err = myLocalProfiles.SetProfileData("Mate", "PrimaryLocationLongitude", secondaryLocationLongitudeString) + if (err != nil) { return err } + + if (secondaryLocationCountryExists == true){ + + secondaryLocationCountryIdentifierString := helpers.ConvertIntToString(secondaryLocationCountryIdentifier) + + err := myLocalProfiles.SetProfileData("Mate", "PrimaryLocationCountry", secondaryLocationCountryIdentifierString) + if (err != nil) { return err } + } + + return nil + } + + deleteButton := getWidgetCentered(widget.NewButtonWithIcon("Delete", theme.DeleteIcon(), func(){ + err := deleteFunction() + if (err != nil){ + setErrorEncounteredPage(window, err, currentPage) + return + } + afterDeletePage() + })) + + page := container.NewVBox(title, backButton, widget.NewSeparator(), subtitle, widget.NewSeparator(), description1, widget.NewSeparator(), countryLabel, locationCountryLabel, widget.NewSeparator(), coordinatesLabel, locationCoordinatesLabel, widget.NewSeparator(), cityLabel, cityContent, widget.NewSeparator(), deleteButton) + + setPageContent(page, window) +} + +func setBuildMateProfilePage_AddLocation_ChooseCountry(window fyne.Window, previousPage func(), visitOnCompletePage func()){ + + currentPage := func(){setBuildMateProfilePage_AddLocation_ChooseCountry(window, previousPage, visitOnCompletePage)} + + title := getPageTitleCentered(translate("Build Mate Profile - General")) + + backButton := getBackButtonCentered(previousPage) + + subtitle := getPageSubtitleCentered(translate("Add Location")) + + description1 := getLabelCentered("Choose the country of the location to add.") + description2 := getLabelCentered("If your country is not listed, select Country Is Missing") + + allCountryObjectsList, err := worldLocations.GetAllCountryObjectsList() + if (err != nil){ + setErrorEncounteredPage(window, err, previousPage) + return + } + + allCountryDescriptionsList := make([]string, 0, len(allCountryObjectsList)) + + // Map Structure: Country Description -> Country identifier + countryIdentifiersMap := make(map[string]int) + + for _, countryObject := range allCountryObjectsList{ + + countryIdentifier := countryObject.Identifier + countryNamesList := countryObject.NamesList + + countryDescription := helpers.TranslateAndJoinStringListItems(countryNamesList, "/") + + countryIdentifiersMap[countryDescription] = countryIdentifier + allCountryDescriptionsList = append(allCountryDescriptionsList, countryDescription) + } + + helpers.SortStringListToUnicodeOrder(allCountryDescriptionsList) + + onSelectedFunction := func(selectedCountryIndex int) { + + selectedCountryDescription := allCountryDescriptionsList[selectedCountryIndex] + + selectedCountryIdentifier, exists := countryIdentifiersMap[selectedCountryDescription] + if (exists == false){ + setErrorEncounteredPage(window, errors.New("countryIdentifiersMap missing country description: " + selectedCountryDescription), currentPage) + return + } + + setBuildMateProfilePage_AddLocation_ChooseCity(window, selectedCountryIdentifier, false, "", currentPage, visitOnCompletePage) + } + + widgetList, err := getFyneWidgetListFromStringList(allCountryDescriptionsList, onSelectedFunction) + if (err != nil){ + setErrorEncounteredPage(window, err, previousPage) + return + } + + header := container.NewVBox(title, backButton, widget.NewSeparator(), subtitle, widget.NewSeparator(), description1, description2, widget.NewSeparator()) + + countryIsMissingButton := getWidgetCentered(widget.NewButton("Country Is Missing", func(){ + setBuildMateProfilePage_AddLocation_CustomCoordinates(window, false, 0, currentPage, visitOnCompletePage) + })) + + page := container.NewBorder(header, countryIsMissingButton, nil, nil, widgetList) + + setPageContent(page, window) +} + +func setBuildMateProfilePage_AddLocation_ChooseCity(window fyne.Window, selectedCountryIdentifier int, searchTermExists bool, searchTerm string, previousPage func(), visitOnCompletePage func()){ + + if (searchTermExists == true){ + setLoadingScreen(window, "Build Mate Profile - General", "Loading Search Results...") + } else { + setLoadingScreen(window, "Build Mate Profile - General", "Loading...") + } + + currentPage := func(){setBuildMateProfilePage_AddLocation_ChooseCity(window, selectedCountryIdentifier, searchTermExists, searchTerm, previousPage, visitOnCompletePage)} + + allCountryCityObjectsList, err := worldLocations.GetAllCityObjectsInCountry(selectedCountryIdentifier) + if (err != nil){ + setErrorEncounteredPage(window, err, previousPage) + return + } + + if (len(allCountryCityObjectsList) == 0){ + // This country has no cities in the database + // We go directly to the custom location screen + setBuildMateProfilePage_AddLocation_CustomCoordinates(window, true, selectedCountryIdentifier, previousPage, visitOnCompletePage) + return + } + + countryObject, err := worldLocations.GetCountryObjectFromCountryIdentifier(selectedCountryIdentifier) + if (err != nil){ + setErrorEncounteredPage(window, err, previousPage) + return + } + + countryNamesList := countryObject.NamesList + + countryDescription := helpers.TranslateAndJoinStringListItems(countryNamesList, "/") + + title := getPageTitleCentered(translate("Build Mate Profile - General")) + + backButton := getBackButtonCentered(previousPage) + + subtitle := getPageSubtitleCentered("Choose Location") + + description1 := getLabelCentered("You must now enter a specific location in " + countryDescription + ".") + description2 := getLabelCentered("You can search for and choose a city from below.") + description3 := getLabelCentered("If your city is not listed, or to add a custom location, select Add Custom") + + addCustomLocationButton := getWidgetCentered(widget.NewButtonWithIcon("Add Custom", theme.ContentAddIcon(), func(){ + setBuildMateProfilePage_AddLocation_CustomCoordinates(window, true, selectedCountryIdentifier, currentPage, visitOnCompletePage) + })) + + enterSearchTermLabel := getBoldLabelCentered("Enter Search Term:") + enterSearchTermEntry := widget.NewEntry() + if (searchTermExists == true){ + enterSearchTermEntry.SetText(searchTerm) + } else { + enterSearchTermEntry.SetPlaceHolder("Enter search term...") + } + + searchButton := widget.NewButtonWithIcon("Search", theme.SearchIcon(), func(){ + + newSearchTerm := enterSearchTermEntry.Text + if (newSearchTerm == searchTerm){ + return + } + + if (newSearchTerm == ""){ + setBuildMateProfilePage_AddLocation_ChooseCity(window, selectedCountryIdentifier, false, "", previousPage, visitOnCompletePage) + return + } + + setBuildMateProfilePage_AddLocation_ChooseCity(window, selectedCountryIdentifier, true, newSearchTerm, previousPage, visitOnCompletePage) + }) + + enterSearchTermRow := getContainerCentered(container.NewGridWithRows(1, enterSearchTermLabel, enterSearchTermEntry, searchButton)) + + header := container.NewVBox(title, backButton, widget.NewSeparator(), subtitle, widget.NewSeparator(), description1, description2, description3, widget.NewSeparator(), addCustomLocationButton, widget.NewSeparator(), enterSearchTermRow, widget.NewSeparator()) + + getCityObjectsToDisplayList := func()[]worldLocations.CityObject{ + + if (searchTermExists == false){ + + return allCountryCityObjectsList + } + + allCityObjectsWithSearchTermList := make([]worldLocations.CityObject, 0) + + searchTermLowercase := strings.ToLower(searchTerm) + + for _, cityObject := range allCountryCityObjectsList{ + + cityName := cityObject.Name + cityNameLowercase := strings.ToLower(cityName) + + containsAny := strings.Contains(cityNameLowercase, searchTermLowercase) + if (containsAny == true){ + allCityObjectsWithSearchTermList = append(allCityObjectsWithSearchTermList, cityObject) + } + } + + return allCityObjectsWithSearchTermList + } + + cityObjectsToDisplayList := getCityObjectsToDisplayList() + + if (len(cityObjectsToDisplayList) == 0){ + + noCitiesFoundLabel := getBoldLabelCentered("No cities found.") + + page := container.NewVBox(header, noCitiesFoundLabel) + + setPageContent(page, window) + return + } + + cityNamesFormattedList := make([]string, 0, len(cityObjectsToDisplayList)) + + for _, cityObject := range cityObjectsToDisplayList{ + + stateName := cityObject.State + cityName := cityObject.Name + + nameFormatted := cityName + ", " + stateName + + nameFormattedAndTrimmed, _, err := helpers.TrimAndFlattenString(nameFormatted, 50) + if (err != nil) { + setErrorEncounteredPage(window, err, previousPage) + return + } + + cityNamesFormattedList = append(cityNamesFormattedList, nameFormattedAndTrimmed) + } + + onClickedFunction := func(cityIndex int){ + + selectedCityObject := cityObjectsToDisplayList[cityIndex] + + selectedCityLatitude := selectedCityObject.Latitude + selectedCityLongitude := selectedCityObject.Longitude + + setBuildMateProfilePage_ConfirmAddLocation(window, true, selectedCountryIdentifier, selectedCityLatitude, selectedCityLongitude, currentPage, visitOnCompletePage) + } + + citySearchResultsWidgetList, err := getFyneWidgetListFromStringList(cityNamesFormattedList, onClickedFunction) + if (err != nil){ + setErrorEncounteredPage(window, err, previousPage) + return + } + + page := container.NewBorder(header, nil, nil, nil, citySearchResultsWidgetList) + + setPageContent(page, window) + return +} + +func setBuildMateProfilePage_AddLocation_CustomCoordinates(window fyne.Window, countryExists bool, countryIdentifier int, previousPage func(), visitOnCompletePage func()){ + + currentPage := func(){setBuildMateProfilePage_AddLocation_CustomCoordinates(window, countryExists, countryIdentifier, previousPage, visitOnCompletePage)} + + title := getPageTitleCentered(translate("Build Mate Profile - General")) + + backButton := getBackButtonCentered(previousPage) + + subtitle := getPageSubtitleCentered(translate("Add Custom Location")) + + getDescription1Text := func()(string, error){ + if (countryExists == false){ + result := translate("Enter a location.") + return result, nil + } + + countryObject, err := worldLocations.GetCountryObjectFromCountryIdentifier(countryIdentifier) + if (err != nil){ return "", err } + + countryNamesList := countryObject.NamesList + + countryDescription := helpers.TranslateAndJoinStringListItems(countryNamesList, "/") + + result := translate("Enter a location in ") + countryDescription + + return result, nil + } + + description1Text, err := getDescription1Text() + if (err != nil){ + setErrorEncounteredPage(window, err, previousPage) + return + } + + description1 := getLabelCentered(description1Text) + description2 := getBoldLabelCentered("We advise against entering your exact location.") + description3 := getLabelCentered("Choose a location that is not a house.") + description4 := getLabelCentered("Use an online map explorer to find the coordinates.") + description5 := getItalicLabelCentered("Latitude Examples: N 31.7619°, S -40.7128°, -65.358579") + description6 := getItalicLabelCentered("Longitude Examples: E 74.0060°, W -106.4850°, -14.1592") + + enterLatitudeLabel := getBoldLabelCentered("Enter Latitude:") + enterLongitudeLabel := getBoldLabelCentered("Enter Longitude:") + + latitudeEntry := widget.NewEntry() + longitudeEntry := widget.NewEntry() + + latitudeEntry.SetPlaceHolder("Enter Latitude...") + longitudeEntry.SetPlaceHolder("Enter Longitude...") + + entryGrid := getContainerCentered(container.NewGridWithColumns(2, enterLatitudeLabel, latitudeEntry, enterLongitudeLabel, longitudeEntry)) + + confirmButton := getWidgetCentered(widget.NewButtonWithIcon("Confirm", theme.ConfirmIcon(), func(){ + + latitudeEntered := latitudeEntry.Text + longitudeEntered := longitudeEntry.Text + + latitudeFormattedA := strings.TrimRight(latitudeEntered, "°NS") + latitudeFormattedB := strings.TrimLeft(latitudeFormattedA, "NS") + latitudeFormattedC := strings.TrimSpace(latitudeFormattedB) + + longitudeFormattedA := strings.TrimRight(longitudeEntered, "°EW") + longitudeFormattedB := strings.TrimLeft(longitudeFormattedA, "EW") + longitudeFormattedC := strings.TrimSpace(longitudeFormattedB) + + latitudeFloat64, err := helpers.ConvertStringToFloat64(latitudeFormattedC) + if (err != nil){ + title := translate("Invalid Latitude.") + dialogMessage := getLabelCentered(translate("Latitude is not a number.")) + dialogContent := container.NewVBox(dialogMessage) + dialog.ShowCustom(title, translate("Close"), dialogContent, window) + return + } + + longitudeFloat64, err := helpers.ConvertStringToFloat64(longitudeFormattedC) + if (err != nil){ + title := translate("Invalid Longitude.") + dialogMessage := getLabelCentered(translate("Longitude is not a number.")) + dialogContent := container.NewVBox(dialogMessage) + dialog.ShowCustom(title, translate("Close"), dialogContent, window) + return + } + + latitudeIsValid := helpers.VerifyLatitude(latitudeFloat64) + if (latitudeIsValid == false){ + title := translate("Invalid Latitude.") + dialogMessage := getLabelCentered(translate("Latitude should be a number between -90 and 90")) + dialogContent := container.NewVBox(dialogMessage) + dialog.ShowCustom(title, translate("Close"), dialogContent, window) + return + } + + longitudeIsValid := helpers.VerifyLongitude(longitudeFloat64) + if (longitudeIsValid == false){ + title := translate("Invalid Longitude.") + dialogMessage := getLabelCentered(translate("Longitude should be a number between -180 and 180")) + dialogContent := container.NewVBox(dialogMessage) + dialog.ShowCustom(title, translate("Close"), dialogContent, window) + return + } + + setBuildMateProfilePage_ConfirmAddLocation(window, countryExists, countryIdentifier, latitudeFloat64, longitudeFloat64, currentPage, visitOnCompletePage) + })) + + page := container.NewVBox(title, backButton, widget.NewSeparator(), subtitle, widget.NewSeparator(), description1, description2, description3, description4, widget.NewSeparator(), description5, description6, widget.NewSeparator(), entryGrid, confirmButton) + + setPageContent(page, window) +} + +func setBuildMateProfilePage_ConfirmAddLocation(window fyne.Window, newLocationCountryExists bool, newLocationCountryIdentifier int, newLocationLatitude float64, newLocationLongitude float64, previousPage func(), visitOnCompletePage func()){ + + currentPage := func(){setBuildMateProfilePage_ConfirmAddLocation(window, newLocationCountryExists, newLocationCountryIdentifier, newLocationLatitude, newLocationLongitude, previousPage, visitOnCompletePage)} + + setLoadingScreen(window, "Build Mate Profile - General", "Loading...") + + title := getPageTitleCentered(translate("Build Mate Profile - General")) + + backButton := getBackButtonCentered(previousPage) + + subtitle := getPageSubtitleCentered(translate("Add Location")) + + description1 := getBoldLabelCentered("Confirm Add Location?") + description2 := getLabelCentered("This location will be publicly displayed on your profile.") + + countryLabel := getLabelCentered("Country:") + + getLocationCountryLabelText := func()(string, error){ + if (newLocationCountryExists == false){ + result := translate("None") + return result, nil + } + + countryObject, err := worldLocations.GetCountryObjectFromCountryIdentifier(newLocationCountryIdentifier) + if (err != nil){ return "", err } + + countryNamesList := countryObject.NamesList + + countryDescription := helpers.TranslateAndJoinStringListItems(countryNamesList, "/") + + result := translate(countryDescription) + return result, nil + } + + locationCountryLabelText, err := getLocationCountryLabelText() + if (err != nil){ + setErrorEncounteredPage(window, err, previousPage) + return + } + + locationCountryLabel := getBoldLabel(locationCountryLabelText) + + coordinatesLabel := getLabelCentered("Coordinates:") + + locationLatitudeString := helpers.ConvertFloat64ToString(newLocationLatitude) + locationLongitudeString := helpers.ConvertFloat64ToString(newLocationLongitude) + + locationCoordinatesLabel := getBoldLabel(locationLatitudeString + "°, " + locationLongitudeString + "°") + + cityLabel := getLabelCentered("City:") + + getCityContent := func()(*fyne.Container, error){ + + // We will either find the exact city, or find the closest city + + cityName, cityState, cityCountryIdentifier, cityDistanceKilometers, err := worldLocations.GetClosestCityFromCoordinates(newLocationLatitude, newLocationLongitude) + if (err != nil) { return nil, err } + + if (cityDistanceKilometers == 0){ + + locationCityFormatted := cityName + ", " + cityState + + locationCityLabel := getBoldLabelCentered(locationCityFormatted) + + return locationCityLabel, nil + } + + getNearbyCityDistanceFormattedString := func()(string, error){ + + currentUnitsExist, currentUnits, err := globalSettings.GetSetting("MetricOrImperial") + if (err != nil){ return "", err } + + if (currentUnitsExist == true && currentUnits == "Imperial"){ + + distanceMiles, err := helpers.ConvertKilometersToMiles(cityDistanceKilometers) + if (err != nil) { return "", err } + + distanceMilesString := helpers.ConvertFloat64ToStringRounded(distanceMiles, 1) + + result := distanceMilesString + " miles" + + return result, nil + } + + distanceKilometersString := helpers.ConvertFloat64ToStringRounded(cityDistanceKilometers, 1) + + result := distanceKilometersString + " kilometers" + + return result, nil + } + + nearbyCityDistanceFormattedString, err := getNearbyCityDistanceFormattedString() + if (err != nil) { return nil, err } + + getNearbyCityNameFormatted := func()(string, error){ + + if (cityCountryIdentifier == newLocationCountryIdentifier){ + result := cityName + ", " + cityState + return result, nil + } + + cityCountryObject, err := worldLocations.GetCountryObjectFromCountryIdentifier(cityCountryIdentifier) + if (err != nil){ return "", err } + + cityCountryNamesList := cityCountryObject.NamesList + + countryPrimaryName := cityCountryNamesList[0] + + countryNameTranslated := translate(countryPrimaryName) + + result := cityName + ", " + cityState + ", " + countryNameTranslated + + return result, nil + } + + nearbyCityNameFormatted, err := getNearbyCityNameFormatted() + if (err != nil) { return nil, err } + + nearbyCityNameFormattedAndTrimmed, _, err := helpers.TrimAndFlattenString(nearbyCityNameFormatted, 40) + if (err != nil) { return nil, err } + + locationDistanceLabel := getBoldLabelCentered(nearbyCityDistanceFormattedString + " from") + + locationCityNameLabel := getBoldLabelCentered(nearbyCityNameFormattedAndTrimmed) + + cityInfoContainer := container.NewVBox(locationDistanceLabel, locationCityNameLabel) + + return cityInfoContainer, nil + } + + cityContent, err := getCityContent() + if (err != nil){ + setErrorEncounteredPage(window, err, previousPage) + return + } + + addLocationFunction := func()error{ + + //Outputs: + // -bool: Location exists + // -float64: Location latitude + // -float64: Location longitude + // -bool: Location country exists + // -int: Location country identifier + // -error + getMyLocationInfo := func(rank string)(bool, float64, float64, bool, int, error){ + + if (rank != "Primary" && rank != "Secondary"){ + return false, 0, 0, false, 0, errors.New("getMyLocationInfo called with invalid rank: " + rank) + } + + exists, locationLatitude, err := myLocalProfiles.GetProfileData("Mate", rank + "LocationLatitude") + if (err != nil) { return false, 0, 0, false, 0, err } + if (exists == false){ + + return false, 0, 0, false, 0, nil + } + + exists, locationLongitude, err := myLocalProfiles.GetProfileData("Mate", rank + "LocationLongitude") + if (err != nil) { return false, 0, 0, false, 0, err } + if (exists == false){ + return false, 0, 0, false, 0, errors.New("MyLocalProfiles contains " + rank + "LocationLatitude but missing " + rank + "LocationLongitude") + } + + locationLatitudeFloat64, err := helpers.ConvertStringToFloat64(locationLatitude) + if (err != nil){ + return false, 0, 0, false, 0, errors.New("MyLocalProfiles contains invalid " + rank + "LocationLatitude") + } + + locationLongitudeFloat64, err := helpers.ConvertStringToFloat64(locationLongitude) + if (err != nil){ + return false, 0, 0, false, 0, errors.New("MyLocalProfiles contains invalid " + rank + "LocationLongitude") + } + + exists, locationCountryIdentifier, err := myLocalProfiles.GetProfileData("Mate", rank + "LocationCountry") + if (err != nil) { return false, 0, 0, false, 0, err } + if (exists == false){ + return true, locationLatitudeFloat64, locationLongitudeFloat64, false, 0, nil + } + + locationCountryIdentifierInt, err := helpers.ConvertStringToInt(locationCountryIdentifier) + if (err != nil){ + return false, 0, 0, false, 0, errors.New("MyLocalProfiles contains invalid " + rank + "LocationCountry: " + locationCountryIdentifier) + } + + return true, locationLatitudeFloat64, locationLongitudeFloat64, true, locationCountryIdentifierInt, nil + } + + primaryLocationExists, primaryLocationLatitude, primaryLocationLongitude, primaryLocationCountryExists, primaryLocationCountryIdentifier, err := getMyLocationInfo("Primary") + if (err != nil){ return err } + + secondaryLocationExists, _, _, _, _, err := getMyLocationInfo("Secondary") + if (err != nil){ return err } + + if (primaryLocationExists == false && secondaryLocationExists == true){ + return errors.New("My Profile has a secondary location, but not a primary location.") + } + if (primaryLocationExists == true && secondaryLocationExists == true){ + return errors.New("Trying to add a location to a profile with 2 locations.") + } + + if (primaryLocationExists == false){ + + err := myLocalProfiles.SetProfileData("Mate", "PrimaryLocationLatitude", locationLatitudeString) + if (err != nil) { return err } + + err = myLocalProfiles.SetProfileData("Mate", "PrimaryLocationLongitude", locationLongitudeString) + if (err != nil) { return err } + + if (newLocationCountryExists == true){ + + newLocationCountryIdentifierString := helpers.ConvertIntToString(newLocationCountryIdentifier) + + err := myLocalProfiles.SetProfileData("Mate", "PrimaryLocationCountry", newLocationCountryIdentifierString) + if (err != nil) { return err } + } + + return nil + } + + // Primary location exists + // We see if the location already exists + + if (primaryLocationLatitude == newLocationLatitude && primaryLocationLongitude == newLocationLongitude){ + if (newLocationCountryExists == false && primaryLocationCountryExists == false){ + // Location is identical, Nothing left to do. + return nil + } + if (newLocationCountryExists == true && primaryLocationCountryExists == true && primaryLocationCountryIdentifier == newLocationCountryIdentifier){ + // Location is identical, Nothing left to do. + return nil + } + } + + err = myLocalProfiles.SetProfileData("Mate", "SecondaryLocationLatitude", locationLatitudeString) + if (err != nil) { return err } + + err = myLocalProfiles.SetProfileData("Mate", "SecondaryLocationLongitude", locationLongitudeString) + if (err != nil) { return err } + + if (newLocationCountryExists == true){ + + newLocationCountryIdentifierString := helpers.ConvertIntToString(newLocationCountryIdentifier) + + err := myLocalProfiles.SetProfileData("Mate", "SecondaryLocationCountry", newLocationCountryIdentifierString) + if (err != nil) { return err } + } + + return nil + } + + confirmButton := getWidgetCentered(widget.NewButtonWithIcon("Confirm", theme.ConfirmIcon(), func(){ + + err := addLocationFunction() + if (err != nil){ + setErrorEncounteredPage(window, err, currentPage) + return + } + + visitOnCompletePage() + })) + + page := container.NewVBox(title, backButton, widget.NewSeparator(), subtitle, widget.NewSeparator(), description1, description2, widget.NewSeparator(), countryLabel, locationCountryLabel, widget.NewSeparator(), coordinatesLabel, locationCoordinatesLabel, widget.NewSeparator(), cityLabel, cityContent, widget.NewSeparator(), confirmButton) + + setPageContent(page, window) +} + + +func setBuildMateProfilePage_Photos(window fyne.Window, currentPhotoIndex int, previousPage func()){ + + currentPage := func(){setBuildMateProfilePage_Photos(window, currentPhotoIndex, previousPage)} + + pageTitle := getPageTitleCentered(translate("Build Mate Profile - General")) + + backButton := getBackButtonCentered(previousPage) + + pageSubtitle := getBoldLabelCentered(translate("Photos")) + + description1 := getLabelCentered(translate("Add photos to your Mate profile.")) + description2 := getLabelCentered(translate("You can add 5 photos.")) + + addImageFileCallbackFunction := func(fileObject fyne.URIReadCloser, err error){ + + if (err != nil) { + title := translate("Failed to open image file.") + dialogMessage := getLabelCentered(translate("Report this error to Seekia developers: " + err.Error())) + dialogContent := container.NewVBox(dialogMessage) + dialog.ShowCustom(title, translate("Close"), dialogContent, window) + return + } + + if (fileObject == nil) { + return + } + + setLoadingScreen(window, "Add Image", "Importing image...") + + filePath := fileObject.URI().String() + filePath = strings.TrimPrefix(filePath, "file://") + + fileExists, ableToReadImage, imageObject, err := imagery.ReadImageFile(filePath) + if (err != nil) { + currentPage() + title := translate("Failed to open image file.") + dialogMessage := getLabelCentered(translate("Report this error to Seekia developers:") + " " + err.Error()) + dialogContent := container.NewVBox(dialogMessage) + dialog.ShowCustom(title, translate("Close"), dialogContent, window) + return + } + if (fileExists == false) { + currentPage() + title := translate("Failed to open image file.") + dialogMessage := getLabelCentered(translate("Report this error to Seekia developers: Image file not found.")) + dialogContent := container.NewVBox(dialogMessage) + dialog.ShowCustom(title, translate("Close"), dialogContent, window) + return + } + + if (ableToReadImage == false) { + currentPage() + title := translate("Failed to import image file.") + dialogMessageA := getLabelCentered(translate("Seekia only supports these image file formats:")) + dialogMessageB := getLabelCentered("JPG, JPEG, PNG, WEBP") + dialogContent := container.NewVBox(dialogMessageA, dialogMessageB) + dialog.ShowCustom(title, translate("Close"), dialogContent, window) + return + } + + // We resize image to something that will be more managable when we edit it + // When we export it, we will downsize it even further + imageObjectResized, err := imagery.DownsizeGolangImage(imageObject, 1500) + if (err != nil) { + currentPage() + title := translate("Failed To Process Image File.") + dialogMessageA := getLabelCentered(translate("Your file may be too large.")) + + errorString := err.Error() + + errorTrimmed, _, err := helpers.TrimAndFlattenString(errorString, 20) + if (err != nil){ + setErrorEncounteredPage(window, err, currentPage) + return + } + + errorTrimmedLabel := getBoldLabel("Error: " + errorTrimmed) + + viewFullErrorButton := widget.NewButtonWithIcon("", theme.VisibilityIcon(), func(){ + setViewTextPage(window, "Viewing Error", errorString, false, currentPage) + }) + + errorDescriptionRow := container.NewHBox(layout.NewSpacer(), errorTrimmedLabel, viewFullErrorButton, layout.NewSpacer()) + + dialogContent := container.NewVBox(dialogMessageA, errorDescriptionRow) + dialog.ShowCustom(title, translate("Close"), dialogContent, window) + return + } + + setSubmitImageToSubmitPageFunction := func(finalImage image.Image, prevPage func()){ + + setLoadingScreen(window, "Add Image To Mate Profile", "Compressing Image...") + + newImageBase64String, err := imagery.ConvertImageObjectToStandardWebpBase64String(finalImage) + if (err != nil) { + setErrorEncounteredPage(window, err, prevPage) + return + } + + setConfirmAddImageToMyMateProfilePage(window, newImageBase64String, prevPage, currentPage) + } + + setEditImagePage(window, imageObjectResized, false, nil, imageObjectResized, currentPage, setSubmitImageToSubmitPageFunction) + } + + //Outputs: + // -bool: Any images exist + // -[]string: Webp Base64 images list + // -error + getCurrentImagesList := func()(bool, []string, error){ + + exists, photosAttributeString, err := myLocalProfiles.GetProfileData("Mate", "Photos") + if (err != nil){ return false, nil, err } + if (exists == false){ + return false, nil, nil + } + + webpImagesList := strings.Split(photosAttributeString, "+") + + if (len(webpImagesList) > 5){ + return false, nil, errors.New("My Photos attribute malformed: More than 5 photos") + } + + return true, webpImagesList, nil + } + + anyImagesExist, myCurrentBase64WebpsList, err := getCurrentImagesList() + if (err != nil){ + setErrorEncounteredPage(window, err, previousPage) + return + } + if (anyImagesExist == false){ + + noImagesExistLabel := getBoldLabelCentered("No Photos Exist.") + + addImageButton := getWidgetCentered(widget.NewButtonWithIcon("Add Image", theme.ContentAddIcon(), func(){ + dialog.ShowFileOpen(addImageFileCallbackFunction, window) + })) + + page := container.NewVBox(pageTitle, backButton, widget.NewSeparator(), pageSubtitle, widget.NewSeparator(), description1, description2, widget.NewSeparator(), addImageButton, widget.NewSeparator(), noImagesExistLabel) + + setPageContent(page, window) + return + } + + numberOfImages := len(myCurrentBase64WebpsList) + + addImageButton := getWidgetCentered(widget.NewButtonWithIcon("Add Image", theme.ContentAddIcon(), func(){ + + if (numberOfImages >= 5){ + title := translate("Image Limit Reached.") + dialogMessageA := getLabelCentered(translate("Your profile can only contain 5 images.")) + dialogMessageB := getLabelCentered(translate("Delete existing images to add more.")) + dialogContent := container.NewVBox(dialogMessageA, dialogMessageB) + dialog.ShowCustom(title, translate("Close"), dialogContent, window) + return + } + + dialog.ShowFileOpen(addImageFileCallbackFunction, window) + })) + + myPhotosLabel := getBoldLabelCentered("My Photos:") + + // We use this function to fix an out-of-bound currentPhotoIndex + getCurrentImageIndex := func()int{ + if (currentPhotoIndex < 0){ + return 0 + } + + finalIndex := len(myCurrentBase64WebpsList) - 1 + if (currentPhotoIndex > finalIndex){ + return finalIndex + } + return currentPhotoIndex + } + + currentImageIndex := getCurrentImageIndex() + + selectButtonsRow := container.NewHBox(layout.NewSpacer()) + + for imageIndex, _ := range myCurrentBase64WebpsList{ + + imageIndexString := helpers.ConvertIntToString(imageIndex+1) + + selectButton := widget.NewButton(imageIndexString, func(){ + setBuildMateProfilePage_Photos(window, imageIndex, previousPage) + }) + + if (imageIndex == currentImageIndex){ + selectButton.Importance = widget.HighImportance + } + + selectButtonsRow.Add(selectButton) + } + + selectButtonsRow.Add(layout.NewSpacer()) + + currentImageBase64 := myCurrentBase64WebpsList[currentImageIndex] + + imageObject, err := imagery.ConvertWEBPBase64StringToCroppedDownsizedImageObject(currentImageBase64) + if (err != nil) { + setErrorEncounteredPage(window, err, previousPage) + return + } + + fyneImageObject := canvas.NewImageFromImage(imageObject) + fyneImageObject.FillMode = canvas.ImageFillContain + fyneImageSize := getCustomFyneSize(50) + fyneImageObject.SetMinSize(fyneImageSize) + + getBackButton := func()fyne.Widget{ + if (currentImageIndex <= 0) { + button := widget.NewButton("", nil) + return button + } + button := widget.NewButtonWithIcon("", theme.NavigateBackIcon(), func(){ + newImageIndex := currentImageIndex - 1 + setBuildMateProfilePage_Photos(window, newImageIndex, previousPage) + }) + return button + } + getNextButton := func()fyne.Widget{ + if (currentImageIndex >= numberOfImages-1) { + button := widget.NewButton("", nil) + return button + } + button := widget.NewButtonWithIcon("", theme.NavigateNextIcon(), func(){ + newImageIndex := currentImageIndex + 1 + setBuildMateProfilePage_Photos(window, newImageIndex, previousPage) + }) + return button + } + + navigateBackButton := getBackButton() + navigateNextButton := getNextButton() + + zoomButton := widget.NewButtonWithIcon("", theme.ZoomInIcon(), func(){ + setViewFullpageImagePage(window, imageObject, currentPage) + }) + + navAndZoomButtonsRow := getContainerCentered(container.NewGridWithRows(1, navigateBackButton, zoomButton, navigateNextButton)) + + deleteButton := getWidgetCentered(widget.NewButtonWithIcon("Delete", theme.DeleteIcon(), func(){ + + confirmDialogCallbackFunction := func(response bool){ + if (response == false){ + return + } + if (numberOfImages == 1){ + err := myLocalProfiles.DeleteProfileData("Mate", "Photos") + if (err != nil){ + setErrorEncounteredPage(window, err, currentPage) + return + } + currentPage() + return + } + + newList, err := helpers.DeleteIndexFromStringList(myCurrentBase64WebpsList, currentImageIndex) + if (err != nil) { + setErrorEncounteredPage(window, err, currentPage) + return + } + newPhotosAttribute := strings.Join(newList, "+") + + err = myLocalProfiles.SetProfileData("Mate", "Photos", newPhotosAttribute) + if (err != nil){ + setErrorEncounteredPage(window, err, currentPage) + return + } + + currentPage() + } + + dialogTitle := translate("Confirm Delete Image?") + dialogMessage := getLabelCentered("Confirm to delete this image?") + dialogContent := container.NewVBox(dialogMessage) + dialog.ShowCustomConfirm(dialogTitle, translate("Yes"), translate("No"), dialogContent, confirmDialogCallbackFunction, window) + })) + + page := container.NewVBox(pageTitle, backButton, widget.NewSeparator(), pageSubtitle, widget.NewSeparator(), description1, description2, widget.NewSeparator(), addImageButton, widget.NewSeparator(), myPhotosLabel, selectButtonsRow, widget.NewSeparator(), fyneImageObject, widget.NewSeparator(), navAndZoomButtonsRow, deleteButton) + + setPageContent(page, window) +} + +func setConfirmAddImageToMyMateProfilePage(window fyne.Window, newImageBase64String string, previousPage func(), nextPage func()){ + + currentPage := func(){setConfirmAddImageToMyMateProfilePage(window, newImageBase64String, previousPage, nextPage)} + + title := getPageTitleCentered(translate("Add Image To Mate Profile")) + + backButton := getBackButtonCentered(previousPage) + + description1 := getBoldLabelCentered("Confirm add image to your Mate profile?") + description2 := getLabelCentered("This image will be displayed on your profile.") + description3 := getLabelCentered("Be aware that the image has been compressed.") + + submitButton := getWidgetCentered(widget.NewButtonWithIcon("Add Image", theme.ConfirmIcon(), func(){ + + setLoadingScreen(window, "Add Image To Mate Profile", "Adding Image...") + + addImageFunction := func()error{ + + getNewAttributeValue := func()(string, error){ + + exists, currentPhotosAttributeValue, err := myLocalProfiles.GetProfileData("Mate", "Photos") + if (err != nil) { return "", err } + if (exists == false){ + return newImageBase64String, nil + } + existingWebpPhotosList := strings.Split(currentPhotosAttributeValue, "+") + + newList := append(existingWebpPhotosList, newImageBase64String) + + newListJoined := strings.Join(newList, "+") + + return newListJoined, nil + } + + newAttributeValue, err := getNewAttributeValue() + if (err != nil){ return err } + + err = myLocalProfiles.SetProfileData("Mate", "Photos", newAttributeValue) + if (err != nil){ return err } + + return nil + } + + err := addImageFunction() + if (err != nil) { + setErrorEncounteredPage(window, err, currentPage) + return + } + nextPage() + })) + + croppedImageObject, err := imagery.ConvertWEBPBase64StringToCroppedDownsizedImageObject(newImageBase64String) + if (err != nil){ + setErrorEncounteredPage(window, err, previousPage) + return + } + + currentImageFyne := canvas.NewImageFromImage(croppedImageObject) + currentImageFyne.FillMode = canvas.ImageFillContain + + viewFullpageButton := getWidgetCentered(widget.NewButtonWithIcon("", theme.ZoomInIcon(), func(){ + setViewFullpageImagePage(window, croppedImageObject, currentPage) + })) + + emptyLabelA := widget.NewLabel("") + + zoomButtonWithSpacer := container.NewVBox(viewFullpageButton, emptyLabelA) + + header := container.NewVBox(title, backButton, widget.NewSeparator(), description1, description2, description3, widget.NewSeparator(), submitButton, widget.NewSeparator()) + + page := container.NewBorder(header, zoomButtonWithSpacer, nil, nil, currentImageFyne) + + setPageContent(page, window) +} + + +func setBuildProfilePage_Avatar(window fyne.Window, profileType string, previousPage func()){ + + currentPage := func(){setBuildProfilePage_Avatar(window, profileType, previousPage)} + + title := getPageTitleCentered(translate("Build " + profileType + " Profile - General")) + + backButton := getBackButtonCentered(previousPage) + + subtitle := getPageSubtitleCentered(translate("Avatar")) + + description := getLabelCentered(translate("Choose your avatar.")) + + currentAvatarLabel := getBoldLabelCentered("Current Avatar:") + + getCurrentAvatarIdentifier := func()(int, error){ + + exists, myAvatarIdentifier, err := myLocalProfiles.GetProfileData(profileType, "Avatar") + if (err != nil) { return 0, err } + if (exists == false){ + return 2929, nil + } + myAvatarIdentifierInt, err := helpers.ConvertStringToInt(myAvatarIdentifier) + if (err != nil) { + return 0, errors.New("MyLocalProfile is malformed: Invalid avatar: " + myAvatarIdentifier) + } + + return myAvatarIdentifierInt, nil + } + + myAvatarIdentifier, err := getCurrentAvatarIdentifier() + if (err != nil){ + setErrorEncounteredPage(window, err, previousPage) + return + } + + emojiImage, err := getEmojiImageObject(myAvatarIdentifier) + if (err != nil){ + setErrorEncounteredPage(window, err, previousPage) + return + } + + emojiImageFyne := canvas.NewImageFromImage(emojiImage) + + nextPageFunction := func(newEmojiIdentifier int){ + + isValid := imageFiles.VerifyEmojiIdentifier(newEmojiIdentifier) + if (isValid == false){ + setErrorEncounteredPage(window, errors.New("Invalid emoji identifier selected from chooseEmojiPage"), currentPage) + return + } + + newEmojiIdentifierString := helpers.ConvertIntToString(newEmojiIdentifier) + + err := myLocalProfiles.SetProfileData(profileType, "Avatar", newEmojiIdentifierString) + if (err != nil){ + setErrorEncounteredPage(window, err, currentPage) + return + } + currentPage() + } + + chooseAvatarButton := getWidgetCentered(widget.NewButtonWithIcon("Choose Avatar", theme.GridIcon(), func(){ + setChooseEmojiPage(window, "Choose Avatar", "People", 0, currentPage, nextPageFunction) + })) + + emptyLabelA := widget.NewLabel("") + emptyLabelB := widget.NewLabel("") + emptyLabelC := widget.NewLabel("") + + chooseAvatarButtonHeightened := container.NewVBox(chooseAvatarButton, emptyLabelA, emptyLabelB, emptyLabelC) + + avatarWithButton := getContainerCentered(container.NewGridWithColumns(1, emojiImageFyne, chooseAvatarButtonHeightened)) + + page := container.NewVBox(title, backButton, widget.NewSeparator(), subtitle, widget.NewSeparator(), description, widget.NewSeparator(), currentAvatarLabel, avatarWithButton) + + setPageContent(page, window) +} + + +func setBuildMateProfilePage_Sexuality(window fyne.Window, previousPage func()){ + + currentPage := func(){setBuildMateProfilePage_Sexuality(window, previousPage)} + + title := getPageTitleCentered(translate("Build Mate Profile - General")) + + backButton := getBackButtonCentered(previousPage) + + pageSubtitle := getPageSubtitleCentered(translate("Sexuality")) + + description := getLabelCentered(translate("What sex(es) are you interested in mating with?")) + + option1Translated := translate("Male") + option2Translated := translate("Female") + option3Translated := translate("Male And Female") + + untranslatedOptionsMap := map[string]string{ + option1Translated: "Male", + option2Translated: "Female", + option3Translated: "Male And Female", + } + + sexualitySelectorOptions := []string{option1Translated, option2Translated, option3Translated} + + sexualitySelector := widget.NewRadioGroup(sexualitySelectorOptions, func(response string){ + + if (response == ""){ + myLocalProfiles.DeleteProfileData("Mate", "Sexuality") + return + } + + responseUntranslated, exists := untranslatedOptionsMap[response] + if (exists == false){ + setErrorEncounteredPage(window, errors.New("untranslatedOptionsMap missing response: " + response), currentPage) + return + } + myLocalProfiles.SetProfileData("Mate", "Sexuality", responseUntranslated) + }) + + exists, currentSexuality, err := myLocalProfiles.GetProfileData("Mate", "Sexuality") + if (err != nil){ + setErrorEncounteredPage(window, err, previousPage) + return + } + if (exists == true){ + if (currentSexuality != "Male" && currentSexuality != "Female" && currentSexuality != "Male And Female"){ + setErrorEncounteredPage(window, errors.New("MyLocalProfiles contains invalid sexuality: " + currentSexuality), previousPage) + return + } + sexualitySelector.Selected = translate(currentSexuality) + } + sexualitySelectorCentered := getWidgetCentered(sexualitySelector) + + noResponseButton := getWidgetCentered(widget.NewButtonWithIcon(translate("No Response"), theme.CancelIcon(), func(){ + myLocalProfiles.DeleteProfileData("Mate", "Sexuality") + currentPage() + })) + + page := container.NewVBox(title, backButton, widget.NewSeparator(), pageSubtitle, widget.NewSeparator(), description, widget.NewSeparator(), sexualitySelectorCentered, noResponseButton) + + setPageContent(page, window) +} + + +func setBuildMateProfilePage_Tags(window fyne.Window, previousPage func()){ + + currentPage := func(){setBuildMateProfilePage_Tags(window, previousPage)} + + title := getPageTitleCentered(translate("Build Mate Profile - General")) + + backButton := getBackButtonCentered(previousPage) + + subtitle := getBoldLabelCentered(translate("Tags")) + + description1 := getLabelCentered("Add tags to your profile.") + description2 := getLabelCentered("Users can search for matches whose tags match custom terms.") + + addTagButton := getWidgetCentered(widget.NewButtonWithIcon("Add Tag", theme.ContentAddIcon(), func(){ + setBuildMateProfilePage_AddTag(window, currentPage, currentPage) + })) + + myTagsExist, myTagsAttributeValue, err := myLocalProfiles.GetProfileData("Mate", "Tags") + if (err != nil) { + setErrorEncounteredPage(window, err, previousPage) + return + } + if (myTagsExist == false){ + + noTagsExistLabel := getBoldLabelCentered("No tags exist.") + + page := container.NewVBox(title, backButton, widget.NewSeparator(), subtitle, widget.NewSeparator(), description1, description2, widget.NewSeparator(), addTagButton, widget.NewSeparator(), noTagsExistLabel) + + setPageContent(page, window) + return + } + + getTagsGrid := func()(*fyne.Container, error){ + + myTagsList := strings.Split(myTagsAttributeValue, "+&") + + nameLabel := getItalicLabelCentered("Name") + emptyLabel := widget.NewLabel("") + + tagNameColumn := container.NewVBox(nameLabel, widget.NewSeparator()) + deleteButtonsColumn := container.NewVBox(emptyLabel, widget.NewSeparator()) + + for _, tagName := range myTagsList{ + + tagNameLabel := getBoldLabelCentered(tagName) + + deleteButton := widget.NewButtonWithIcon("", theme.DeleteIcon(), func(){ + + newList, deletedAny := helpers.DeleteAllMatchingItemsFromStringList(myTagsList, tagName) + if (deletedAny == false){ + setErrorEncounteredPage(window, errors.New("Cannot delete tag: tag not found."), currentPage) + return + } + + if (len(newList) == 0){ + err := myLocalProfiles.DeleteProfileData("Mate", "Tags") + if (err != nil){ + setErrorEncounteredPage(window, err, currentPage) + return + } + currentPage() + return + } + + newTagsAttributeValue := strings.Join(newList, "+&") + + err := myLocalProfiles.SetProfileData("Mate", "Tags", newTagsAttributeValue) + if (err != nil){ + setErrorEncounteredPage(window, err, currentPage) + return + } + currentPage() + }) + + tagNameColumn.Add(tagNameLabel) + deleteButtonsColumn.Add(deleteButton) + + tagNameColumn.Add(widget.NewSeparator()) + deleteButtonsColumn.Add(widget.NewSeparator()) + } + + tagsGrid := container.NewHBox(layout.NewSpacer(), tagNameColumn, deleteButtonsColumn, layout.NewSpacer()) + + return tagsGrid, nil + } + + tagsGrid, err := getTagsGrid() + if (err != nil){ + setErrorEncounteredPage(window, err, previousPage) + return + } + + page := container.NewVBox(title, backButton, widget.NewSeparator(), subtitle, widget.NewSeparator(), description1, description2, widget.NewSeparator(), addTagButton, widget.NewSeparator(), tagsGrid) + + setPageContent(page, window) +} + +func setBuildMateProfilePage_AddTag(window fyne.Window, previousPage func(), nextPage func()){ + + currentPage := func(){setBuildMateProfilePage_AddTag(window, previousPage, nextPage)} + + title := getPageTitleCentered(translate("Build Mate Profile - General")) + + backButton := getBackButtonCentered(previousPage) + + subtitle := getPageSubtitleCentered(translate("Add Tag")) + + enterTagNameLabel := getBoldLabelCentered(translate("Enter Tag Name:")) + + enterTagEntry := widget.NewEntry() + enterTagEntry.SetPlaceHolder("Enter Tag Name...") + + myTagsExist, myTagsAttributeValue, err := myLocalProfiles.GetProfileData("Mate", "Tags") + if (err != nil) { + setErrorEncounteredPage(window, err, previousPage) + return + } + + addTagButton := getWidgetCentered(widget.NewButtonWithIcon("Add Tag", theme.ContentAddIcon(), func(){ + + newTag := enterTagEntry.Text + + if (newTag == ""){ + title := translate("Cannot Add Tag.") + dialogMessage := getLabelCentered(translate("Tag is empty.")) + dialogContent := container.NewVBox(dialogMessage) + dialog.ShowCustom(title, translate("Close"), dialogContent, window) + return + } + + containsDelimiter := strings.Contains(newTag, "+&") + if (containsDelimiter == true){ + title := translate("Cannot Add Tag.") + dialogMessageA := getLabelCentered(translate("Tag contains invalid substring: " + `"+&"`)) + dialogMessageB := getLabelCentered(translate("Remove this substring and resubmit.")) + dialogContent := container.NewVBox(dialogMessageA, dialogMessageB) + dialog.ShowCustom(title, translate("Close"), dialogContent, window) + return + } + + newTagBytesLength := len(newTag) + + if (newTagBytesLength > 40){ + + newTagBytesLengthString := helpers.ConvertIntToString(newTagBytesLength) + + title := translate("Cannot Add Tag.") + dialogMessageA := getLabelCentered(translate("Tag is too long.")) + dialogMessageB := getLabelCentered(translate("Tag cannot be longer than 40 bytes.")) + dialogMessageC := getLabelCentered(translate("Your tag length:") + newTagBytesLengthString) + dialogContent := container.NewVBox(dialogMessageA, dialogMessageB, dialogMessageC) + dialog.ShowCustom(title, translate("Close"), dialogContent, window) + return + } + + if (myTagsExist == false){ + + err := myLocalProfiles.SetProfileData("Mate", "Tags", newTag) + if (err != nil){ + setErrorEncounteredPage(window, err, previousPage) + return + } + nextPage() + return + } + + existingTagsList := strings.Split(myTagsAttributeValue, "+&") + + if (len(existingTagsList) >= 30){ + title := translate("Cannot Create Tag.") + dialogMessageA := getLabelCentered(translate("You cannot have more than 30 tags.")) + dialogMessageB := getLabelCentered(translate("You must first delete an existing tag.")) + dialogContent := container.NewVBox(dialogMessageA, dialogMessageB) + dialog.ShowCustom(title, translate("Close"), dialogContent, window) + return + } + + existingTagsTotalByteLength := 0 + + for _, tagString := range existingTagsList{ + + if (tagString == ""){ + setErrorEncounteredPage(window, errors.New("My tags are invalid: Existing tag is empty."), currentPage) + return + } + + tagByteLength := len(tagString) + + if (tagByteLength > 40){ + setErrorEncounteredPage(window, errors.New("My tags are invalid: Existing tag is too long."), currentPage) + return + } + existingTagsTotalByteLength += tagByteLength + } + + newTagListByteLength := existingTagsTotalByteLength + newTagBytesLength + + if (newTagListByteLength > 500){ + title := translate("Cannot Add Tag.") + dialogMessageA := getLabelCentered(translate("Byte length of all tags exceeds 500.")) + dialogMessageB := getLabelCentered(translate("Enter a shorter tag or delete an existing tag.")) + dialogContent := container.NewVBox(dialogMessageA, dialogMessageB) + dialog.ShowCustom(title, translate("Close"), dialogContent, window) + return + } + + tagAlreadyExists := slices.Contains(existingTagsList, newTag) + if (tagAlreadyExists == true){ + title := translate("Cannot Add Tag.") + dialogMessage := getLabelCentered(translate("Tag already exists.")) + dialogContent := container.NewVBox(dialogMessage) + dialog.ShowCustom(title, translate("Close"), dialogContent, window) + return + } + + isAllowed := allowedText.VerifyStringIsAllowed(newTag) + if (isAllowed == false){ + title := translate("Invalid Tag") + dialogMessageA := getLabelCentered(translate("Your tag contains 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 + } + + containsTabOrNewline := helpers.CheckIfStringContainsTabsOrNewlines(newTag) + if (containsTabOrNewline == true){ + title := translate("Invalid Tag") + dialogMessageA := getLabelCentered(translate("Your tag contains a tab or newline character.")) + dialogMessageB := getLabelCentered(translate("Remove the character and resubmit.")) + dialogContent := container.NewVBox(dialogMessageA, dialogMessageB) + dialog.ShowCustom(title, translate("Close"), dialogContent, window) + return + } + + newTagsList := append(existingTagsList, newTag) + + newTagsAttributeValue := strings.Join(newTagsList, "+&") + + err := myLocalProfiles.SetProfileData("Mate", "Tags", newTagsAttributeValue) + if (err != nil){ + setErrorEncounteredPage(window, err, previousPage) + return + } + + nextPage() + })) + + enterTagEntryWithLabel := getContainerCentered(container.NewGridWithColumns(1, enterTagNameLabel, enterTagEntry)) + + page := container.NewVBox(title, backButton, widget.NewSeparator(), subtitle, widget.NewSeparator(), enterTagEntryWithLabel, addTagButton) + + setPageContent(page, window) +} + + +func setBuildMateProfilePage_Questionnaire(window fyne.Window, previousPage func()){ + + currentPage := func(){setBuildMateProfilePage_Questionnaire(window, previousPage)} + + pageTitle := getPageTitleCentered(translate("Build Mate Profile - General")) + + backButton := getBackButtonCentered(previousPage) + + pageSubtitle := getPageSubtitleCentered(translate("Questionnaire")) + + description1 := getLabelCentered("Create a questionnaire for users to fill out.") + description2 := getLabelCentered("You can filter users who choose your desired answers.") + description3 := getLabelCentered("Your desired answers are private.") + + buildQuestionnaireIcon, err := getFyneImageIcon("BuildQuestionnaire") + if (err != nil){ + setErrorEncounteredPage(window, err, previousPage) + return + } + + desiresIcon, err := getFyneImageIcon("Desires") + if (err != nil){ + setErrorEncounteredPage(window, err, previousPage) + return + } + + viewQuestionnaireIcon, err := getFyneImageIcon("Questionnaire") + if (err != nil){ + setErrorEncounteredPage(window, err, previousPage) + return + } + + buildQuestionnaireButton := widget.NewButton("Build Questionnaire", func(){ + setBuildMateProfilePage_QuestionnaireQuestions(window, 0, currentPage) + }) + chooseDesiresButton := widget.NewButton("Choose Desired Answers", func(){ + //TODO + showUnderConstructionDialog(window) + }) + viewQuestionnaireButton := widget.NewButton("View My Questionnaire", func(){ + + exists, myQuestionnaireRaw, err := myLocalProfiles.GetProfileData("Mate", "Questionnaire") + if (err != nil) { + setErrorEncounteredPage(window, err, previousPage) + return + } + if (exists == false){ + title := translate("No Questions Exist") + dialogMessageA := getLabelCentered("You must add a question before viewing your questionnaire.") + dialogMessageB := getLabelCentered("Add a question on the Build Questionnaire page.") + dialogContent := container.NewVBox(dialogMessageA, dialogMessageB) + dialog.ShowCustom(title, translate("Close"), dialogContent, window) + return + } + + myQuestionnaireObject, err := mateQuestionnaire.ReadQuestionnaireString(myQuestionnaireRaw) + if (err != nil){ + setErrorEncounteredPage(window, err, previousPage) + return + } + + submitPageFunction := func(_ string, _ func()){ + currentPage() + } + + emptyMap := make(map[string]string) + setTakeQuestionnairePage(window, myQuestionnaireObject, 0, emptyMap, currentPage, submitPageFunction) + }) + + buildQuestionnaireButtonWithIcon := container.NewGridWithColumns(1, buildQuestionnaireIcon, buildQuestionnaireButton) + chooseDesiresButtonWithIcon := container.NewGridWithColumns(1, desiresIcon, chooseDesiresButton) + viewQuestionnaireButtonWithIcon := container.NewGridWithColumns(1, viewQuestionnaireIcon, viewQuestionnaireButton) + + buttonsSection := getContainerCentered(container.NewGridWithColumns(1, buildQuestionnaireButtonWithIcon, chooseDesiresButtonWithIcon, viewQuestionnaireButtonWithIcon)) + + page := container.NewVBox(pageTitle, backButton, widget.NewSeparator(), pageSubtitle, widget.NewSeparator(), description1, description2, description3, widget.NewSeparator(), buttonsSection) + + setPageContent(page, window) +} + +func setBuildMateProfilePage_QuestionnaireQuestions(window fyne.Window, pageIndex int, previousPage func()){ + + setLoadingScreen(window, "View Questionnaire Questions ", "Loading questionnaire questions...") + + currentPage := func(){setBuildMateProfilePage_QuestionnaireQuestions(window, pageIndex, previousPage)} + + pageTitle := getPageTitleCentered(translate("Build Mate Profile - General")) + + backButton := getBackButtonCentered(previousPage) + + pageSubtitle := getPageSubtitleCentered(translate("Questionnaire Questions")) + + exists, myQuestionnaireRaw, err := myLocalProfiles.GetProfileData("Mate", "Questionnaire") + if (err != nil) { + setErrorEncounteredPage(window, err, previousPage) + return + } + if (exists == false) { + noQuestionsExistLabel := getBoldLabelCentered("No questions exist.") + + addQuestionButton := getWidgetCentered(widget.NewButtonWithIcon("Add Question", theme.ContentAddIcon(), func(){ + setBuildMateProfilePage_AddQuestionnaireQuestion(window, currentPage, currentPage) + })) + + page := container.NewVBox(pageTitle, backButton, widget.NewSeparator(), pageSubtitle, widget.NewSeparator(), noQuestionsExistLabel, addQuestionButton) + + setPageContent(page, window) + return + } + + myQuestionnaireObject, err := mateQuestionnaire.ReadQuestionnaireString(myQuestionnaireRaw) + if (err != nil) { + setErrorEncounteredPage(window, err, previousPage) + return + } + + deleteQuestionnaireQuestionFunction := func(questionIdentifier string)error{ + + newQuestionnaireQuestionsList := make([]mateQuestionnaire.QuestionObject, 0) + + for _, questionObject := range myQuestionnaireObject{ + + currentQuestionIdentifier := questionObject.Identifier + + if (currentQuestionIdentifier != questionIdentifier){ + newQuestionnaireQuestionsList = append(newQuestionnaireQuestionsList, questionObject) + } + } + + if (len(newQuestionnaireQuestionsList) == 0){ + err := myLocalProfiles.DeleteProfileData("Mate", "Questionnaire") + if (err != nil) { return err } + + return nil + } + + newQuestionnaireRaw, err := mateQuestionnaire.CreateQuestionnaireString(newQuestionnaireQuestionsList) + if (err != nil) { return err } + + err = myLocalProfiles.SetProfileData("Mate", "Questionnaire", newQuestionnaireRaw) + if (err != nil) { return err } + + return nil + } + + increaseOrDecreaseQuestionIndexFunction := func(questionIdentifier string, increaseOrDecrease string)error{ + + // Increase: Move question towards the end of questionnaire + // Decrease: Move question towards the beginning of questionnaire + + if (increaseOrDecrease != "Increase" && increaseOrDecrease != "Decrease"){ + return errors.New("increaseOrDecreaseQuestionIndexFunction called with invalid increaseOrDecrease: " + increaseOrDecrease) + } + + if (len(myQuestionnaireObject) <= 1){ + return errors.New("increaseOrDecreaseQuestionIndexFunction called when questionnaire has <=1 question.") + } + + getQuestionCurrentIndex := func()(int, error){ + + for index, questionObject := range myQuestionnaireObject{ + + currentQuestionIdentifier := questionObject.Identifier + + if (currentQuestionIdentifier == questionIdentifier){ + return index, nil + } + } + //Should not happen, buttons will only be shown for existing questions + return 0, errors.New("Question to increase/decrease not found.") + } + + questionIndex, err := getQuestionCurrentIndex() + if (err != nil) { return err } + + finalIndex := len(myQuestionnaireObject) - 1 + + if (questionIndex == 0 && increaseOrDecrease == "Decrease"){ + return errors.New("Trying to decrease the index of first question.") + } + if (questionIndex == finalIndex && increaseOrDecrease == "Increase"){ + return errors.New("Trying to increase the index of last question.") + } + + swapFunction := reflect.Swapper(myQuestionnaireObject) + + if (increaseOrDecrease == "Increase"){ + swapFunction(questionIndex, questionIndex+1) + } else { + swapFunction(questionIndex-1, questionIndex) + } + + newRawQuestionnaire, err := mateQuestionnaire.CreateQuestionnaireString(myQuestionnaireObject) + if (err != nil) { return err } + + err = myLocalProfiles.SetProfileData("Mate", "Questionnaire", newRawQuestionnaire) + if (err != nil) { return err } + + return nil + } + + addQuestionButton := getWidgetCentered(widget.NewButtonWithIcon("Add Question", theme.ContentAddIcon(), func(){ + + if (len(myQuestionnaireObject) >= 25){ + title := translate("Maximum Question Limit Reached") + dialogMessageA := getLabelCentered("You have added the maximum of 25 questions.") + dialogMessageB := getLabelCentered("You must delete a question to add a new question.") + dialogContent := container.NewVBox(dialogMessageA, dialogMessageB) + dialog.ShowCustom(title, translate("Close"), dialogContent, window) + return + } + + setViewFinalQuestionnaireQuestionsPage := func(){setBuildMateProfilePage_QuestionnaireQuestions(window, 25, previousPage)} + setBuildMateProfilePage_AddQuestionnaireQuestion(window, currentPage, setViewFinalQuestionnaireQuestionsPage) + })) + + getQuestionViewIndex := func()int{ + + if (pageIndex <= 0){ + return 0 + } + + numberOfQuestions := len(myQuestionnaireObject) + + finalQuestionIndex := numberOfQuestions - 1 + + if (pageIndex > finalQuestionIndex){ + + // Index is out of range, show last page of questions + + if (numberOfQuestions <= 5){ + return 0 + } + + lastPageNumberOfQuestions := numberOfQuestions % 5 + if (lastPageNumberOfQuestions == 0){ + lastPageViewIndex := finalQuestionIndex - 4 + return lastPageViewIndex + } + lastPageViewIndex := finalQuestionIndex - lastPageNumberOfQuestions + 1 + return lastPageViewIndex + } + + return pageIndex + } + + questionViewIndex := getQuestionViewIndex() + + getQuestionsGrid := func()(*fyne.Container, error){ + + emptyLabelA := widget.NewLabel("") + typeLabel := getItalicLabelCentered("Type") + contentLabel := getItalicLabelCentered("Content") + emptyLabelB := widget.NewLabel("") + + questionIndexColumn := container.NewVBox(emptyLabelA, widget.NewSeparator()) + questionTypeColumn := container.NewVBox(typeLabel, widget.NewSeparator()) + questionContentColumn := container.NewVBox(contentLabel, widget.NewSeparator()) + buttonsColumn := container.NewVBox(emptyLabelB, widget.NewSeparator()) + + addQuestionRow := func(questionIndex int, questionObject mateQuestionnaire.QuestionObject)error{ + + questionIdentifier := questionObject.Identifier + questionType := questionObject.Type + questionContent := questionObject.Content + questionOptions := questionObject.Options + + getQuestionTypeText := func()(string, error){ + + if (questionType == "Entry"){ + questionTypeText := "Entry - " + questionOptions + + return questionTypeText, nil + } + if (questionType == "Choice"){ + + maximumAnswersAllowed, choicesListString, delimiterFound := strings.Cut(questionOptions, "#") + if (delimiterFound == false){ + return "", errors.New("Invalid choice question options: " + questionOptions) + } + + choicesList := strings.Split(choicesListString, "$¥") + numberOfOptionsString := helpers.ConvertIntToString(len(choicesList)) + if (len(choicesList) > 6){ + return "", errors.New("Invalid choices list: too many choices.") + } + + getMaximumAnswersAllowedAdjusted := func()(int, error){ + + maximumAnswersAllowedInt, err := helpers.ConvertStringToInt(maximumAnswersAllowed) + if (err != nil) { return 0, err } + + if (maximumAnswersAllowedInt < 1 || maximumAnswersAllowedInt > 6){ + return 0, errors.New("Invalid choice maximum answers allowed") + } + if (maximumAnswersAllowedInt > len(choicesList)){ + return len(choicesList), nil + } + return maximumAnswersAllowedInt, nil + } + + maximumAnswersAllowedAdjusted, err := getMaximumAnswersAllowedAdjusted() + if (err != nil) { return "", err } + + maximumAnswersAllowedAdjustedString := helpers.ConvertIntToString(maximumAnswersAllowedAdjusted) + + questionTypeText := numberOfOptionsString + " Choices - Max: " + maximumAnswersAllowedAdjustedString + + return questionTypeText, nil + } + return "", errors.New("Malformed question map: invalid question type: " + questionType) + } + + questionTypeText, err := getQuestionTypeText() + if (err != nil) { return err } + + questionContentTrimmed, _, err := helpers.TrimAndFlattenString(questionContent, 15) + if (err != nil) { return err } + + emptyLabelA := widget.NewLabel("") + questionIndexString := helpers.ConvertIntToString(questionIndex + 1) + questionIndexLabel := getBoldLabel(questionIndexString + ".") + questionIndexCell := container.NewVBox(questionIndexLabel, emptyLabelA) + + emptyLabelB := widget.NewLabel("") + questionTypeLabel := getBoldLabelCentered(questionTypeText) + questionTypeCell := container.NewVBox(questionTypeLabel, emptyLabelB) + + emptyLabelC := widget.NewLabel("") + questionContentLabel := getBoldLabelCentered(questionContentTrimmed) + questionContentCell := container.NewVBox(questionContentLabel, emptyLabelC) + + getQuestionIncreaseDecreaseIndexButtons := func()*fyne.Container{ + + getUpButton := func()fyne.Widget{ + if (questionIndex == 0){ + emptyButton := widget.NewButton("", nil) + return emptyButton + } + upButton := widget.NewButtonWithIcon("", theme.MoveUpIcon(), func(){ + err := increaseOrDecreaseQuestionIndexFunction(questionIdentifier, "Decrease") + if (err != nil) { + setErrorEncounteredPage(window, err, previousPage) + return + } + currentPage() + }) + return upButton + } + + getDownButton := func()fyne.Widget{ + if (questionIndex >= len(myQuestionnaireObject)-1){ + emptyButton := widget.NewButton("", nil) + return emptyButton + } + downButton := widget.NewButtonWithIcon("", theme.MoveDownIcon(), func(){ + err := increaseOrDecreaseQuestionIndexFunction(questionIdentifier, "Increase") + if (err != nil) { + setErrorEncounteredPage(window, err, previousPage) + return + } + currentPage() + }) + return downButton + } + upButton := getUpButton() + downButton := getDownButton() + + buttonsGrid := container.NewGridWithColumns(1, upButton, downButton) + return buttonsGrid + } + + deleteQuestionButtonFunction := func(){ + + confirmDialogCallbackFunction := func(response bool){ + if (response == false){ + return + } + err := deleteQuestionnaireQuestionFunction(questionIdentifier) + if (err != nil) { + setErrorEncounteredPage(window, err, previousPage) + return + } + currentPage() + } + + dialogTitle := translate("Confirm Delete Question?") + dialogMessage := getLabelCentered("Confirm to delete this question?") + dialogContent := container.NewVBox(dialogMessage) + dialog.ShowCustomConfirm(dialogTitle, translate("Yes"), translate("No"), dialogContent, confirmDialogCallbackFunction, window) + } + + deleteQuestionButton := widget.NewButtonWithIcon("Delete", theme.DeleteIcon(), deleteQuestionButtonFunction) + + previewQuestionButton := widget.NewButtonWithIcon("Preview", theme.VisibilityIcon(), func(){ + setPreviewMyQuestionnaireQuestionPage(window, questionObject, currentPage) + }) + + deletePreviewButtons := container.NewVBox(previewQuestionButton, deleteQuestionButton) + + getButtonsCell := func()*fyne.Container{ + + if (len(myQuestionnaireObject) < 2){ + return deletePreviewButtons + } + + questionIncreaseDecreaseIndexButtons := getQuestionIncreaseDecreaseIndexButtons() + + buttonsCell := container.NewHBox(questionIncreaseDecreaseIndexButtons, deletePreviewButtons) + + return buttonsCell + } + + buttonsCell := getButtonsCell() + + questionIndexColumn.Add(questionIndexCell) + questionTypeColumn.Add(questionTypeCell) + questionContentColumn.Add(questionContentCell) + buttonsColumn.Add(buttonsCell) + + questionIndexColumn.Add(widget.NewSeparator()) + questionTypeColumn.Add(widget.NewSeparator()) + questionContentColumn.Add(widget.NewSeparator()) + buttonsColumn.Add(widget.NewSeparator()) + + return nil + } + + pageQuestionsList := myQuestionnaireObject[questionViewIndex:] + + questionsGrid := container.NewHBox(layout.NewSpacer(), questionIndexColumn, questionTypeColumn, questionContentColumn, buttonsColumn, layout.NewSpacer()) + + counter := 0 + for index, questionObject := range pageQuestionsList{ + + if (counter >= 5){ + break + } + + questionIndex := questionViewIndex + index + err := addQuestionRow(questionIndex, questionObject) + if (err != nil) { return nil, err } + + counter += 1 + } + + return questionsGrid, nil + } + + //Outputs: + // -bool: Either button exists + // -*fyne.Container: Buttons + getPageNavigationButtons := func()(bool, *fyne.Container){ + + finalQuestionIndex := len(myQuestionnaireObject) -1 + + if (questionViewIndex == 0 && questionViewIndex > (finalQuestionIndex-5)){ + return false, nil + } + + getPreviousPageButton := func()fyne.Widget{ + if (questionViewIndex == 0){ + emptyButton := widget.NewButton("", nil) + return emptyButton + } + previousButton := widget.NewButtonWithIcon("", theme.NavigateBackIcon(), func(){ + setBuildMateProfilePage_QuestionnaireQuestions(window, questionViewIndex-5, previousPage) + }) + return previousButton + } + + getNextPageButton := func()fyne.Widget{ + if (questionViewIndex > (finalQuestionIndex-5)){ + emptyButton := widget.NewButton("", nil) + return emptyButton + } + + nextButton := widget.NewButtonWithIcon("", theme.NavigateNextIcon(), func(){ + setBuildMateProfilePage_QuestionnaireQuestions(window, questionViewIndex+5, previousPage) + }) + + return nextButton + } + + previousPageButton := getPreviousPageButton() + nextPageButton := getNextPageButton() + + buttonsCentered := getContainerCentered(container.NewGridWithColumns(2, previousPageButton, nextPageButton)) + + return true, buttonsCentered + } + + page := container.NewVBox(pageTitle, backButton, widget.NewSeparator(), pageSubtitle, widget.NewSeparator(), addQuestionButton, widget.NewSeparator()) + + navigationButtonsNeeded, navigationButtons := getPageNavigationButtons() + if (navigationButtonsNeeded == true){ + page.Add(navigationButtons) + } + + questionsGrid, err := getQuestionsGrid() + if (err != nil) { + setErrorEncounteredPage(window, err, previousPage) + return + } + + questionsGridCentered := getContainerCentered(questionsGrid) + page.Add(questionsGridCentered) + + setPageContent(page, window) +} + +func setPreviewMyQuestionnaireQuestionPage(window fyne.Window, questionObject mateQuestionnaire.QuestionObject, previousPage func()){ + + currentPage := func(){setPreviewMyQuestionnaireQuestionPage(window, questionObject, previousPage)} + + title := getPageTitleCentered(translate("Preview Question")) + + backButton := getBackButtonCentered(previousPage) + + myResponsesMap := make(map[string]string) + + viewQuestionContainer, err := getViewQuestionnaireQuestionContainer(window, currentPage, questionObject, myResponsesMap) + if (err != nil){ + setErrorEncounteredPage(window, err, previousPage) + return + } + + page := container.NewVBox(title, backButton, widget.NewSeparator(), viewQuestionContainer) + + setPageContent(page, window) +} + + +func setBuildMateProfilePage_AddQuestionnaireQuestion(window fyne.Window, previousPage func(), afterCreatePage func()){ + + currentPage := func(){setBuildMateProfilePage_AddQuestionnaireQuestion(window, previousPage, afterCreatePage)} + + pageTitle := getPageTitleCentered(translate("Build Mate Profile - General")) + + backButton := getBackButtonCentered(previousPage) + + pageSubtitle := getPageSubtitleCentered(translate("Add Questionnaire Question")) + + addQuestionToMyQuestionnaireFunction := func(newQuestionObject mateQuestionnaire.QuestionObject)error{ + + getNewQuestionnaireObject := func()([]mateQuestionnaire.QuestionObject, error){ + + exists, myQuestionnaireRaw, err := myLocalProfiles.GetProfileData("Mate", "Questionnaire") + if (err != nil) { return nil, err } + if (exists == false) { + + newQuestionnaireObject := []mateQuestionnaire.QuestionObject{newQuestionObject} + + return newQuestionnaireObject, nil + } + + existingQuestionnaireObject, err := mateQuestionnaire.ReadQuestionnaireString(myQuestionnaireRaw) + if (err != nil) { return nil, err } + + newQuestionnaireObject := append(existingQuestionnaireObject, newQuestionObject) + + return newQuestionnaireObject, nil + } + + newQuestionnaireObject, err := getNewQuestionnaireObject() + if (err != nil) { return err } + + newQuestionnaireString, err := mateQuestionnaire.CreateQuestionnaireString(newQuestionnaireObject) + if (err != nil) { return err } + + err = myLocalProfiles.SetProfileData("Mate", "Questionnaire", newQuestionnaireString) + if (err != nil) { return err } + + return nil + } + + addQuestionDescription := getLabelCentered("Choose the type of question to add:") + + iconSize := getCustomFyneSize(10) + + choiceIcon, err := getFyneImageIcon("Choice") + if (err != nil){ + setErrorEncounteredPage(window, err, previousPage) + return + } + choiceIcon.SetMinSize(iconSize) + + entryIcon, err := getFyneImageIcon("Entry") + if (err != nil){ + setErrorEncounteredPage(window, err, previousPage) + return + } + entryIcon.SetMinSize(iconSize) + + choiceButton := widget.NewButton("Choice", func(){ + setBuildMateProfilePage_AddQuestionnaireQuestion_Choice(window, addQuestionToMyQuestionnaireFunction, currentPage, afterCreatePage) + + }) + entryButton := widget.NewButton("Entry", func(){ + setBuildMateProfilePage_AddQuestionnaireQuestion_Entry(window, addQuestionToMyQuestionnaireFunction, currentPage, afterCreatePage) + }) + + choiceButtonWithIcon := container.NewVBox(choiceIcon, choiceButton) + entryButtonWithIcon := container.NewVBox(entryIcon, entryButton) + + choicesSection := container.NewHBox(layout.NewSpacer(), choiceButtonWithIcon, entryButtonWithIcon, layout.NewSpacer()) + + page := container.NewVBox(pageTitle, backButton, widget.NewSeparator(), pageSubtitle, widget.NewSeparator(), addQuestionDescription, choicesSection) + + setPageContent(page, window) +} + + +func setBuildMateProfilePage_AddQuestionnaireQuestion_Choice(window fyne.Window, addQuestionFunction func(mateQuestionnaire.QuestionObject)error, previousPage func(), afterCreatePage func()){ + + title := getPageTitleCentered("Add Questionnaire Question - Choice") + + backButton := getBackButtonCentered(previousPage) + + description := getLabelCentered("A choice question has multiple answers to choose from.") + + enterQuestionLabel := getBoldLabelCentered(" Enter Question: ") + + enterQuestionEntry := widget.NewEntry() + enterQuestionEntry.SetPlaceHolder("Enter question.") + enterQuestionEntryBoxed := getWidgetBoxed(enterQuestionEntry) + + enterQuestionLabelWithEntry := getContainerCentered(container.NewGridWithColumns(1, enterQuestionLabel, enterQuestionEntryBoxed)) + + maximumAnswersAllowedLabel := widget.NewLabel("Maximum Answers Allowed:") + selectOptions := []string{"1", "2", "3", "4", "5", "6"} + maximumAnswersAllowedSelector := widget.NewSelect(selectOptions, nil) + maximumAnswersAllowedSelector.Selected = "6" + maximumAnswersAllowedInfoButton := widget.NewButtonWithIcon("", theme.QuestionIcon(), func(){ + + dialogTitle := translate("Maximum Answers Allowed") + dialogMessageA := getLabelCentered(translate("Choose the maximum number of answers a user can submit.")) + dialogMessageB := getLabelCentered(translate("For example, if you select 1, users can only select 1 choice.")) + dialogContent := container.NewVBox(dialogMessageA, dialogMessageB) + dialog.ShowCustom(dialogTitle, translate("Close"), dialogContent, window) + }) + + maximumAnswersAllowedRow := container.NewHBox(layout.NewSpacer(), maximumAnswersAllowedLabel, maximumAnswersAllowedSelector, maximumAnswersAllowedInfoButton, layout.NewSpacer()) + + emptyLabelA := widget.NewLabel("") + choiceContentLabel := getItalicLabelCentered(" Choice Content ") + emptyLabelB := widget.NewLabel("") + + choiceIndexColumn := container.NewVBox(emptyLabelA, widget.NewSeparator()) + choiceEntryColumn := container.NewVBox(choiceContentLabel, widget.NewSeparator()) + clearChoiceButtonsColumn := container.NewVBox(emptyLabelB, widget.NewSeparator()) + + addChoiceRow := func(index string)func()string{ + + indexLabel := getBoldLabelCentered(index) + + choiceEntry := widget.NewEntry() + choiceEntry.SetPlaceHolder("Enter choice...") + + clearChoiceButton := widget.NewButtonWithIcon("Clear", theme.CancelIcon(), func(){ + choiceEntry.SetText("") + choiceEntry.SetPlaceHolder("Enter choice...") + }) + + choiceEntryColumn.Add(choiceEntry) + choiceIndexColumn.Add(indexLabel) + clearChoiceButtonsColumn.Add(clearChoiceButton) + + getChoiceFunction := func()string{ + return choiceEntry.Text + } + + return getChoiceFunction + } + + getChoice1Function := addChoiceRow("1.") + getChoice2Function := addChoiceRow("2.") + getChoice3Function := addChoiceRow("3.") + getChoice4Function := addChoiceRow("4.") + getChoice5Function := addChoiceRow("5.") + getChoice6Function := addChoiceRow("6.") + + choicesGrid := container.NewHBox(layout.NewSpacer(), choiceIndexColumn, choiceEntryColumn, clearChoiceButtonsColumn, layout.NewSpacer()) + + submitButton := getWidgetCentered(widget.NewButtonWithIcon("Create Question", theme.ConfirmIcon(), func(){ + + newQuestionContent := enterQuestionEntry.Text + + if (newQuestionContent == ""){ + dialogTitle := translate("No question provided.") + dialogMessage := translate("You must enter a question.") + dialogContent := getLabelCentered(dialogMessage) + dialog.ShowCustom(dialogTitle, translate("Close"), dialogContent, window) + return + } + + isAllowed := allowedText.VerifyStringIsAllowed(newQuestionContent) + if (isAllowed == false){ + dialogTitle := translate("Question Is Invalid.") + dialogMessageA := getLabelCentered(translate("Question 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 + } + + // The choice cannot contain these strings because we user them for encoding + unallowedStringsList := []string{"+&", "%¢"} + + for _, unallowedString := range unallowedStringsList{ + + isContained := strings.Contains(newQuestionContent, unallowedString) + if (isContained == true){ + dialogTitle := translate("Question Is Invalid.") + dialogMessageA := getLabelCentered(translate("Question contains prohibited string: ") + unallowedString) + dialogMessageB := getLabelCentered(translate("Remove this string and resubmit.")) + dialogContent := container.NewVBox(dialogMessageA, dialogMessageB) + dialog.ShowCustom(dialogTitle, translate("Close"), dialogContent, window) + return + } + } + + if (len(newQuestionContent) > 500){ + + questionNumberOfBytesString := helpers.ConvertIntToString(len(newQuestionContent)) + + dialogTitle := translate("Question Is Invalid.") + dialogMessageA := getLabelCentered(translate("Question is longer than 500 bytes.")) + dialogMessageB := getLabelCentered(translate("Your question byte count: " + questionNumberOfBytesString)) + dialogContent := container.NewVBox(dialogMessageA, dialogMessageB) + dialog.ShowCustom(dialogTitle, translate("Close"), dialogContent, window) + return + } + + choice1 := getChoice1Function() + choice2 := getChoice2Function() + choice3 := getChoice3Function() + choice4 := getChoice4Function() + choice5 := getChoice5Function() + choice6 := getChoice6Function() + + choiceEntryValuesList := []string{choice1, choice2, choice3, choice4, choice5, choice6} + + newChoicesList := make([]string, 0) + + // We use a map to detect duplicates. + choicesMap := make(map[string]struct{}) + + for index, choiceString := range choiceEntryValuesList{ + + if (choiceString == ""){ + continue + } + + choiceIndexString := helpers.ConvertIntToString(index+1) + + choiceIsAllowed := allowedText.VerifyStringIsAllowed(choiceString) + if (choiceIsAllowed == false){ + dialogTitle := translate("Choice " + choiceIndexString + " Is Invalid.") + dialogMessageA := getLabelCentered(translate("Choice " + choiceIndexString + " 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 + } + + unallowedStringsList := []string{"+&", "%¢", "$¥"} + + for _, unallowedString := range unallowedStringsList{ + + isContained := strings.Contains(choiceString, unallowedString) + if (isContained == true){ + dialogTitle := translate("Question Is Invalid.") + dialogMessageA := getLabelCentered(translate("Choice " + choiceIndexString + " contains prohibited string: ") + unallowedString) + dialogMessageB := getLabelCentered(translate("Remove this string and resubmit.")) + dialogContent := container.NewVBox(dialogMessageA, dialogMessageB) + dialog.ShowCustom(dialogTitle, translate("Close"), dialogContent, window) + return + } + } + + if (len(choiceString) > 100){ + + choiceNumberOfBytesString := helpers.ConvertIntToString(len(choiceString)) + + dialogTitle := translate("Choice Is Invalid.") + dialogMessageA := getLabelCentered(translate("Choice " + choiceIndexString + " is longer than 100 bytes.")) + dialogMessageB := getLabelCentered(translate("Choice byte count: " + choiceNumberOfBytesString)) + dialogContent := container.NewVBox(dialogMessageA, dialogMessageB) + dialog.ShowCustom(dialogTitle, translate("Close"), dialogContent, window) + return + } + + _, exists := choicesMap[choiceString] + if (exists == true){ + dialogTitle := translate("Duplicate Choice Exists") + dialogMessage := translate("Each choice must be unique.") + dialogContent := getLabelCentered(dialogMessage) + dialog.ShowCustom(dialogTitle, translate("Close"), dialogContent, window) + return + } + + choicesMap[choiceString] = struct{}{} + + newChoicesList = append(newChoicesList, choiceString) + } + + if (len(newChoicesList) < 2){ + dialogTitle := translate("Not enough choices.") + dialogMessage := translate("You must enter at least 2 choices.") + dialogContent := getLabelCentered(dialogMessage) + dialog.ShowCustom(dialogTitle, translate("Close"), dialogContent, window) + return + } + + maximumAnswersAllowedString := maximumAnswersAllowedSelector.Selected + + newQuestionIdentifier, err := helpers.GetNewRandomHexString(9) + if (err != nil){ + setErrorEncounteredPage(window, err, previousPage) + return + } + + questionOptionsChoicesJoined := strings.Join(newChoicesList, "$¥") + questionOptions := maximumAnswersAllowedString + "#" + questionOptionsChoicesJoined + + newQuestionObject := mateQuestionnaire.QuestionObject{ + Identifier: newQuestionIdentifier, + Type: "Choice", + Content: newQuestionContent, + Options: questionOptions, + } + + err = addQuestionFunction(newQuestionObject) + if (err != nil){ + setErrorEncounteredPage(window, err, previousPage) + return + } + + afterCreatePage() + })) + + page := container.NewVBox(title, backButton, widget.NewSeparator(), description, widget.NewSeparator(), enterQuestionLabelWithEntry, widget.NewSeparator(), maximumAnswersAllowedRow, widget.NewSeparator(), choicesGrid, submitButton) + + setPageContent(page, window) +} + + +func setBuildMateProfilePage_AddQuestionnaireQuestion_Entry(window fyne.Window, addQuestionFunction func(mateQuestionnaire.QuestionObject)error, previousPage func(), afterCreatePage func()){ + + title := getPageTitleCentered(translate("Add Questionnaire Question - Entry")) + + backButton := getBackButtonCentered(previousPage) + + description1 := getLabelCentered("Entry questions allow responders to enter any text.") + description2 := getLabelCentered("Select Numeric to restrict responses to only allow numbers.") + description3 := getLabelCentered("Numeric allows you to filter users who respond within a desired range.") + + enterQuestionLabel := getBoldLabelCentered("Enter Question:") + enterQuestionEntry := widget.NewMultiLineEntry() + enterQuestionEntry.Wrapping = 3 + enterQuestionEntry.SetPlaceHolder("Enter question.") + enterQuestionEntryBoxed := getWidgetBoxed(enterQuestionEntry) + + numericCheckbox := widget.NewCheck("Numeric Responses Only", nil) + + submitButton := getWidgetCentered(widget.NewButtonWithIcon("Add Question", theme.ConfirmIcon(), func(){ + + newQuestionContent := enterQuestionEntry.Text + + if (newQuestionContent == ""){ + dialogTitle := translate("No question provided.") + dialogMessage := translate("You must enter a question.") + dialogContent := getLabelCentered(dialogMessage) + dialog.ShowCustom(dialogTitle, translate("Close"), dialogContent, window) + return + } + + if (len(newQuestionContent) > 500){ + + newQuestionBytesString := helpers.ConvertIntToString(len(newQuestionContent)) + + dialogTitle := translate("Question Is Invalid.") + dialogMessageA := getLabelCentered(translate("Question cannot be longer than 500 bytes.")) + dialogMessageB := getLabelCentered(translate("Provided question byte count: " + newQuestionBytesString)) + dialogContent := container.NewVBox(dialogMessageA, dialogMessageB) + dialog.ShowCustom(dialogTitle, translate("Close"), dialogContent, window) + return + } + + isAllowed := allowedText.VerifyStringIsAllowed(newQuestionContent) + if (isAllowed == false){ + dialogTitle := translate("Question Is Invalid.") + dialogMessageA := getLabelCentered(translate("Question 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 + } + + // The question cannot contain these strings because we user them for encoding + unallowedStringsList := []string{"+&", "%¢"} + + for _, unallowedString := range unallowedStringsList{ + + isContained := strings.Contains(newQuestionContent, unallowedString) + if (isContained == true){ + dialogTitle := translate("Question Is Invalid.") + dialogMessageA := getLabelCentered(translate("Question contains prohibited string: ") + unallowedString) + dialogMessageB := getLabelCentered(translate("Remove this string and resubmit.")) + dialogContent := container.NewVBox(dialogMessageA, dialogMessageB) + dialog.ShowCustom(dialogTitle, translate("Close"), dialogContent, window) + return + } + } + + isNumeric := numericCheckbox.Checked + + newQuestionIdentifier, err := helpers.GetNewRandomHexString(10) + if (err != nil){ + setErrorEncounteredPage(window, err, previousPage) + return + } + + getQuestionOptions := func()string{ + if (isNumeric == true){ + return "Numeric" + } + return "Any" + } + questionOptions := getQuestionOptions() + + newQuestionObject := mateQuestionnaire.QuestionObject{ + + Identifier: newQuestionIdentifier, + Type: "Entry", + Content: newQuestionContent, + Options: questionOptions, + } + + err = addQuestionFunction(newQuestionObject) + if (err != nil){ + setErrorEncounteredPage(window, err, previousPage) + return + } + + afterCreatePage() + })) + + submitButtonWithCheckbox := container.NewVBox(numericCheckbox, submitButton) + + entryWithCheckBoxAndSubmitButton := getContainerCentered(container.NewGridWithColumns(1, enterQuestionEntryBoxed, submitButtonWithCheckbox)) + + page := container.NewVBox(title, backButton, widget.NewSeparator(), description1, description2, description3, widget.NewSeparator(), enterQuestionLabel, entryWithCheckBoxAndSubmitButton) + + setPageContent(page, window) +} + + + + diff --git a/gui/buildProfileGui_Lifestyle.go b/gui/buildProfileGui_Lifestyle.go new file mode 100644 index 0000000..3d8bae0 --- /dev/null +++ b/gui/buildProfileGui_Lifestyle.go @@ -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) +} + + + diff --git a/gui/buildProfileGui_Mental.go b/gui/buildProfileGui_Mental.go new file mode 100644 index 0000000..ebf93b3 --- /dev/null +++ b/gui/buildProfileGui_Mental.go @@ -0,0 +1,1108 @@ + +package gui + +// buildProfileGui_Mental.go implements pages to build the Mental portion of a mate profile + +import "fyne.io/fyne/v2" +import "fyne.io/fyne/v2/container" +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/resources/worldLanguages" + +import "seekia/internal/allowedText" +import "seekia/internal/helpers" +import "seekia/internal/profiles/myLocalProfiles" + +import "strings" +import "errors" + + +func setBuildMateProfileCategoryPage_Mental(window fyne.Window, previousPage func()){ + + currentPage := func(){setBuildMateProfileCategoryPage_Mental(window, previousPage)} + + title := getPageTitleCentered(translate("Build Mate Profile - Mental")) + + backButton := getBackButtonCentered(previousPage) + + languageButton := widget.NewButton(translate("Language"), func(){ + setBuildMateProfilePage_Language(window, currentPage) + }) + + beliefsButton := widget.NewButton(translate("Beliefs"), func(){ + setBuildMateProfilePage_Beliefs(window, currentPage) + }) + + genderIdentityButton := widget.NewButton(translate("Gender Identity"), func(){ + setBuildMateProfilePage_GenderIdentity(window, currentPage) + }) + + petsButton := widget.NewButton(translate("Pets"), func(){ + setBuildMateProfilePage_Pets(window, currentPage) + }) + + buttonsGrid := container.NewGridWithColumns(1, languageButton, beliefsButton, genderIdentityButton, petsButton) + + buttonsGridCentered := getContainerCentered(buttonsGrid) + + buttonsGridPadded := container.NewPadded(buttonsGridCentered) + + page := container.NewVBox(title, backButton, widget.NewSeparator(), buttonsGridPadded) + + setPageContent(page, window) +} + + +func setBuildMateProfilePage_Language(window fyne.Window, previousPage func()){ + + currentPage := func(){setBuildMateProfilePage_Language(window, previousPage)} + + title := getPageTitleCentered(translate("Build Mate Profile - Mental")) + + backButton := getBackButtonCentered(previousPage) + + subtitle := getPageSubtitleCentered("Language") + + description1 := getLabelCentered("Select the languages you understand.") + description2 := getLabelCentered("Rate your ability to understand each language.") + description3 := getLabelCentered("1 = Low fluency, 5 = High fluency") + + myLanguagesAttributeExists, myLanguagesAttribute, err := myLocalProfiles.GetProfileData("Mate", "Language") + if (err != nil){ + setErrorEncounteredPage(window, err, previousPage) + return + } + + addLanguageButton := getWidgetCentered(widget.NewButtonWithIcon("Add Language", theme.ContentAddIcon(), func(){ + setBuildMateProfilePage_ChooseLanguage(window, currentPage, currentPage) + })) + + if (myLanguagesAttributeExists == false){ + + noLanguagesExistLabel := getBoldLabelCentered("No Languages Exist.") + + page := container.NewVBox(title, backButton, widget.NewSeparator(), subtitle, widget.NewSeparator(), description1, description2, description3, widget.NewSeparator(), noLanguagesExistLabel, addLanguageButton) + setPageContent(page, window) + return + } + + getLanguagesGrid := func()(*fyne.Container, error){ + + worldLanguageObjectsMap, err := worldLanguages.GetWorldLanguageObjectsMap() + if (err != nil) { return nil, err } + + languageNameLabel := getItalicLabelCentered(translate("Language Name")) + + fluencyLabel := getItalicLabelCentered(translate("Fluency")) + + emptyLabel := widget.NewLabel("") + + languageDescriptionsColumn := container.NewVBox(languageNameLabel, widget.NewSeparator()) + languageRatingsColumn := container.NewVBox(fluencyLabel, widget.NewSeparator()) + manageLanguageButtonsColumn := container.NewVBox(emptyLabel, widget.NewSeparator()) + + myLanguageItemsList := strings.Split(myLanguagesAttribute, "+&") + + for _, languageItem := range myLanguageItemsList{ + + languageName, languageRating, delimiterFound := strings.Cut(languageItem, "$") + if (delimiterFound == false){ + return nil, errors.New("MyLocalProfiles contains invalid language item: missing $: " + languageItem) + } + + languageRatingInt, err := helpers.ConvertStringToInt(languageRating) + if (err != nil){ + return nil, errors.New("MyLocalProfiles contains invalid language item: Language rating not an int: " + languageRating) + } + + if (languageRatingInt < 1 || languageRatingInt > 5){ + return nil, errors.New("MyLocalProfiles contains invalid language item: Language rating is invalid: " + languageRating) + } + + getLanguageDescription := func()string{ + + languageObject, exists := worldLanguageObjectsMap[languageName] + if (exists == false){ + // Language name must be custom + return languageName + } + + languageNamesList := languageObject.NamesList + + languageDescription := helpers.TranslateAndJoinStringListItems(languageNamesList, "/") + + return languageDescription + } + + languageDescription := getLanguageDescription() + + languageDescriptionLabel := getBoldLabelCentered(languageDescription) + + languageRatingLabel := getBoldLabelCentered(languageRating + "/5") + + manageLanguageButton := widget.NewButtonWithIcon("", theme.VisibilityIcon(), func(){ + setBuildMateProfilePage_ManageLanguage(window, languageName, currentPage, currentPage) + }) + + languageDescriptionsColumn.Add(languageDescriptionLabel) + languageRatingsColumn.Add(languageRatingLabel) + manageLanguageButtonsColumn.Add(manageLanguageButton) + + languageDescriptionsColumn.Add(widget.NewSeparator()) + languageRatingsColumn.Add(widget.NewSeparator()) + manageLanguageButtonsColumn.Add(widget.NewSeparator()) + } + + languagesGrid := container.NewHBox(layout.NewSpacer(), languageDescriptionsColumn, languageRatingsColumn, manageLanguageButtonsColumn, layout.NewSpacer()) + + return languagesGrid, nil + } + + languagesGrid, err := getLanguagesGrid() + if (err != nil){ + setErrorEncounteredPage(window, err, previousPage) + return + } + + page := container.NewVBox(title, backButton, widget.NewSeparator(), subtitle, widget.NewSeparator(), description1, description2, description3, widget.NewSeparator(), addLanguageButton, widget.NewSeparator(), languagesGrid) + + setPageContent(page, window) +} + + +func setBuildMateProfilePage_ChooseLanguage(window fyne.Window, previousPage func(), onCompletePage func()){ + + currentPage := func(){setBuildMateProfilePage_ChooseLanguage(window, previousPage, onCompletePage)} + + title := getPageTitleCentered(translate("Build Mate Profile - Mental")) + + backButton := getBackButtonCentered(previousPage) + + subtitle := getPageSubtitleCentered("Choose Language") + + description1 := getLabelCentered("Choose a language.") + description2 := getLabelCentered("If your language is not listed, select Language Is Missing.") + + 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 the language names + // If a language has multiple names, the first name is used + //Map Structure: Language Description -> Language primary name + worldLanguageNamesMap := make(map[string]string) + + for _, languageObject := range worldLanguageObjectsList{ + + languageNamesList := languageObject.NamesList + + languagePrimaryName := languageNamesList[0] + + languageDescription := helpers.TranslateAndJoinStringListItems(languageNamesList, "/") + + worldLanguageDescriptionsList = append(worldLanguageDescriptionsList, languageDescription) + + worldLanguageNamesMap[languageDescription] = languagePrimaryName + } + + helpers.SortStringListToUnicodeOrder(worldLanguageDescriptionsList) + + onSelectedFunction := func(itemIndex int){ + + languageDescription := worldLanguageDescriptionsList[itemIndex] + + languagePrimaryName, exists := worldLanguageNamesMap[languageDescription] + if (exists == false){ + setErrorEncounteredPage(window, errors.New("worldLanguageNamesMap missing languageDescription"), currentPage) + return + } + + setBuildMateProfilePage_AddLanguage(window, languagePrimaryName, currentPage, onCompletePage) + } + + languagesWidgetList, err := getFyneWidgetListFromStringList(worldLanguageDescriptionsList, onSelectedFunction) + if (err != nil){ + setErrorEncounteredPage(window, err, previousPage) + return + } + + languageIsMissingButton := getWidgetCentered(widget.NewButton("Language Is Missing", func(){ + setBuildMateProfilePage_EnterCustomLanguage(window, currentPage, onCompletePage) + })) + + header := container.NewVBox(title, backButton, widget.NewSeparator(), subtitle, widget.NewSeparator(), description1, description2, widget.NewSeparator()) + + page := container.NewBorder(header, languageIsMissingButton, nil, nil, languagesWidgetList) + + setPageContent(page, window) +} + +func setBuildMateProfilePage_EnterCustomLanguage(window fyne.Window, previousPage func(), onCompletePage func()){ + + currentPage := func(){setBuildMateProfilePage_EnterCustomLanguage(window, previousPage, onCompletePage)} + + title := getPageTitleCentered(translate("Buid Mate Profile - Mental")) + + backButton := getBackButtonCentered(previousPage) + + subtitle := getPageSubtitleCentered("Add Custom Language") + + description := widget.NewLabel("Enter your custom language.") + + languageEntry := widget.NewEntry() + languageEntry.SetPlaceHolder(translate("Enter language...")) + + languageEntryWithDescription := getContainerCentered(container.NewGridWithColumns(1, description, languageEntry)) + + submitButton := getWidgetCentered(widget.NewButtonWithIcon("Submit", theme.ConfirmIcon(), func(){ + + newLanguage := languageEntry.Text + + if (newLanguage == ""){ + dialogTitle := translate("Language Is Empty.") + dialogMessage := getLabelCentered(translate("You must provide a language.")) + dialogContent := container.NewVBox(dialogMessage) + dialog.ShowCustom(dialogTitle, translate("Close"), dialogContent, window) + return + } + + if (len(newLanguage) > 30){ + dialogTitle := translate("Language Is Too Long.") + dialogMessage := getLabelCentered(translate("Language cannot be longer than 30 bytes.")) + dialogContent := container.NewVBox(dialogMessage) + dialog.ShowCustom(dialogTitle, translate("Close"), dialogContent, window) + return + } + + isAllowed := allowedText.VerifyStringIsAllowed(newLanguage) + if (isAllowed == false){ + dialogTitle := translate("Invalid Language") + dialogMessageA := getLabelCentered(translate("Your language contains an invalid character.")) + dialogMessageB := getLabelCentered(translate("It must be encoded in UTF-8.")) + dialogContent := container.NewVBox(dialogMessageA, dialogMessageB) + dialog.ShowCustom(dialogTitle, translate("Close"), dialogContent, window) + return + } + + containsTabsOrNewlines := helpers.CheckIfStringContainsTabsOrNewlines(newLanguage) + if (containsTabsOrNewlines == true){ + dialogTitle := translate("Invalid Language") + dialogMessageA := getLabelCentered(translate("Your language contains a tab or a newline character.")) + dialogMessageB := getLabelCentered(translate("Remove the character and resubmit.")) + dialogContent := container.NewVBox(dialogMessageA, dialogMessageB) + dialog.ShowCustom(dialogTitle, translate("Close"), dialogContent, window) + return + } + + containsDelimiter := strings.Contains(newLanguage, "+&") + if (containsDelimiter == true){ + dialogTitle := translate("Cannot Add Language.") + dialogMessageA := getLabelCentered(translate("Language contains invalid substring: " + `"+&"`)) + dialogMessageB := getLabelCentered(translate("Remove this substring and resubmit.")) + dialogContent := container.NewVBox(dialogMessageA, dialogMessageB) + dialog.ShowCustom(dialogTitle, translate("Close"), dialogContent, window) + return + } + + containsDelimiter = strings.Contains(newLanguage, "$") + if (containsDelimiter == true){ + dialogTitle := translate("Cannot Add Language.") + dialogMessageA := getLabelCentered(translate("Language contains invalid substring: " + `"$"`)) + dialogMessageB := getLabelCentered(translate("Remove this substring and resubmit.")) + dialogContent := container.NewVBox(dialogMessageA, dialogMessageB) + dialog.ShowCustom(dialogTitle, translate("Close"), dialogContent, window) + return + } + + setBuildMateProfilePage_AddLanguage(window, newLanguage, currentPage, onCompletePage) + })) + + page := container.NewVBox(title, backButton, widget.NewSeparator(), subtitle, widget.NewSeparator(), languageEntryWithDescription, submitButton) + + setPageContent(page, window) +} + + +func setBuildMateProfilePage_AddLanguage(window fyne.Window, languagePrimaryName string, previousPage func(), onCompletePage func()){ + + currentPage := func(){setBuildMateProfilePage_AddLanguage(window, languagePrimaryName, previousPage, onCompletePage)} + + title := getPageTitleCentered(translate("Build Mate Profile - Mental")) + + backButton := getBackButtonCentered(previousPage) + + subtitle := getPageSubtitleCentered("Add Language") + + languageNameLabel := getLabelCentered("Language Name:") + + getLanguageNameText := func()(string, error){ + + worldLanguageObjectsMap, err := worldLanguages.GetWorldLanguageObjectsMap() + if (err != nil) { return "", err } + + languageObject, exists := worldLanguageObjectsMap[languagePrimaryName] + if (exists == false){ + // Language name must be custom + return languagePrimaryName, nil + } + + languageNamesList := languageObject.NamesList + + languageNamesListFormatted := helpers.TranslateAndJoinStringListItems(languageNamesList, "/") + + return languageNamesListFormatted, nil + } + + languageNameText, err := getLanguageNameText() + if (err != nil){ + setErrorEncounteredPage(window, err, previousPage) + return + } + + languageNameTextLabel := getBoldLabelCentered(languageNameText) + + description1 := getLabelCentered("Choose how well you understand this language.") + description2 := getLabelCentered("1 = Least Fluency, 5 = Highest Fluency") + + optionsList := []string{"1", "2", "3", "4", "5"} + fluencySelector := widget.NewSelect(optionsList, nil) + + fluencySelector.PlaceHolder = translate("Select one...") + + fluencySelectorCentered := getWidgetCentered(fluencySelector) + + submitFunction := func(){ + + myLanguageAttributeExists, existingMyLanguageAttribute, err := myLocalProfiles.GetProfileData("Mate", "Language") + if (err != nil){ + setErrorEncounteredPage(window, err, currentPage) + return + } + + if (myLanguageAttributeExists == true){ + existingMyLanguageAttributeList := strings.Split(existingMyLanguageAttribute, "+&") + + if (len(existingMyLanguageAttributeList) >= 100){ + dialogTitle := translate("Language Limit Reached.") + dialogMessage := getLabelCentered(translate("You cannot add more than 100 languages.")) + dialogContent := container.NewVBox(dialogMessage) + dialog.ShowCustom(dialogTitle, translate("Close"), dialogContent, window) + return + } + } + + newLanguageRating := fluencySelector.Selected + + if (newLanguageRating == ""){ + dialogTitle := translate("Missing Language Fluency Rating.") + dialogMessage := getLabelCentered(translate("You must choose a language fluency rating.")) + dialogContent := container.NewVBox(dialogMessage) + dialog.ShowCustom(dialogTitle, translate("Close"), dialogContent, window) + return + } + + newLanguageItem := languagePrimaryName + "$" + newLanguageRating + + getNewLanguageAttribute := func()(string, error){ + + if (myLanguageAttributeExists == false){ + return newLanguageItem, nil + } + + // We check to see if language already exists + // If so, we replace existing language + + existingMyLanguageAttributeList := strings.Split(existingMyLanguageAttribute, "+&") + + newMyLanguageAttributeList := make([]string, 0) + + for _, languageItem := range existingMyLanguageAttributeList{ + + itemLanguageName, _, delimiterFound := strings.Cut(languageItem, "$") + if (delimiterFound == false){ + return "", errors.New("MyLocalProfiles contains invalid myLanguage attribute: " + languageItem) + } + + if (languagePrimaryName == itemLanguageName){ + // Language already exists, we will skip this language item + continue + } + newMyLanguageAttributeList = append(newMyLanguageAttributeList, languageItem) + } + + newMyLanguageAttributeList = append(newMyLanguageAttributeList, newLanguageItem) + + newMyLanguageAttribute := strings.Join(newMyLanguageAttributeList, "+&") + + return newMyLanguageAttribute, nil + } + + newLanguageAttribute, err := getNewLanguageAttribute() + if (err != nil){ + setErrorEncounteredPage(window, err, currentPage) + return + } + + err = myLocalProfiles.SetProfileData("Mate", "Language", newLanguageAttribute) + if (err != nil){ + setErrorEncounteredPage(window, err, currentPage) + return + } + + onCompletePage() + } + + submitButton := getWidgetCentered(widget.NewButtonWithIcon(translate("Submit"), theme.ConfirmIcon(), submitFunction)) + + page := container.NewVBox(title, backButton, widget.NewSeparator(), subtitle, widget.NewSeparator(), languageNameLabel, languageNameTextLabel, widget.NewSeparator(), description1, description2, widget.NewSeparator(), fluencySelectorCentered, submitButton) + + setPageContent(page, window) +} + + +func setBuildMateProfilePage_ManageLanguage(window fyne.Window, languagePrimaryName string, previousPage func(), afterDeletePage func()){ + + currentPage := func(){setBuildMateProfilePage_ManageLanguage(window, languagePrimaryName, previousPage, afterDeletePage)} + + title := getPageTitleCentered(translate("Build Mate Profile - Mental")) + + backButton := getBackButtonCentered(previousPage) + + subtitle := getPageSubtitleCentered("Manage Language") + + languageNameLabel := getLabelCentered(translate("Language Name:")) + + getLanguageNameText := func()(string, error){ + + worldLanguageObjectsMap, err := worldLanguages.GetWorldLanguageObjectsMap() + if (err != nil) { return "", err } + + languageObject, exists := worldLanguageObjectsMap[languagePrimaryName] + if (exists == false){ + // Language name must be custom + return languagePrimaryName, nil + } + + languageNamesList := languageObject.NamesList + + languageNamesListFormatted := helpers.TranslateAndJoinStringListItems(languageNamesList, "/") + + return languageNamesListFormatted, nil + } + + languageNameText, err := getLanguageNameText() + if (err != nil) { + setErrorEncounteredPage(window, err, previousPage) + return + } + + languageNameTextLabel := getBoldLabelCentered(languageNameText) + + myLanguageAttributeExists, myLanguageAttribute, err := myLocalProfiles.GetProfileData("Mate", "Language") + if (err != nil){ + setErrorEncounteredPage(window, err, previousPage) + return + } + if (myLanguageAttributeExists == false){ + setErrorEncounteredPage(window, errors.New("setBuildMateProfilePage_ManageLanguage called when language attribute is empty."), previousPage) + return + } + + myLanguageAttributeList := strings.Split(myLanguageAttribute, "+&") + + getMyLanguageCurrentRating := func()(string, error){ + + for _, languageItem := range myLanguageAttributeList{ + + itemLanguageName, languageRating, delimiterFound := strings.Cut(languageItem, "$") + if (delimiterFound == false){ + return "", errors.New("MyLocalProfiles contains invalid Location attribute: Missing $: " + languageItem) + } + if (itemLanguageName == languagePrimaryName){ + return languageRating, nil + } + } + + return "", errors.New("setBuildMateProfilePage_ManageLanguage called when my language rating not found.") + } + + myCurrentLanguageRating, err := getMyLanguageCurrentRating() + if (err != nil){ + setErrorEncounteredPage(window, err, previousPage) + return + } + + handleSelectFunction := func(newLanguageRating string){ + + if (newLanguageRating == myCurrentLanguageRating){ + return + } + + newLanguageItem := languagePrimaryName + "$" + newLanguageRating + + newMyLanguageAttributeList := make([]string, 0) + + languageFound := false + + for _, languageItem := range myLanguageAttributeList{ + + itemLanguageName, languageRating, delimiterFound := strings.Cut(languageItem, "$") + if (delimiterFound == false){ + setErrorEncounteredPage(window, errors.New("MyLocalProfiles contains invalid Location attribute: Missing $: " + languageItem), currentPage) + return + } + if (itemLanguageName == languagePrimaryName){ + + if (languageFound == true){ + setErrorEncounteredPage(window, errors.New("My mate Language attribute contains duplicate language item."), currentPage) + return + } + + if (languageRating == newLanguageRating){ + // We already checked for this earlier. This should not happen + setErrorEncounteredPage(window, errors.New("languageRating is the same after already checking."), currentPage) + return + } + // We will add the language with the revised rating + languageFound = true + newMyLanguageAttributeList = append(newMyLanguageAttributeList, newLanguageItem) + continue + } + newMyLanguageAttributeList = append(newMyLanguageAttributeList, languageItem) + } + + if (languageFound == false){ + setErrorEncounteredPage(window, errors.New("My language not found after being found already."), currentPage) + return + } + + newMyLanguageAttribute := strings.Join(newMyLanguageAttributeList, "+&") + + err = myLocalProfiles.SetProfileData("Mate", "Language", newMyLanguageAttribute) + if (err != nil){ + setErrorEncounteredPage(window, err, currentPage) + return + } + + currentPage() + } + + selectorOptionsList := []string{"1", "2", "3", "4", "5"} + fluencySelector := widget.NewSelect(selectorOptionsList, handleSelectFunction) + + fluencySelector.Selected = myCurrentLanguageRating + + fluencyLabel := getLabelCentered("Fluency:") + + fluencySelectorCentered := getWidgetCentered(fluencySelector) + + deleteButton := getWidgetCentered(widget.NewButtonWithIcon("Delete", theme.DeleteIcon(), func(){ + + newMyLanguageAttributeList := make([]string, 0) + + for _, languageItem := range myLanguageAttributeList{ + + itemLanguageName, _, delimiterFound := strings.Cut(languageItem, "$") + if (delimiterFound == false){ + setErrorEncounteredPage(window, errors.New("MyLocalProfiles contains invalid Location attribute: Missing $: " + languageItem), currentPage) + return + } + if (itemLanguageName == languagePrimaryName){ + continue + } + newMyLanguageAttributeList = append(newMyLanguageAttributeList, languageItem) + } + + if (len(newMyLanguageAttributeList) != (len(myLanguageAttributeList)-1)){ + setErrorEncounteredPage(window, errors.New("Failed to delete language item."), currentPage) + return + } + + if (len(newMyLanguageAttributeList) == 0){ + + err = myLocalProfiles.DeleteProfileData("Mate", "Language") + if (err != nil){ + setErrorEncounteredPage(window, err, currentPage) + return + } + + afterDeletePage() + return + } + + newMyLanguageAttribute := strings.Join(newMyLanguageAttributeList, "+&") + + err = myLocalProfiles.SetProfileData("Mate", "Language", newMyLanguageAttribute) + if (err != nil){ + setErrorEncounteredPage(window, err, currentPage) + return + } + + afterDeletePage() + })) + + page := container.NewVBox(title, backButton, widget.NewSeparator(), subtitle, widget.NewSeparator(), languageNameLabel, languageNameTextLabel, widget.NewSeparator(), fluencyLabel, fluencySelectorCentered, widget.NewSeparator(), deleteButton) + + setPageContent(page, window) +} + + + +func setBuildMateProfilePage_Beliefs(window fyne.Window, previousPage func()){ + + currentPage := func(){setBuildMateProfilePage_Beliefs(window, previousPage)} + + pageTitle := getPageTitleCentered(translate("Build Mate Profile - Mental")) + + backButton := getBackButtonCentered(previousPage) + + pageSubtitle := getPageSubtitleCentered(translate("Beliefs")) + + description1 := getLabelCentered(translate("Enter your beliefs.")) + description2 := getLabelCentered(translate("Beliefs may include your worldview, religion, and philosophies.")) + + currentBeliefsExist, currentBeliefs, err := myLocalProfiles.GetProfileData("Mate", "Beliefs") + if (err != nil) { + setErrorEncounteredPage(window, err, previousPage) + return + } + + getMyCurrentBeliefsRow := func()(*fyne.Container, error){ + + myBeliefsLabel := widget.NewLabel("My Beliefs:") + + if (currentBeliefsExist == false){ + + noResponseLabel := getBoldItalicLabel("No Response") + currentBeliefsRow := container.NewHBox(layout.NewSpacer(), myBeliefsLabel, noResponseLabel, layout.NewSpacer()) + + return currentBeliefsRow, nil + } + + isAllowed := allowedText.VerifyStringIsAllowed(currentBeliefs) + if (isAllowed == false){ + return nil, errors.New("Current mate beliefs is not allowed: " + currentBeliefs) + } + + if( len(currentBeliefs) > 1000){ + return nil, errors.New("Current mate beliefs is too long: " + currentBeliefs) + } + + currentBeliefsTrimmed, _, err := helpers.TrimAndFlattenString(currentBeliefs, 15) + if (err != nil) { return nil, err } + + currentBeliefsLabel := getBoldLabel(currentBeliefsTrimmed) + + viewMyBeliefsButton := widget.NewButtonWithIcon("", theme.VisibilityIcon(), func(){ + setViewTextPage(window, "Viewing Beliefs", currentBeliefs, false, currentPage) + }) + + currentBeliefsRow := container.NewHBox(layout.NewSpacer(), myBeliefsLabel, currentBeliefsLabel, viewMyBeliefsButton, layout.NewSpacer()) + + return currentBeliefsRow, nil + } + + currentBeliefsRow, err := getMyCurrentBeliefsRow() + if (err != nil){ + setErrorEncounteredPage(window, err, previousPage) + return + } + + editButton := widget.NewButtonWithIcon("Edit", theme.DocumentCreateIcon(), func(){ + setBuildMateProfilePage_EditBeliefs(window, currentPage, currentPage) + }) + + noResponseButton := widget.NewButtonWithIcon(translate("No Response"), theme.CancelIcon(), func(){ + + if (currentBeliefsExist == false){ + return + } + + setBuildMateProfilePage_DeleteBeliefs(window, currentPage, currentPage) + }) + + buttonsGrid := getContainerCentered(container.NewGridWithColumns(1, editButton, noResponseButton)) + + page := container.NewVBox(pageTitle, backButton, widget.NewSeparator(), pageSubtitle, widget.NewSeparator(), description1, description2, widget.NewSeparator(), currentBeliefsRow, widget.NewSeparator(), buttonsGrid) + + setPageContent(page, window) +} + + +func setBuildMateProfilePage_DeleteBeliefs(window fyne.Window, previousPage func(), nextPage func()){ + + title := getPageTitleCentered("Delete Beliefs") + + backButton := getBackButtonCentered(previousPage) + + description1 := getBoldLabelCentered("Delete Beliefs?") + description2 := getLabelCentered("Confirm to delete your beliefs?") + + confirmButton := getWidgetCentered(widget.NewButtonWithIcon("Delete", theme.DeleteIcon(), func(){ + myLocalProfiles.DeleteProfileData("Mate", "Beliefs") + nextPage() + })) + + page := container.NewVBox(title, backButton, widget.NewSeparator(), description1, description2, confirmButton) + + setPageContent(page, window) +} + + +func setBuildMateProfilePage_EditBeliefs(window fyne.Window, previousPage func(), nextPage func()){ + + title := getPageTitleCentered("Edit Mate Beliefs") + + backButton := getBackButtonCentered(previousPage) + + beliefsEntry := widget.NewMultiLineEntry() + beliefsEntry.Wrapping = 3 + + //Outputs: + // -bool: Current beliefs exist + // -string: Current beliefs + // -error + getCurrentBeliefs := func()(bool, string, error){ + + exists, currentBeliefs, err := myLocalProfiles.GetProfileData("Mate", "Beliefs") + if (err != nil) { return false, "", err } + if (exists == false){ + return false, "", nil + } + + isAllowed := allowedText.VerifyStringIsAllowed(currentBeliefs) + if (isAllowed == false){ + return false, "", errors.New("My current mate beliefs is not allowed: " + currentBeliefs) + } + + if(len(currentBeliefs) > 1000){ + return false, "", errors.New("My current mate beliefs is too long: " + currentBeliefs) + } + + return true, currentBeliefs, nil + } + + currentBeliefsExist, currentBeliefs, err := getCurrentBeliefs() + if (err != nil){ + setErrorEncounteredPage(window, err, previousPage) + return + } + + if (currentBeliefsExist == false){ + beliefsEntry.SetPlaceHolder("Enter your beliefs...") + } else { + beliefsEntry.SetText(currentBeliefs) + } + + submitButton := getWidgetCentered(widget.NewButtonWithIcon(translate("Save"), theme.ConfirmIcon(), func(){ + + newBeliefs := beliefsEntry.Text + + isAllowed := allowedText.VerifyStringIsAllowed(newBeliefs) + if (isAllowed == false){ + title := translate("Invalid Beliefs") + dialogMessageA := getLabelCentered(translate("Your beliefs 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(newBeliefs) + if (currentBytesLength > 1000){ + + currentLengthString := helpers.ConvertIntToString(currentBytesLength) + + title := translate("Invalid Beliefs") + + dialogMessageA := getLabelCentered(translate("Your Beliefs are too long.")) + dialogMessageB := getLabelCentered(translate("Beliefs 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 (newBeliefs == ""){ + myLocalProfiles.DeleteProfileData("Mate", "Beliefs") + } else { + myLocalProfiles.SetProfileData("Mate", "Beliefs", newBeliefs) + } + + nextPage() + })) + + emptyLabel := widget.NewLabel("") + + submitButtonWithSpacer := container.NewVBox(submitButton, emptyLabel) + + beliefsEntryBoxed := getWidgetBoxed(beliefsEntry) + + beliefsEntryWithSubmitButton := container.NewBorder(nil, submitButtonWithSpacer, nil, nil, beliefsEntryBoxed) + + header := container.NewVBox(title, backButton, widget.NewSeparator()) + + page := container.NewBorder(header, nil, nil, nil, beliefsEntryWithSubmitButton) + + setPageContent(page, window) +} + + +func setBuildMateProfilePage_GenderIdentity(window fyne.Window, previousPage func()){ + + currentPage := func(){setBuildMateProfilePage_GenderIdentity(window, previousPage)} + + title := getPageTitleCentered(translate("Build Mate Profile - Mental")) + + backButton := getBackButtonCentered(previousPage) + + pageSubtitle := getPageSubtitleCentered(translate("Gender Identity")) + + description1 := getLabelCentered("Enter the gender that you identify as.") + description2 := getLabelCentered("Choose Man, Woman, or enter a custom value.") + + standardGendersList := []string{translate("Man"), translate("Woman")} + + genderSelectEntry := widget.NewSelectEntry(standardGendersList) + + myGenderIdentityExists, myGenderIdentity, err := myLocalProfiles.GetProfileData("Mate", "GenderIdentity") + if (err != nil){ + setErrorEncounteredPage(window, err, previousPage) + return + } + if (myGenderIdentityExists == true){ + + if (len(myGenderIdentity) > 50){ + setErrorEncounteredPage(window, errors.New("MyLocalProfiles contains invalid GenderIdentity: " + myGenderIdentity), previousPage) + return + } + + genderSelectEntry.SetText(myGenderIdentity) + } else { + genderSelectEntry.SetPlaceHolder(translate("Select or enter gender...")) + } + + getCurrentGenderIdentityLabel := func()fyne.Widget{ + + if (myGenderIdentityExists == false){ + result := translate("No Response") + + noResponseLabel := getBoldItalicLabel(result) + return noResponseLabel + } + + if (myGenderIdentity == "Man"){ + genderIdentityLabel := getBoldLabel(translate("Man")) + return genderIdentityLabel + } + if (myGenderIdentity == "Woman"){ + genderIdentityLabel := getBoldLabel(translate("Woman")) + return genderIdentityLabel + } + + genderIdentityLabel := getBoldLabel(myGenderIdentity) + return genderIdentityLabel + } + + currentGenderIdentityLabel := getCurrentGenderIdentityLabel() + + myGenderIdentityLabel := widget.NewLabel("My Gender Identity:") + + currentGenderIdentityRow := container.NewHBox(layout.NewSpacer(), myGenderIdentityLabel, currentGenderIdentityLabel, layout.NewSpacer()) + + submitChangesButton := widget.NewButtonWithIcon("Submit", theme.ConfirmIcon(), func(){ + + getNewGenderIdentity := func()string{ + + newGenderIdentity := genderSelectEntry.Text + + if (newGenderIdentity == translate("Man")){ + return "Man" + } + if (newGenderIdentity == translate("Woman")){ + return "Woman" + } + + return newGenderIdentity + } + + newGenderIdentity := getNewGenderIdentity() + + if (newGenderIdentity == ""){ + title := translate("Invalid Gender") + dialogMessageA := getLabelCentered(translate("Invalid gender entered.")) + dialogMessageB := getLabelCentered(translate("Your gender cannot be empty.")) + dialogContent := container.NewVBox(dialogMessageA, dialogMessageB) + dialog.ShowCustom(title, translate("Close"), dialogContent, window) + return + } + + if (len(newGenderIdentity) > 50){ + + genderSelectEntry.SetText("") + genderSelectEntry.SetPlaceHolder(translate("Select or enter gender...")) + + title := translate("Invalid Gender") + dialogMessageA := getLabelCentered(translate("Invalid gender entered.")) + dialogMessageB := getLabelCentered(translate("It cannot be longer than 50 bytes.")) + dialogContent := container.NewVBox(dialogMessageA, dialogMessageB) + dialog.ShowCustom(title, translate("Close"), dialogContent, window) + return + } + + isAllowed := allowedText.VerifyStringIsAllowed(newGenderIdentity) + if (isAllowed == false){ + title := translate("Invalid Gender") + dialogMessageA := getLabelCentered(translate("Your gender contains 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 + } + + containsTabsOrNewlines := helpers.CheckIfStringContainsTabsOrNewlines(newGenderIdentity) + if (containsTabsOrNewlines == true){ + dialogTitle := translate("Invalid Gender") + dialogMessageA := getLabelCentered(translate("Your gender contains a tab or a newline character.")) + dialogMessageB := getLabelCentered(translate("Remove the character and resubmit.")) + dialogContent := container.NewVBox(dialogMessageA, dialogMessageB) + dialog.ShowCustom(dialogTitle, translate("Close"), dialogContent, window) + return + } + + myLocalProfiles.SetProfileData("Mate", "GenderIdentity", newGenderIdentity) + currentPage() + }) + + genderSelectEntryWithSubmitButton := getContainerCentered(container.NewGridWithRows(1, genderSelectEntry, submitChangesButton)) + + noResponseButton := getWidgetCentered(widget.NewButtonWithIcon(translate("No Response"), theme.CancelIcon(), func(){ + myLocalProfiles.DeleteProfileData("Mate", "GenderIdentity") + currentPage() + })) + + page := container.NewVBox(title, backButton, widget.NewSeparator(), pageSubtitle, widget.NewSeparator(), description1, description2, widget.NewSeparator(), currentGenderIdentityRow, widget.NewSeparator(), genderSelectEntryWithSubmitButton, noResponseButton) + + setPageContent(page, window) +} + + +func setBuildMateProfilePage_Pets(window fyne.Window, previousPage func()){ + + currentPage := func(){setBuildMateProfilePage_Pets(window, previousPage)} + + pageTitle := getPageTitleCentered(translate("Build Mate Profile - Mental")) + + backButton := getBackButtonCentered(previousPage) + + pageSubtitle := getPageSubtitleCentered(translate("Pets")) + + description1 := getLabelCentered(translate("Describe how much you enjoy owning pets.")) + description2 := getLabelCentered(translate("1 = Strongly Against, 10 = Strongly Support")) + + optionsList := []string{"1", "2", "3", "4", "5", "6", "7", "8", "9", "10"} + + handleSelectionFunction := func(response string){ + myLocalProfiles.SetProfileData("Mate", "PetsRating", response) + } + + petsRatingSelector := widget.NewSelect(optionsList, handleSelectionFunction) + + currentPetsRatingExists, currentPetsRating, err := myLocalProfiles.GetProfileData("Mate", "PetsRating") + if (err != nil){ + setErrorEncounteredPage(window, err, previousPage) + return + } + + if (currentPetsRatingExists == true){ + petsRatingSelector.Selected = currentPetsRating + } else { + petsRatingSelector.PlaceHolder = translate("Choose rating...") + } + + petsRatingSelectorCentered := getWidgetCentered(petsRatingSelector) + + petsNoResponseButton := getWidgetCentered(widget.NewButtonWithIcon(translate("No Response"), theme.CancelIcon(), func(){ + err := myLocalProfiles.DeleteProfileData("Mate", "PetsRating") + if (err != nil){ + setErrorEncounteredPage(window, err, currentPage) + return + } + currentPage() + })) + + description3 := getLabelCentered(translate("Describe how much you enjoy each animal as a pet.")) + description4 := getLabelCentered(translate("1 = Strongly Against, 10 = Strongly Support")) + + getPetsGrid := func()(*fyne.Container, error){ + + petsGrid := container.NewGridWithColumns(3) + + petsList := []string{"Dogs", "Cats"} + + for _, petName := range petsList{ + + petAttributeName := petName + "Rating" + + petNameLabel := getBoldLabelCentered(translate(petName)) + + optionsList := []string{"1", "2", "3", "4", "5", "6", "7", "8", "9", "10"} + + handleSelectionFunction := func(response string){ + err := myLocalProfiles.SetProfileData("Mate", petAttributeName, response) + if (err != nil){ + setErrorEncounteredPage(window, err, currentPage) + return + } + } + + petRatingSelector := widget.NewSelect(optionsList, handleSelectionFunction) + + currentRatingExists, currentPetRating, err := myLocalProfiles.GetProfileData("Mate", petAttributeName) + if (err != nil){ return nil, err } + + if (currentRatingExists == true){ + petRatingSelector.Selected = currentPetRating + } else { + petRatingSelector.PlaceHolder = translate("Choose rating...") + } + + noResponseButton := getWidgetCentered(widget.NewButtonWithIcon(translate("No Response"), theme.CancelIcon(), func(){ + err := myLocalProfiles.DeleteProfileData("Mate", petAttributeName) + if (err != nil){ + setErrorEncounteredPage(window, err, currentPage) + return + } + currentPage() + })) + + petsGrid.Add(petNameLabel) + petsGrid.Add(petRatingSelector) + petsGrid.Add(noResponseButton) + } + + petsGridCentered := getContainerCentered(petsGrid) + + return petsGridCentered, nil + } + + petsGrid, err := getPetsGrid() + if (err != nil){ + setErrorEncounteredPage(window, err, previousPage) + return + } + + page := container.NewVBox(pageTitle, backButton, widget.NewSeparator(), pageSubtitle, widget.NewSeparator(), description1, description2, petsRatingSelectorCentered, petsNoResponseButton, widget.NewSeparator(), description3, description4, widget.NewSeparator(), petsGrid) + + setPageContent(page, window) +} + + diff --git a/gui/buildProfileGui_Physical.go b/gui/buildProfileGui_Physical.go new file mode 100644 index 0000000..14c1aa2 --- /dev/null +++ b/gui/buildProfileGui_Physical.go @@ -0,0 +1,3035 @@ +package gui + +// buildProfileGui_Physical.go implements pages to build the Physical portion of a user's mate profile + +import "fyne.io/fyne/v2" +import "fyne.io/fyne/v2/canvas" +import "fyne.io/fyne/v2/container" +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/resources/geneticReferences/traits" +import "seekia/resources/geneticReferences/monogenicDiseases" +import "seekia/resources/geneticReferences/polygenicDiseases" + +import "seekia/internal/allowedText" +import "seekia/internal/genetics/companyAnalysis" +import "seekia/internal/genetics/myChosenAnalysis" +import "seekia/internal/genetics/myGenomes" +import "seekia/internal/genetics/myPeople" +import "seekia/internal/globalSettings" +import "seekia/internal/helpers" +import "seekia/internal/profiles/myLocalProfiles" + +import "slices" +import "strings" +import "errors" + +func setBuildMateProfileCategoryPage_Physical(window fyne.Window, previousPage func()){ + + currentPage := func(){ setBuildMateProfileCategoryPage_Physical(window, previousPage) } + + title := getPageTitleCentered(translate("Build Mate Profile - Physical")) + + backButton := getBackButtonCentered(previousPage) + + sexButton := widget.NewButton(translate("Sex"), func(){ + setBuildMateProfilePage_Sex(window, currentPage) + }) + + ageButton := widget.NewButton(translate("Age"), func(){ + setBuildMateProfilePage_Age(window, currentPage) + }) + + ancestryCompositionButton := widget.NewButton("Ancestry Composition", func(){ + setBuildMateProfilePage_23andMe_AncestryComposition(window, currentPage) + }) + + neanderthalVariantsButton := widget.NewButton("Neanderthal Variants", func(){ + setBuildMateProfilePage_23andMe_NeanderthalVariants(window, currentPage) + }) + + maternalHaplogroupButton := widget.NewButton("Maternal Haplogroup", func(){ + setBuildMateProfilePage_23andMe_Haplogroup(window, "Maternal", currentPage) + }) + + paternalHaplogroupButton := widget.NewButton("Paternal Haplogroup", func(){ + setBuildMateProfilePage_23andMe_Haplogroup(window, "Paternal", currentPage) + }) + + geneticInformationButton := widget.NewButton(translate("Genetic Information"), func(){ + setBuildMateProfilePage_GeneticInformation(window, currentPage) + }) + + heightButton := widget.NewButton(translate("Height"), func(){ + setBuildMateProfilePage_Height(window, currentPage) + }) + + bodyButton := widget.NewButton(translate("Body"), func(){ + setBuildMateProfilePage_Body(window, currentPage) + }) + + eyeColorButton := widget.NewButton(translate("Eye Color"), func(){ + setBuildMateProfilePage_EyeColor(window, currentPage) + }) + + hairColorButton := widget.NewButton(translate("Hair Color"), func(){ + setBuildMateProfilePage_HairColor(window, currentPage) + }) + + hairTextureButton := widget.NewButton(translate("Hair Texture"), func(){ + setBuildMateProfilePage_HairTexture(window, currentPage) + }) + + skinColorButton := widget.NewButton(translate("Skin Color"), func(){ + setBuildMateProfilePage_SkinColor(window, currentPage) + }) + + infectionsButton := widget.NewButton(translate("Infections"), func(){ + setBuildMateProfilePage_Infections(window, currentPage) + }) + + buttonsGrid := container.NewGridWithColumns(1, sexButton, ageButton, ancestryCompositionButton, neanderthalVariantsButton, maternalHaplogroupButton, paternalHaplogroupButton, geneticInformationButton, heightButton, bodyButton, eyeColorButton, hairColorButton, hairTextureButton, skinColorButton, infectionsButton) + + buttonsGridCentered := getContainerCentered(buttonsGrid) + + buttonsGridPadded := container.NewPadded(buttonsGridCentered) + + page := container.NewVBox(title, backButton, widget.NewSeparator(), buttonsGridPadded) + + setPageContent(page, window) +} + + +func setBuildMateProfilePage_Sex(window fyne.Window, previousPage func()){ + + currentPage := func(){setBuildMateProfilePage_Sex(window, previousPage)} + + pageTitle := getPageTitleCentered(translate("Build Mate Profile - Physical")) + + backButton := getBackButtonCentered(previousPage) + + pageSubtitle := getPageSubtitleCentered(translate("Sex")) + + sexDescription := widget.NewLabel(translate("What is your physical sex?")) + sexHelpButton := widget.NewButtonWithIcon("", theme.QuestionIcon(), func(){ + setSexExplainerPage(window, currentPage) + }) + + sexDescriptionRow := container.NewHBox(layout.NewSpacer(), sexDescription, sexHelpButton, layout.NewSpacer()) + + option1Translated := translate("Male") + option2Translated := translate("Female") + option3Translated := translate("Intersex Male") + option4Translated := translate("Intersex Female") + option5Translated := translate("Intersex") + + untranslatedOptionsMap := map[string]string{ + option1Translated: "Male", + option2Translated: "Female", + option3Translated: "Intersex Male", + option4Translated: "Intersex Female", + option5Translated: "Intersex", + } + + sexSelectorOptions := []string{option1Translated, option2Translated, option3Translated, option4Translated, option5Translated} + + sexSelector := widget.NewRadioGroup(sexSelectorOptions, func(response string){ + + if (response == ""){ + err := myLocalProfiles.DeleteProfileData("Mate", "Sex") + if (err != nil){ + setErrorEncounteredPage(window, err, currentPage) + return + } + return + } + + responseUntranslated, exists := untranslatedOptionsMap[response] + if (exists == false){ + setErrorEncounteredPage(window, errors.New("untranslatedOptionsMap missing response: " + response), currentPage) + return + } + err := myLocalProfiles.SetProfileData("Mate", "Sex", responseUntranslated) + if (err != nil){ + setErrorEncounteredPage(window, err, currentPage) + return + } + }) + + exists, currentSex, err := myLocalProfiles.GetProfileData("Mate", "Sex") + if (err != nil){ + setErrorEncounteredPage(window, err, previousPage) + return + } + if (exists == true){ + if (currentSex != "Male" && currentSex != "Female" && currentSex != "Intersex Male" && currentSex != "Intersex Female" && currentSex != "Intersex"){ + setErrorEncounteredPage(window, errors.New("MyLocalProfiles contains invalid Sex: " + currentSex), previousPage) + return + } + sexSelector.Selected = translate(currentSex) + } + sexSelectorCentered := getWidgetCentered(sexSelector) + + sexNoResponseButton := getWidgetCentered(widget.NewButtonWithIcon(translate("No Response"), theme.CancelIcon(), func(){ + err := myLocalProfiles.DeleteProfileData("Mate", "Sex") + if (err != nil){ + setErrorEncounteredPage(window, err, currentPage) + return + } + currentPage() + })) + + page := container.NewVBox(pageTitle, backButton, widget.NewSeparator(), pageSubtitle, widget.NewSeparator(), sexDescriptionRow, sexSelectorCentered, sexNoResponseButton) + + setPageContent(page, window) +} + +func setBuildMateProfilePage_23andMe_AncestryComposition(window fyne.Window, previousPage func()){ + + currentPage := func(){setBuildMateProfilePage_23andMe_AncestryComposition(window, previousPage)} + + title := getPageTitleCentered(translate("Build Mate Profile - Physical")) + + backButton := getBackButtonCentered(previousPage) + + subtitle := getPageSubtitleCentered("23andMe Ancestry Composition") + + description := getLabelCentered("Enter your 23andMe ancestry location composition.") + + exists, myAncestryCompositionAttribute, err := myLocalProfiles.GetProfileData("Mate", "23andMe_AncestryComposition") + if (err != nil){ + setErrorEncounteredPage(window, err, previousPage) + return + } + if (exists == false){ + + noCompositionFoundLabel := getBoldLabelCentered("No ancestry composition exists.") + + addCompositionButton := getWidgetCentered(widget.NewButtonWithIcon("Add Composition", theme.ContentAddIcon(), func(){ + + emptyMapA := make(map[string]float64) + emptyMapB := make(map[string]float64) + emptyMapC := make(map[string]float64) + setBuildMateProfilePage_23andMe_EditAncestryComposition(window, emptyMapA, emptyMapB, emptyMapC, currentPage, currentPage) + })) + + page := container.NewVBox(title, backButton, widget.NewSeparator(), subtitle, widget.NewSeparator(), description, widget.NewSeparator(), noCompositionFoundLabel, addCompositionButton) + + setPageContent(page, window) + return + } + + visibilityLabel := getBoldLabel("Visibility:") + + visibilityOptions := []string{translate("Show On Profile"), translate("Hide From Profile")} + visibilitySelector := widget.NewSelect(visibilityOptions, func(newVisibility string){ + + getNewVisibilityStatus := func()string{ + + if (newVisibility == translate("Show On Profile")){ + return "Yes" + } + + return "No" + } + + newVisibilityStatus := getNewVisibilityStatus() + + err := myLocalProfiles.SetProfileData("Mate", "VisibilityStatus_23andMe_AncestryComposition", newVisibilityStatus) + if (err != nil){ + setErrorEncounteredPage(window, err, currentPage) + return + } + }) + + exists, visibilityStatus, err := myLocalProfiles.GetProfileData("Mate", "VisibilityStatus_23andMe_AncestryComposition") + if (err != nil){ + setErrorEncounteredPage(window, err, previousPage) + return + } + if (exists == true && visibilityStatus == "No"){ + visibilitySelector.Selected = translate("Hide From Profile") + } else { + visibilitySelector.Selected = translate("Show On Profile") + } + + profileVisibilityHelpButton := widget.NewButtonWithIcon("", theme.QuestionIcon(), func(){ + setMateProfileAttributeVisibilityExplainerPage(window, currentPage) + }) + + visibilitySelectorRow := container.NewHBox(layout.NewSpacer(), visibilityLabel, visibilitySelector, profileVisibilityHelpButton, layout.NewSpacer()) + + attributeIsValid, myContinentPercentagesMap, myRegionPercentagesMap, mySubregionPercentagesMap, err := companyAnalysis.ReadAncestryCompositionAttribute_23andMe(true, myAncestryCompositionAttribute) + if (err != nil){ + setErrorEncounteredPage(window, err, currentPage) + return + } + if (attributeIsValid == false){ + setErrorEncounteredPage(window, errors.New("MyLocalProfiles 23andMe ancestry composition attribute is invalid."), currentPage) + return + } + + editCompositionButton := widget.NewButtonWithIcon("Edit", theme.DocumentCreateIcon(), func(){ + setBuildMateProfilePage_23andMe_EditAncestryComposition(window, myContinentPercentagesMap, myRegionPercentagesMap, mySubregionPercentagesMap, currentPage, currentPage) + }) + + noResponseButton := widget.NewButtonWithIcon("No Response", theme.CancelIcon(), func(){ + setBuildMateProfilePage_23andMe_ConfirmDeleteAncestryComposition(window, currentPage, currentPage) + }) + + buttonsGrid := getContainerCentered(container.NewGridWithColumns(1, editCompositionButton, noResponseButton)) + + myCompositionLabel := getBoldLabelCentered("My Composition:") + + myCompositionDisplay, err := get23andMeAncestryCompositionDisplay(myContinentPercentagesMap, myRegionPercentagesMap, mySubregionPercentagesMap) + if (err != nil){ + setErrorEncounteredPage(window, err, previousPage) + return + } + + header := container.NewVBox(title, backButton, widget.NewSeparator(), subtitle, widget.NewSeparator(), description, widget.NewSeparator(), visibilitySelectorRow, widget.NewSeparator(), buttonsGrid, widget.NewSeparator(), myCompositionLabel) + + page := container.NewBorder(header, nil, nil, nil, myCompositionDisplay) + + setPageContent(page, window) +} + +func setBuildMateProfilePage_23andMe_ConfirmDeleteAncestryComposition(window fyne.Window, previousPage func(), nextPage func()){ + + currentPage := func(){setBuildMateProfilePage_23andMe_ConfirmDeleteAncestryComposition(window, previousPage, nextPage)} + + title := getPageTitleCentered(translate("Build Mate Profile - Physical")) + + backButton := getBackButtonCentered(previousPage) + + description1 := getBoldLabelCentered("Delete Ancestry Composition?") + + description2 := getLabelCentered("Confirm to delete your 23andMe ancestry composition?") + confirmButton := getWidgetCentered(widget.NewButtonWithIcon("Confirm", theme.ConfirmIcon(), func(){ + err := myLocalProfiles.DeleteProfileData("Mate", "23andMe_AncestryComposition") + if (err != nil){ + setErrorEncounteredPage(window, err, currentPage) + return + } + nextPage() + })) + + page := container.NewVBox(title, backButton, widget.NewSeparator(), description1, description2, confirmButton) + + setPageContent(page, window) +} + +func setBuildMateProfilePage_23andMe_EditAncestryComposition(window fyne.Window, myContinentPercentagesMap map[string]float64, myRegionPercentagesMap map[string]float64, mySubregionPercentagesMap map[string]float64, previousPage func(), afterCompletePage func()){ + + currentPage := func(){setBuildMateProfilePage_23andMe_EditAncestryComposition(window, myContinentPercentagesMap, myRegionPercentagesMap, mySubregionPercentagesMap, previousPage, afterCompletePage)} + + title := getPageTitleCentered(translate("Build Mate Profile - Physical")) + + backButtonFunction := func(){ + + confirmDialogCallbackFunction := func(response bool){ + if (response == true){ + previousPage() + } + } + + dialogTitle := translate("Confirm Go Back") + dialogMessageA := getLabelCentered("Confirm to go back?") + dialogMessageB := getLabelCentered("You will lose any changes you made.") + dialogContent := container.NewVBox(dialogMessageA, dialogMessageB) + dialog.ShowCustomConfirm(dialogTitle, translate("Yes"), translate("No"), dialogContent, confirmDialogCallbackFunction, window) + } + + backButton := getBackButtonCentered(backButtonFunction) + + subtitle := getPageSubtitleCentered("Build 23andMe Ancestry Composition") + + // We use this function to make changes and return to this page + currentPageWithNewPercentagesFunction := func(newContinentPercentagesMap map[string]float64, newRegionPercentagesMap map[string]float64, newSubregionPercentagesMap map[string]float64){ + + setBuildMateProfilePage_23andMe_EditAncestryComposition(window, newContinentPercentagesMap, newRegionPercentagesMap, newSubregionPercentagesMap, previousPage, afterCompletePage) + } + + addLocationButton := widget.NewButtonWithIcon("Add Location", theme.ContentAddIcon(), func(){ + + setBuildMateProfilePage_23andMe_AncestryComposition_AddLocation(window, false, "", false, "", false, "", myContinentPercentagesMap, myRegionPercentagesMap, mySubregionPercentagesMap, currentPage, currentPageWithNewPercentagesFunction) + }) + + if (len(myContinentPercentagesMap) == 0 && len(myRegionPercentagesMap) == 0 && len(mySubregionPercentagesMap) == 0){ + + addLocationButtonCentered := getWidgetCentered(addLocationButton) + + noCompositionExistsLabel := getBoldLabelCentered("No locations added.") + + page := container.NewVBox(title, backButton, widget.NewSeparator(), subtitle, widget.NewSeparator(), addLocationButtonCentered, widget.NewSeparator(), noCompositionExistsLabel) + + setPageContent(page, window) + return + } + + deleteLocationButton := widget.NewButtonWithIcon("Delete Location", theme.DeleteIcon(), func(){ + + setBuildMateProfilePage_23andMe_AncestryComposition_DeleteLocation(window, "Continent", myContinentPercentagesMap, myRegionPercentagesMap, mySubregionPercentagesMap, currentPage, currentPageWithNewPercentagesFunction) + }) + + addAndDeleteLocationButtons := getContainerCentered(container.NewGridWithRows(1, addLocationButton, deleteLocationButton)) + + submitButton := getWidgetCentered(widget.NewButtonWithIcon("Submit", theme.ConfirmIcon(), func(){ + + inputsAreValid, newAncestryCompositionAttribute, err := companyAnalysis.CreateAncestryCompositionAttribute_23andMe(myContinentPercentagesMap, myRegionPercentagesMap, mySubregionPercentagesMap) + if (err != nil){ + setErrorEncounteredPage(window, err, currentPage) + return + } + if (inputsAreValid == false){ + dialogTitle := translate("Invalid Ancestry Composition") + dialogMessageA := getBoldLabelCentered(translate("The ancestry composition you entered is invalid.")) + dialogMessageB := getLabelCentered(translate("All continent percentages must add to 100.")) + dialogMessageC := getLabelCentered(translate("All region percentages must add up to their continent percentage.")) + dialogMessageD := getLabelCentered(translate("All subregion percentages must add up to their region percentage.")) + dialogContent := container.NewVBox(dialogMessageA, dialogMessageB, dialogMessageC, dialogMessageD) + dialog.ShowCustom(dialogTitle, translate("Close"), dialogContent, window) + return + } + + setBuildMateProfilePage_23andMe_ConfirmUpdateAncestryComposition(window, newAncestryCompositionAttribute, currentPage, afterCompletePage) + })) + + emptyLabel := widget.NewLabel("") + + footer := container.NewVBox(widget.NewSeparator(), submitButton, emptyLabel) + + myCompositionTree, err := get23andMeAncestryCompositionDisplay(myContinentPercentagesMap, myRegionPercentagesMap, mySubregionPercentagesMap) + if (err != nil){ + setErrorEncounteredPage(window, err, previousPage) + return + } + + header := container.NewVBox(title, backButton, widget.NewSeparator(), subtitle, widget.NewSeparator(), addAndDeleteLocationButtons, widget.NewSeparator()) + + page := container.NewBorder(header, footer, nil, nil, myCompositionTree) + + setPageContent(page, window) +} + +func setBuildMateProfilePage_23andMe_ConfirmUpdateAncestryComposition(window fyne.Window, newAncestryCompositionAttribute string, previousPage func(), nextPage func()){ + + currentPage := func(){setBuildMateProfilePage_23andMe_ConfirmUpdateAncestryComposition(window, newAncestryCompositionAttribute, previousPage, nextPage)} + + title := getPageTitleCentered(translate("Build Mate Profile - Physical")) + + backButton := getBackButtonCentered(previousPage) + + description1 := getBoldLabelCentered("Add Ancestry Composition?") + + description2 := getLabelCentered("Confirm update your ancestry composition?") + description3 := getLabelCentered("This will be visible on your profile.") + + confirmButton := getWidgetCentered(widget.NewButtonWithIcon("Confirm", theme.ConfirmIcon(), func(){ + err := myLocalProfiles.SetProfileData("Mate", "23andMe_AncestryComposition", newAncestryCompositionAttribute) + if (err != nil){ + setErrorEncounteredPage(window, err, currentPage) + return + } + + nextPage() + })) + + page := container.NewVBox(title, backButton, widget.NewSeparator(), description1, description2, description3, confirmButton) + + setPageContent(page, window) +} + +func setBuildMateProfilePage_23andMe_AncestryComposition_AddLocation( + window fyne.Window, + continentProvided bool, + currentContinent string, + regionProvided bool, + currentRegion string, + subregionProvided bool, + currentSubregion string, + myContinentPercentagesMap map[string]float64, + myRegionPercentagesMap map[string]float64, + mySubregionPercentagesMap map[string]float64, + previousPage func(), + nextPage func(map[string]float64, map[string]float64, map[string]float64)){ + + title := getPageTitleCentered(translate("Build Mate Profile - Physical")) + + backButton := getBackButtonCentered(previousPage) + + subtitle := getPageSubtitleCentered("Add 23andMe Ancestry Location") + + getPageContent := func()(*fyne.Container, error){ + + continentLabel := getBoldLabel("Continent:") + + allContinentsList := companyAnalysis.GetAncestryContinentsList_23andMe() + + handleContinentSelectFunction := func(newContinent string){ + if (continentProvided == true && newContinent == currentContinent){ + return + } + + setBuildMateProfilePage_23andMe_AncestryComposition_AddLocation(window, true, newContinent, false, "", false, "", myContinentPercentagesMap, myRegionPercentagesMap, mySubregionPercentagesMap, previousPage, nextPage) + } + + continentSelector := widget.NewSelect(allContinentsList, handleContinentSelectFunction) + if (continentProvided == true){ + continentSelector.Selected = currentContinent + } + + continentRow := container.NewHBox(layout.NewSpacer(), continentLabel, continentSelector, layout.NewSpacer()) + + if (continentProvided == false){ + + pageContent := container.NewVBox(continentRow) + return pageContent, nil + } + + percentageLabel := getBoldLabelCentered("Percentage:") + + percentageEntry := widget.NewEntry() + percentageEntry.SetPlaceHolder("Enter percentage.") + + percentageRow := getContainerCentered(container.NewGridWithRows(1, percentageLabel, percentageEntry)) + + showInvalidPercentageDialog := func(){ + dialogTitle := translate("Invalid Percentage") + dialogMessageA := getLabelCentered(translate("The location percentage is invalid.")) + dialogMessageB := getLabelCentered(translate("You must enter a number between 0 and 100.")) + dialogContent := container.NewVBox(dialogMessageA, dialogMessageB) + dialog.ShowCustom(dialogTitle, translate("Close"), dialogContent, window) + } + + regionsList, err := companyAnalysis.GetAncestryContinentRegionsList_23andMe(currentContinent) + if (err != nil) { return nil, err } + + if (len(regionsList) == 0){ + + submitButton := getWidgetCentered(widget.NewButtonWithIcon("Submit", theme.ConfirmIcon(), func(){ + + newPercentageRaw := percentageEntry.Text + newPercentage := strings.TrimSuffix(newPercentageRaw, "%") + + newPercentageFloat64, err := helpers.ConvertStringToFloat64(newPercentage) + if (err != nil){ + showInvalidPercentageDialog() + return + } + + if (newPercentageFloat64 <= 0 || newPercentageFloat64 > 100){ + showInvalidPercentageDialog() + return + } + + addContinentFunction := func(){ + myContinentPercentagesMap[currentContinent] = newPercentageFloat64 + + nextPage(myContinentPercentagesMap, myRegionPercentagesMap, mySubregionPercentagesMap) + } + + existingPercentage, exists := myContinentPercentagesMap[currentContinent] + if (exists == false){ + + addContinentFunction() + return + } + + // We allow user to confirm they want to overwrite existing value + + currentPercentageString := helpers.ConvertFloat64ToStringRounded(existingPercentage, 1) + + confirmDialogCallbackFunction := func(response bool){ + if (response == true){ + addContinentFunction() + } + } + + dialogTitle := translate("Confirm Overwrite Existing") + dialogMessageA := getLabelCentered("You already have a percentage for " + currentContinent) + dialogMessageB := getLabelCentered("Current Percentage: " + currentPercentageString + "%") + dialogMessageC := getLabelCentered("Confirm to overwrite this percentage?") + dialogContent := container.NewVBox(dialogMessageA, dialogMessageB, dialogMessageC) + dialog.ShowCustomConfirm(dialogTitle, translate("Yes"), translate("No"), dialogContent, confirmDialogCallbackFunction, window) + })) + + pageContent := container.NewVBox(continentRow, widget.NewSeparator(), percentageRow, submitButton) + + return pageContent, nil + } + + regionLabel := getBoldLabel("Region:") + + handleRegionSelectFunction := func(newRegion string){ + + if (regionProvided == true && currentRegion == newRegion){ + return + } + + setBuildMateProfilePage_23andMe_AncestryComposition_AddLocation(window, true, currentContinent, true, newRegion, false, "", myContinentPercentagesMap, myRegionPercentagesMap, mySubregionPercentagesMap, previousPage, nextPage) + } + + regionSelector := widget.NewSelect(regionsList, handleRegionSelectFunction) + + if (regionProvided == true){ + regionSelector.Selected = currentRegion + } + + regionRow := container.NewHBox(layout.NewSpacer(), regionLabel, regionSelector, layout.NewSpacer()) + + if (regionProvided == false){ + + pageContent := container.NewVBox(continentRow, widget.NewSeparator(), regionRow) + + return pageContent, nil + } + + subregionsList, err := companyAnalysis.GetAncestryRegionSubregionsList_23andMe(currentContinent, currentRegion) + if (err != nil) { return nil, err } + + if (len(subregionsList) == 0){ + + submitButton := getWidgetCentered(widget.NewButtonWithIcon("Submit", theme.ConfirmIcon(), func(){ + + newPercentageRaw := percentageEntry.Text + newPercentage := strings.TrimSuffix(newPercentageRaw, "%") + + newPercentageFloat64, err := helpers.ConvertStringToFloat64(newPercentage) + if (err != nil){ + showInvalidPercentageDialog() + return + } + + if (newPercentageFloat64 <= 0 || newPercentageFloat64 > 100){ + showInvalidPercentageDialog() + return + } + + addRegionFunction := func(){ + myRegionPercentagesMap[currentRegion] = newPercentageFloat64 + + nextPage(myContinentPercentagesMap, myRegionPercentagesMap, mySubregionPercentagesMap) + } + + existingPercentage, exists := myRegionPercentagesMap[currentRegion] + if (exists == false){ + + addRegionFunction() + return + } + + // We allow user to confirm they want to overwrite existing value + + currentPercentageString := helpers.ConvertFloat64ToStringRounded(existingPercentage, 1) + + confirmDialogCallbackFunction := func(response bool){ + if (response == true){ + addRegionFunction() + } + } + + dialogTitle := translate("Confirm Overwrite Existing") + dialogMessageA := getLabelCentered("You already have a percentage for " + currentRegion) + dialogMessageB := getLabelCentered("Current Percentage: " + currentPercentageString + "%") + dialogMessageC := getLabelCentered("Confirm to overwrite this percentage?") + dialogContent := container.NewVBox(dialogMessageA, dialogMessageB, dialogMessageC) + dialog.ShowCustomConfirm(dialogTitle, translate("Yes"), translate("No"), dialogContent, confirmDialogCallbackFunction, window) + })) + + pageContent := container.NewVBox(continentRow, widget.NewSeparator(), regionRow, widget.NewSeparator(), percentageRow, submitButton) + + return pageContent, nil + } + + subregionLabel := getBoldLabel("Subregion:") + + handleSubregionSelectFunction := func(newSubregion string){ + + if (subregionProvided == true && currentSubregion == newSubregion){ + return + } + + setBuildMateProfilePage_23andMe_AncestryComposition_AddLocation(window, true, currentContinent, true, currentRegion, true, newSubregion, myContinentPercentagesMap, myRegionPercentagesMap, mySubregionPercentagesMap, previousPage, nextPage) + } + + subregionSelector := widget.NewSelect(subregionsList, handleSubregionSelectFunction) + if (subregionProvided == true){ + subregionSelector.Selected = currentSubregion + } + + subregionRow := container.NewHBox(layout.NewSpacer(), subregionLabel, subregionSelector, layout.NewSpacer()) + + submitButton := getWidgetCentered(widget.NewButtonWithIcon("Submit", theme.ConfirmIcon(), func(){ + + if (subregionProvided == false){ + + dialogTitle := translate("Missing Subregion") + dialogMessage := getLabelCentered(translate("You must select a subregion.")) + dialogContent := container.NewVBox(dialogMessage) + dialog.ShowCustom(dialogTitle, translate("Close"), dialogContent, window) + return + } + + newPercentageRaw := percentageEntry.Text + newPercentage := strings.TrimSuffix(newPercentageRaw, "%") + + newPercentageFloat64, err := helpers.ConvertStringToFloat64(newPercentage) + if (err != nil){ + showInvalidPercentageDialog() + return + } + + if (newPercentageFloat64 <= 0 || newPercentageFloat64 > 100){ + showInvalidPercentageDialog() + return + } + + addSubregionFunction := func(){ + mySubregionPercentagesMap[currentSubregion] = newPercentageFloat64 + + nextPage(myContinentPercentagesMap, myRegionPercentagesMap, mySubregionPercentagesMap) + } + + existingPercentage, exists := mySubregionPercentagesMap[currentSubregion] + if (exists == false){ + + addSubregionFunction() + return + } + + // We allow user to confirm they want to overwrite existing value + + currentPercentageString := helpers.ConvertFloat64ToStringRounded(existingPercentage, 1) + + confirmDialogCallbackFunction := func(response bool){ + if (response == true){ + addSubregionFunction() + } + } + + dialogTitle := translate("Confirm Overwrite Existing") + dialogMessageA := getLabelCentered("You already have a percentage for " + currentSubregion) + dialogMessageB := getLabelCentered("Current Percentage: " + currentPercentageString + "%") + dialogMessageC := getLabelCentered("Confirm to overwrite this percentage?") + dialogContent := container.NewVBox(dialogMessageA, dialogMessageB, dialogMessageC) + dialog.ShowCustomConfirm(dialogTitle, translate("Yes"), translate("No"), dialogContent, confirmDialogCallbackFunction, window) + })) + + pageContent := container.NewVBox(continentRow, widget.NewSeparator(), regionRow, widget.NewSeparator(), subregionRow, widget.NewSeparator(), percentageRow, submitButton) + + return pageContent, nil + } + + pageContent, err := getPageContent() + if (err != nil){ + setErrorEncounteredPage(window, err, previousPage) + return + } + + page := container.NewVBox(title, backButton, widget.NewSeparator(), subtitle, widget.NewSeparator(), pageContent) + + setPageContent(page, window) +} + +func setBuildMateProfilePage_23andMe_AncestryComposition_DeleteLocation( + window fyne.Window, + locationType string, + myContinentPercentagesMap map[string]float64, + myRegionPercentagesMap map[string]float64, + mySubregionPercentagesMap map[string]float64, + previousPage func(), + nextPage func(map[string]float64, map[string]float64, map[string]float64)){ + + if (locationType != "Continent" && locationType != "Region" && locationType != "Subregion"){ + setErrorEncounteredPage(window, errors.New("setBuildMateProfilePage_23andMe_AncestryComposition_DeleteLocation called with invalid locationType: " + locationType), previousPage) + return + } + + title := getPageTitleCentered(translate("Build Mate Profile - Physical")) + + backButton := getBackButtonCentered(previousPage) + + description := getLabelCentered("Select a location to delete.") + + locationTypeLabel := getBoldLabelCentered("Location Type") + + locationTypesList := []string{"Continent", "Region", "Subregion"} + + handleLocationTypeSelectFunction := func(newLocationType string){ + if (newLocationType == locationType){ + return + } + + setBuildMateProfilePage_23andMe_AncestryComposition_DeleteLocation(window, newLocationType, myContinentPercentagesMap, myRegionPercentagesMap, mySubregionPercentagesMap, previousPage, nextPage) + } + + locationTypeSelector := widget.NewSelect(locationTypesList, handleLocationTypeSelectFunction) + locationTypeSelector.Selected = locationType + + locationTypeSelectorCentered := getWidgetCentered(locationTypeSelector) + + // This function either returns a "No (locationType) Exists" label or the locations grid. + getLocationsSection := func()(*fyne.Container, error){ + + if (locationType == "Continent" && len(myContinentPercentagesMap) == 0){ + locationsSection := getBoldLabelCentered("No continents exist.") + return locationsSection, nil + } + if (locationType == "Region" && len(myRegionPercentagesMap) == 0){ + locationsSection := getBoldLabelCentered("No regions exist.") + return locationsSection, nil + } + if (locationType == "Subregion" && len(mySubregionPercentagesMap) == 0){ + locationsSection := getBoldLabelCentered("No subregions exist.") + return locationsSection, nil + } + + locationLabel := getItalicLabelCentered(locationType) + percentageLabel := getItalicLabelCentered("Percentage") + emptyLabel := widget.NewLabel("") + + locationNameColumn := container.NewVBox(locationLabel, widget.NewSeparator()) + locationPercentageColumn := container.NewVBox(percentageLabel, widget.NewSeparator()) + deleteButtonsColumn := container.NewVBox(emptyLabel, widget.NewSeparator()) + + addLocationRow := func(locationName string, locationPercentage float64)error{ + + locationNameLabel := getBoldLabelCentered(locationName) + locationPercentageString := helpers.ConvertFloat64ToStringRounded(locationPercentage, 1) + + locationPercentageFormatted := locationPercentageString + "%" + locationPercentageLabel := getBoldLabelCentered(locationPercentageFormatted) + + deleteLocationButton := widget.NewButtonWithIcon("", theme.DeleteIcon(), func(){ + + if (locationType == "Continent"){ + + delete(myContinentPercentagesMap, locationName) + + } else if (locationType == "Region"){ + + delete(myRegionPercentagesMap, locationName) + + } else if (locationType == "Subregion") { + + delete(mySubregionPercentagesMap, locationName) + } + + nextPage(myContinentPercentagesMap, myRegionPercentagesMap, mySubregionPercentagesMap) + }) + + locationNameColumn.Add(locationNameLabel) + locationPercentageColumn.Add(locationPercentageLabel) + deleteButtonsColumn.Add(deleteLocationButton) + + locationNameColumn.Add(widget.NewSeparator()) + locationPercentageColumn.Add(widget.NewSeparator()) + deleteButtonsColumn.Add(widget.NewSeparator()) + + return nil + } + + if (locationType == "Continent"){ + + for continentName, continentPercentage := range myContinentPercentagesMap{ + err := addLocationRow(continentName, continentPercentage) + if (err != nil){ return nil, err } + } + } else if (locationType == "Region"){ + for regionName, regionPercentage := range myRegionPercentagesMap{ + err := addLocationRow(regionName, regionPercentage) + if (err != nil){ return nil, err } + } + } else if (locationType == "Subregion"){ + + for subregionName, subregionPercentage := range mySubregionPercentagesMap{ + err := addLocationRow(subregionName, subregionPercentage) + if (err != nil){ return nil, err } + } + } + + locationsGrid := container.NewHBox(layout.NewSpacer(), locationNameColumn, locationPercentageColumn, deleteButtonsColumn, layout.NewSpacer()) + + return locationsGrid, nil + } + + locationsSection, err := getLocationsSection() + if (err != nil){ + setErrorEncounteredPage(window, err, previousPage) + return + } + + page := container.NewVBox(title, backButton, widget.NewSeparator(), description, widget.NewSeparator(), locationTypeLabel, locationTypeSelectorCentered, widget.NewSeparator(), locationsSection) + + setPageContent(page, window) +} + +func setBuildMateProfilePage_23andMe_NeanderthalVariants(window fyne.Window, previousPage func()){ + + currentPage := func(){setBuildMateProfilePage_23andMe_NeanderthalVariants(window, previousPage)} + + title := getPageTitleCentered(translate("Build Mate Profile - Physical")) + + backButton := getBackButtonCentered(previousPage) + + pageSubtitle := getPageSubtitleCentered(translate("23andMe - Neanderthal Variants")) + + description := getLabelCentered(translate("Enter how many neanderthal variants you have.")) + + neanderthalVariantsEntry := widget.NewEntry() + + variantCountExists, currentNeanderthalVariants, err := myLocalProfiles.GetProfileData("Mate", "23andMe_NeanderthalVariants") + if (err != nil){ + setErrorEncounteredPage(window, err, previousPage) + return + } + if (variantCountExists == true){ + isValid, err := helpers.VerifyStringIsIntWithinRange(currentNeanderthalVariants, 0, 7462) + if (err != nil){ + setErrorEncounteredPage(window, err, previousPage) + return + } + if (isValid == false){ + setErrorEncounteredPage(window, errors.New("MyLocalProfile contains invalid 23andMe neanderthalvariants."), previousPage) + return + } + + neanderthalVariantsEntry.SetText(currentNeanderthalVariants) + } else { + neanderthalVariantsEntry.SetPlaceHolder(translate("Enter Variant Count")) + } + + getCurrentNeanderthalVariantsCountLabel := func()fyne.Widget{ + + if (variantCountExists == false){ + + result := translate("No Response") + + variantsLabel := getBoldItalicLabel(result) + return variantsLabel + } + + variantsLabel := getBoldLabel(currentNeanderthalVariants) + + return variantsLabel + } + + currentVariantCountLabel := getCurrentNeanderthalVariantsCountLabel() + + myVariantCountLabel := widget.NewLabel("My Variant Count:") + + currentVariantsCountRow := container.NewHBox(layout.NewSpacer(), myVariantCountLabel, currentVariantCountLabel, layout.NewSpacer()) + + submitChangesButton := widget.NewButtonWithIcon("Submit", theme.ConfirmIcon(), func(){ + + newVariantsCount := neanderthalVariantsEntry.Text + + isValid, err := helpers.VerifyStringIsIntWithinRange(newVariantsCount, 0, 7462) + if (err != nil){ + setErrorEncounteredPage(window, err, currentPage) + return + } + if (isValid == false){ + neanderthalVariantsEntry.SetText("") + neanderthalVariantsEntry.SetPlaceHolder(translate("Enter Variant Count")) + + dialogTitle := translate("Invalid Variant Count") + dialogMessageA := getLabelCentered(translate("Invalid neanderthal variants count entered.")) + dialogMessageB := getLabelCentered(translate("You must provide a number between 0 and 7462 variants.")) + dialogContent := container.NewVBox(dialogMessageA, dialogMessageB) + dialog.ShowCustom(dialogTitle, translate("Close"), dialogContent, window) + return + } + + err = myLocalProfiles.SetProfileData("Mate", "23andMe_NeanderthalVariants", newVariantsCount) + if (err != nil){ + setErrorEncounteredPage(window, err, currentPage) + return + } + + currentPage() + }) + + variantsEntryWithSubmitButton := getContainerCentered(container.NewGridWithRows(1, neanderthalVariantsEntry, submitChangesButton)) + + noResponseButton := getWidgetCentered(widget.NewButtonWithIcon(translate("No Response"), theme.CancelIcon(), func(){ + err := myLocalProfiles.DeleteProfileData("Mate", "23andMe_NeanderthalVariants") + if (err != nil){ + setErrorEncounteredPage(window, err, currentPage) + return + } + currentPage() + })) + + page := container.NewVBox(title, backButton, widget.NewSeparator(), pageSubtitle, widget.NewSeparator(), description, widget.NewSeparator(), currentVariantsCountRow, widget.NewSeparator(), variantsEntryWithSubmitButton, noResponseButton) + + setPageContent(page, window) +} + +func setBuildMateProfilePage_23andMe_Haplogroup(window fyne.Window, maternalOrPaternal string, previousPage func()){ + + if (maternalOrPaternal != "Maternal" && maternalOrPaternal != "Paternal"){ + setErrorEncounteredPage(window, errors.New("setBuildMateProfilePage_23andMe_Haplogroup called with invalid maternalOrPaternal: " + maternalOrPaternal), previousPage) + return + } + + currentPage := func(){setBuildMateProfilePage_23andMe_Haplogroup(window, maternalOrPaternal, previousPage)} + + title := getPageTitleCentered(translate("Build Mate Profile - Physical")) + + backButton := getBackButtonCentered(previousPage) + + pageSubtitle := getPageSubtitleCentered(translate("23andMe - " + maternalOrPaternal + " Haplogroup")) + + maternalOrPaternalLowercase := strings.ToLower(maternalOrPaternal) + + description1 := getLabelCentered("Choose your " + maternalOrPaternalLowercase + " haplogroup.") + description2 := getLabelCentered("If it is not listed, enter it in manually.") + + getKnownHaplogroupsList := func()[]string{ + + if (maternalOrPaternal == "Maternal"){ + maternalHaplogroupsList := companyAnalysis.GetKnownMaternalHaplogroupsList_23andMe() + return maternalHaplogroupsList + } + + paternalHaplogroupsList := companyAnalysis.GetKnownPaternalHaplogroupsList_23andMe() + return paternalHaplogroupsList + } + + knownHaplogroupsList := getKnownHaplogroupsList() + + haplogroupEntry := widget.NewSelectEntry(knownHaplogroupsList) + + attributeName := "23andMe_" + maternalOrPaternal + "Haplogroup" + + haplogroupExists, currentHaplogroup, err := myLocalProfiles.GetProfileData("Mate", attributeName) + if (err != nil){ + setErrorEncounteredPage(window, err, previousPage) + return + } + if (haplogroupExists == true){ + + if (len(currentHaplogroup) > 25){ + setErrorEncounteredPage(window, errors.New("MyLocalProfiles contains invalid haplogroup: " + currentHaplogroup), previousPage) + return + } + + haplogroupEntry.SetText(currentHaplogroup) + } else { + haplogroupEntry.SetPlaceHolder(translate("Select or enter haplogroup...")) + } + + getCurrentHaplogroupLabel := func()fyne.Widget{ + + if (haplogroupExists == false){ + result := translate("No Response") + + haplogroupLabel := getBoldItalicLabel(result) + return haplogroupLabel + } + + haplogroupLabel := getBoldLabel(currentHaplogroup) + return haplogroupLabel + } + + currentHaplogroupLabel := getCurrentHaplogroupLabel() + + myHaplogroupLabel := widget.NewLabel("My " + maternalOrPaternal + " Haplogroup:") + + currentHaplogroupRow := container.NewHBox(layout.NewSpacer(), myHaplogroupLabel, currentHaplogroupLabel, layout.NewSpacer()) + + submitChangesButton := widget.NewButtonWithIcon("Submit", theme.ConfirmIcon(), func(){ + + newHaplogroup := haplogroupEntry.Text + + if (newHaplogroup == ""){ + title := translate("Invalid Haplogroup") + dialogMessageA := getLabelCentered(translate("Invalid haplogroup entered.")) + dialogMessageB := getLabelCentered(translate("Your haplogroup cannot be empty.")) + dialogContent := container.NewVBox(dialogMessageA, dialogMessageB) + dialog.ShowCustom(title, translate("Close"), dialogContent, window) + return + } + + if (len(newHaplogroup) > 25){ + + haplogroupEntry.SetText("") + haplogroupEntry.SetPlaceHolder(translate("Select or enter haplogroup...")) + + title := translate("Invalid Haplogroup") + dialogMessageA := getLabelCentered(translate("Invalid haplogroup entered.")) + dialogMessageB := getLabelCentered(translate("It must be less than 25 bytes.")) + dialogMessageC := getLabelCentered(translate("Contact the Seekia developers if this is too short to fit your haplogroup.")) + dialogContent := container.NewVBox(dialogMessageA, dialogMessageB, dialogMessageC) + dialog.ShowCustom(title, translate("Close"), dialogContent, window) + return + } + + isAllowed := allowedText.VerifyStringIsAllowed(newHaplogroup) + if (isAllowed == false){ + title := translate("Invalid Haplogroup") + dialogMessageA := getLabelCentered(translate("Your haplogroup contains 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 + } + + containsTabsOrNewlines := helpers.CheckIfStringContainsTabsOrNewlines(newHaplogroup) + if (containsTabsOrNewlines == true){ + title := translate("Invalid Haplogroup") + dialogMessageA := getLabelCentered(translate("Your haplogroup contains a tab or a newline.")) + dialogMessageB := getLabelCentered(translate("Remove the character and resubmit.")) + dialogContent := container.NewVBox(dialogMessageA, dialogMessageB) + dialog.ShowCustom(title, translate("Close"), dialogContent, window) + return + } + + err := myLocalProfiles.SetProfileData("Mate", attributeName, newHaplogroup) + if (err != nil){ + setErrorEncounteredPage(window, err, currentPage) + return + } + currentPage() + }) + + haplogroupEntryWithSubmitButton := getContainerCentered(container.NewGridWithRows(1, haplogroupEntry, submitChangesButton)) + + noResponseButton := getWidgetCentered(widget.NewButtonWithIcon(translate("No Response"), theme.CancelIcon(), func(){ + err := myLocalProfiles.DeleteProfileData("Mate", attributeName) + if (err != nil){ + setErrorEncounteredPage(window, err, currentPage) + return + } + currentPage() + })) + + page := container.NewVBox(title, backButton, widget.NewSeparator(), pageSubtitle, widget.NewSeparator(), description1, description2, widget.NewSeparator(), currentHaplogroupRow, widget.NewSeparator(), haplogroupEntryWithSubmitButton, noResponseButton) + + setPageContent(page, window) +} + +func setBuildMateProfilePage_Age(window fyne.Window, previousPage func()){ + + currentPage := func(){setBuildMateProfilePage_Age(window, previousPage)} + + pageTitle := getPageTitleCentered(translate("Build Mate Profile - Physical")) + + backButton := getBackButtonCentered(previousPage) + + pageSubtitle := getPageSubtitleCentered(translate("Age")) + + description1 := getLabelCentered(translate("Enter your age. You must be at least 18.")) + description2 := getLabelCentered(translate("You should update your age as you get older.")) + + ageEntry := widget.NewEntry() + + ageExists, currentAge, err := myLocalProfiles.GetProfileData("Mate", "Age") + if (err != nil){ + setErrorEncounteredPage(window, err, previousPage) + return + } + if (ageExists == true){ + isValid, err := helpers.VerifyStringIsIntWithinRange(currentAge, 18, 150) + if (err != nil){ + setErrorEncounteredPage(window, err, previousPage) + return + } + if (isValid == false){ + setErrorEncounteredPage(window, errors.New("MyLocalProfiles contains invalid age."), previousPage) + return + } + + ageEntry.SetText(currentAge) + } else { + ageEntry.SetPlaceHolder(translate("Enter Age")) + } + + getCurrentAgeLabel := func()fyne.Widget{ + + if (ageExists == false){ + result := translate("No Response") + + ageLabel := getBoldItalicLabel(result) + return ageLabel + } + + ageLabel := getBoldLabel(currentAge) + return ageLabel + } + + currentAgeLabel := getCurrentAgeLabel() + + myAgeLabel := widget.NewLabel("My Age:") + + currentAgeRow := container.NewHBox(layout.NewSpacer(), myAgeLabel, currentAgeLabel, layout.NewSpacer()) + + submitChangesButton := widget.NewButtonWithIcon("Submit", theme.ConfirmIcon(), func(){ + + newAge := ageEntry.Text + + isValid, err := helpers.VerifyStringIsIntWithinRange(newAge, 18, 150) + if (err != nil){ + setErrorEncounteredPage(window, err, currentPage) + return + } + if (isValid == false){ + + ageEntry.SetText("") + ageEntry.SetPlaceHolder(translate("Enter Age")) + + title := translate("Invalid Age") + dialogMessageA := getLabelCentered(translate("Invalid age entered.")) + dialogMessageB := getLabelCentered(translate("You must be at least 18 to use Seekia.")) + dialogContent := container.NewVBox(dialogMessageA, dialogMessageB) + dialog.ShowCustom(title, translate("Close"), dialogContent, window) + return + } + + err = myLocalProfiles.SetProfileData("Mate", "Age", newAge) + if (err != nil){ + setErrorEncounteredPage(window, err, currentPage) + return + } + + currentPage() + }) + + ageEntryWithSubmitButton := getContainerCentered(container.NewGridWithRows(1, ageEntry, submitChangesButton)) + + noResponseButton := getWidgetCentered(widget.NewButtonWithIcon(translate("No Response"), theme.CancelIcon(), func(){ + err := myLocalProfiles.DeleteProfileData("Mate", "Age") + if (err != nil){ + setErrorEncounteredPage(window, err, currentPage) + return + } + + currentPage() + })) + + page := container.NewVBox(pageTitle, backButton, widget.NewSeparator(), pageSubtitle, widget.NewSeparator(), description1, description2, widget.NewSeparator(), currentAgeRow, widget.NewSeparator(), ageEntryWithSubmitButton, noResponseButton) + + setPageContent(page, window) +} + + +func setBuildMateProfilePage_GeneticInformation(window fyne.Window, previousPage func()){ + + currentPage := func(){setBuildMateProfilePage_GeneticInformation(window, previousPage)} + + title := getPageTitleCentered(translate("Build Mate Profile - Physical")) + + backButton := getBackButtonCentered(previousPage) + + subtitle := getPageSubtitleCentered(translate("Genetic Information")) + + description1 := widget.NewLabel("You can display your genetic information on your profile.") + shareGeneticInfoHelpButton := widget.NewButtonWithIcon("", theme.QuestionIcon(), func(){ + setProfileGenomeInfoExplainerPage(window, currentPage) + }) + + description1Row := container.NewHBox(layout.NewSpacer(), description1, shareGeneticInfoHelpButton, layout.NewSpacer()) + + description2 := getLabelCentered("You must link your genome person to your profile.") + description3 := getLabelCentered("You can hide some or all of your genetic information.") + + myGenomePersonIdentifierExists, myGenomePersonIdentifier, err := myLocalProfiles.GetProfileData("Mate", "GenomePersonIdentifier") + if (err != nil){ + setErrorEncounteredPage(window, err, previousPage) + return + } + if (myGenomePersonIdentifierExists == true){ + personFound, _, _, _, err := myPeople.GetPersonInfo(myGenomePersonIdentifier) + if (err != nil){ + setErrorEncounteredPage(window, err, previousPage) + return + } + if (personFound == false){ + // Genome person was deleted + // We will delete the missing person and refresh the page + + // We show a loading screen so the user can tell if this wrongfully keeps happening + setLoadingScreen(window, "Build Mate Profile - Physical", "Deleting old genome person identifier...") + + err := myLocalProfiles.DeleteProfileData("Mate", "GenomePersonIdentifier") + if (err != nil){ + setErrorEncounteredPage(window, err, previousPage) + return + } + currentPage() + return + } + } + + myPersonLabel := widget.NewLabel("My Person:") + + getCurrentPersonNameLabel := func()(fyne.Widget, error){ + + if (myGenomePersonIdentifierExists == false){ + noneLabel := getBoldItalicLabel(translate("None")) + return noneLabel, nil + } + + personFound, personName, _, _, err := myPeople.GetPersonInfo(myGenomePersonIdentifier) + if (err != nil){ return nil, err } + if (personFound == false){ + return nil, errors.New("Genome person not found after being found already.") + } + + personNameLabel := getBoldLabel(personName) + + return personNameLabel, nil + } + + currentPersonNameLabel, err := getCurrentPersonNameLabel() + if (err != nil){ + setErrorEncounteredPage(window, err, previousPage) + return + } + + currentPersonNameRow := container.NewHBox(layout.NewSpacer(), myPersonLabel, currentPersonNameLabel, layout.NewSpacer()) + + getChooseOrChangeButtonTextAndIcon := func()(string, fyne.Resource){ + + if (myGenomePersonIdentifierExists == false){ + result := translate("Choose Person") + return result, theme.NavigateNextIcon() + } + + result := translate("Change Person") + + return result, theme.DocumentCreateIcon() + } + + chooseOrChangeButtonText, chooseOrChangeButtonIcon := getChooseOrChangeButtonTextAndIcon() + + chooseOrChangePersonButton := getWidgetCentered(widget.NewButtonWithIcon(chooseOrChangeButtonText, chooseOrChangeButtonIcon, func(){ + setBuildMateProfilePage_EditGenomePerson(window, currentPage, currentPage) + })) + + if (myGenomePersonIdentifierExists == false){ + + page := container.NewVBox(title, backButton, widget.NewSeparator(), subtitle, widget.NewSeparator(), description1Row, description2, description3, widget.NewSeparator(), currentPersonNameRow, chooseOrChangePersonButton) + + setPageContent(page, window) + return + } + + currentlySharingLabel := getItalicLabelCentered("Currently Sharing:") + + //Outputs: + // -int: Total number of monogenic diseases + // -int: Number of monogenic diseases being shared + // -error + getNumberOfMonogenicDiseasesBeingSharedInfo := func()(int, int, error){ + + monogenicDiseaseNamesList, err := monogenicDiseases.GetMonogenicDiseaseNamesList() + if (err != nil) { return 0, 0, err } + + totalMonogenicDiseases := len(monogenicDiseaseNamesList) + totalMonogenicDiseasesBeingShared := 0 + + for _, diseaseName := range monogenicDiseaseNamesList{ + + shareDiseaseInfoAttributeName := "ShareMonogenicDiseaseInfo_" + diseaseName + + currentSharePreferenceExists, currentSharePreference, err := myLocalProfiles.GetProfileData("Mate", shareDiseaseInfoAttributeName) + if (err != nil){ return 0, 0, err } + + if (currentSharePreferenceExists == true && currentSharePreference == "Yes"){ + totalMonogenicDiseasesBeingShared += 1 + } + } + + return totalMonogenicDiseases, totalMonogenicDiseasesBeingShared, nil + } + + //Outputs: + // -int: Total number of polygenic diseases + // -int: Number of polygenic diseases being shared + // -error + getNumberOfPolygenicDiseasesBeingSharedInfo := func()(int, int, error){ + + polygenicDiseaseNamesList, err := polygenicDiseases.GetPolygenicDiseaseNamesList() + if (err != nil) { return 0, 0, err } + + totalPolygenicDiseases := len(polygenicDiseaseNamesList) + totalPolygenicDiseasesBeingShared := 0 + + for _, diseaseName := range polygenicDiseaseNamesList{ + + sharePolygenicDiseaseInfoAttributeName := "SharePolygenicDiseaseInfo_" + diseaseName + + currentSharePreferenceExists, currentSharePreference, err := myLocalProfiles.GetProfileData("Mate", sharePolygenicDiseaseInfoAttributeName) + if (err != nil){ return 0, 0, err } + + if (currentSharePreferenceExists == true && currentSharePreference == "Yes"){ + totalPolygenicDiseasesBeingShared += 1 + } + } + + return totalPolygenicDiseases, totalPolygenicDiseasesBeingShared, nil + } + + //Outputs: + // -int: Total number of traits + // -int: Number of traits being shared + // -error + getNumberOfTraitsBeingSharedInfo := func()(int, int, error){ + + traitNamesList, err := traits.GetTraitNamesList() + if (err != nil) { return 0, 0, err } + + totalTraits := len(traitNamesList) + totalTraitsBeingShared := 0 + + for _, traitName := range traitNamesList{ + + shareTraitInfoAttributeName := "ShareTraitInfo_" + traitName + + currentSharePreferenceExists, currentSharePreference, err := myLocalProfiles.GetProfileData("Mate", shareTraitInfoAttributeName) + if (err != nil){ return 0, 0, err } + + if (currentSharePreferenceExists == true && currentSharePreference == "Yes"){ + totalTraitsBeingShared += 1 + } + } + + return totalTraits, totalTraitsBeingShared, nil + } + + totalMonogenicDiseases, numberOfMonogenicDiseasesBeingShared, err := getNumberOfMonogenicDiseasesBeingSharedInfo() + if (err != nil){ + setErrorEncounteredPage(window, err, previousPage) + return + } + + totalPolygenicDiseases, numberOfPolygenicDiseasesBeingShared, err := getNumberOfPolygenicDiseasesBeingSharedInfo() + if (err != nil){ + setErrorEncounteredPage(window, err, previousPage) + return + } + + totalTraits, numberOfTraitsBeingShared, err := getNumberOfTraitsBeingSharedInfo() + if (err != nil){ + setErrorEncounteredPage(window, err, previousPage) + return + } + + totalMonogenicDiseasesString := helpers.ConvertIntToString(totalMonogenicDiseases) + numberOfMonogenicDiseasesBeingSharedString := helpers.ConvertIntToString(numberOfMonogenicDiseasesBeingShared) + numberOfMonogenicDiseasesBeingSharedLabel := getBoldLabelCentered("Monogenic Diseases: " + numberOfMonogenicDiseasesBeingSharedString + "/" + totalMonogenicDiseasesString) + + totalPolygenicDiseasesString := helpers.ConvertIntToString(totalPolygenicDiseases) + numberOfPolygenicDiseasesBeingSharedString := helpers.ConvertIntToString(numberOfPolygenicDiseasesBeingShared) + numberOfPolygenicDiseasesBeingSharedLabel := getBoldLabelCentered("Polygenic Diseases: " + numberOfPolygenicDiseasesBeingSharedString + "/" + totalPolygenicDiseasesString) + + totalTraitsString := helpers.ConvertIntToString(totalTraits) + numberOfTraitsBeingSharedString := helpers.ConvertIntToString(numberOfTraitsBeingShared) + numberOfTraitsBeingSharedLabel := getBoldLabelCentered("Traits: " + numberOfTraitsBeingSharedString + "/" + totalTraitsString) + + changeVisibilityButton := getWidgetCentered(widget.NewButtonWithIcon("Change Visibility", theme.DocumentCreateIcon(), func(){ + setBuildMateProfilePage_ChangeGeneticInformationVisibility(window, "MonogenicDiseases", currentPage) + })) + + currentGenomeLabel := widget.NewLabel("Current Genome:") + + getCurrentGenomeText := func()(string, error){ + + myGenomesMapList, err := myGenomes.GetAllPersonGenomesMapList(myGenomePersonIdentifier) + if (err != nil) { return "", err } + + if (len(myGenomesMapList) == 0){ + genomeText := translate("None") + return genomeText, nil + } + if (len(myGenomesMapList) == 1){ + myGenomeMap := myGenomesMapList[0] + + myGenomeCompanyName, exists := myGenomeMap["CompanyName"] + if (exists == false){ + return "", errors.New("MyGenomesMapList is malformed: Genome map missing CompanyName") + } + + return myGenomeCompanyName, nil + } + + currentCombinedGenomeToUse, err := myChosenAnalysis.GetMyCombinedGenomeToUse() + if (err != nil){ return "", err } + + result := translate(currentCombinedGenomeToUse) + return result, nil + } + + currentGenomeText, err := getCurrentGenomeText() + if (err != nil){ + setErrorEncounteredPage(window, err, previousPage) + return + } + + currentGenomeTextLabel := getBoldLabel(currentGenomeText) + + currentGenomeRow := container.NewHBox(layout.NewSpacer(), currentGenomeLabel, currentGenomeTextLabel, layout.NewSpacer()) + + chooseGenomeButton := getWidgetCentered(widget.NewButtonWithIcon("Choose Genome", theme.DocumentCreateIcon(), func(){ + setBuildMateProfilePage_ChooseGeneticInformationGenome(window, currentPage) + })) + + page := container.NewVBox(title, backButton, widget.NewSeparator(), subtitle, widget.NewSeparator(), description1Row, description2, description3, widget.NewSeparator(), currentPersonNameRow, chooseOrChangePersonButton, widget.NewSeparator(), currentlySharingLabel, numberOfMonogenicDiseasesBeingSharedLabel, numberOfPolygenicDiseasesBeingSharedLabel, numberOfTraitsBeingSharedLabel, changeVisibilityButton, widget.NewSeparator(), currentGenomeRow, chooseGenomeButton) + + setPageContent(page, window) +} + +func setBuildMateProfilePage_EditGenomePerson(window fyne.Window, previousPage func(), nextPage func()){ + + currentPage := func(){setBuildMateProfilePage_EditGenomePerson(window, previousPage, nextPage)} + + title := getPageTitleCentered(translate("Build Mate Profile - Physical")) + + backButton := getBackButtonCentered(previousPage) + + subtitle := getPageSubtitleCentered("Choose Genome Person") + + description1 := getLabelCentered("Choose your genome person.") + description2 := getLabelCentered("You must first import your genome(s) on the Genetics page.") + description3 := getLabelCentered("As you import or remove genomes, the changes will be reflected in your profile.") + description4 := getLabelCentered("To avoid sharing information, you can hide some or all of your genetic information.") + description5 := getLabelCentered("Linking your genome person will enable you to view offspring analyses when viewing matches.") + + myGenomePersonIdentifierExists, currentGenomePersonIdentifier, err := myLocalProfiles.GetProfileData("Mate", "GenomePersonIdentifier") + if (err != nil){ + setErrorEncounteredPage(window, err, previousPage) + return + } + if (myGenomePersonIdentifierExists == true){ + personFound, _, _, _, err := myPeople.GetPersonInfo(currentGenomePersonIdentifier) + if (err != nil){ + setErrorEncounteredPage(window, err, previousPage) + return + } + if (personFound == false){ + // Genome person was deleted + // We will delete the missing person and refresh the page + + // We show a loading screen so the user can tell if this wrongfully keeps happening + setLoadingScreen(window, "Build Mate Profile - Physical", "Deleting old genome person identifier...") + + err := myLocalProfiles.DeleteProfileData("Mate", "GenomePersonIdentifier") + if (err != nil){ + setErrorEncounteredPage(window, err, previousPage) + return + } + currentPage() + return + } + } + + getCurrentPersonNameLabel := func()(fyne.Widget, error){ + + if (myGenomePersonIdentifierExists == false){ + result := translate("None") + + noResponseLabel := getBoldItalicLabel(result) + return noResponseLabel, nil + } + + personFound, personName, _, _, err := myPeople.GetPersonInfo(currentGenomePersonIdentifier) + if (err != nil){ return nil, err } + if (personFound == false){ + return nil, errors.New("Genome person not found after being found already.") + } + + personNameLabel := getBoldLabel(personName) + + return personNameLabel, nil + } + + currentPersonNameLabel, err := getCurrentPersonNameLabel() + if (err != nil){ + setErrorEncounteredPage(window, err, previousPage) + return + } + + currentPersonLabel := widget.NewLabel("Current Person:") + + currentPersonNameRow := container.NewHBox(layout.NewSpacer(), currentPersonLabel, currentPersonNameLabel, layout.NewSpacer()) + + getSelectGenomePersonSection := func()(*fyne.Container, error){ + + allMyGenomePeopleMapList, err := myPeople.GetMyGenomePeopleMapList() + if (err != nil){ return nil, err } + + if (len(allMyGenomePeopleMapList) == 0){ + + noPeopleExistLabel := getBoldLabel("No People Exist.") + + createAPersonLabel := getLabelCentered("Create a person and import their genomes on the Genetics page.") + + selectPersonSection := container.NewVBox(noPeopleExistLabel, createAPersonLabel) + + return selectPersonSection, nil + } + + // Map Structure: Person Name -> Person Identifier + allGenomePersonNamesMap := make(map[string]string) + + for _, personMap := range allMyGenomePeopleMapList{ + + personName, exists := personMap["PersonName"] + if (exists == false){ + return nil, errors.New("MyGenomePeople map list is malformed: Item missing PersonName") + } + + personIdentifier, exists := personMap["PersonIdentifier"] + if (exists == false){ + return nil, errors.New("MyGenomePeople map list is malformed: Item missing PersonIdentifier") + } + + _, exists = allGenomePersonNamesMap[personName] + if (exists == true){ + return nil, errors.New("MyGenomePeople map list is malformed: Duplicate person names exist.") + } + + allGenomePersonNamesMap[personName] = personIdentifier + } + + allGenomePersonNamesList := helpers.GetListOfMapKeys(allGenomePersonNamesMap) + + personSelector := widget.NewSelect(allGenomePersonNamesList, nil) + + if (myGenomePersonIdentifierExists == true){ + + personFound, personName, _, _, err := myPeople.GetPersonInfo(currentGenomePersonIdentifier) + if (err != nil){ return nil, err } + if (personFound == false){ + return nil, errors.New("Genome person not found after being found already.") + } + + personSelector.Selected = personName + } else { + personSelector.PlaceHolder = translate("Select Person...") + } + + submitButton := widget.NewButtonWithIcon("Submit", theme.ConfirmIcon(), func(){ + + newPersonName := personSelector.Selected + + if (newPersonName == ""){ + dialogTitle := translate("No Person Chosen") + dialogMessageA := getLabelCentered(translate("No person was chosen.")) + dialogMessageB := getLabelCentered(translate("Select a person from the selector.")) + dialogContent := container.NewVBox(dialogMessageA, dialogMessageB) + dialog.ShowCustom(dialogTitle, translate("Close"), dialogContent, window) + return + } + + newPersonIdentifier, exists := allGenomePersonNamesMap[newPersonName] + if (exists == false){ + setErrorEncounteredPage(window, errors.New("New genome person identifier not found."), currentPage) + return + } + + err := myLocalProfiles.SetProfileData("Mate", "GenomePersonIdentifier", newPersonIdentifier) + if (err != nil){ + setErrorEncounteredPage(window, err, currentPage) + return + } + nextPage() + }) + + personSelectorWithSubmitButtonRow := container.NewHBox(layout.NewSpacer(), personSelector, submitButton, layout.NewSpacer()) + + removePersonButton := getWidgetCentered(widget.NewButtonWithIcon(translate("Remove Person"), theme.CancelIcon(), func(){ + err := myLocalProfiles.DeleteProfileData("Mate", "GenomePersonIdentifier") + if (err != nil){ + setErrorEncounteredPage(window, err, currentPage) + return + } + nextPage() + })) + + selectGenomePersonSection := container.NewVBox(personSelectorWithSubmitButtonRow, removePersonButton) + + return selectGenomePersonSection, nil + } + + selectGenomePersonSection, err := getSelectGenomePersonSection() + if (err != nil){ + setErrorEncounteredPage(window, err, previousPage) + return + } + + page := container.NewVBox(title, backButton, widget.NewSeparator(), subtitle, widget.NewSeparator(), description1, description2, description3, description4, description5, widget.NewSeparator(), currentPersonNameRow, widget.NewSeparator(), selectGenomePersonSection) + + setPageContent(page, window) +} + +func setBuildMateProfilePage_ChangeGeneticInformationVisibility(window fyne.Window, informationCategory string, previousPage func()){ + + if (informationCategory != "MonogenicDiseases" && informationCategory != "PolygenicDiseases" && informationCategory != "Traits"){ + setErrorEncounteredPage(window, errors.New("setBuildMateProfilePage_ChangeGeneticInformationVisibility called with invalid informationCategory: " + informationCategory), previousPage) + return + } + + currentPage := func(){setBuildMateProfilePage_ChangeGeneticInformationVisibility(window, informationCategory, previousPage)} + + title := getPageTitleCentered(translate("Build Mate Profile - Physical")) + + backButton := getBackButtonCentered(previousPage) + + subtitle := getPageSubtitleCentered("Change Genetic Information Visibility") + + description1 := getLabelCentered("Choose which information from your genome you want to share on your profile.") + description2 := getLabelCentered("You will still always be able to view offspring analyses for your matches.") + + privacyWarningButton := getWidgetCentered(widget.NewButtonWithIcon("Privacy Warning", theme.WarningIcon(), func(){ + setMateProfileGeneticInformationPrivacyWarningPage(window, currentPage) + })) + + currentCategoryLabel := getItalicLabelCentered("Current Category:") + + monogenicDiseasesTranslated := translate("Monogenic Diseases") + polygenicDiseasesTranslated := translate("Polygenic Diseases") + traitsTranslated := translate("Traits") + + categoriesList := []string{monogenicDiseasesTranslated, polygenicDiseasesTranslated, traitsTranslated} + + handleSelectFunction := func(newChoice string){ + + getNewCategory := func()string{ + + if (newChoice == monogenicDiseasesTranslated){ + return "MonogenicDiseases" + } + if (newChoice == polygenicDiseasesTranslated){ + return "PolygenicDiseases" + } + return "Traits" + } + + newCategory := getNewCategory() + + setBuildMateProfilePage_ChangeGeneticInformationVisibility(window, newCategory, previousPage) + } + + categorySelector := widget.NewSelect(categoriesList, handleSelectFunction) + + if (informationCategory == "MonogenicDiseases"){ + categorySelector.Selected = monogenicDiseasesTranslated + + } else if (informationCategory == "PolygenicDiseases"){ + categorySelector.Selected = polygenicDiseasesTranslated + + } else { + categorySelector.Selected = traitsTranslated + } + + categorySelectorCentered := getWidgetCentered(categorySelector) + + // This returns a list of all MonogenicDiseaseNames, PolygenicDiseaseNames, or TraitNames + getItemNamesList := func()([]string, error){ + + if (informationCategory == "MonogenicDiseases"){ + + monogenicDiseaseNamesList, err := monogenicDiseases.GetMonogenicDiseaseNamesList() + if (err != nil) { return nil, err } + + return monogenicDiseaseNamesList, nil + } + if (informationCategory == "PolygenicDiseases"){ + + polygenicDiseaseNamesList, err := polygenicDiseases.GetPolygenicDiseaseNamesList() + if (err != nil) { return nil, err } + + return polygenicDiseaseNamesList, nil + } + + // informationCategory == "Traits" + + traitNamesList, err := traits.GetTraitNamesList() + if (err != nil) { return nil, err } + + return traitNamesList, nil + } + + allItemNamesList, err := getItemNamesList() + if (err != nil) { + setErrorEncounteredPage(window, err, previousPage) + return + } + + getCategoryTypeShareInfoPrefix := func()string{ + + if (informationCategory == "MonogenicDiseases"){ + return "ShareMonogenicDiseaseInfo_" + } + if (informationCategory == "PolygenicDiseases"){ + return "SharePolygenicDiseaseInfo_" + } + return "ShareTraitInfo_" + } + + categoryTypeShareInfoPrefix := getCategoryTypeShareInfoPrefix() + + setAllItemShareAttributesToValue := func(yesOrNo string)error{ + + if (yesOrNo != "Yes" && yesOrNo != "No"){ + return errors.New("setAllItemShareAttributesToValue called with invalid yesOrNo: " + yesOrNo) + } + + for _, itemName := range allItemNamesList{ + + itemAttributeName := categoryTypeShareInfoPrefix + itemName + + err := myLocalProfiles.SetProfileData("Mate", itemAttributeName, yesOrNo) + if (err != nil) { return err } + } + + return nil + } + + shareAllButton := widget.NewButtonWithIcon("Share All", theme.VisibilityIcon(), func(){ + + err := setAllItemShareAttributesToValue("Yes") + if (err != nil){ + setErrorEncounteredPage(window, err, currentPage) + return + } + currentPage() + }) + + hideAllButton := widget.NewButtonWithIcon("Hide All", theme.VisibilityOffIcon(), func(){ + err := setAllItemShareAttributesToValue("No") + if (err != nil){ + setErrorEncounteredPage(window, err, currentPage) + return + } + currentPage() + }) + + shareAndHideButtons := getContainerCentered(container.NewGridWithRows(1, shareAllButton, hideAllButton)) + + getCategoryItemsGrid := func()(*fyne.Container, error){ + + getItemTypeName := func()string{ + if (informationCategory == "MonogenicDiseases"){ + result := translate("Monogenic Disease") + return result + } + + if (informationCategory == "PolygenicDiseases"){ + result := translate("Polygenic Disease") + return result + } + result := translate("Trait") + + return result + } + + itemTypeName := getItemTypeName() + + itemTypeNameLabel := getItalicLabelCentered(itemTypeName + " " + translate("Name")) + + shareInformationLabel := getItalicLabelCentered(translate("Share Information?")) + + itemNameColumn := container.NewVBox(itemTypeNameLabel, widget.NewSeparator()) + itemChecksColumn := container.NewVBox(shareInformationLabel, widget.NewSeparator()) + + for _, itemName := range allItemNamesList{ + + itemNameLabel := getBoldLabelCentered(itemName) + + itemAttributeName := categoryTypeShareInfoPrefix + itemName + + itemCheck := widget.NewCheck("", func(newResponse bool){ + + newValueString := helpers.ConvertBoolToYesOrNoString(newResponse) + + err := myLocalProfiles.SetProfileData("Mate", itemAttributeName, newValueString) + if (err != nil) { + setErrorEncounteredPage(window, err, currentPage) + return + } + }) + + currentSharePreferenceExists, currentSharePreference, err := myLocalProfiles.GetProfileData("Mate", itemAttributeName) + if (err != nil){ return nil, err } + + if (currentSharePreferenceExists == true && currentSharePreference == "Yes"){ + itemCheck.Checked = true + } + + itemCheckCentered := getWidgetCentered(itemCheck) + + itemNameColumn.Add(itemNameLabel) + itemChecksColumn.Add(itemCheckCentered) + + itemNameColumn.Add(widget.NewSeparator()) + itemChecksColumn.Add(widget.NewSeparator()) + } + + itemsGrid := container.NewHBox(layout.NewSpacer(), itemNameColumn, itemChecksColumn, layout.NewSpacer()) + + return itemsGrid, nil + } + + categoryItemsGrid, err := getCategoryItemsGrid() + if (err != nil){ + setErrorEncounteredPage(window, err, previousPage) + return + } + + page := container.NewVBox(title, backButton, widget.NewSeparator(), subtitle, widget.NewSeparator(), description1, description2, privacyWarningButton, widget.NewSeparator(), currentCategoryLabel, categorySelectorCentered, widget.NewSeparator(), shareAndHideButtons, widget.NewSeparator(), categoryItemsGrid) + + setPageContent(page, window) +} + +func setMateProfileGeneticInformationPrivacyWarningPage(window fyne.Window, previousPage func()){ + + title := getPageTitleCentered(translate("Build Mate Profile - Physical")) + + backButton := getBackButtonCentered(previousPage) + + subtitle := getPageSubtitleCentered("Genetic Information Privacy Warning") + + description1 := getLabelCentered("By sharing genome information, users can read an analysis of an offspring created from you and them.") + description2 := getLabelCentered("When you share Polygenic Disease and Trait information, you are sharing specific locations on your genome.") + description3 := getLabelCentered("These locations represent a tiny fraction of your entire genome.") + description4 := getLabelCentered("These locations can still be used to create a unique genetic fingerprint, which can identify you.") + description5 := getLabelCentered("People will also be able to read your trait and polygenic disease risk information.") + description6 := getLabelCentered("For example, if you share Breast Cancer, users will know your Breast Cancer risk.") + description7 := getLabelCentered("When sharing Monogenic Disease information, you do not share specific locations.") + description8 := getLabelCentered("You instead share the probability of passing a monogenic disease variant to your offspring.") + + page := container.NewVBox(title, backButton, widget.NewSeparator(), subtitle, widget.NewSeparator(), description1, description2, description3, description4, description5, description6, description7, description8) + + setPageContent(page, window) +} + + +func setBuildMateProfilePage_ChooseGeneticInformationGenome(window fyne.Window, previousPage func()){ + + currentPage := func(){setBuildMateProfilePage_ChooseGeneticInformationGenome(window, previousPage)} + + title := getPageTitleCentered(translate("Build Mate Profile - Physical")) + + backButton := getBackButtonCentered(previousPage) + + subtitle := getPageSubtitleCentered("Choose Genetic Information Genome") + + description1 := getLabelCentered("A single genome from your genetic analysis will be used for genetic calculations with other users.") + description2 := getLabelCentered("Your profile will also contain information from this genome if you choose to share any information.") + description3 := getLabelCentered("If you have only imported one genome, that will be the genome whose information is used.") + description4 := getLabelCentered("If you have multiple genomes, only 1 of the two combined genomes will be used.") + description5 := getLabelCentered("You can choose below which genome you want to use if you have imported multiple genomes.") + + currentGenomeLabel := widget.NewLabel("Current Genome:") + + myGenomePersonIdentifierExists, myGenomePersonIdentifier, err := myLocalProfiles.GetProfileData("Mate", "GenomePersonIdentifier") + if (err != nil){ + setErrorEncounteredPage(window, err, previousPage) + return + } + if (myGenomePersonIdentifierExists == false){ + setErrorEncounteredPage(window, errors.New("setBuildMateProfilePage_ChooseGeneticInformationGenome called when genome person is not established."), previousPage) + return + } + + currentCombinedGenomeToUse, err := myChosenAnalysis.GetMyCombinedGenomeToUse() + if (err != nil){ + setErrorEncounteredPage(window, err, previousPage) + return + } + + getCurrentGenomeText := func()(string, error){ + + myGenomesMapList, err := myGenomes.GetAllPersonGenomesMapList(myGenomePersonIdentifier) + if (err != nil) { return "", err } + + if (len(myGenomesMapList) == 0){ + genomeText := translate("None") + return genomeText, nil + } + if (len(myGenomesMapList) == 1){ + myGenomeMap := myGenomesMapList[0] + + myGenomeCompanyName, exists := myGenomeMap["CompanyName"] + if (exists == false){ + return "", errors.New("MyGenomesMapList is malformed: Genome map missing CompanyName") + } + + return myGenomeCompanyName, nil + } + + result := translate(currentCombinedGenomeToUse) + + return result, nil + } + + currentGenomeText, err := getCurrentGenomeText() + if (err != nil){ + setErrorEncounteredPage(window, err, previousPage) + return + } + + currentGenomeTextLabel := getBoldLabel(currentGenomeText) + + currentGenomeRow := container.NewHBox(layout.NewSpacer(), currentGenomeLabel, currentGenomeTextLabel, layout.NewSpacer()) + + combinedGenomeToUseLabel := getItalicLabel("Combined Genome To Use:") + + option1Translated := translate("Only Include Shared") + option2Translated := translate("Only Exclude Conflicts") + + untranslatedOptionsMap := map[string]string{ + option1Translated: "Only Include Shared", + option2Translated: "Only Exclude Conflicts", + } + + selectorOptionsList := []string{option1Translated, option2Translated} + + combinedGenomeSelector := widget.NewSelect(selectorOptionsList, func(newSelection string){ + + newSelectionUntranslated, exists := untranslatedOptionsMap[newSelection] + if (exists == false){ + setErrorEncounteredPage(window, errors.New("untranslatedOptionsMap missing selection: " + newSelection), currentPage) + return + } + + err = myChosenAnalysis.SetMyCombinedGenomeToUse(newSelectionUntranslated) + if (err != nil) { + setErrorEncounteredPage(window, err, currentPage) + return + } + + currentPage() + }) + + combinedGenomeSelector.Selected = translate(currentCombinedGenomeToUse) + + combinedGenomeHelpButton := widget.NewButtonWithIcon("", theme.QuestionIcon(), func(){ + setCombinedGenomesExplainerPage(window, currentPage) + }) + + combinedGenomeSelectorWithHelpButton := container.NewBorder(nil, nil, nil, combinedGenomeHelpButton, combinedGenomeSelector) + + combinedGenomeSelectorWithLabel := getContainerCentered(container.NewGridWithColumns(1, combinedGenomeToUseLabel, combinedGenomeSelectorWithHelpButton)) + + page := container.NewVBox(title, backButton, widget.NewSeparator(), subtitle, widget.NewSeparator(), description1, description2, description3, description4, description5, widget.NewSeparator(), currentGenomeRow, widget.NewSeparator(), combinedGenomeSelectorWithLabel) + + setPageContent(page, window) +} + +func setBuildMateProfilePage_Height(window fyne.Window, previousPage func()){ + + currentPage := func(){setBuildMateProfilePage_Height(window, previousPage)} + + pageTitle := getPageTitleCentered(translate("Build Mate Profile - Physical")) + backButton := getBackButtonCentered(previousPage) + + pageSubtitle := getPageSubtitleCentered(translate("Height")) + + description := getLabelCentered(translate("Enter your height.")) + + getCurrentMetricOrImperial := func()(string, error){ + + exists, metricOrImperial, err := globalSettings.GetSetting("MetricOrImperial") + if (err != nil) { return "", err } + if (exists == false){ + return "Metric", nil + } + if (metricOrImperial != "Metric" && metricOrImperial != "Imperial"){ + return "", errors.New("Malformed globalSettings: Invalid metricOrImperial: " + metricOrImperial) + } + + return metricOrImperial, nil + } + + currentMetricOrImperial, err := getCurrentMetricOrImperial() + if (err != nil) { + setErrorEncounteredPage(window, err, previousPage) + return + } + + changeMetricOrImperialButton, err := getMetricImperialSwitchButton(window, currentPage) + if (err != nil){ + setErrorEncounteredPage(window, err, previousPage) + return + } + + currentUnitsLabel := getItalicLabel(translate("Current Units:")) + + metricOrImperialRow := container.NewHBox(layout.NewSpacer(), currentUnitsLabel, changeMetricOrImperialButton, layout.NewSpacer()) + + currentHeightExists, currentHeightCentimeters, err := myLocalProfiles.GetProfileData("Mate", "Height") + if (err != nil){ + setErrorEncounteredPage(window, err, previousPage) + return + } + + getCurrentHeightLabel := func()(fyne.Widget, error){ + + if (currentHeightExists == false){ + labelText := translate("No Response") + + label := getBoldItalicLabel(labelText) + return label, nil + } + + myHeightCentimetersFloat64, err := helpers.ConvertStringToFloat64(currentHeightCentimeters) + if (err != nil){ + return nil, errors.New("MyLocalProfiles malformed: Contains invalid Height") + } + + if (currentMetricOrImperial == "Metric"){ + + myHeightRounded := helpers.ConvertFloat64ToStringRounded(myHeightCentimetersFloat64, 2) + + centimetersTranslated := translate("centimeters") + + formattedResult := myHeightRounded + " " + centimetersTranslated + + label := getBoldLabel(formattedResult) + + return label, nil + } + + feetInchesString, err := helpers.ConvertCentimetersToFeetInchesTranslatedString(myHeightCentimetersFloat64) + if (err != nil) { return nil, err } + + label := getBoldLabel(feetInchesString) + + return label, nil + } + + currentHeightLabel, err := getCurrentHeightLabel() + if (err != nil){ + setErrorEncounteredPage(window, err, previousPage) + return + } + + myHeightLabel := widget.NewLabel("My Height:") + + currentHeightRow := container.NewHBox(layout.NewSpacer(), myHeightLabel, currentHeightLabel, layout.NewSpacer()) + + getEnterHeightRow := func()(*fyne.Container, error){ + + if (currentMetricOrImperial == "Metric"){ + + centimetersEntry := widget.NewEntry() + + if (currentHeightExists == false){ + centimetersEntry.SetPlaceHolder(translate("Enter Centimeters")) + } else { + centimetersEntry.SetText(currentHeightCentimeters) + } + + centimetersSubmitButton := widget.NewButtonWithIcon(translate("Submit"), theme.ConfirmIcon(), func(){ + + newHeightCentimeters := centimetersEntry.Text + + isValid, err := helpers.VerifyStringIsFloatWithinRange(newHeightCentimeters, 30, 400) + if (err != nil) { + setErrorEncounteredPage(window, err, currentPage) + return + } + if (isValid == false){ + + centimetersEntry.SetText("") + centimetersEntry.SetPlaceHolder(translate("Enter Centimeters")) + + title := translate("Invalid Height") + dialogMessageA := getLabelCentered(translate("Invalid height entered.")) + dialogMessageB := getLabelCentered(translate("Height must be between 30 and 400 centimeters.")) + dialogContent := container.NewVBox(dialogMessageA, dialogMessageB) + dialog.ShowCustom(title, translate("Close"), dialogContent, window) + return + } + + err = myLocalProfiles.SetProfileData("Mate", "Height", newHeightCentimeters) + if (err != nil){ + setErrorEncounteredPage(window, err, currentPage) + return + } + currentPage() + }) + + centimetersLabel := widget.NewLabel(translate("Centimeters")) + + centimetersEntryWithLabel := container.NewGridWithRows(1, centimetersEntry, centimetersLabel) + + enterHeightRow := container.NewHBox(layout.NewSpacer(), centimetersEntryWithLabel, centimetersSubmitButton, layout.NewSpacer()) + + return enterHeightRow, nil + } + + // MetricOrImperial == "Imperial" + + /// Feet + Inches + feetLabel := widget.NewLabel(translate("Feet")) + inchesLabel := widget.NewLabel(translate("Inches")) + + feetEntry := widget.NewEntry() + inchesEntry := widget.NewEntry() + + if (currentHeightExists == false){ + feetEntry.SetPlaceHolder(translate("Enter Feet")) + inchesEntry.SetPlaceHolder(translate("Enter Inches")) + } else { + + isValid, err := helpers.VerifyStringIsFloatWithinRange(currentHeightCentimeters, 30, 400) + if (err != nil) { return nil, err } + if (isValid == false){ return nil, errors.New("MyLocalProfiles is malformed: Contains invalid height.") } + + currentHeightCentimetersFloat64, err := helpers.ConvertStringToFloat64(currentHeightCentimeters) + if (err != nil) { return nil, err } + + currentHeightFeet, currentHeightInches, err := helpers.ConvertCentimetersToFeetInches(currentHeightCentimetersFloat64) + if (err != nil) { return nil, err } + + currentHeightFeetString := helpers.ConvertIntToString(currentHeightFeet) + currentHeightInchesString := helpers.ConvertFloat64ToStringRounded(currentHeightInches, 1) + + feetEntry.SetText(currentHeightFeetString) + inchesEntry.SetText(currentHeightInchesString) + } + + feetInchesSubmitButton := widget.NewButtonWithIcon(translate("Submit"), theme.ConfirmIcon(), func(){ + + newHeightFeet := feetEntry.Text + newHeightInches := inchesEntry.Text + + newHeightFeetInt, err := helpers.ConvertStringToInt(newHeightFeet) + if (err != nil){ + title := translate("Invalid Height") + dialogMessageA := getLabelCentered(translate("Invalid feet entered.")) + dialogMessageB := getLabelCentered(translate("Feet must be a number.")) + dialogContent := container.NewVBox(dialogMessageA, dialogMessageB) + dialog.ShowCustom(title, translate("Close"), dialogContent, window) + return + } + + newHeightInchesFloat64, err := helpers.ConvertStringToFloat64(newHeightInches) + if (err != nil){ + title := translate("Invalid Height") + dialogMessageA := getLabelCentered(translate("Invalid inches entered.")) + dialogMessageB := getLabelCentered(translate("Inches must be a number.")) + dialogContent := container.NewVBox(dialogMessageA, dialogMessageB) + dialog.ShowCustom(title, translate("Close"), dialogContent, window) + return + } + + newHeightCentimeters, err := helpers.ConvertFeetInchesToCentimeters(newHeightFeetInt, newHeightInchesFloat64) + if (err != nil){ + title := translate("Invalid Height") + dialogMessageA := getLabelCentered(translate("Invalid height entered.")) + dialogMessageB := getLabelCentered(translate("Height must be between 30 and 400 centimeters.")) + dialogContent := container.NewVBox(dialogMessageA, dialogMessageB) + dialog.ShowCustom(title, translate("Close"), dialogContent, window) + return + } + + if (newHeightCentimeters < 30 || newHeightCentimeters > 400){ + title := translate("Invalid Height") + dialogMessageA := getLabelCentered(translate("Invalid height entered.")) + dialogMessageB := getLabelCentered(translate("Height must be between 30 and 400 centimeters.")) + dialogContent := container.NewVBox(dialogMessageA, dialogMessageB) + dialog.ShowCustom(title, translate("Close"), dialogContent, window) + return + } + + newHeightCentimetersString := helpers.ConvertFloat64ToString(newHeightCentimeters) + + err = myLocalProfiles.SetProfileData("Mate", "Height", newHeightCentimetersString) + if (err != nil){ + setErrorEncounteredPage(window, err, currentPage) + return + } + currentPage() + }) + + feetEntryWithLabel := container.NewGridWithRows(1, feetEntry, feetLabel) + inchesEntryWithLabel := container.NewGridWithRows(1, inchesEntry, inchesLabel) + + feetInchesEntryRow := getContainerCentered(container.NewHBox(layout.NewSpacer(), feetEntryWithLabel, inchesEntryWithLabel, feetInchesSubmitButton, layout.NewSpacer())) + + return feetInchesEntryRow, nil + } + + enterHeightRow, err := getEnterHeightRow() + if (err != nil){ + setErrorEncounteredPage(window, err, previousPage) + return + } + + noResponseButton := getWidgetCentered(widget.NewButtonWithIcon(translate("No Response"), theme.CancelIcon(), func(){ + err := myLocalProfiles.DeleteProfileData("Mate", "Height") + if (err != nil){ + setErrorEncounteredPage(window, err, currentPage) + return + } + currentPage() + })) + + page := container.NewVBox(pageTitle, backButton, widget.NewSeparator(), pageSubtitle, widget.NewSeparator(), description, widget.NewSeparator(), metricOrImperialRow, widget.NewSeparator(), currentHeightRow, widget.NewSeparator(), enterHeightRow, noResponseButton) + + setPageContent(page, window) +} + + +func setBuildMateProfilePage_Body(window fyne.Window, previousPage func()){ + + currentPage := func(){setBuildMateProfilePage_Body(window, previousPage)} + + title := getPageTitleCentered(translate("Build Mate Profile - Physical")) + + backButton := getBackButtonCentered(previousPage) + + subtitle := getPageSubtitleCentered("Body") + + description1 := getLabelCentered("Describe your body.") + description2 := getLabelCentered("Choose a value between 1 and 4 to describe each characteristic.") + description3 := getLabelCentered("1 = least fat/muscular human, 4 = most fat/muscular human.") + + fatLabel := getBoldLabelCentered("Fat") + + fatSelector := widget.NewSelect([]string{"1", "2", "3", "4"}, func(newChoice string){ + err := myLocalProfiles.SetProfileData("Mate", "BodyFat", newChoice) + if (err != nil){ + setErrorEncounteredPage(window, err, currentPage) + return + } + }) + fatSelector.PlaceHolder = "Select one..." + + fatNoResponseButton := widget.NewButtonWithIcon("No Response", theme.CancelIcon(), func(){ + + err := myLocalProfiles.DeleteProfileData("Mate", "BodyFat") + if (err != nil){ + setErrorEncounteredPage(window, err, currentPage) + return + } + currentPage() + }) + + fatColumn := container.NewVBox(fatLabel, fatSelector, fatNoResponseButton) + + currentFatValueExists, currentFatValue, err := myLocalProfiles.GetProfileData("Mate", "BodyFat") + if (err != nil){ + setErrorEncounteredPage(window, err, previousPage) + return + } + if (currentFatValueExists == true){ + fatSelector.Selected = currentFatValue + } + + muscleLabel := getBoldLabelCentered("Muscle") + muscleSelector := widget.NewSelect([]string{"1", "2", "3", "4"}, func(newChoice string){ + err := myLocalProfiles.SetProfileData("Mate", "BodyMuscle", newChoice) + if (err != nil){ + setErrorEncounteredPage(window, err, currentPage) + return + } + }) + muscleSelector.PlaceHolder = "Select one..." + + muscleNoResponseButton := widget.NewButtonWithIcon("No Response", theme.CancelIcon(), func(){ + + err := myLocalProfiles.DeleteProfileData("Mate", "BodyMuscle") + if (err != nil){ + setErrorEncounteredPage(window, err, currentPage) + return + } + currentPage() + }) + + muscleColumn := container.NewVBox(muscleLabel, muscleSelector, muscleNoResponseButton) + + currentMuscleValueExists, currentMuscleValue, err := myLocalProfiles.GetProfileData("Mate", "BodyMuscle") + if (err != nil){ + setErrorEncounteredPage(window, err, previousPage) + return + } + if (currentMuscleValueExists == true){ + muscleSelector.Selected = currentMuscleValue + } + + pageColumns := container.NewHBox(layout.NewSpacer(), widget.NewSeparator(), fatColumn, widget.NewSeparator(), muscleColumn, widget.NewSeparator(), layout.NewSpacer()) + + page := container.NewVBox(title, backButton, widget.NewSeparator(), subtitle, widget.NewSeparator(), description1, description2, description3, widget.NewSeparator(), pageColumns) + + setPageContent(page, window) +} + + +func setBuildMateProfilePage_EyeColor(window fyne.Window, previousPage func()){ + + currentPage := func(){setBuildMateProfilePage_EyeColor(window, previousPage)} + + pageTitle := getPageTitleCentered(translate("Build Mate Profile - Physical")) + + backButton := getBackButtonCentered(previousPage) + + pageSubtitle := getPageSubtitleCentered(translate("Eye Color")) + + description1 := getLabelCentered(translate("Select your eye color.")) + description2 := getLabelCentered(translate("Select more than 1 color if your eyes contain multiple colors.")) + + eyeColorExists, currentEyeColorAttribute, err := myLocalProfiles.GetProfileData("Mate", "EyeColor") + if (err != nil){ + setErrorEncounteredPage(window, err, previousPage) + return + } + + getCurrentEyeColorList := func()[]string{ + + if (eyeColorExists == false){ + emptyList := make([]string, 0) + return emptyList + } + + currentEyeColorList := strings.Split(currentEyeColorAttribute, "+") + + return currentEyeColorList + } + + currentEyeColorList := getCurrentEyeColorList() + + if (len(currentEyeColorList) > 4){ + setErrorEncounteredPage(window, errors.New("MyLocalProfiles contains invalid eye color attribute"), previousPage) + return + } + + getCurrentEyeColorLabel := func()fyne.Widget{ + + if (eyeColorExists == false){ + result := translate("No Response") + + eyeColorLabel := getBoldItalicLabel(result) + return eyeColorLabel + } + + currentEyeColorFormatted := strings.Join(currentEyeColorList, ", ") + + eyeColorLabel := getBoldLabel(currentEyeColorFormatted) + return eyeColorLabel + } + + currentEyeColorLabel := getCurrentEyeColorLabel() + + myEyeColorLabel := widget.NewLabel("My Eye Color:") + + currentEyeColorRow := container.NewHBox(layout.NewSpacer(), myEyeColorLabel, currentEyeColorLabel, layout.NewSpacer()) + + getEyeColorColumn := func(colorName string, colorsList []string)(*fyne.Container, error){ + + colorColumn := container.NewGridWithColumns(1) + + for _, colorCode := range colorsList{ + + colorSquare, err := getColorSquareAsFyneImage(colorCode) + if (err != nil){ return nil, err } + + colorSquare.FillMode = canvas.ImageFillStretch + + colorColumn.Add(colorSquare) + } + + chooseColorCheck := widget.NewCheck(colorName, func(newChoice bool){ + + getNewAttributeList := func()[]string{ + + if (newChoice == false){ + newList, _ := helpers.DeleteAllMatchingItemsFromStringList(currentEyeColorList, colorName) + return newList + } + + newList := helpers.AddItemToStringListAndAvoidDuplicate(currentEyeColorList, colorName) + + return newList + } + + newAttributeList := getNewAttributeList() + + if (len(newAttributeList) == 0){ + err := myLocalProfiles.DeleteProfileData("Mate", "EyeColor") + if (err != nil){ + setErrorEncounteredPage(window, err, currentPage) + return + } + currentPage() + return + } + + newEyeColorAttribute := strings.Join(newAttributeList, "+") + + err := myLocalProfiles.SetProfileData("Mate", "EyeColor", newEyeColorAttribute) + if (err != nil){ + setErrorEncounteredPage(window, err, currentPage) + return + } + currentPage() + }) + + colorSelected := slices.Contains(currentEyeColorList, colorName) + if (colorSelected == true){ + chooseColorCheck.Checked = true + } + + colorColumn.Add(chooseColorCheck) + + return colorColumn, nil + } + + //TODO: Add Gray + + blueColorsList := []string{"4d71a6", "9fa8a8", "345269"} + greenColorsList := []string{"333b26", "6d712b", "505216"} + amberColorsList := []string{"6e5c18", "694c19", "684818"} + brownColorsList := []string{"522613", "47240f", "2a110c"} + + blueEyeColorColumn, err := getEyeColorColumn("Blue", blueColorsList) + if (err != nil){ + setErrorEncounteredPage(window, err, previousPage) + return + } + + greenEyeColorColumn, err := getEyeColorColumn("Green", greenColorsList) + if (err != nil){ + setErrorEncounteredPage(window, err, previousPage) + return + } + + amberEyeColorColumn, err := getEyeColorColumn("Amber", amberColorsList) + if (err != nil){ + setErrorEncounteredPage(window, err, previousPage) + return + } + + brownEyeColorColumn, err := getEyeColorColumn("Brown", brownColorsList) + if (err != nil){ + setErrorEncounteredPage(window, err, previousPage) + return + } + + selectEyeColorGrid := getContainerCentered(container.NewGridWithColumns(4, blueEyeColorColumn, greenEyeColorColumn, amberEyeColorColumn, brownEyeColorColumn)) + + noResponseButton := getWidgetCentered(widget.NewButtonWithIcon(translate("No Response"), theme.CancelIcon(), func(){ + err := myLocalProfiles.DeleteProfileData("Mate", "EyeColor") + if (err != nil){ + setErrorEncounteredPage(window, err, currentPage) + return + } + currentPage() + })) + + page := container.NewVBox(pageTitle, backButton, widget.NewSeparator(), pageSubtitle, widget.NewSeparator(), description1, description2, widget.NewSeparator(), currentEyeColorRow, widget.NewSeparator(), selectEyeColorGrid, noResponseButton) + + setPageContent(page, window) +} + +func setBuildMateProfilePage_HairColor(window fyne.Window, previousPage func()){ + + currentPage := func(){setBuildMateProfilePage_HairColor(window, previousPage)} + + pageTitle := getPageTitleCentered(translate("Build Mate Profile - Physical")) + + backButton := getBackButtonCentered(previousPage) + + pageSubtitle := getPageSubtitleCentered(translate("Hair Color")) + + description1 := getLabelCentered(translate("Select your natural hair color.")) + description2 := getLabelCentered(translate("Select 2 colors if your hair color is an average of two colors.")) + //TODO: What if hair is gray/white + // Users should probably select the color their hair was when they were a young adult. We should add gray or white as an option? + + hairColorExists, currentHairColorAttribute, err := myLocalProfiles.GetProfileData("Mate", "HairColor") + if (err != nil){ + setErrorEncounteredPage(window, err, previousPage) + return + } + + getCurrentHairColorList := func()[]string{ + + if (hairColorExists == false){ + emptyList := make([]string, 0) + return emptyList + } + + currentHairColorList := strings.Split(currentHairColorAttribute, "+") + + return currentHairColorList + } + + currentHairColorList := getCurrentHairColorList() + + if (len(currentHairColorList) > 4){ + setErrorEncounteredPage(window, errors.New("MyLocalProfiles contains invalid hair color attribute: " + currentHairColorAttribute), previousPage) + return + } + + getCurrentHairColorLabel := func()fyne.Widget{ + + if (hairColorExists == false){ + result := translate("No Response") + + hairColorLabel := getBoldItalicLabel(result) + return hairColorLabel + } + + currentHairColorFormatted := strings.Join(currentHairColorList, "-") + + hairColorLabel := getBoldLabel(currentHairColorFormatted) + return hairColorLabel + } + + currentHairColorLabel := getCurrentHairColorLabel() + + myHairColorLabel := widget.NewLabel("My Hair Color:") + + currentHairColorRow := container.NewHBox(layout.NewSpacer(), myHairColorLabel, currentHairColorLabel, layout.NewSpacer()) + + getHairColorColumn := func(colorName string, colorsList []string)(*fyne.Container, error){ + + colorColumn := container.NewGridWithColumns(1) + + for _, colorCode := range colorsList{ + + colorSquare, err := getColorSquareAsFyneImage(colorCode) + if (err != nil){ return nil, err } + + colorSquare.FillMode = canvas.ImageFillStretch + + colorColumn.Add(colorSquare) + } + + chooseColorCheck := widget.NewCheck(colorName, func(newChoice bool){ + + getNewAttributeList := func()[]string{ + + if (newChoice == false){ + newList, _ := helpers.DeleteAllMatchingItemsFromStringList(currentHairColorList, colorName) + return newList + } + + newList := helpers.AddItemToStringListAndAvoidDuplicate(currentHairColorList, colorName) + + return newList + } + + newAttributeList := getNewAttributeList() + + if (len(newAttributeList) > 2){ + + currentPage() + + title := translate("Too Many Colors") + dialogMessageA := getLabelCentered(translate("You can only select two colors.")) + dialogContent := container.NewVBox(dialogMessageA) + dialog.ShowCustom(title, translate("Close"), dialogContent, window) + return + } + + if (len(newAttributeList) == 0){ + err := myLocalProfiles.DeleteProfileData("Mate", "HairColor") + if (err != nil){ + setErrorEncounteredPage(window, err, currentPage) + return + } + currentPage() + return + } + + newHairColorAttribute := strings.Join(newAttributeList, "+") + + err := myLocalProfiles.SetProfileData("Mate", "HairColor", newHairColorAttribute) + if (err != nil){ + setErrorEncounteredPage(window, err, currentPage) + return + } + currentPage() + }) + + colorSelected := slices.Contains(currentHairColorList, colorName) + if (colorSelected == true){ + chooseColorCheck.Checked = true + } + + colorColumn.Add(chooseColorCheck) + + return colorColumn, nil + } + + blackColorsList := []string{"242424", "1d1719", "070300"} + brownColorsList := []string{"5d3629", "362219", "472c24"} + blondeColorsList := []string{"efd8a6", "edd096", "a57d4d"} + orangeColorsList := []string{"f5743a", "8c5125", "883713"} + + blackHairColorColumn, err := getHairColorColumn("Black", blackColorsList) + if (err != nil){ + setErrorEncounteredPage(window, err, previousPage) + return + } + + brownHairColorColumn, err := getHairColorColumn("Brown", brownColorsList) + if (err != nil){ + setErrorEncounteredPage(window, err, previousPage) + return + } + + blondeHairColorColumn, err := getHairColorColumn("Blonde", blondeColorsList) + if (err != nil){ + setErrorEncounteredPage(window, err, previousPage) + return + } + + orangeHairColorColumn, err := getHairColorColumn("Orange", orangeColorsList) + if (err != nil){ + setErrorEncounteredPage(window, err, previousPage) + return + } + + selectHairColorGrid := getContainerCentered(container.NewGridWithColumns(4, blackHairColorColumn, brownHairColorColumn, blondeHairColorColumn, orangeHairColorColumn)) + + noResponseButton := getWidgetCentered(widget.NewButtonWithIcon(translate("No Response"), theme.CancelIcon(), func(){ + err := myLocalProfiles.DeleteProfileData("Mate", "HairColor") + if (err != nil){ + setErrorEncounteredPage(window, err, currentPage) + return + } + currentPage() + })) + + page := container.NewVBox(pageTitle, backButton, widget.NewSeparator(), pageSubtitle, widget.NewSeparator(), description1, description2, widget.NewSeparator(), currentHairColorRow, widget.NewSeparator(), selectHairColorGrid, noResponseButton) + + setPageContent(page, window) +} + +func setBuildMateProfilePage_HairTexture(window fyne.Window, previousPage func()){ + + currentPage := func(){setBuildMateProfilePage_HairTexture(window, previousPage)} + + title := getPageTitleCentered(translate("Build Mate Profile - Physical")) + + backButton := getBackButtonCentered(previousPage) + + pageSubtitle := getPageSubtitleCentered(translate("Hair Texture")) + + description := getLabelCentered(translate("Enter your natural hair texture.")) + + option1Translated := translate("1 - Straight Hair") + option2Translated := translate("2 - Slightly Wavy Hair") + option3Translated := translate("3 - Wavy Hair") + option4Translated := translate("4 - Big Curls") + option5Translated := translate("5 - Small Curls") + option6Translated := translate("6 - Very Tight Curls") + + hairTextureOptionsMap := map[string]string{ + option1Translated: "1", + option2Translated: "2", + option3Translated: "3", + option4Translated: "4", + option5Translated: "5", + option6Translated: "6", + } + + hairTextureSelectorOptionsList := []string{option1Translated, option2Translated, option3Translated, option4Translated, option5Translated, option6Translated} + + hairTextureSelector := widget.NewRadioGroup(hairTextureSelectorOptionsList, func(response string){ + + if (response == ""){ + myLocalProfiles.DeleteProfileData("Mate", "HairTexture") + return + } + + attributeValue, exists := hairTextureOptionsMap[response] + if (exists == false){ + setErrorEncounteredPage(window, errors.New("hairTextureOptionsMap missing translated option: " + response), currentPage) + return + } + myLocalProfiles.SetProfileData("Mate", "HairTexture", attributeValue) + }) + + exists, currentHairTexture, err := myLocalProfiles.GetProfileData("Mate", "HairTexture") + if (err != nil){ + setErrorEncounteredPage(window, err, previousPage) + return + } + if (exists == true){ + isValid, err := helpers.VerifyStringIsIntWithinRange(currentHairTexture, 1, 6) + if (err != nil){ + setErrorEncounteredPage(window, err, previousPage) + return + } + if (isValid == false){ + setErrorEncounteredPage(window, errors.New("MyLocalProfiles contains invalid hair texture: " + currentHairTexture), previousPage) + return + } + if (currentHairTexture == "1"){ + + hairTextureSelector.Selected = option1Translated + } else if (currentHairTexture == "2"){ + hairTextureSelector.Selected = option2Translated + } else if (currentHairTexture == "3"){ + hairTextureSelector.Selected = option3Translated + } else if (currentHairTexture == "4"){ + hairTextureSelector.Selected = option4Translated + } else if (currentHairTexture == "5"){ + hairTextureSelector.Selected = option5Translated + } else if (currentHairTexture == "6"){ + hairTextureSelector.Selected = option6Translated + } + } + hairTextureSelectorCentered := getWidgetCentered(hairTextureSelector) + + noResponseButton := getWidgetCentered(widget.NewButtonWithIcon(translate("No Response"), theme.CancelIcon(), func(){ + myLocalProfiles.DeleteProfileData("Mate", "HairTexture") + currentPage() + })) + + page := container.NewVBox(title, backButton, widget.NewSeparator(), pageSubtitle, widget.NewSeparator(), description, widget.NewSeparator(), hairTextureSelectorCentered, noResponseButton) + + setPageContent(page, window) +} + +func setBuildMateProfilePage_SkinColor(window fyne.Window, previousPage func()){ + + currentPage := func(){setBuildMateProfilePage_SkinColor(window, previousPage)} + + pageTitle := getPageTitleCentered(translate("Build Mate Profile - Physical")) + + backButton := getBackButtonCentered(previousPage) + + pageSubtitle := getPageSubtitleCentered(translate("Skin Color")) + + description1 := getLabelCentered(translate("Select your skin color.")) + description2 := getLabelCentered(translate("This may be difficult because skin color changes under different conditions.")) + description3 := getLabelCentered(translate("Try your best to choose the best option.")) + description4 := getLabelCentered(translate("Users are reminded of this when choosing their skin color desires.")) + + mySkinColorExists, currentSkinColorAttribute, err := myLocalProfiles.GetProfileData("Mate", "SkinColor") + if (err != nil){ + setErrorEncounteredPage(window, err, previousPage) + return + } + + if (mySkinColorExists == true){ + + currentSkinColorInt, err := helpers.ConvertStringToInt(currentSkinColorAttribute) + if (err != nil){ + setErrorEncounteredPage(window, errors.New("MyLocalProfiles SkinColor is invalid: " + currentSkinColorAttribute), previousPage) + return + } + + if (currentSkinColorInt < 1 || currentSkinColorInt > 6){ + setErrorEncounteredPage(window, errors.New("MyLocalProfiles SkinColor is invalid: " + currentSkinColorAttribute), previousPage) + return + } + } + + getCurrentSkinColorLabel := func()fyne.Widget{ + + if (mySkinColorExists == false){ + result := translate("No Response") + + skinColorLabel := getBoldItalicLabel(result) + return skinColorLabel + } + + skinColorLabel := getBoldLabel(currentSkinColorAttribute) + return skinColorLabel + } + + currentSkinColorLabel := getCurrentSkinColorLabel() + + mySkinColorLabel := widget.NewLabel("My Skin Color:") + + currentSkinColorRow := container.NewHBox(layout.NewSpacer(), mySkinColorLabel, currentSkinColorLabel, layout.NewSpacer()) + + getSkinColorColumn := func(colorIdentifier string, colorCode string)(*fyne.Container, error){ + + colorSquare, err := getColorSquareAsFyneImage(colorCode) + if (err != nil){ return nil, err } + + colorSquare.FillMode = canvas.ImageFillStretch + + chooseColorCheck := widget.NewCheck(colorIdentifier, func(newChoice bool){ + + err := myLocalProfiles.DeleteProfileData("Mate", "SkinColor") + if (err != nil){ + setErrorEncounteredPage(window, err, currentPage) + return + } + + err = myLocalProfiles.SetProfileData("Mate", "SkinColor", colorIdentifier) + if (err != nil){ + setErrorEncounteredPage(window, err, currentPage) + return + } + currentPage() + }) + + if (colorIdentifier == currentSkinColorAttribute){ + chooseColorCheck.Checked = true + } + + colorColumn := container.NewGridWithColumns(1, colorSquare, chooseColorCheck) + + return colorColumn, nil + } + + skinColorColumn_1, err := getSkinColorColumn("1", "f4e3da") + if (err != nil){ + setErrorEncounteredPage(window, err, previousPage) + return + } + + skinColorColumn_2, err := getSkinColorColumn("2", "f5d6b9") + if (err != nil){ + setErrorEncounteredPage(window, err, previousPage) + return + } + + skinColorColumn_3, err := getSkinColorColumn("3", "dabe91") + if (err != nil){ + setErrorEncounteredPage(window, err, previousPage) + return + } + + skinColorColumn_4, err := getSkinColorColumn("4", "ba9175") + if (err != nil){ + setErrorEncounteredPage(window, err, previousPage) + return + } + + skinColorColumn_5, err := getSkinColorColumn("5", "916244") + if (err != nil){ + setErrorEncounteredPage(window, err, previousPage) + return + } + + skinColorColumn_6, err := getSkinColorColumn("6", "744d2d") + if (err != nil){ + setErrorEncounteredPage(window, err, previousPage) + return + } + + selectSkinColorGrid := getContainerCentered(container.NewGridWithColumns(6, skinColorColumn_1, skinColorColumn_2, skinColorColumn_3, skinColorColumn_4, skinColorColumn_5, skinColorColumn_6)) + + noResponseButton := getWidgetCentered(widget.NewButtonWithIcon(translate("No Response"), theme.CancelIcon(), func(){ + err := myLocalProfiles.DeleteProfileData("Mate", "SkinColor") + if (err != nil){ + setErrorEncounteredPage(window, err, currentPage) + return + } + currentPage() + })) + + page := container.NewVBox(pageTitle, backButton, widget.NewSeparator(), pageSubtitle, widget.NewSeparator(), description1, description2, description3, description4, widget.NewSeparator(), currentSkinColorRow, widget.NewSeparator(), selectSkinColorGrid, noResponseButton) + + setPageContent(page, window) +} + + +func setBuildMateProfilePage_Infections(window fyne.Window, previousPage func()){ + + currentPage := func(){setBuildMateProfilePage_Infections(window, previousPage)} + + pageTitle := getPageTitleCentered(translate("Build Mate Profile - Physical")) + + backButton := getBackButtonCentered(previousPage) + + pageSubtitle := getPageSubtitleCentered(translate("Infections")) + + description := getLabelCentered(translate("Select the infections that you have.")) + + infectionNameColumn := container.NewVBox(widget.NewSeparator()) + radioButtonsColumn := container.NewVBox(widget.NewSeparator()) + noResponseButtonsColumn := container.NewVBox(widget.NewSeparator()) + + addInfectionRow := func(infectionName string, infectionAttributeName string)error{ + + infectionNameLabel := getBoldLabelCentered(infectionName) + + yesTranslated := translate("Yes") + noTranslated := translate("No") + + untranslatedOptionsMap := map[string]string{ + yesTranslated: "Yes", + noTranslated: "No", + } + + optionsList := []string{yesTranslated, noTranslated} + + yesNoSelector := widget.NewRadioGroup(optionsList, func(response string){ + + if (response == ""){ + + _ = myLocalProfiles.DeleteProfileData("Mate", infectionAttributeName) + + return + } + + responseUntranslated, exists := untranslatedOptionsMap[response] + if (exists == false){ + setErrorEncounteredPage(window, errors.New("untranslatedOptionsMap missing response: " + response), currentPage) + return + } + + _ = myLocalProfiles.SetProfileData("Mate", infectionAttributeName, responseUntranslated) + }) + + yesNoSelector.Horizontal = true + + myCurrentStatusExists, myCurrentInfectionStatus, err := myLocalProfiles.GetProfileData("Mate", infectionAttributeName) + if (err != nil){ return err } + + if (myCurrentStatusExists == true){ + + yesNoSelector.Selected = translate(myCurrentInfectionStatus) + } + + noResponseButton := getWidgetCentered(widget.NewButtonWithIcon(translate("No Response"), theme.CancelIcon(), func(){ + err := myLocalProfiles.DeleteProfileData("Mate", infectionAttributeName) + if (err != nil){ + setErrorEncounteredPage(window, err, currentPage) + return + } + currentPage() + })) + + infectionNameColumn.Add(infectionNameLabel) + radioButtonsColumn.Add(yesNoSelector) + noResponseButtonsColumn.Add(noResponseButton) + + infectionNameColumn.Add(widget.NewSeparator()) + radioButtonsColumn.Add(widget.NewSeparator()) + noResponseButtonsColumn.Add(widget.NewSeparator()) + + return nil + } + + err := addInfectionRow("HIV", "HasHIV") + if (err != nil){ + setErrorEncounteredPage(window, err, previousPage) + return + } + err = addInfectionRow("Genital Herpes", "HasGenitalHerpes") + if (err != nil){ + setErrorEncounteredPage(window, err, previousPage) + return + } + + infectionsGrid := container.NewHBox(layout.NewSpacer(), infectionNameColumn, radioButtonsColumn, noResponseButtonsColumn, layout.NewSpacer()) + + page := container.NewVBox(pageTitle, backButton, widget.NewSeparator(), pageSubtitle, widget.NewSeparator(), description, widget.NewSeparator(), infectionsGrid) + + setPageContent(page, window) +} + + diff --git a/gui/chatGui.go b/gui/chatGui.go new file mode 100644 index 0000000..49e4454 --- /dev/null +++ b/gui/chatGui.go @@ -0,0 +1,3256 @@ +package gui + +// chatGui.go implements pages for a user to view and sort their chat conversations, send chat messages, and view their chat statistics + +import "fyne.io/fyne/v2" +import "fyne.io/fyne/v2/canvas" +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/resources/currencies" + +import "seekia/internal/allowedText" +import "seekia/internal/appMemory" +import "seekia/internal/encoding" +import "seekia/internal/globalSettings" +import "seekia/internal/helpers" +import "seekia/internal/identity" +import "seekia/internal/imagery" +import "seekia/internal/messaging/myChatConversations" +import "seekia/internal/messaging/myChatFilters" +import "seekia/internal/messaging/myChatFilterStatistics" +import "seekia/internal/messaging/myChatMessages" +import "seekia/internal/messaging/myConversationIndexes" +import "seekia/internal/messaging/myMessageQueue" +import "seekia/internal/messaging/myReadStatus" +import "seekia/internal/messaging/peerChatKeys" +import "seekia/internal/messaging/readMessages" +import "seekia/internal/messaging/sendMessages" +import "seekia/internal/myBlockedUsers" +import "seekia/internal/myContacts" +import "seekia/internal/myIdentity" +import "seekia/internal/mySettings" +import "seekia/internal/network/appNetworkType/getAppNetworkType" +import "seekia/internal/network/myAccountCredit" +import "seekia/internal/parameters/getParameters" +import "seekia/internal/profiles/attributeDisplay" +import "seekia/internal/profiles/myLocalProfiles" +import "seekia/internal/profiles/myProfileStatus" +import "seekia/internal/profiles/profileStorage" +import "seekia/internal/profiles/readProfiles" +import "seekia/internal/profiles/viewableProfiles" + +import "time" +import "image" +import "errors" +import "strings" +import "slices" +import "sync" + +//TODO: Page to prune Chat Messages, which allows deletion of messages older than X date + +func setChatPage(window fyne.Window){ + + setLoadingScreen(window, "Chat", "Loading chat page...") + + currentPage := func(){setChatPage(window)} + + appMemory.SetMemoryEntry("CurrentViewedPage", "Chat") + + title := getPageTitleCentered("Chat") + + getChatPageIdentityType := func()(string, error){ + + exists, myIdentityType, err := mySettings.GetSetting("ChatPageIdentityType") + if (err != nil) { return "", err } + if (exists == false){ + return "Mate", nil + } + if (myIdentityType != "Mate" && myIdentityType != "Moderator"){ + return "", errors.New("Invalid ChatPageIdentityType: " + myIdentityType) + } + return myIdentityType, nil + } + + myIdentityType, err := getChatPageIdentityType() + if (err != nil){ + setErrorEncounteredPage(window, err, func(){setHomePage(window)}) + return + } + + checkIfPageHasChangedFunction := func()(bool, error){ + + exists, currentViewedPage := appMemory.GetMemoryEntry("CurrentViewedPage") + if (exists == false || currentViewedPage != "Chat"){ + return true, nil + } + exists, pageIdentityType, err := mySettings.GetSetting("ChatPageIdentityType") + if (err != nil) { return false, err } + if (exists == false){ + if (myIdentityType == "Mate"){ + return false, nil + } + return true, nil + } + if (myIdentityType != pageIdentityType){ + return true, nil + } + return false, nil + } + + creditIcon, err := getFyneImageIcon("Funds") + if (err != nil){ + setErrorEncounteredPage(window, err, func(){setHomePage(window)}) + return + } + + creditButton := widget.NewButton("Credit", func(){ + setViewMyAccountCreditPage(window, myIdentityType, currentPage) + }) + creditButtonWithIcon := container.NewGridWithRows(2, creditIcon, creditButton) + + filtersIcon, err := getFyneImageIcon("Desires") + if (err != nil){ + setErrorEncounteredPage(window, err, func(){setHomePage(window)}) + return + } + filtersButton := widget.NewButton("Filters", func(){ + setMyChatFiltersPage(window, myIdentityType) + }) + filtersButtonWithIcon := container.NewGridWithRows(2, filtersIcon, filtersButton) + + statsIcon, err := getFyneImageIcon("Stats") + if (err != nil){ + setErrorEncounteredPage(window, err, func(){setHomePage(window)}) + return + } + statsButton := widget.NewButton("Stats", func(){ + setChatStatisticsPage(window, myIdentityType, currentPage) + }) + statsButtonWithIcon := container.NewGridWithRows(2, statsIcon, statsButton) + + contactsIcon, err := getFyneImageIcon("Contacts") + if (err != nil){ + setErrorEncounteredPage(window, err, func(){setHomePage(window)}) + return + } + contactsButton := widget.NewButton("Contacts", func(){ + setMyContactsPage(window, myIdentityType, currentPage) + }) + contactsButtonWithIcon := container.NewGridWithRows(2, contactsIcon, contactsButton) + + getIdentityTypeLabelOrChangeButton := func()(fyne.Widget, error){ + + getModeratorModeEnabledBool := func()(bool, error){ + exists, moderatorModeOnOffStatus, err := mySettings.GetSetting("ModeratorModeOnOffStatus") + if (err != nil) { return false, err } + if (exists == true && moderatorModeOnOffStatus == "On"){ + return true, nil + } + return false, nil + } + + moderatorModeEnabled, err := getModeratorModeEnabledBool() + if (err != nil) { return nil, err } + + moderatorIdentityExists, _, err := myIdentity.GetMyIdentityHash("Moderator") + if (err != nil) { return nil, err } + + if (moderatorIdentityExists == false && moderatorModeEnabled == false){ + mateLabel := getBoldLabel("Mate") + return mateLabel, nil + } + + getNextIdentityType := func()string{ + if (myIdentityType == "Mate"){ + return "Moderator" + } + return "Mate" + } + + nextIdentityType := getNextIdentityType() + + changeIdentityTypeButton := widget.NewButton(myIdentityType, func(){ + err := mySettings.SetSetting("ChatPageIdentityType", nextIdentityType) + if (err != nil){ + setErrorEncounteredPage(window, err, func(){setChatPage(window)}) + return + } + currentPage() + }) + + return changeIdentityTypeButton, nil + } + + identityTypeLabelOrChangeButton, err := getIdentityTypeLabelOrChangeButton() + if (err != nil){ + setErrorEncounteredPage(window, err, func(){setHomePage(window)}) + return + } + + identityTypeIcon, err := getIdentityTypeIcon(myIdentityType, -20) + if (err != nil) { + setErrorEncounteredPage(window, err, func(){setHomePage(window)}) + return + } + + identityTypeLabelOrChangeButtonWithIcon := container.NewGridWithColumns(1, identityTypeIcon, identityTypeLabelOrChangeButton) + + pageButtonsRow := getContainerCentered(container.NewGridWithRows(1, creditButtonWithIcon, filtersButtonWithIcon, identityTypeLabelOrChangeButtonWithIcon, statsButtonWithIcon, contactsButtonWithIcon)) + + currentSortByAttribute, err := myChatConversations.GetConversationsSortByAttribute(myIdentityType) + if (err != nil){ + setErrorEncounteredPage(window, err, func(){setHomePage(window)}) + return + } + + sortingByLabel := getBoldLabel("Sorting By:") + + sortByAttributeTitle, _, formatSortByAttributeValuesFunction, sortByAttributeUnits, unknownSortByAttributeText, err := attributeDisplay.GetProfileAttributeDisplayInfo(currentSortByAttribute) + if (err != nil){ + setErrorEncounteredPage(window, err, func(){setHomePage(window)}) + return + } + + sortByButton := widget.NewButton(sortByAttributeTitle, func(){ + setSelectMyConversationsSortByAttributePage(window, myIdentityType, currentPage) + }) + + getSortDirectionButtonWithIcon := func()(fyne.Widget, error){ + + currentSortDirection, err := myChatConversations.GetConversationsSortDirection(myIdentityType) + if (err != nil) { return nil, err } + + if (currentSortDirection == "Ascending"){ + + button := widget.NewButtonWithIcon(translate("Ascending"), theme.MoveUpIcon(), func(){ + appMemory.SetMemoryEntry("StopConversationsGenerationYesNo", "Yes") + _ = mySettings.SetSetting(myIdentityType + "ChatConversations_SortDirection", "Descending") + _ = mySettings.SetSetting(myIdentityType + "ChatConversationsSortedStatus", "No") + _ = mySettings.SetSetting(myIdentityType + "ChatConversations_ViewIndex", "0") + currentPage() + }) + + return button, nil + } + + button := widget.NewButtonWithIcon(translate("Descending"), theme.MoveDownIcon(), func(){ + appMemory.SetMemoryEntry("StopConversationsGenerationYesNo", "Yes") + _ = mySettings.SetSetting(myIdentityType + "ChatConversations_SortDirection", "Ascending") + _ = mySettings.SetSetting(myIdentityType + "ChatConversationsSortedStatus", "No") + _ = mySettings.SetSetting(myIdentityType + "ChatConversations_ViewIndex", "0") + currentPage() + }) + + return button, nil + } + + sortByDirectionButton, err := getSortDirectionButtonWithIcon() + if (err != nil) { + setErrorEncounteredPage(window, err, func(){setHomePage(window)}) + return + } + + sortByRow := container.NewHBox(layout.NewSpacer(), sortingByLabel, sortByButton, sortByDirectionButton, layout.NewSpacer()) + + appNetworkType, err := getAppNetworkType.GetAppNetworkType() + if (err != nil) { + setErrorEncounteredPage(window, err, func(){setHomePage(window)}) + return + } + + messagesReady, err := myChatConversations.GetMyChatConversationsReadyStatus(myIdentityType, appNetworkType) + if (err != nil) { + setErrorEncounteredPage(window, err, func(){setHomePage(window)}) + return + } + + if (messagesReady == false) { + + progressPercentageBinding := binding.NewFloat() + progressDescriptionBinding := binding.NewString() + + updateConversationsAndLoadingBarFunction := func(){ + + err = myChatConversations.StartUpdatingMyConversations(myIdentityType, appNetworkType) + if (err != nil) { + setErrorEncounteredPage(window, err, currentPage) + return + } + + var encounteredError error + + for{ + + pageHasChanged, err := checkIfPageHasChangedFunction() + if (err != nil){ + //TODO: Log error in logger + return + } + if (pageHasChanged == true){ + appMemory.SetMemoryEntry("StopBuildMyConversationsYesNo", "Yes") + return + } + + buildEncounteredError, errorEncounteredString, buildIsStopped, conversationsAreReady, currentPercentageProgress, err := myChatConversations.GetChatConversationsBuildStatus(myIdentityType, appNetworkType) + if (err != nil){ + encounteredError = err + break + } + if (buildEncounteredError == true){ + encounteredError = errors.New(errorEncounteredString) + break + } + + if (buildIsStopped == true) { + return + } + + if (conversationsAreReady == true){ + progressPercentageBinding.Set(1) + + // We wait so that the loading bar will appear complete. + time.Sleep(100 * time.Millisecond) + + setChatPage(window) + return + } + + progressPercentageBinding.Set(currentPercentageProgress) + + if (currentPercentageProgress >= 0.50){ + progressDescriptionBinding.Set("Sorting Conversations...") + } + + time.Sleep(100 * time.Millisecond) + } + // This should only be reached if an error is encountered + errorToShow := errors.New("Error encountered while generating conversations: " + encounteredError.Error()) + setErrorEncounteredPage(window, errorToShow, currentPage) + } + + loadingLabel := getBoldLabelCentered("Loading conversations...") + + loadingBar := getWidgetCentered(widget.NewProgressBarWithData(progressPercentageBinding)) + + loadingDetailsLabel := widget.NewLabelWithData(progressDescriptionBinding) + loadingDetailsLabel.TextStyle = getFyneTextStyle_Italic() + loadingDetailsLabelCentered := getWidgetCentered(loadingDetailsLabel) + + page := container.NewVBox(title, widget.NewSeparator(), pageButtonsRow, widget.NewSeparator(), sortByRow, widget.NewSeparator(), loadingLabel, loadingBar, loadingDetailsLabelCentered) + + setPageContent(page, window) + + go updateConversationsAndLoadingBarFunction() + return + } + + getConversationsContainer := func()(*fyne.Container, error){ + + conversationsAreReady, conversationsList, err := myChatConversations.GetMyChatConversationsMapList(myIdentityType, appNetworkType) + if (err != nil) { return nil, err } + if (conversationsAreReady == false){ + return nil, errors.New("Chat conversations not ready after being ready already.") + } + + getRefreshResultsButtonText := func()(string, error){ + needsRefresh, err := myChatConversations.CheckIfMyChatConversationsNeedRefresh(myIdentityType) + 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(myIdentityType + "ChatMessagesReadyStatus", "No") + _ = mySettings.SetSetting(myIdentityType + "ChatConversationsGeneratedStatus", "No") + _ = mySettings.SetSetting(myIdentityType + "ChatConversations_ViewIndex", "0") + currentPage() + })) + + numberOfConversations := len(conversationsList) + numberOfConversationsString := helpers.ConvertIntToString(numberOfConversations) + + if (numberOfConversations == 0) { + + lowercaseIdentityType := strings.ToLower(myIdentityType) + + noConversationsFoundText := getBoldLabelCentered("No " + lowercaseIdentityType + " conversations found.") + + numberOfFilters, err := myChatFilters.GetNumberOfEnabledChatFilters(myIdentityType) + if (err != nil) { return nil, err } + if (numberOfFilters == 0){ + noConversationsFoundLabelWithRefreshButton := container.NewVBox(noConversationsFoundText, refreshResultsButton) + return noConversationsFoundLabelWithRefreshButton, nil + } + + numberOfFiltersString := helpers.ConvertIntToString(numberOfFilters) + + getActiveFiltersText := func()string{ + if (numberOfFilters == 1){ + return "active filter" + } + return "active filters" + } + + activeFiltersText := getActiveFiltersText() + + activeFiltersLabel := getItalicLabelCentered(numberOfFiltersString + " " + activeFiltersText) + + noConversationsFoundTextWithFilters := container.NewVBox(noConversationsFoundText, activeFiltersLabel, refreshResultsButton) + + return noConversationsFoundTextWithFilters, nil + } + + getViewIndex := func()(int, error){ + + exists, viewIndexString, err := mySettings.GetSetting(myIdentityType + "ChatConversations_ViewIndex") + if (err != nil) { return 0, err } + if (exists == false){ + return 0, nil + } + + viewIndex, err := helpers.ConvertStringToInt(viewIndexString) + if (err != nil){ + return 0, errors.New("Invalid chat conversations view index: " + viewIndexString) + } + if (viewIndex < 0) { + + return 0, nil + } + + maximumViewIndex := numberOfConversations-1 + if (viewIndex > maximumViewIndex){ + + return maximumViewIndex, nil + } + return viewIndex, nil + } + + viewIndex, err := getViewIndex() + if (err != nil) { return nil, err } + + getNavigateToBeginningButton := func()fyne.Widget{ + + if (numberOfConversations <= 5 || viewIndex == 0){ + + emptyButton := widget.NewButton(" ", nil) + return emptyButton + } + + goToBeginningButton := widget.NewButtonWithIcon("", theme.MediaSkipPreviousIcon(), func(){ + mySettings.SetSetting(myIdentityType + "ChatConversations_ViewIndex", "0") + currentPage() + }) + + return goToBeginningButton + } + + getNavigateToEndButton := func()fyne.Widget{ + + emptyButton := widget.NewButton(" ", nil) + + if (numberOfConversations <= 5){ + + return emptyButton + } + + finalPageMinimumIndex := numberOfConversations - 5 + + if (viewIndex >= finalPageMinimumIndex){ + return emptyButton + } + + goToEndButton := widget.NewButtonWithIcon("", theme.MediaSkipNextIcon(), func(){ + finalPageIndexString := helpers.ConvertIntToString(finalPageMinimumIndex) + _ = mySettings.SetSetting(myIdentityType + "ChatConversations_ViewIndex", finalPageIndexString) + currentPage() + }) + + return goToEndButton + } + + getNavigateLeftButton := func()fyne.Widget{ + + if (numberOfConversations <= 5 || viewIndex == 0){ + + emptyButton := widget.NewButton(" ", nil) + return emptyButton + } + + button := widget.NewButtonWithIcon("", theme.NavigateBackIcon(), func(){ + newIndex := helpers.ConvertIntToString(viewIndex-5) + _ = mySettings.SetSetting(myIdentityType + "ChatConversations_ViewIndex", newIndex) + currentPage() + }) + + return button + } + + getNavigateRightButton := func()fyne.Widget{ + + emptyButton := widget.NewButton(" ", nil) + + if (numberOfConversations <= 5){ + return emptyButton + } + + finalPageMinimumIndex := numberOfConversations - 5 + + if (viewIndex >= finalPageMinimumIndex){ + return emptyButton + } + + button := widget.NewButtonWithIcon("", theme.NavigateNextIcon(), func(){ + newIndex := helpers.ConvertIntToString(viewIndex+5) + _ = mySettings.SetSetting(myIdentityType + "ChatConversations_ViewIndex", newIndex) + currentPage() + }) + + return button + } + + getViewingConversationsRow := func()*fyne.Container{ + + getConversationOrConversationsText := func()string{ + if (numberOfConversationsString == "1"){ + return "Conversation" + } + return "Conversations" + } + conversationOrConversationsText := getConversationOrConversationsText() + + viewingConversationsText := getBoldLabel("Viewing " + numberOfConversationsString + " " + conversationOrConversationsText) + + if (numberOfConversations <= 5){ + viewingConversationsRow := getWidgetCentered(viewingConversationsText) + return viewingConversationsRow + } + + navigateToBeginningButton := getNavigateToBeginningButton() + navigateToEndButton := getNavigateToEndButton() + + navigateLeftButton := getNavigateLeftButton() + navigateRightButton := getNavigateRightButton() + + viewingConversationsRow := container.NewHBox(layout.NewSpacer(), navigateToBeginningButton, navigateLeftButton, viewingConversationsText, navigateRightButton, navigateToEndButton, layout.NewSpacer()) + + return viewingConversationsRow + } + + viewingConversationsRow := getViewingConversationsRow() + + viewIndexOnwardsConversationsList := conversationsList[viewIndex:] + + conversationsContainer := container.NewVBox() + + if (viewIndex == 0){ + conversationsContainer.Add(refreshResultsButton) + conversationsContainer.Add(widget.NewSeparator()) + } + + for index, conversationMap := range viewIndexOnwardsConversationsList{ + + resultIndex := viewIndex + index + 1 + resultIndexString := helpers.ConvertIntToString(resultIndex) + + myIdentityHashString, exists := conversationMap["MyIdentityHash"] + if (exists == false) { + return nil, errors.New("Malformed conversation map: Missing MyIdentityHash") + } + + theirIdentityHashString, exists := conversationMap["TheirIdentityHash"] + if (exists == false) { + return nil, errors.New("Malformed conversation map: Missing TheirIdentityHash") + } + + myIdentityHash, _, err := identity.ReadIdentityHashString(myIdentityHashString) + if (err != nil){ + return nil, errors.New("Malformed conversation map: Contains invalid MyIdentityHash: " + myIdentityHashString) + } + + theirIdentityHash, _, err := identity.ReadIdentityHashString(theirIdentityHashString) + if (err != nil){ + return nil, errors.New("Malformed conversation map: Contains invalid TheirIdentityHash: " + theirIdentityHashString) + } + + getAllowUnknownViewableStatusBool := func()bool{ + + if (myIdentityType == "Mate"){ + return false + } + return true + } + + allowUnknownViewableStatusBool := getAllowUnknownViewableStatusBool() + + theirProfileExists, _, getAnyAttributeFromTheirProfileFunction, err := viewableProfiles.GetRetrieveAnyNewestViewableUserProfileAttributeFunction(theirIdentityHash, appNetworkType, true, allowUnknownViewableStatusBool, true) + if (err != nil) { return nil, err } + + getAvatarOrImage := func()(image.Image, error){ + + if (theirProfileExists == true){ + + attributeExists, _, photosAttributeValue, err := getAnyAttributeFromTheirProfileFunction("Photos") + if (err != nil) { return nil, err } + if (attributeExists == true){ + + base64PhotosList := strings.Split(photosAttributeValue, "+") + firstPhotoBase64 := base64PhotosList[0] + + userImageObject, err := imagery.ConvertWEBPBase64StringToCroppedDownsizedImageObject(firstPhotoBase64) + if (err != nil) { + return nil, errors.New("Database corrupt: Contains profile with invalid photos attribute.") + } + + return userImageObject, nil + } + } + + getContactEmojiIdentifier := func()(int, error){ + + if (theirProfileExists == false){ + return 2929, nil + } + + attributeExists, _, avatarAttributeValue, err := getAnyAttributeFromTheirProfileFunction("Avatar") + if (err != nil) { return 0, err } + if (attributeExists == false){ + return 2929, nil + } + + userEmojiIdentifier, err := helpers.ConvertStringToInt(avatarAttributeValue) + if (err != nil) { + return 0, errors.New("Database corrupt: Contains profile with invalid emoji attribute: " + avatarAttributeValue) + } + return userEmojiIdentifier, nil + } + contactEmojiIdentifier, err := getContactEmojiIdentifier() + if (err != nil) { return nil, err } + + emojiImageObject, err := getEmojiImageObject(contactEmojiIdentifier) + if (err != nil) { return nil, err } + + return emojiImageObject, nil + } + + getSortByAttributeBox := func()(*fyne.Container, error){ + + sortByAttributeTitleLabel := getLabelCentered(sortByAttributeTitle) + + getSortByAttributeValueText := func()(string, error){ + + if (theirProfileExists == false){ + return unknownSortByAttributeText, nil + } + + exists, _, attributeValue, err := getAnyAttributeFromTheirProfileFunction(currentSortByAttribute) + if (err != nil) { return "", err } + if (exists == false){ + return unknownSortByAttributeText, nil + } + + attributeValueFormatted, err := formatSortByAttributeValuesFunction(attributeValue) + if (err != nil) { return "", err } + + result := attributeValueFormatted + sortByAttributeUnits + + return result, nil + } + + sortByAttributeValueText, err := getSortByAttributeValueText() + if (err != nil) { return nil, err } + + attributeValueLabel := getBoldLabelCentered(sortByAttributeValueText) + sortByAttributeBox := getContainerBoxed(container.NewVBox(sortByAttributeTitleLabel, attributeValueLabel)) + + return sortByAttributeBox, nil + } + + getReadUnreadStatusButton := func()(fyne.Widget, error){ + + conversationReadUnreadStatus, err := myReadStatus.GetConversationReadUnreadStatus(myIdentityHash, theirIdentityHash, appNetworkType) + if (err != nil) { return nil, err } + if (conversationReadUnreadStatus == "Unread"){ + + unreadStatusButton := widget.NewButtonWithIcon("Unread", theme.WarningIcon(), func(){ + dialogTitle := translate("Conversation Is Unread") + dialogMessageA := getLabelCentered(translate("This conversation is unread.")) + dialogMessageB := getLabelCentered(translate("It contains new messages for you to read.")) + dialogContent := container.NewVBox(dialogMessageA, dialogMessageB) + dialog.ShowCustom(dialogTitle, translate("Close"), dialogContent, window) + }) + + unreadStatusButton.Importance = widget.HighImportance + + return unreadStatusButton, nil + } + + readStatusButton := widget.NewButtonWithIcon("Read", theme.VisibilityIcon(), func(){ + dialogTitle := translate("Conversation Is Read") + dialogMessageA := getLabelCentered(translate("This conversation is read.")) + dialogMessageB := getLabelCentered(translate("It does not contain any new messages for you to read.")) + dialogContent := container.NewVBox(dialogMessageA, dialogMessageB) + dialog.ShowCustom(dialogTitle, translate("Close"), dialogContent, window) + }) + + return readStatusButton, nil + } + + getUserName := func()(string, error){ + + if (theirProfileExists == false){ + // We haven't downloaded their profile yet + // Their profile may not exist on the network + return "Unknown", nil + } + + exists, _, username, err := getAnyAttributeFromTheirProfileFunction("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 + } + + avatarOrImage, err := getAvatarOrImage() + if (err != nil) { return nil, err } + + userFyneImage := canvas.NewImageFromImage(avatarOrImage) + userFyneImage.FillMode = canvas.ImageFillContain + imageSize := getCustomFyneSize(10) + userFyneImage.SetMinSize(imageSize) + userImageBoxed := getFyneImageBoxed(userFyneImage) + + currentTheirUsername, err := getUserName() + if (err != nil) { return nil, err } + + userNameLabel := getBoldLabelCentered(currentTheirUsername) + theirIdentityHashTrimmed, _, err := helpers.TrimAndFlattenString(theirIdentityHashString, 15) + if (err != nil) { return nil, err } + theirIdentityHashLabel := widget.NewLabel(theirIdentityHashTrimmed) + + userNameColumn := getContainerBoxed(container.NewVBox(userNameLabel, theirIdentityHashLabel)) + + sortByAttributeBox, err := getSortByAttributeBox() + if (err != nil) { return nil, err } + + readUnreadStatusButton, err := getReadUnreadStatusButton() + if (err != nil) { return nil, err } + + chatButton := widget.NewButtonWithIcon("Chat", theme.MailComposeIcon(), func(){ + setViewAConversationPage(window, theirIdentityHash, true, currentPage) + }) + + resultIndexBoldLabel := getBoldLabel(resultIndexString + ".") + + conversationRow := container.NewHBox(layout.NewSpacer(), resultIndexBoldLabel, userImageBoxed, userNameColumn, sortByAttributeBox, readUnreadStatusButton, chatButton, layout.NewSpacer()) + + conversationsContainer.Add(conversationRow) + + if (index >= 4) { + break + } + conversationsContainer.Add(widget.NewSeparator()) + } + + conversationsContainerScrollable := container.NewVScroll(conversationsContainer) + conversationsContainerBoxed := getWidgetBoxed(conversationsContainerScrollable) + + resultsContainer := container.NewBorder(viewingConversationsRow, nil, nil, nil, conversationsContainerBoxed) + + return resultsContainer, nil + } + + conversationsContent, err := getConversationsContainer() + if (err != nil){ + setErrorEncounteredPage(window, err, func(){setHomePage(window)}) + return + } + + pageHeader := container.NewVBox(title, widget.NewSeparator(), pageButtonsRow, widget.NewSeparator(), sortByRow, widget.NewSeparator()) + + content := container.NewBorder(pageHeader, nil, nil, nil, conversationsContent) + + setPageContent(content, window) +} + + +func setSelectMyConversationsSortByAttributePage(window fyne.Window, identityType string, previousPage func()){ + + appMemory.SetMemoryEntry("CurrentViewedPage", "SortBySelectPage_Chat") + + title := getPageTitleCentered("Select Sort By Attribute") + + backButton := getBackButtonCentered(previousPage) + + description := getLabelCentered("Select the attribute to sort your chat conversations by.") + + getPageContent := func()(*fyne.Container, error){ + + if (identityType == "Mate"){ + + generalAttributeButtonsGrid := container.NewGridWithColumns(1) + physicalAttributeButtonsGrid := container.NewGridWithColumns(1) + lifestyleAttributeButtonsGrid := container.NewGridWithColumns(1) + mentalAttributeButtonsGrid := container.NewGridWithColumns(1) + + addAttributeSelectButton := func(attributeType string, attributeName string, sortDirection string)error{ + + attributeTitle, _, _, _, _, err := attributeDisplay.GetProfileAttributeDisplayInfo(attributeName) + if (err != nil) { return err } + + attributeButton := widget.NewButton(attributeTitle, func(){ + _ = mySettings.SetSetting(identityType + "ChatConversationsSortedStatus", "No") + _ = mySettings.SetSetting(identityType + "ChatConversations_SortByAttribute", attributeName) + _ = mySettings.SetSetting(identityType + "ChatConversations_SortDirection", sortDirection) + _ = mySettings.SetSetting(identityType + "ChatConversations_ViewIndex", "0") + + previousPage() + }) + + if (attributeType == "General"){ + + generalAttributeButtonsGrid.Add(attributeButton) + + } else if (attributeType == "Physical"){ + + physicalAttributeButtonsGrid.Add(attributeButton) + + } else if (attributeType == "Lifestyle"){ + + lifestyleAttributeButtonsGrid.Add(attributeButton) + + } else if (attributeType == "Mental"){ + + mentalAttributeButtonsGrid.Add(attributeButton) + + } else { + return errors.New("addSelectButton called with invalid attributeType: " + attributeType) + } + + return nil + } + + generalLabel := getBoldLabelCentered("General") + + err := addAttributeSelectButton("General", "MatchScore", "Descending") + if (err != nil) { return nil, err } + err = addAttributeSelectButton("General", "Distance", "Ascending") + if (err != nil) { return nil, err } + err = addAttributeSelectButton("General", "SearchTermsCount", "Descending") + if (err != nil) { return nil, err } + + physicalLabel := getBoldLabelCentered("Physical") + + err = addAttributeSelectButton("Physical", "Age", "Ascending") + if (err != nil) { return nil, err } + err = addAttributeSelectButton("Physical", "Height", "Descending") + if (err != nil) { return nil, err } + err = addAttributeSelectButton("Physical", "BodyFat", "Ascending") + if (err != nil) { return nil, err } + err = addAttributeSelectButton("Physical", "BodyMuscle", "Descending") + if (err != nil) { return nil, err } + err = addAttributeSelectButton("Physical", "SkinColor", "Ascending") + if (err != nil) { return nil, err } + err = addAttributeSelectButton("Physical", "HairTexture", "Ascending") + if (err != nil) { return nil, err } + + err = addAttributeSelectButton("Physical", "RacialSimilarity", "Descending") + if (err != nil) { return nil, err } + + err = addAttributeSelectButton("Physical", "EyeColorSimilarity", "Descending") + if (err != nil) { return nil, err } + err = addAttributeSelectButton("Physical", "EyeColorGenesSimilarity", "Descending") + if (err != nil) { return nil, err } + err = addAttributeSelectButton("Physical", "HairColorSimilarity", "Descending") + if (err != nil) { return nil, err } + err = addAttributeSelectButton("Physical", "HairColorGenesSimilarity", "Descending") + if (err != nil) { return nil, err } + err = addAttributeSelectButton("Physical", "SkinColorSimilarity", "Descending") + if (err != nil) { return nil, err } + err = addAttributeSelectButton("Physical", "SkinColorGenesSimilarity", "Descending") + if (err != nil) { return nil, err } + err = addAttributeSelectButton("Physical", "HairTextureSimilarity", "Descending") + if (err != nil) { return nil, err } + err = addAttributeSelectButton("Physical", "HairTextureGenesSimilarity", "Descending") + if (err != nil) { return nil, err } + err = addAttributeSelectButton("Physical", "FacialStructureGenesSimilarity", "Descending") + if (err != nil) { return nil, err } + err = addAttributeSelectButton("Physical", "23andMe_AncestralSimilarity", "Descending") + if (err != nil) { return nil, err } + err = addAttributeSelectButton("Physical", "23andMe_MaternalHaplogroupSimilarity", "Descending") + if (err != nil) { return nil, err } + err = addAttributeSelectButton("Physical", "23andMe_PaternalHaplogroupSimilarity", "Descending") + if (err != nil) { return nil, err } + + err = addAttributeSelectButton("Physical", "OffspringProbabilityOfAnyMonogenicDisease", "Ascending") + if (err != nil) { return nil, err } + + err = addAttributeSelectButton("Physical", "TotalPolygenicDiseaseRiskScore", "Ascending") + if (err != nil) { return nil, err } + + err = addAttributeSelectButton("Physical", "OffspringTotalPolygenicDiseaseRiskScore", "Ascending") + if (err != nil) { return nil, err } + + offspringLactoseToleranceProbabilityButton := widget.NewButton("Offspring Lactose Tolerance Probability", func(){ + //TODO + showUnderConstructionDialog(window) + }) + physicalAttributeButtonsGrid.Add(offspringLactoseToleranceProbabilityButton) + + offspringCurlyHairProbabilityButton := widget.NewButton("Offspring Curly Hair Probability", func(){ + //TODO + showUnderConstructionDialog(window) + }) + physicalAttributeButtonsGrid.Add(offspringCurlyHairProbabilityButton) + + offspringStraightHairProbabilityButton := widget.NewButton("Offspring Straight Hair Probability", func(){ + //TODO + showUnderConstructionDialog(window) + }) + physicalAttributeButtonsGrid.Add(offspringStraightHairProbabilityButton) + + err = addAttributeSelectButton("Physical", "23andMe_NeanderthalVariants", "Descending") + if (err != nil) { return nil, err } + + lifestyleLabel := getBoldLabelCentered("Lifestyle") + + err = addAttributeSelectButton("Lifestyle", "WealthInGold", "Descending") + if (err != nil) { return nil, err } + + err = addAttributeSelectButton("Lifestyle", "Fame", "Descending") + if (err != nil) { return nil, err } + + err = addAttributeSelectButton("Lifestyle", "FruitRating", "Descending") + if (err != nil) { return nil, err } + err = addAttributeSelectButton("Lifestyle", "VegetablesRating", "Descending") + if (err != nil) { return nil, err } + err = addAttributeSelectButton("Lifestyle", "NutsRating", "Descending") + if (err != nil) { return nil, err } + err = addAttributeSelectButton("Lifestyle", "GrainsRating", "Descending") + if (err != nil) { return nil, err } + err = addAttributeSelectButton("Lifestyle", "DairyRating", "Descending") + if (err != nil) { return nil, err } + err = addAttributeSelectButton("Lifestyle", "SeafoodRating", "Descending") + if (err != nil) { return nil, err } + err = addAttributeSelectButton("Lifestyle", "BeefRating", "Descending") + if (err != nil) { return nil, err } + err = addAttributeSelectButton("Lifestyle", "PorkRating", "Descending") + if (err != nil) { return nil, err } + err = addAttributeSelectButton("Lifestyle", "PoultryRating", "Descending") + if (err != nil) { return nil, err } + err = addAttributeSelectButton("Lifestyle", "EggsRating", "Descending") + if (err != nil) { return nil, err } + err = addAttributeSelectButton("Lifestyle", "BeansRating", "Descending") + if (err != nil) { return nil, err } + + err = addAttributeSelectButton("Lifestyle", "AlcoholFrequency", "Ascending") + if (err != nil) { return nil, err } + err = addAttributeSelectButton("Lifestyle", "TobaccoFrequency", "Ascending") + if (err != nil) { return nil, err } + err = addAttributeSelectButton("Lifestyle", "CannabisFrequency", "Ascending") + if (err != nil) { return nil, err } + + mentalLabel := getBoldLabelCentered("Mental") + + err = addAttributeSelectButton("Mental", "PetsRating", "Descending") + if (err != nil) { return nil, err } + err = addAttributeSelectButton("Mental", "CatsRating", "Descending") + if (err != nil) { return nil, err } + err = addAttributeSelectButton("Mental", "DogsRating", "Descending") + if (err != nil) { return nil, err } + + generalAttributeButtonsGridCentered := getContainerCentered(generalAttributeButtonsGrid) + physicalAttributeButtonsGridCentered := getContainerCentered(physicalAttributeButtonsGrid) + lifestyleAttributeButtonsGridCentered := getContainerCentered(lifestyleAttributeButtonsGrid) + mentalAttributeButtonsGridCentered := getContainerCentered(mentalAttributeButtonsGrid) + + pageContent := container.NewVBox(generalLabel, widget.NewSeparator(), generalAttributeButtonsGridCentered, widget.NewSeparator(), physicalLabel, widget.NewSeparator(), physicalAttributeButtonsGridCentered, widget.NewSeparator(), lifestyleLabel, widget.NewSeparator(), lifestyleAttributeButtonsGridCentered, widget.NewSeparator(), mentalLabel, widget.NewSeparator(), mentalAttributeButtonsGridCentered) + + return pageContent, nil + + } else if (identityType == "Moderator"){ + + getSelectButton := func(attributeTitle string, attributeName string, sortDirection string) fyne.Widget{ + + button := widget.NewButton(translate(attributeTitle), func(){ + _ = mySettings.SetSetting("ModeratorChatConversationsSortedStatus", "No") + _ = mySettings.SetSetting("ModeratorChatConversations_SortByAttribute", attributeName) + _ = mySettings.SetSetting("ModeratorChatConversations_SortDirection", sortDirection) + _ = mySettings.SetSetting("ModeratorChatConversations_ViewIndex", "0") + + previousPage() + }) + + return button + } + + identityScoreButton := getSelectButton("Identity Score", "IdentityScore", "Descending") + + buttonsGrid := container.NewGridWithColumns(1, identityScoreButton) + + pageContent := getContainerCentered(buttonsGrid) + + return pageContent, nil + } + + return nil, errors.New("setSelectMyConversationsSortByAttributePage called with invalid identityType: " + identityType) + } + + pageContent, err := getPageContent() + if (err != nil){ + setErrorEncounteredPage(window, err, previousPage) + return + } + + page := container.NewVBox(title, backButton, widget.NewSeparator(), description, widget.NewSeparator(), pageContent) + + setPageContent(page, window) +} + + +func setMyChatFiltersPage(window fyne.Window, identityType string){ + + appMemory.SetMemoryEntry("CurrentViewedPage", "ChatFilters") + + title := getPageTitleCentered(identityType + " Chat Filters") + + previousPage := func(){setChatPage(window)} + + backButton := getBackButtonCentered(previousPage) + + filtersDescription := getLabelCentered("Choose your chat conversation filters.") + + getChatFiltersGrid := func()(*fyne.Container, error){ + + filterDescriptionsColumn := container.NewVBox() + filterChecksColumn := container.NewVBox() + + addChatFilterRow := func(chatFilterDescription string, chatFilterName string)error{ + + currentStatus, err := myChatFilters.GetChatFilterOnOffStatus(identityType, chatFilterName) + if (err != nil) { return err } + + chatFilterDescriptionLabel := getBoldLabelCentered(chatFilterDescription) + + chatFilterCheck := widget.NewCheck("", func(response bool){ + err := myChatFilters.SetChatFilterOnOffStatus(identityType, chatFilterName, response) + if (err != nil){ + setErrorEncounteredPage(window, err, previousPage) + } + }) + if (currentStatus == true){ + chatFilterCheck.Checked = true + } + + filterDescriptionsColumn.Add(chatFilterDescriptionLabel) + filterChecksColumn.Add(chatFilterCheck) + + filterDescriptionsColumn.Add(widget.NewSeparator()) + filterChecksColumn.Add(widget.NewSeparator()) + + return nil + } + + err := addChatFilterRow("Only show conversations with my contacts.", "ShowMyContactsOnly") + if (err != nil){ return nil, err } + + if (identityType == "Mate"){ + err := addChatFilterRow("Only show conversations with my matches.", "ShowMyMatchesOnly") + if (err != nil){ return nil, err } + } + + err = addChatFilterRow("Only show conversations with users who have messaged me.", "ShowHasMessagedMeOnly") + if (err != nil){ return nil, err } + + if (identityType == "Mate"){ + + err = addChatFilterRow("Only show conversations with users I have liked.", "OnlyShowLikedUsers") + if (err != nil) { return nil, err } + + err = addChatFilterRow("Hide conversations with users I have ignored.", "HideIgnoredUsers") + if (err != nil) { return nil, err } + } + + //TODO: Hide conversations with users who are banned + + chatFiltersGrid := container.NewHBox(layout.NewSpacer(), filterDescriptionsColumn, filterChecksColumn, layout.NewSpacer()) + + return chatFiltersGrid, nil + } + + chatFiltersGrid, err := getChatFiltersGrid() + if (err != nil){ + setErrorEncounteredPage(window, err, previousPage) + return + } + + page := container.NewVBox(title, backButton, widget.NewSeparator(), filtersDescription, widget.NewSeparator(), chatFiltersGrid) + + setPageContent(page, window) +} + +func setViewAConversationPage(window fyne.Window, theirIdentityHash [16]byte, resetConversationIndex bool, previousPage func()){ + + setLoadingScreen(window, "View Conversation", "Loading conversation...") + + currentPage := func(){ setViewAConversationPage(window, theirIdentityHash, false, previousPage) } + currentPageWithNewestView := func(){setViewAConversationPage(window, theirIdentityHash, true, previousPage)} + + title := getPageTitleCentered("Viewing Conversation") + + backButton := getBackButtonCentered(previousPage) + + theirIdentityHashString, theirIdentityType, err := identity.EncodeIdentityHashBytesToString(theirIdentityHash) + if (err != nil) { + setErrorEncounteredPage(window, err, previousPage) + return + } + if (theirIdentityType == "Host"){ + title := getPageTitleCentered("Chat") + description1 := getBoldLabelCentered("Recipient is a Host profile.") + description2 := getLabelCentered("They cannot be chatted with.") + page := container.NewVBox(title, backButton, widget.NewSeparator(), description1, description2) + setPageContent(page, window) + return + } + + myIdentityExists, myIdentityHash, err := myIdentity.GetMyIdentityHash(theirIdentityType) + if (err != nil) { + setErrorEncounteredPage(window, err, previousPage) + return + } + if (myIdentityExists == false){ + + // This should not happen, because conversations are generated from user's existing identitites. + // A user's chat conversations should be regenerated whenever a user deletes/changes their identity + + err := mySettings.SetSetting(theirIdentityType + "ChatConversationsGeneratedStatus", "No") + if (err != nil) { + setErrorEncounteredPage(window, err, previousPage) + return + } + + title := getPageTitleCentered("Create Chat Identity") + + description1 := getBoldLabelCentered("Recipient " + theirIdentityHashString + " is a " + theirIdentityType + " identity.") + description2 := getLabelCentered("You do not have a " + theirIdentityType + " identity.") + description3 := getLabelCentered("Create your " + theirIdentityType + " identity to chat from?") + + createIdentityButton := getWidgetCentered(widget.NewButtonWithIcon("Create " + theirIdentityType + " Identity", theme.NavigateNextIcon(), func(){ + setChooseNewIdentityHashPage(window, theirIdentityType, currentPage, currentPage) + })) + + page := container.NewVBox(title, backButton, widget.NewSeparator(), description1, description2, description3, createIdentityButton) + + setPageContent(page, window) + return + } + + if (myIdentityHash == theirIdentityHash){ + // This should not happen + setErrorEncounteredPage(window, errors.New("Trying to chat with myself."), previousPage) + return + } + + myIdentityType := theirIdentityType + + appNetworkType, err := getAppNetworkType.GetAppNetworkType() + if (err != nil) { + setErrorEncounteredPage(window, err, previousPage) + return + } + + getTheyAreDisabledStatus := func()(bool, error){ + + theirProfileExists, _, _, _, _, theirNewestProfileRawMap, err := profileStorage.GetNewestUserProfile(theirIdentityHash, appNetworkType) + if (err != nil) { return false, err } + if (theirProfileExists == false){ + return false, nil + } + + theyAreDisabled, _, err := readProfiles.GetFormattedProfileAttributeFromRawProfileMap(theirNewestProfileRawMap, "Disabled") + if (err != nil) { return false, err } + if (theyAreDisabled == true){ + return true, nil + } + return false, nil + } + + theyAreDisabled, err := getTheyAreDisabledStatus() + if (err != nil){ + setErrorEncounteredPage(window, err, previousPage) + return + } + + getIAmDisabledStatus := func()(bool, error){ + + exists, iAmDisabled, err := myLocalProfiles.GetProfileData(myIdentityType, "Disabled") + if (err != nil) { return false, err } + if (exists == true && iAmDisabled == "Yes"){ + return true, nil + } + return false, nil + } + + iAmDisabled, err := getIAmDisabledStatus() + if (err != nil){ + setErrorEncounteredPage(window, err, previousPage) + return + } + + getRecipientInfoColumn := func()(*fyne.Container, error){ + + chattingWithTitle := getBoldLabelCentered("Chatting With:") + + getRecipientUserName := func()(string, error){ + + theirProfileExists, _, _, _, _, theirRawProfileMap, err := profileStorage.GetNewestUserProfile(theirIdentityHash, appNetworkType) + if (err != nil) { return "", err } + if (theirProfileExists == false){ + result := translate("Unknown") + return result, nil + } + + exists, username, err := readProfiles.GetFormattedProfileAttributeFromRawProfileMap(theirRawProfileMap, "Username") + if (err != nil) { return "", err } + if (exists == false) { + result := translate("Anonymous") + return result, nil + } + + trimmedUsername, _, err := helpers.TrimAndFlattenString(username, 15) + if (err != nil) { return "", err } + + return trimmedUsername, nil + } + + recipientUserName, err := getRecipientUserName() + if (err != nil) { return nil, err } + + recipientUsernameLabel := getBoldItalicLabelCentered(recipientUserName) + + trimmedIdentityHash, _, err := helpers.TrimAndFlattenString(theirIdentityHashString, 15) + if (err != nil) { return nil, err } + theirIdentityHashLabel := getLabelCentered(trimmedIdentityHash) + + getViewTheirProfileButton := func()(*fyne.Container, error){ + + if (theyAreDisabled == true){ + disabledButton := getWidgetCentered(widget.NewButtonWithIcon("Disabled", theme.VisibilityOffIcon(), func(){ + dialogTitle := translate("User Is Disabled") + dialogMessageA := getLabelCentered(translate("This user has disabled their profile.")) + dialogMessageB := getLabelCentered(translate("They have no profile to view.")) + dialogContent := container.NewVBox(dialogMessageA, dialogMessageB) + dialog.ShowCustom(dialogTitle, translate("Close"), dialogContent, window) + })) + + return disabledButton, nil + } + + viewProfileButton := getWidgetCentered(widget.NewButtonWithIcon("View Profile", theme.VisibilityIcon(), func(){ + setViewPeerProfilePageFromIdentityHash(window, theirIdentityHash, currentPage) + })) + + return viewProfileButton, nil + } + + viewRecipientProfileButton, err := getViewTheirProfileButton() + if (err != nil) { return nil, err } + + getMyContactSection := func()(*fyne.Container, error){ + // Will show add to my contacts if not a contact + // will show contact name if is already a contact + + contactExists, contactName, _, _, _, err := myContacts.GetMyContactDetails(theirIdentityHash) + if (err != nil) { return nil, err } + + if (contactExists == false){ + + addContactButton := container.NewVBox(widget.NewButtonWithIcon("Add Contact", theme.ContentAddIcon(), func(){ + setAddContactFromIdentityHashPage(window, theirIdentityHash, currentPage, currentPage) + })) + + return addContactButton, nil + } + + trimmedContactName, _, err := helpers.TrimAndFlattenString(contactName, 15) + if (err != nil) { return nil, err } + + contactNameLabel := getItalicLabelCentered("Contact Name:") + contactNameText := getWidgetCentered(getBoldItalicLabel(trimmedContactName)) + + contactSection := container.NewVBox(contactNameLabel, contactNameText) + return contactSection, nil + } + + myContactSection, err := getMyContactSection() + if (err != nil) { return nil, err } + + actionsButton := widget.NewButtonWithIcon("Actions", theme.ContentRedoIcon(), func(){ + setViewPeerActionsPage(window, theirIdentityHash, currentPage) + }) + + recipientInfoColumn := getContainerBoxed(container.NewVBox(chattingWithTitle, widget.NewSeparator(), recipientUsernameLabel, theirIdentityHashLabel, viewRecipientProfileButton, widget.NewSeparator(), myContactSection, widget.NewSeparator(), actionsButton)) + + return recipientInfoColumn, nil + } + + recipientInfoColumn, err := getRecipientInfoColumn() + if (err != nil){ + setErrorEncounteredPage(window, err, previousPage) + return + } + + getMyInfoColumn := func()(*fyne.Container, error){ + + myIdentityLabel := getBoldLabelCentered("My Identity:") + + getMyUserName := func()(string, error){ + + exists, username, err := myLocalProfiles.GetProfileData(myIdentityType, "Username") + if (err != nil) { return "", err } + if (exists == false) { + return "Anonymous", nil + } + + trimmedUsername, _, err := helpers.TrimAndFlattenString(username, 15) + if (err != nil) { return "", err } + + return trimmedUsername, nil + } + + myUserName, err := getMyUserName() + if (err != nil) { return nil, err } + + myUsernameLabel := getWidgetCentered(getBoldItalicLabel(myUserName)) + + myIdentityHashString, _, err := identity.EncodeIdentityHashBytesToString(myIdentityHash) + if (err != nil){ return nil, err } + + trimmedIdentityHash, _, err := helpers.TrimAndFlattenString(myIdentityHashString, 15) + if (err != nil) { return nil, err } + myIdentityHashLabel := getLabelCentered(trimmedIdentityHash) + + getViewMyProfileButton := func()(*fyne.Container, error){ + + if (iAmDisabled == true){ + disabledButton := getWidgetCentered(widget.NewButtonWithIcon("Disabled", theme.VisibilityOffIcon(), func(){ + title := translate("Your Profile Is Disabled") + dialogMessageA := getLabelCentered(translate("You have disabled your " + myIdentityType + " profile.")) + dialogMessageB := getLabelCentered(translate("You have no profile to view.")) + dialogContent := container.NewVBox(dialogMessageA, dialogMessageB) + dialog.ShowCustom(title, translate("Close"), dialogContent, window) + })) + + return disabledButton, nil + } + + viewProfileButton := getWidgetCentered(widget.NewButtonWithIcon("View Profile", theme.VisibilityIcon(), func(){ + setViewMyProfilePage(window, myIdentityType, "Public", currentPage) + })) + + return viewProfileButton, nil + } + + viewMyProfileButton, err := getViewMyProfileButton() + if (err != nil) { return nil, err } + + myIdentityTypeIcon, err := getIdentityTypeIcon(myIdentityType, 0) + if (err != nil) { return nil, err } + myIdentityTypeIconCentered := getFyneImageCentered(myIdentityTypeIcon) + + myIdentityTypeLabel := getBoldLabelCentered(myIdentityType) + + myInfoColumn := getContainerBoxed(container.NewVBox(myIdentityLabel, widget.NewSeparator(), myUsernameLabel, myIdentityHashLabel, viewMyProfileButton, widget.NewSeparator(), myIdentityTypeIconCentered, myIdentityTypeLabel)) + + return myInfoColumn, nil + } + + myInfoColumn, err := getMyInfoColumn() + if (err != nil) { + setErrorEncounteredPage(window, err, previousPage) + return + } + + getConversationContainer := func()(*fyne.Container, error){ + + myIdentityFound, conversationExists, messagesList, _, _, iHaveRejectedThem, _, _, theyHaveRejectedMe, _, err := myChatMessages.GetMyConversationInfoAndSortedMessagesList(myIdentityHash, theirIdentityHash, appNetworkType) + if (err != nil) { return nil, err } + if (myIdentityFound == false){ + return nil, errors.New("My identity not found after being found already.") + } + + // We use this list to store the non seen indicating messages + // Seen messages are messages that are used to indicate that a user has seen another user's message + // They are not shown to the user as messages, but are instead are shown as a color status next to each message + nonSeenIndicatingMessagesList := make([]myChatMessages.ConversationMessage, 0) + + // We have to split seen indicating messages into two maps: Ones which I have seen, and ones which they have seen + // Otherwise, a malicious conversator could make us falsely believe + // we had sent a seen indicating message for one of the messages they sent us. + // + // This map stores the message hashes which have been seen by me + mySeenMessagesMap := make(map[[26]byte]struct{}) + // This map stores the message hashes which they have seen + theirSeenMessagesMap := make(map[[26]byte]struct{}) + + for _, messageObject := range messagesList{ + + currentMessageContent := messageObject.Communication + + seenMessageHashHex, isSeenMessage := strings.CutPrefix(currentMessageContent, ">!>Seen=") + if (isSeenMessage == false){ + // This message is not a seen indicating message + nonSeenIndicatingMessagesList = append(nonSeenIndicatingMessagesList, messageObject) + continue + } + + seenMessageHash, err := readMessages.ReadMessageHashHex(seenMessageHashHex) + if (err != nil){ + // This should not happen + // Seekia should prevent anyone from sending a message with this prefix that does not contain a valid message hash + // The myChatMessages package should also reject any invalid messages from being added. + return nil, errors.New("GetMyConversationInfoAndSortedMessagesList returning message which contains invalid seen indicating message: " + seenMessageHashHex) + } + + currentMessageIAmSender := messageObject.IAmSender + + if (currentMessageIAmSender == true){ + mySeenMessagesMap[seenMessageHash] = struct{}{} + } else { + theirSeenMessagesMap[seenMessageHash] = struct{}{} + } + } + + // We use this function to determine if a message in the conversation has been seen by the user it was sent to + // Inputs: + // -bool: This is the IAmSender for the message which we are trying to get the Seen status of + // -[26]byte: This is the message hash we are trying to get the Seen status of + // Output: + // -bool: Message is seen + getMessageIsSeenStatus := func(iAmSender bool, messageHash [26]byte)bool{ + + if (iAmSender == true){ + _, exists := theirSeenMessagesMap[messageHash] + + return exists + } + + _, exists := mySeenMessagesMap[messageHash] + + return exists + } + + // Conversation view index is the index of the first message to display (ignoring seen-indicating messages) + // Will show the next 5 messages after the view index, unless less than 5 messages exist, in which case it will show whatever is left + // If more than five messages exist, the first page will show the remainder of messages, which may not be 5, because every other page displays exactly 5 messages + //TODO: Conversation view index should actually be calculated from a message hash/message identifier + // Otherwise, new messages will shift the current conversation view index + // For example, 5 messages are sent, so the conversation view index is now shifted by 1 page + + numberOfMessages := len(nonSeenIndicatingMessagesList) + + getMaximumViewIndex := func()int{ + + if (numberOfMessages <= 5){ + return 0 + } + + // Maximum view index = index of first message for the newest page of messages + maximumViewIndex := numberOfMessages - 5 + + return maximumViewIndex + } + + maximumViewIndex := getMaximumViewIndex() + + getConversationViewIndex := func()(int, error){ + // We show 5 messages per page + + if (numberOfMessages <= 5){ + + return 0, nil + } + + cacheConversationIndexExists, cacheConversationIndex, err := myConversationIndexes.GetConversationMessageViewIndex(myIdentityHash, theirIdentityHash, appNetworkType) + if (err != nil){ return 0, err } + if (cacheConversationIndexExists == false || cacheConversationIndex > maximumViewIndex || resetConversationIndex == true){ + + err := myConversationIndexes.SetConversationMessageViewIndex(myIdentityHash, theirIdentityHash, appNetworkType, maximumViewIndex) + if (err != nil) { return 0, err } + + return maximumViewIndex, nil + } + if (cacheConversationIndex <= 0){ + + return 0, nil + } + + return cacheConversationIndex, nil + } + + conversationViewIndex, err := getConversationViewIndex() + if (err != nil) { return nil, err } + + err = myConversationIndexes.SetConversationMessageViewIndex(myIdentityHash, theirIdentityHash, appNetworkType, conversationViewIndex) + if (err != nil) { return nil, err } + + // Last Message Index is the index of the last message on display + getLastMessageIndex := func()int{ + + if (numberOfMessages == 0){ + return 0 + } + if (numberOfMessages <= 5){ + + finalMessageIndex := numberOfMessages-1 + + return finalMessageIndex + } + if (conversationViewIndex == 0){ + // First page will not show next five messages + // It will show the remainder of messages, because all other pages show 5 messages each + + dividedFiveRemainder := numberOfMessages % 5 + if (dividedFiveRemainder != 0){ + return dividedFiveRemainder - 1 + } + } + + return conversationViewIndex + 4 + } + + lastMessageIndex := getLastMessageIndex() + + getConversationTopBar := func()(*fyne.Container, error){ + + theyAreBlocked, _, _, _, err := myBlockedUsers.CheckIfUserIsBlocked(theirIdentityHash) + if (err != nil){ return nil, err } + if (theyAreBlocked == true){ + + blockedDescription := getBoldLabel("You Have Blocked This User.") + blockedHelpButton := widget.NewButtonWithIcon("", theme.InfoIcon(), func(){ + setViewPeerActionsPage(window, theirIdentityHash, currentPage) + }) + blockedDescriptionRow := container.NewHBox(layout.NewSpacer(), blockedDescription, blockedHelpButton, layout.NewSpacer()) + + return blockedDescriptionRow, nil + } + + if (conversationExists == false) { + topBar := getBoldLabelCentered("No messages exist.") + return topBar, nil + } + + getTopBar := func()*fyne.Container{ + + getMessagesViewingText := func()string{ + if (numberOfMessages == 1 || lastMessageIndex == 0){ + return "Viewing 1" + } + + totalMessagesString := helpers.ConvertIntToString(numberOfMessages) + if (numberOfMessages <= 5){ + return "Viewing 1 - " + totalMessagesString + } + + startNumberString := helpers.ConvertIntToString(conversationViewIndex + 1) + + lastMessageViewingString := helpers.ConvertIntToString(lastMessageIndex + 1) + + return "Viewing " + startNumberString + " - " + lastMessageViewingString + } + + messagesViewingText := getMessagesViewingText() + + numberOfMessagesTotalString := helpers.ConvertIntToString(numberOfMessages) + numberOfMessagesText := messagesViewingText + " of " + numberOfMessagesTotalString + " messages" + + if (numberOfMessages <= 5){ + + // No navigation buttons needed + + topBar := getBoldLabelCentered(numberOfMessagesText) + return topBar + } + + numberOfMessagesLabel := getBoldLabel(numberOfMessagesText) + + getNavigateToBeginningButton := func()fyne.Widget{ + + if (conversationViewIndex <= 0){ + blankButton := widget.NewButton(" ", nil) + + return blankButton + } + + viewBeginningButton := widget.NewButtonWithIcon("", theme.MediaFastRewindIcon(), func(){ + + err := myConversationIndexes.SetConversationMessageViewIndex(myIdentityHash, theirIdentityHash, appNetworkType, 0) + if (err != nil) { + setErrorEncounteredPage(window, err, previousPage) + return + } + currentPage() + }) + + return viewBeginningButton + } + + getViewOlderMessagesButton := func()fyne.Widget{ + + if (conversationViewIndex <= 0){ + blankButton := widget.NewButton(" ", nil) + return blankButton + } + viewOlderButton := widget.NewButtonWithIcon("", theme.NavigateBackIcon(), func(){ + + newViewIndex := conversationViewIndex - 5 + + err := myConversationIndexes.SetConversationMessageViewIndex(myIdentityHash, theirIdentityHash, appNetworkType, newViewIndex) + if (err != nil) { + setErrorEncounteredPage(window, err, previousPage) + return + } + currentPage() + }) + return viewOlderButton + } + + getViewNewerMessagesButton := func()fyne.Widget{ + + if (conversationViewIndex >= maximumViewIndex){ + blankButton := widget.NewButton(" ", nil) + return blankButton + } + viewNewerButton := widget.NewButtonWithIcon("", theme.NavigateNextIcon(), func(){ + + newViewIndex := lastMessageIndex + 1 + + err := myConversationIndexes.SetConversationMessageViewIndex(myIdentityHash, theirIdentityHash, appNetworkType, newViewIndex) + if (err != nil) { + setErrorEncounteredPage(window, err, previousPage) + return + } + currentPage() + }) + return viewNewerButton + } + + getNavigateToEndButton := func()fyne.Widget{ + + if (conversationViewIndex >= maximumViewIndex){ + + blankButton := widget.NewButton(" ", nil) + return blankButton + } + + navigateToEndButton := widget.NewButtonWithIcon("", theme.MediaFastForwardIcon(), currentPageWithNewestView) + + return navigateToEndButton + } + + navigateToBeginningButton := getNavigateToBeginningButton() + viewOlderMessagesButton := getViewOlderMessagesButton() + viewNewerMessagesButton := getViewNewerMessagesButton() + navigateToEndButton := getNavigateToEndButton() + + leftSideNavigationButtons := container.NewGridWithRows(1, navigateToBeginningButton, viewOlderMessagesButton) + rightSideNavigationButton := container.NewGridWithRows(1, viewNewerMessagesButton, navigateToEndButton) + + topBar := container.NewHBox(layout.NewSpacer(), leftSideNavigationButtons, numberOfMessagesLabel, rightSideNavigationButton, layout.NewSpacer()) + + return topBar + } + + topBar := getTopBar() + + if (iHaveRejectedThem == false && theyHaveRejectedMe == false){ + return topBar, nil + } + + getRejectionInfoDescription := func()string{ + + if (iHaveRejectedThem == true && theyHaveRejectedMe == false){ + return "You have rejected this user." + } + if (iHaveRejectedThem == false && theyHaveRejectedMe == true){ + return "This user has rejected you." + } + return "You have rejected eachother." + } + + rejectionInfoDescription := getRejectionInfoDescription() + rejectionInfoButton := widget.NewButtonWithIcon("", theme.InfoIcon(), func(){ + setGreetAndRejectExplainerPage(window, currentPage) + }) + rejectionInfoLabel := getBoldLabel(rejectionInfoDescription) + rejectionInfoRow := container.NewHBox(layout.NewSpacer(), rejectionInfoLabel, rejectionInfoButton, layout.NewSpacer()) + + topBarWithDescription := container.NewVBox(topBar, widget.NewSeparator(), rejectionInfoRow) + + return topBarWithDescription, nil + } + + getMessagesContainer := func()(*fyne.Container, error){ + + if (conversationExists == false){ + emptyBox := getContainerBoxed(container.NewHBox()) + return emptyBox, nil + } + + // This will return true if we should pixelate received images + // We will never pixelate images that we sent + getPixelateReceivedImagesBool := func()(bool, error){ + + exists, pixelateStatus, err := mySettings.GetSetting("PixelateImagesOnOffStatus") + if (err != nil) { return false, err } + if (exists == false){ + return true, nil + } + if (pixelateStatus == "Off"){ + return false, nil + } + return true, nil + } + + pixelateReceivedImagesBool, err := getPixelateReceivedImagesBool() + if (err != nil){ return nil, err } + + if (lastMessageIndex > (len(nonSeenIndicatingMessagesList) - 1)){ + return nil, errors.New("Maximum index out of range.") + } + + messagesToDisplayList := nonSeenIndicatingMessagesList[conversationViewIndex:lastMessageIndex+1] + + messageRowsContainer := container.NewVBox() + + for _, messageObject := range messagesToDisplayList{ + + messageStatus := messageObject.MessageStatus + messageTimeSent := messageObject.TimeSent + messageCommunication := messageObject.Communication + iAmSender := messageObject.IAmSender + + getMessageContentBox := func()(*fyne.Container, error){ + + isAPhoto := strings.HasPrefix(messageCommunication, ">!>Photo=") + if (isAPhoto == true) { + + imageBase64 := strings.TrimPrefix(messageCommunication, ">!>Photo=") + imageObject, err := imagery.ConvertWEBPBase64StringToCroppedDownsizedImageObject(imageBase64) + if (err != nil) { + return nil, errors.New("MyChatMessages contains photo message with invalid photo: " + err.Error()) + } + + getThumbnail := func()(*canvas.Image, error){ + if (iAmSender == true || pixelateReceivedImagesBool == false){ + fyneImageObject := canvas.NewImageFromImage(imageObject) + return fyneImageObject, nil + } + photoIcon, err := getFyneImageIcon("Photo") + if (err != nil) { return nil, err } + return photoIcon, nil + } + + imageThumbnail, err := getThumbnail() + if (err != nil) { return nil, err } + imageThumbnail.FillMode = canvas.ImageFillContain + + viewImageButton := getWidgetCentered(widget.NewButtonWithIcon("View Image", theme.VisibilityIcon(), func(){ + if (iAmSender == true || pixelateReceivedImagesBool == false){ + setViewFullpageImagePage(window, imageObject, currentPage) + return + } + + setSlowlyRevealImagePage(window, imageObject, 0, currentPage) + })) + imageWithButton := container.NewGridWithColumns(1, imageThumbnail, viewImageButton) + + imageWithButtonBoxed := getContainerBoxed(imageWithButton) + return imageWithButtonBoxed, nil + } + + isGreet := strings.HasPrefix(messageCommunication, ">!>Greet") + if (isGreet == true){ + greetIcon, err := getFyneImageIcon("Greet") + if (err != nil) { return nil, err } + + emojiSize := getCustomFyneSize(20) + greetIcon.SetMinSize(emojiSize) + + greetDescription := getBoldLabel("I Greet You.") + greetHelpButton := widget.NewButtonWithIcon("", theme.QuestionIcon(), func(){ + setGreetAndRejectExplainerPage(window, currentPage) + }) + greetDescriptionRow := container.NewHBox(layout.NewSpacer(), greetDescription, greetHelpButton, layout.NewSpacer()) + + greetMessageBox := getContainerBoxed(container.NewVBox(greetIcon, greetDescriptionRow)) + + return greetMessageBox, nil + } + + isReject := strings.HasPrefix(messageCommunication, ">!>Reject") + if (isReject == true){ + rejectIcon, err := getFyneImageIcon("Reject") + if (err != nil) { return nil, err } + + emojiSize := getCustomFyneSize(20) + rejectIcon.SetMinSize(emojiSize) + + rejectDescription := getBoldLabel("I Reject You.") + rejectHelpButton := widget.NewButtonWithIcon("", theme.QuestionIcon(), func(){ + setGreetAndRejectExplainerPage(window, currentPage) + }) + rejectDescriptionRow := container.NewHBox(layout.NewSpacer(), rejectDescription, rejectHelpButton, layout.NewSpacer()) + + rejectMessageBox := getContainerBoxed(container.NewVBox(rejectIcon, rejectDescriptionRow)) + + return rejectMessageBox, nil + } + + isEmoji := strings.HasPrefix(messageCommunication, ">!>Emoji=") + if (isEmoji == true){ + + emojiIdentifierString := strings.TrimPrefix(messageCommunication, ">!>Emoji=") + + emojiIdentifierInt, err := helpers.ConvertStringToInt(emojiIdentifierString) + if (err != nil){ + return nil, errors.New("MyChatMessages contains invalid message: " + messageCommunication) + } + + emojiImageObject, err := getEmojiImageObject(emojiIdentifierInt) + if (err != nil) { return nil, err } + + emojiFyneImage := canvas.NewImageFromImage(emojiImageObject) + emojiFyneImage.FillMode = canvas.ImageFillContain + emojiSize := getCustomFyneSize(30) + emojiFyneImage.SetMinSize(emojiSize) + + emojiImageBoxed := getFyneImageBoxed(emojiFyneImage) + return emojiImageBoxed, nil + } + + //TODO: Add Questionnaire + + // This will return true if we can display the unflattened and untrimmed message + checkIfMessageTrimmingAndFlatteningIsNeeded := func()bool{ + + numberOfNewlines := strings.Count(messageCommunication, "\n") + if (numberOfNewlines > 15){ + // Message is too tall to show in full + return true + } + + // We need to count tabs as 5 runes + numberOfTabs := strings.Count(messageCommunication, "\t") + + numberOfRunes := len([]rune(messageCommunication)) + (numberOfTabs*4) + + if (numberOfRunes > 200){ + return true + } + + return false + } + + messageTrimmingAndFlatteningIsNeeded := checkIfMessageTrimmingAndFlatteningIsNeeded() + if (messageTrimmingAndFlatteningIsNeeded == true){ + + messagePreviewText, _, err := helpers.TrimAndFlattenString(messageCommunication, 200) + if (err != nil) { return nil, err } + + messagePreviewLabel := widget.NewLabel(messagePreviewText) + messagePreviewLabel.TextStyle = getFyneTextStyle_Bold() + messagePreviewLabel.Wrapping = 3 + + viewFullMessageButton := getWidgetCentered(widget.NewButtonWithIcon("Read All", theme.VisibilityIcon(), func(){ + setViewTextPage(window, "Viewing Message", messageCommunication, false, currentPage) + })) + + messageContentBoxed := getContainerBoxed(container.NewVBox(messagePreviewLabel, viewFullMessageButton)) + return messageContentBoxed, nil + } + + messageContentTextLabel := widget.NewLabel(messageCommunication) + messageContentTextLabel.TextStyle = getFyneTextStyle_Bold() + messageContentTextLabel.Wrapping = 3 + messageContentTextBoxed := getWidgetBoxed(messageContentTextLabel) + return messageContentTextBoxed, nil + } + + // This will show a button to represent Queued, Failed, Seen, or Unseen + getMessageStatusButton := func()(fyne.Widget, error){ + + if (messageStatus == "Queued"){ + + queuedButton := widget.NewButtonWithIcon("Queued", theme.HistoryIcon(), func(){ + dialogTitle := translate("Message Is Queued") + dialogMessageA := getLabelCentered(translate("This message is queued.")) + dialogMessageB := getLabelCentered(translate("Seekia is still trying to send the message.")) + dialogMessageC := getLabelCentered(translate("This could take a while if the recipient's chat keys are missing.")) + dialogMessageD := getLabelCentered(translate("In that case, Seekia will try to download the recipient's chat keys.")) + dialogMessageE := getLabelCentered(translate("If Seekia cannot find the user's chat keys, it will eventually stop trying.")) + dialogContent := container.NewVBox(dialogMessageA, dialogMessageB, dialogMessageC, dialogMessageD, dialogMessageE) + dialog.ShowCustom(dialogTitle, translate("Close"), dialogContent, window) + }) + + queuedButton.Importance = widget.HighImportance + + return queuedButton, nil + } + if (messageStatus == "Failed"){ + + failedButton := widget.NewButtonWithIcon("Failed", theme.CancelIcon(), func(){ + dialogTitle := translate("Message Failed") + dialogMessageA := getLabelCentered(translate("This message failed to send.")) + dialogMessageB := getLabelCentered(translate("This is probably due to the user's profile being missing or disabled.")) + dialogMessageC := getLabelCentered(translate("You can try again.")) + + dialogContent := container.NewVBox(dialogMessageA, dialogMessageB, dialogMessageC) + dialog.ShowCustom(dialogTitle, translate("Close"), dialogContent, window) + }) + + failedButton.Importance = widget.DangerImportance + + return failedButton, nil + } + + // messageStatus == "Sent" + + messageHash := messageObject.MessageHash + + messageSeenUnseenStatus := getMessageIsSeenStatus(iAmSender, messageHash) + + if (messageSeenUnseenStatus == false && iAmSender == true){ + + unseenButton := widget.NewButtonWithIcon("Unseen", theme.VisibilityOffIcon(), func(){ + title := translate("Message is Unseen") + dialogMessageA := getLabelCentered(translate("This message is unseen.")) + dialogMessageB := getLabelCentered(translate("The recipient has not indicated that they have seen the message.")) + dialogContent := container.NewVBox(dialogMessageA, dialogMessageB) + dialog.ShowCustom(title, translate("Close"), dialogContent, window) + }) + return unseenButton, nil + } + if (messageSeenUnseenStatus == false && iAmSender == false){ + unseenButton := widget.NewButtonWithIcon("Unseen", theme.VisibilityOffIcon(), func(){ + setConfirmSendSeenMessagePage(window, myIdentityHash, theirIdentityHash, messageHash, currentPage, currentPage) + }) + return unseenButton, nil + } + if (messageSeenUnseenStatus == true && iAmSender == true){ + + seenButton := widget.NewButtonWithIcon("Seen", theme.VisibilityIcon(), func(){ + title := translate("Message Is Seen") + dialogMessage := getLabelCentered(translate("This message has been seen by the recipient.")) + dialogContent := container.NewVBox(dialogMessage) + dialog.ShowCustom(title, translate("Close"), dialogContent, window) + }) + seenButton.Importance = widget.HighImportance + return seenButton, nil + } + + // messageSeenUnseenStatus == true && iAmSender == false + + seenButton := widget.NewButtonWithIcon("Seen", theme.VisibilityIcon(), func(){ + title := translate("Message Is Seen") + dialogMessage := getLabelCentered(translate("You have notified the recipient that you have seen this message.")) + dialogContent := container.NewVBox(dialogMessage) + dialog.ShowCustom(title, translate("Close"), dialogContent, window) + }) + seenButton.Importance = widget.HighImportance + return seenButton, nil + } + + messageContentBox, err := getMessageContentBox() + if (err != nil) { return nil, err } + + timeSentAgoText, err := helpers.ConvertUnixTimeToTimeFromNowTranslated(messageTimeSent, false) + if (err != nil) { return nil, err } + timeSentAgoLabel := getItalicLabel("Sent " + timeSentAgoText) + + getViewMessageDetailsButton := func()(fyne.Widget, error){ + + if (messageStatus == "Sent"){ + + messageHash := messageObject.MessageHash + + viewMessageDetailsButton := widget.NewButtonWithIcon("", theme.InfoIcon(), func(){ + setViewMessageDetailsButton(window, messageHash, messageTimeSent, currentPage) + }) + return viewMessageDetailsButton, nil + } + + if (messageStatus == "Failed"){ + + showFailedDialog := func(){ + //TODO: Add a page where user can see why message failed + dialogTitle := translate("Message Failed") + dialogMessageA := getLabelCentered(translate("This message failed to send.")) + dialogMessageB := getLabelCentered(translate("This is probably due to the user's profile being missing or disabled.")) + dialogMessageC := getLabelCentered(translate("You can try again.")) + + dialogContent := container.NewVBox(dialogMessageA, dialogMessageB, dialogMessageC) + dialog.ShowCustom(dialogTitle, translate("Close"), dialogContent, window) + } + + viewMessageDetailsButton := widget.NewButtonWithIcon("", theme.InfoIcon(), showFailedDialog) + + return viewMessageDetailsButton, nil + } + if (messageStatus == "Queued"){ + + showQueuedDialog := func(){ + //TODO: Add a page where user can cancel message, and see why message is queued + dialogTitle := translate("Message Is Queued") + dialogMessageA := getLabelCentered(translate("This message is queued.")) + dialogMessageB := getLabelCentered(translate("Seekia is still trying to send the message.")) + dialogMessageC := getLabelCentered(translate("This could take a while if the recipient's chat keys are missing.")) + dialogMessageD := getLabelCentered(translate("In that case, Seekia will try to download the recipient's chat keys.")) + dialogMessageE := getLabelCentered(translate("If Seekia cannot find the user's chat keys, it will eventually stop trying.")) + dialogContent := container.NewVBox(dialogMessageA, dialogMessageB, dialogMessageC, dialogMessageD, dialogMessageE) + dialog.ShowCustom(dialogTitle, translate("Close"), dialogContent, window) + } + + viewMessageDetailsButton := widget.NewButtonWithIcon("", theme.InfoIcon(), showQueuedDialog) + + return viewMessageDetailsButton, nil + } + + return nil, errors.New("messagesToDisplayList contains messageMap with invalid messageStatus: " + messageStatus) + } + + viewMessageDetailsButton, err := getViewMessageDetailsButton() + if (err != nil) { return nil, err } + + messageDetailsRow := container.NewHBox(layout.NewSpacer(), timeSentAgoLabel, viewMessageDetailsButton, layout.NewSpacer()) + + messageStatusButton, err := getMessageStatusButton() + if (err != nil) { return nil, err } + + //messageDetailsBox := getContainerBoxed(container.NewVBox(messageStatusButton, messageDetailsRow)) + + getMessageRow := func()*fyne.Container{ + + if (iAmSender == true){ + + messageBox := getContainerBoxed(container.NewBorder(nil, messageDetailsRow, messageStatusButton, nil, messageContentBox)) + + emptyLabel := widget.NewLabel("") + + messageRow := container.NewBorder(nil, nil, emptyLabel, nil, messageBox) + return messageRow + } + + messageBox := getContainerBoxed(container.NewBorder(nil, messageDetailsRow, nil, messageStatusButton, messageContentBox)) + + emptyLabel := widget.NewLabel("") + + messageRow := container.NewBorder(nil, nil, nil, emptyLabel, messageBox) + return messageRow + } + + messageRow := getMessageRow() + + messageRowsContainer.Add(messageRow) + } + + if (conversationViewIndex >= maximumViewIndex){ + + refreshMessagesButton := getWidgetCentered(widget.NewButtonWithIcon("Refresh", theme.ViewRefreshIcon(), currentPageWithNewestView)) + messageRowsContainer.Add(refreshMessagesButton) + } + + messageRowsScrollable := container.NewVScroll(messageRowsContainer) + boxedMessagesContainer := getScrollContainerBoxed(messageRowsScrollable) + + return boxedMessagesContainer, nil + } + + getChooseMessageTypeToSendRow := func()(*fyne.Container, error){ + + // Outputs: + // -bool: Able to send + showDialogIfUnableToSend := func()bool{ + if (iAmDisabled == true){ + dialogTitle := translate("Unable To Chat") + dialogMessageA := getLabelCentered(translate("Cannot respond: Your identity is disabled.")) + dialogMessageB := getLabelCentered(translate("Enable your profile on the Profiles - Broadcast page.")) + dialogContent := container.NewVBox(dialogMessageA, dialogMessageB) + dialog.ShowCustom(dialogTitle, translate("Close"), dialogContent, window) + return false + } + + if (theyAreDisabled == true){ + + dialogTitle := translate("Unable To Chat") + dialogMessageA := getLabelCentered(translate("Cannot respond: Their identity is disabled.")) + dialogMessageB := getLabelCentered(translate("This user has disabled their Seekia profile.")) + + //TODO: Add button to attempt to download their profile + + dialogContent := container.NewVBox(dialogMessageA, dialogMessageB) + dialog.ShowCustom(dialogTitle, translate("Close"), dialogContent, window) + + return false + } + return true + } + + sendImageButton := widget.NewButtonWithIcon("Send Image", theme.FileImageIcon(), func(){ + + ableToSend := showDialogIfUnableToSend() + if (ableToSend == false){ + return + } + + setSendChatImagePage(window, myIdentityHash, theirIdentityHash, currentPage, currentPageWithNewestView) + }) + + sendTextButton := widget.NewButtonWithIcon("Send Text", theme.DocumentCreateIcon(), func(){ + + ableToSend := showDialogIfUnableToSend() + if (ableToSend == false){ + return + } + + setWriteMessageToSendPage(window, myIdentityHash, theirIdentityHash, "", currentPage, currentPageWithNewestView) + }) + + sendEmojiButton := widget.NewButtonWithIcon("Send Emoji", theme.AccountIcon(), func(){ + + ableToSend := showDialogIfUnableToSend() + if (ableToSend == false){ + return + } + + submitEmojiFunction := func(emojiIdentifier int){ + + emojiIdentifierString := helpers.ConvertIntToString(emojiIdentifier) + + newMessageCommunication := ">!>Emoji=" + emojiIdentifierString + + setFundAndSendChatMessagePage(window, myIdentityHash, theirIdentityHash, newMessageCommunication, false, currentPage, currentPageWithNewestView) + } + + setChooseEmojiPage(window, "Choose Emoji", "Circle Face", 0, currentPage, submitEmojiFunction) + }) + + sendMessageRow := getContainerCentered(container.NewGridWithRows(1, sendImageButton, sendTextButton, sendEmojiButton)) + + return sendMessageRow, nil + } + + topbar, err := getConversationTopBar() + if (err != nil) { return nil, err } + + messagesContainer, err := getMessagesContainer() + if (err != nil){ return nil, err } + + chooseMessageTypeToSendRow, err := getChooseMessageTypeToSendRow() + if (err != nil) { return nil, err } + + conversationMessagesColumn := container.NewBorder(topbar, chooseMessageTypeToSendRow, nil, nil, messagesContainer) + + err = myReadStatus.SetConversationReadUnreadStatus(myIdentityHash, theirIdentityHash, appNetworkType, "Read") + if (err != nil) { return nil, err } + + return conversationMessagesColumn, nil + } + + conversationContainer, err := getConversationContainer() + if (err != nil){ + setErrorEncounteredPage(window, err, previousPage) + return + } + + pageHeader := container.NewVBox(title, backButton, widget.NewSeparator()) + + pageContent := container.NewBorder(pageHeader, nil, recipientInfoColumn, myInfoColumn, conversationContainer) + + setPageContent(pageContent, window) +} + + +func setViewMessageDetailsButton(window fyne.Window, messageHash [26]byte, messageTimeSent int64, previousPage func()){ + + currentPage := func(){setViewMessageDetailsButton(window, messageHash, messageTimeSent, previousPage)} + + title := getPageTitleCentered("View Message Details") + + backButton := getBackButtonCentered(previousPage) + + subtitle := getPageSubtitleCentered("Message Details") + + messageHashLabel := widget.NewLabel("Message Hash:") + + messageHashHex := encoding.EncodeBytesToHexString(messageHash[:]) + + messageHashTrimmed, _, err := helpers.TrimAndFlattenString(messageHashHex, 10) + if (err != nil){ + setErrorEncounteredPage(window, err, previousPage) + return + } + + messageHashText := getBoldLabel(messageHashTrimmed) + viewMessageHashButton := widget.NewButtonWithIcon("", theme.VisibilityIcon(), func(){ + setViewContentHashPage(window, "Message", messageHash[:], currentPage) + }) + + messageHashRow := container.NewHBox(layout.NewSpacer(), messageHashLabel, messageHashText, viewMessageHashButton, layout.NewSpacer()) + + timeSentLabel := widget.NewLabel("Time Sent:") + + messageSentTimeString := helpers.ConvertUnixTimeToTranslatedTime(messageTimeSent) + + messageSentTimeLabel := getBoldLabel(messageSentTimeString) + + sentTimeWarningButton := widget.NewButtonWithIcon("", theme.WarningIcon(), func(){ + title := translate("Sent Time Warning") + dialogMessageA := getLabelCentered("Sent times are not verified.") + dialogMessageB := getLabelCentered("They can be faked by the message author.") + dialogContent := container.NewVBox(dialogMessageA, dialogMessageB) + dialog.ShowCustom(title, translate("Close"), dialogContent, window) + }) + + messageSentTimeRow := container.NewHBox(layout.NewSpacer(), timeSentLabel, messageSentTimeLabel, sentTimeWarningButton, layout.NewSpacer()) + + reportMessageButton := getWidgetCentered(widget.NewButtonWithIcon("Report Message", theme.WarningIcon(), func(){ + //TODO + // This will go to a page where the user can report the message + // They will have to pay for their report, and be warned that the report may contain their identity hash + // It will not be directly linked to their identity hash if the message was sent to a secret inbox. + // We should let the user know if the message was sent to a secret inbox or not. + // The sender will also be able to tell that their message was reported + // If the message contains illegal content, reporting the message may be legally risky for the recipient, because it acknowledges + // that the user saw the message + // We must warn the user of this too. + showUnderConstructionDialog(window) + })) + + //TODO: Show other information: Message size, message moderation status, and when message will expire from network + + page := container.NewVBox(title, backButton, widget.NewSeparator(), subtitle, widget.NewSeparator(), messageHashRow, widget.NewSeparator(), messageSentTimeRow, widget.NewSeparator(), reportMessageButton) + + setPageContent(page, window) +} + + +func setConfirmSendSeenMessagePage(window fyne.Window, myIdentityHash [16]byte, recipientIdentityHash [16]byte, seenMessageHash [26]byte, previousPage func(), afterMessageSentPage func()){ + + currentPage := func(){setConfirmSendSeenMessagePage(window, myIdentityHash, recipientIdentityHash, seenMessageHash, previousPage, afterMessageSentPage)} + + title := getPageTitleCentered(translate("Send Seen Message")) + + backButton := getBackButtonCentered(previousPage) + + subtitle := getPageSubtitleCentered("Confirm Send Seen Message?") + + description1 := getLabelCentered("Are you sure you want to send a seen message?") + description2 := getLabelCentered("This will let the user know you received and saw their message.") + description3 := getLabelCentered("This is informative for the recipient.") + description4 := getLabelCentered("You will pay for the message on the next page.") + + confirmButton := getWidgetCentered(widget.NewButtonWithIcon("Confirm", theme.ConfirmIcon(), func(){ + + seenMessageHashHex := encoding.EncodeBytesToHexString(seenMessageHash[:]) + + seenMessageCommunication := ">!>Seen=" + seenMessageHashHex + + setFundAndSendChatMessagePage(window, myIdentityHash, recipientIdentityHash, seenMessageCommunication, false, currentPage, afterMessageSentPage) + })) + + page := container.NewVBox(title, backButton, widget.NewSeparator(), subtitle, widget.NewSeparator(), description1, description2, description3, description4, confirmButton) + + setPageContent(page, window) +} + +func setWriteMessageToSendPage(window fyne.Window, myIdentityHash [16]byte, recipientIdentityHash [16]byte, messageCommunication string, previousPage func(), afterMessageSentPage func()){ + + title := getPageTitleCentered("Write Message") + + backButton := getBackButtonCentered(previousPage) + + messageEntry := widget.NewMultiLineEntry() + messageEntry.Wrapping = 3 + messageEntry.SetPlaceHolder(translate("Enter message...")) + messageEntry.Text = messageCommunication + + // We use this function so user does not lose their written message if they view Rules/SendMessage page and return back + currentPageWithEntryContent := func(){ + + currentEntryText := messageEntry.Text + + setWriteMessageToSendPage(window, myIdentityHash, recipientIdentityHash, currentEntryText, previousPage, afterMessageSentPage) + } + + description1 := getBoldLabelCentered("Write your message to send.") + description2 := widget.NewLabel("Your message must follow the Seekia rules.") + + seekiaRulesButton := widget.NewButtonWithIcon("", theme.QuestionIcon(), func(){ + + setViewSeekiaRulesPage(window, currentPageWithEntryContent) + }) + + description2Row := container.NewHBox(layout.NewSpacer(), description2, seekiaRulesButton, layout.NewSpacer()) + + sendButton := widget.NewButtonWithIcon("Send", theme.NavigateNextIcon(), func(){ + + //TODO: Length limit + + newMessageCommunication := messageEntry.Text + if (newMessageCommunication == ""){ + return + } + textIsAllowed := allowedText.VerifyStringIsAllowed(newMessageCommunication) + if (textIsAllowed == false){ + dialogTitle := translate("Message Is Not Allowed") + dialogMessageA := getLabelCentered("Text contains unallowed text.") + dialogMessageB := getLabelCentered("It must be encoded in UTF-8.") + dialogMessageC := getLabelCentered("Remove unallowed text and resend message.") + dialogContent := container.NewVBox(dialogMessageA, dialogMessageB, dialogMessageC) + dialog.ShowCustom(dialogTitle, translate("Close"), dialogContent, window) + return + } + + // We use this prefix for special messages (emojis, questionnaire responses, images, seen messages) + isSpecial := strings.HasPrefix(newMessageCommunication, ">!>") + if (isSpecial == true){ + dialogTitle := translate("Message Prefix Is Not Allowed") + dialogMessageA := getLabelCentered("Your message cannot start with >!>.") + dialogMessageB := getLabelCentered("Remove this prefix and resend message.") + dialogContent := container.NewVBox(dialogMessageA, dialogMessageB) + dialog.ShowCustom(dialogTitle, translate("Close"), dialogContent, window) + return + } + + setFundAndSendChatMessagePage(window, myIdentityHash, recipientIdentityHash, newMessageCommunication, false, currentPageWithEntryContent, afterMessageSentPage) + }) + + sendButtonCentered := getWidgetCentered(sendButton) + + spacer := widget.NewLabel("") + + sendButtonWithSpacer := container.NewVBox(sendButtonCentered, spacer) + + header := container.NewVBox(title, backButton, widget.NewSeparator(), description1, description2Row, widget.NewSeparator()) + + page := container.NewBorder(header, sendButtonWithSpacer, nil, nil, messageEntry) + + setPageContent(page, window) +} + + +func setSendChatImagePage(window fyne.Window, myIdentityHash [16]byte, recipientIdentityHash [16]byte, previousPage func(), afterSendPage func()){ + + currentPage := func(){setSendChatImagePage(window, myIdentityHash, recipientIdentityHash, previousPage, afterSendPage)} + + title := getPageTitleCentered("Send Chat Image") + + backButton := getBackButtonCentered(previousPage) + + subtitle := getPageSubtitleCentered("Send Image") + + description1 := getBoldLabelCentered("Select an image file to send.") + description2 := getLabelCentered("You can apply image effects on the next page.") + description3 := getLabelCentered("JPEG, PNG and WEBP files are supported.") + + openFileCallbackFunction := func(fileObject fyne.URIReadCloser, err error){ + if (err != nil) { + title := translate("Failed to open image file.") + dialogMessage := getLabelCentered(translate("Report this error to Seekia developers: " + err.Error())) + dialogContent := container.NewVBox(dialogMessage) + dialog.ShowCustom(title, translate("Close"), dialogContent, window) + return + } + + if (fileObject == nil) { + return + } + + setLoadingScreen(window, "Add Image", "Importing image...") + + filePath := fileObject.URI().String() + filePath = strings.TrimPrefix(filePath, "file://") + + fileExists, ableToReadImage, imageObject, err := imagery.ReadImageFile(filePath) + if (err != nil) { + currentPage() + title := translate("Failed to open image file.") + dialogMessage := getLabelCentered(translate("Report this error to Seekia developers: " + err.Error())) + dialogContent := container.NewVBox(dialogMessage) + dialog.ShowCustom(title, translate("Close"), dialogContent, window) + return + } + if (fileExists == false) { + currentPage() + title := translate("Failed to open image file.") + dialogMessage := getLabelCentered(translate("Report this error to Seekia developers: Image file not found.")) + dialogContent := container.NewVBox(dialogMessage) + dialog.ShowCustom(title, translate("Close"), dialogContent, window) + return + } + + if (ableToReadImage == false) { + currentPage() + title := translate("Failed to import image file.") + dialogMessageA := getLabelCentered(translate("Seekia only supports these image file formats:")) + dialogMessageB := getLabelCentered("JPG, JPEG, PNG, WEBP") + dialogContent := container.NewVBox(dialogMessageA, dialogMessageB) + dialog.ShowCustom(title, translate("Close"), dialogContent, window) + return + } + + // We resize image to something that will be more managable when we edit it + // When we export it, we will downsize it even further + imageObjectResized, err := imagery.DownsizeGolangImage(imageObject, 1500) + if (err != nil) { + currentPage() + title := translate("Failed To Process Image File") + dialogMessageA := getLabelCentered(translate("Your file may be too large.")) + + errorString := err.Error() + + errorTrimmed, _, err := helpers.TrimAndFlattenString(errorString, 20) + if (err != nil){ + setErrorEncounteredPage(window, err, currentPage) + return + } + + errorTrimmedLabel := getBoldLabel("Error: " + errorTrimmed) + + viewFullErrorButton := widget.NewButtonWithIcon("", theme.VisibilityIcon(), func(){ + setViewTextPage(window, "Viewing Error", errorString, false, currentPage) + }) + + errorDescriptionRow := container.NewHBox(layout.NewSpacer(), errorTrimmedLabel, viewFullErrorButton, layout.NewSpacer()) + + dialogContent := container.NewVBox(dialogMessageA, errorDescriptionRow) + + dialog.ShowCustom(title, translate("Close"), dialogContent, window) + return + } + + submitImageToSendFunction := func(newImageObject image.Image, newPreviousPage func()){ + + setLoadingScreen(window, "Send Chat Image", "Compressing Image...") + + newImageBase64String, err := imagery.ConvertImageObjectToStandardWebpBase64String(newImageObject) + if (err != nil) { + setErrorEncounteredPage(window, err, newPreviousPage) + return + } + + setConfirmSendChatImagePage(window, myIdentityHash, recipientIdentityHash, newImageBase64String, newPreviousPage, afterSendPage) + } + + setEditImagePage(window, imageObject, false, nil, imageObjectResized, currentPage, submitImageToSendFunction) + } + + selectImageFileButton := getWidgetCentered(widget.NewButtonWithIcon("Select Image File", theme.FileImageIcon(), func(){ + dialog.ShowFileOpen(openFileCallbackFunction, window) + })) + + page := container.NewVBox(title, backButton, widget.NewSeparator(), subtitle, widget.NewSeparator(), description1, description2, description3, selectImageFileButton) + + setPageContent(page, window) +} + +func setConfirmSendChatImagePage(window fyne.Window, myIdentityHash [16]byte, recipientIdentityHash [16]byte, newImageBase64String string, previousPage func(), afterSendPage func()){ + + currentPage := func(){setConfirmSendChatImagePage(window, myIdentityHash, recipientIdentityHash, newImageBase64String, previousPage, afterSendPage)} + + title := getPageTitleCentered("Send Chat Image") + + backButton := getBackButtonCentered(previousPage) + + subtitle := getPageSubtitleCentered("Confirm Send Image?") + + description1 := getLabelCentered("Are you sure you want to send this image?") + description2 := getLabelCentered("Be aware that the image has been compressed.") + description3 := getLabelCentered("You will pay for the message on the next page.") + + submitButton := getWidgetCentered(widget.NewButtonWithIcon("Send Image", theme.ConfirmIcon(), func(){ + + newMessageCommunication := ">!>Photo=" + newImageBase64String + + setFundAndSendChatMessagePage(window, myIdentityHash, recipientIdentityHash, newMessageCommunication, false, currentPage, afterSendPage) + })) + + croppedImageObject, err := imagery.ConvertWEBPBase64StringToCroppedDownsizedImageObject(newImageBase64String) + if (err != nil){ + setErrorEncounteredPage(window, err, previousPage) + return + } + + newFyneImage := canvas.NewImageFromImage(croppedImageObject) + newFyneImage.FillMode = canvas.ImageFillContain + + viewFullpageButton := getWidgetCentered(widget.NewButtonWithIcon("", theme.ZoomInIcon(), func(){ + setViewFullpageImagePage(window, croppedImageObject, currentPage) + })) + + emptyLabel := widget.NewLabel("") + + zoomButtonWithSpacer := container.NewVBox(viewFullpageButton, emptyLabel) + + header := container.NewVBox(title, backButton, widget.NewSeparator(), subtitle, widget.NewSeparator(), description1, description2, description3, widget.NewSeparator(), submitButton, widget.NewSeparator()) + + page := container.NewBorder(header, zoomButtonWithSpacer, nil, nil, newFyneImage) + + setPageContent(page, window) + +} + +// Network duration time is the time the message should exist on the network before expiring +func setFundAndSendChatMessagePage(window fyne.Window, myIdentityHash [16]byte, recipientIdentityHash [16]byte, messageCommunication string, acceptedMessageQueueWarning bool, previousPage func(), afterMessageSentPage func()){ + + setLoadingScreen(window, "Send Message", "Loading...") + + currentPage := func(){setFundAndSendChatMessagePage(window, myIdentityHash, recipientIdentityHash, messageCommunication, acceptedMessageQueueWarning, previousPage, afterMessageSentPage)} + + isMine, myIdentityType, err := myIdentity.CheckIfIdentityHashIsMine(myIdentityHash) + if (err != nil) { + setErrorEncounteredPage(window, err, previousPage) + return + } + if (isMine == false){ + // should not occur, as send chat message page will restrict user from sending message if their identity does not exist + setErrorEncounteredPage(window, errors.New("Your identity no longer exists."), previousPage) + return + } + if (myIdentityType != "Mate" && myIdentityType != "Moderator"){ + // This should never occur + setErrorEncounteredPage(window, errors.New("Attempting to send from invalid identity type: " + myIdentityType), previousPage) + return + } + + title := getPageTitleCentered("Send Message") + + backButton := getBackButtonCentered(previousPage) + + exists, iAmDisabled, err := myLocalProfiles.GetProfileData(myIdentityType, "Disabled") + if (err != nil) { + setErrorEncounteredPage(window, err, previousPage) + return + } + if (exists == true && iAmDisabled == "Yes"){ + + description1 := getBoldLabelCentered("Your profile is disabled.") + description2 := getLabelCentered("You must enable your profile to send messages.") + description3 := getLabelCentered("Enable your profile on the Profile - Broadcast page.") + + page := container.NewVBox(title, backButton, widget.NewSeparator(), description1, description2, description3) + + setPageContent(page, window) + return + } + + appNetworkType, err := getAppNetworkType.GetAppNetworkType() + if (err != nil) { + setErrorEncounteredPage(window, err, previousPage) + return + } + + myIdentityFound, myProfileIsActiveStatus, err := myProfileStatus.GetMyProfileIsActiveStatus(myIdentityHash, appNetworkType) + if (err != nil) { + setErrorEncounteredPage(window, err, previousPage) + return + } + if (myIdentityFound == false) { + setErrorEncounteredPage(window, errors.New("My identity not found after being found already."), previousPage) + return + } + if (myProfileIsActiveStatus == false){ + + description1 := getBoldLabelCentered("Your profile is not active.") + description2 := getLabelCentered("You must broadcast your profile to chat with users.") + description3 := getLabelCentered("Broadcast your " + myIdentityType + " profile on the Broadcast page.") + + broadcastPageButton := getWidgetCentered(widget.NewButton("Visit Broadcast Page", func(){ + setBroadcastPage(window, myIdentityType, currentPage) + })) + + page := container.NewVBox(title, backButton, widget.NewSeparator(), description1, description2, description3, broadcastPageButton) + setPageContent(page, window) + return + } + + recipientIdentityType, err := identity.GetIdentityTypeFromIdentityHash(recipientIdentityHash) + if (err != nil) { + setErrorEncounteredPage(window, err, previousPage) + return + } + if (recipientIdentityType == "Host"){ + + description1 := getBoldLabelCentered("Recipient is a Host.") + description2 := getLabelCentered("They cannot be chatted with.") + page := container.NewVBox(title, backButton, widget.NewSeparator(), description1, description2) + + setPageContent(page, window) + return + } + + // We check if the recipient is disabled + + getRecipientIsDisabledBool := func()(bool, error){ + + recipientProfileExists, _, _, _, _, recipientRawProfileMap, err := profileStorage.GetNewestUserProfile(recipientIdentityHash, appNetworkType) + if (err != nil) { return false, err } + if (recipientProfileExists == false){ + return false, nil + } + + recipientIsDisabled, _, err := readProfiles.GetFormattedProfileAttributeFromRawProfileMap(recipientRawProfileMap, "Disabled") + if (err != nil) { return false, err } + + return recipientIsDisabled, nil + } + + recipientIsDisabled, err := getRecipientIsDisabledBool() + if (err != nil){ + setErrorEncounteredPage(window, err, previousPage) + return + } + + showPeerIsDisabledPage := func(){ + + userIsDisabledDescriptionA := getBoldLabelCentered("The recipient has disabled their profile.") + userIsDisabledDescriptionB := getLabelCentered("You cannot send them a message.") + + page := container.NewVBox(title, backButton, widget.NewSeparator(), userIsDisabledDescriptionA, userIsDisabledDescriptionB) + + setPageContent(page, window) + } + + if (recipientIsDisabled == true){ + showPeerIsDisabledPage() + return + } + + peerIsDisabled, peerChatKeysExist, _, _, err := peerChatKeys.GetPeerNewestActiveChatKeys(recipientIdentityHash, appNetworkType) + if (err != nil){ + setErrorEncounteredPage(window, err, previousPage) + return + } + if (peerIsDisabled == true){ + showPeerIsDisabledPage() + return + } + if (peerChatKeysExist == false && acceptedMessageQueueWarning == false){ + + description1 := getBoldLabelCentered("You do not have the recipient's chat keys.") + description2 := getLabelCentered("You can add your message to the message queue.") + description3 := getLabelCentered("Seekia will try to download their chat keys in the background.") + description4 := getLabelCentered("You can also try to download their profile immediately.") + description5 := getLabelCentered("Add message to message queue, or try to download chat keys?") + + addToMessageQueueButton := getWidgetCentered(widget.NewButtonWithIcon("Add Message To Message Queue", theme.NavigateNextIcon(), func(){ + setFundAndSendChatMessagePage(window, myIdentityHash, recipientIdentityHash, messageCommunication, true, previousPage, afterMessageSentPage) + })) + + downloadProfileButton := getWidgetCentered(widget.NewButtonWithIcon("Download Profile", theme.DownloadIcon(), func(){ + setDownloadMissingUserProfilePage(window, recipientIdentityHash, true, false, previousPage, currentPage, previousPage) + })) + + buttonsGrid := getContainerCentered(container.NewGridWithColumns(1, addToMessageQueueButton, downloadProfileButton)) + + page := container.NewVBox(title, backButton, widget.NewSeparator(), description1, description2, description3, description4, description5, buttonsGrid) + + setPageContent(page, window) + return + } + + //Outputs: + // -bool: Parameters Exist + // -*fyne.Container: + // -error + getSendMessageContent := func()(bool, *fyne.Container, error){ + + parametersExist, err := getParameters.CheckIfChatParametersExist(appNetworkType) + if (err != nil) { return false, nil, err } + if (parametersExist == false){ + return false, nil, nil + } + + currentCreditBalanceLabel := getLabelCentered("My Credit Balance:") + + getCurrentAppCurrency := func()(string, error){ + exists, currentAppCurrency, err := globalSettings.GetSetting("Currency") + if (err != nil) { return "", err } + if (exists == false){ + return "USD", nil + } + return currentAppCurrency, nil + } + + currentAppCurrencyCode, err := getCurrentAppCurrency() + if (err != nil){ return false, nil, err } + + parametersExist, appCurrencyAccountCreditBalance, err := myAccountCredit.GetMyCreditAccountBalanceInAnyCurrency(myIdentityType, appNetworkType, currentAppCurrencyCode) + if (err != nil) { return false, nil, err } + if (parametersExist == false){ + return false, nil, nil + } + + _, appCurrencySymbol, err := currencies.GetCurrencyInfoFromCurrencyCode(currentAppCurrencyCode) + if (err != nil) { return false, nil, err } + + appCurrencyCreditBalanceString := helpers.ConvertFloat64ToStringRounded(appCurrencyAccountCreditBalance, 3) + + appCurrencySymbolButton := widget.NewButton(appCurrencySymbol, func(){ + setChangeAppCurrencyPage(window, currentPage) + }) + currentBalanceLabel := getBoldLabel(appCurrencyCreditBalanceString + " " + currentAppCurrencyCode) + + currentBalanceRow := container.NewHBox(layout.NewSpacer(), appCurrencySymbolButton, currentBalanceLabel, layout.NewSpacer()) + + manageAccountCreditButton := getWidgetCentered(widget.NewButton("Manage", func(){ + setViewMyAccountCreditPage(window, myIdentityType, currentPage) + })) + + estimatedMessageSize, err := sendMessages.GetEstimatedMessageSize(recipientIdentityHash, messageCommunication) + if (err != nil) { return false, nil, err } + + messageCostLabel := getBoldLabelCentered("Message Cost:") + + appCurrencyCostBinding := binding.NewString() + durationDaysBinding := binding.NewInt() + + appCurrencyCostLabel := widget.NewLabelWithData(appCurrencyCostBinding) + appCurrencyCostLabel.TextStyle = getFyneTextStyle_Bold() + appCurrencyCostLabelCentered := container.NewHBox(layout.NewSpacer(), appCurrencyCostLabel, layout.NewSpacer()) + + // Fewer options improves anonymity, as a user who selects the same option every time will stand out + + //Outputs: + // -bool: Parameters exist + // -error + networkExpirationSelectFunction := func(response string)(bool, error){ + getDurationDays := func()int{ + if (response == "2 Days"){ + return 2 + } + + // response == "2 Weeks" + return 14 + } + desiredDurationDays := getDurationDays() + + desiredDurationSeconds := desiredDurationDays * 86400 + + parametersExist, appCurrencyCost, err := sendMessages.GetMessageNetworkCurrencyCostForProvidedDuration(appNetworkType, estimatedMessageSize, desiredDurationSeconds, currentAppCurrencyCode) + if (err != nil) { return false, err } + if (parametersExist == false){ + return false, nil + } + + currencyCostString := helpers.ConvertFloat64ToStringRounded(appCurrencyCost, 3) + + err = durationDaysBinding.Set(desiredDurationDays) + if (err != nil) { return false, err } + + err = appCurrencyCostBinding.Set(appCurrencySymbol + currencyCostString + " " + currentAppCurrencyCode) + if (err != nil) { return false, err } + + return true, nil + } + + // We initialize the bindings + parametersExist, err = networkExpirationSelectFunction("2 Days") + if (err != nil) { return false, nil, err } + if (parametersExist == false){ + return false, nil, nil + } + + durationToFundText := getBoldLabelCentered("Duration To fund:") + + expirationOptionsList := []string{"2 Days", "2 Weeks"} + + networkDurationSelector := widget.NewSelect(expirationOptionsList, func(response string){ + parametersExist, err := networkExpirationSelectFunction(response) + if (err != nil){ + setErrorEncounteredPage(window, err, currentPage) + } + if (parametersExist == false){ + + // Parameter permissions must have been updated and now existing parameters are invalid + // We will refresh the page + // We will show a loading screen so if this causes an infinite loop, we can detect it + setLoadingScreen(window, "Send Chat Message", "Trying to find parameters...") + time.Sleep(time.Second) + currentPage() + } + }) + + networkDurationSelector.Selected = "2 Days" + + networkDurationSelectorCentered := getWidgetCentered(networkDurationSelector) + + payAndSendButton := getWidgetCentered(widget.NewButtonWithIcon("Pay and Send", theme.ConfirmIcon(), func(){ + + // Outputs: + // -bool: Parameters exist + // -bool: Sufficient credit exist + // -error + sendMessageFunction := func()(bool, bool, error){ + + messageDaysToFund, err := durationDaysBinding.Get() + if (err != nil){ return false, false, err } + + messageDurationToFund := messageDaysToFund * 86400 + + parametersExist, sufficientCreditExist, err := myMessageQueue.AddMessageToMyMessageQueue(myIdentityHash, recipientIdentityHash, appNetworkType, messageDurationToFund, messageCommunication) + if (err != nil) { return false, false, err } + if (parametersExist == false){ + return false, false, nil + } + if (sufficientCreditExist == false){ + return true, false, nil + } + + //TODO: Check to see if their identity balance is known to be expired, here and within the send message code + + return true, true, nil + } + + parametersExist, sufficientCreditExist, err := sendMessageFunction() + if (err != nil){ + setErrorEncounteredPage(window, err, currentPage) + return + } + if (parametersExist == false){ + // Parameter permissions must have been updated and now existing parameters are invalid + // We will refresh the page + // We will show a loading screen so if this causes an infinite loop, we can detect it + setLoadingScreen(window, "Send Chat Message", "Trying to find parameters.") + time.Sleep(time.Second) + currentPage() + return + } + if (sufficientCreditExist == false){ + title := translate("Insufficient Credit") + dialogMessageA := getLabelCentered(translate("You do not have enough credit to send this message.")) + dialogMessageB := getLabelCentered(translate("Add more credit on the Add Credit page.")) + dialogContent := container.NewVBox(dialogMessageA, dialogMessageB) + dialog.ShowCustom(title, translate("Close"), dialogContent, window) + return + } + + afterMessageSentPage() + })) + + sendMessageContent := container.NewVBox(currentCreditBalanceLabel, currentBalanceRow, manageAccountCreditButton, widget.NewSeparator(), messageCostLabel, appCurrencyCostLabelCentered, widget.NewSeparator(), durationToFundText, networkDurationSelectorCentered, payAndSendButton) + + return true, sendMessageContent, nil + } + + parametersExist, sendMessageContent, err := getSendMessageContent() + if (err != nil){ + setErrorEncounteredPage(window, err, previousPage) + return + } + if (parametersExist == false){ + + //TODO: Add button to view download progress and check network connection + missingParametersLabelA := getBoldLabelCentered("Network parameters are not downloaded.") + missingParametersLabelB := getLabelCentered("Please wait for them to download.") + + page := container.NewVBox(title, backButton, widget.NewSeparator(), missingParametersLabelA, missingParametersLabelB) + + setPageContent(page, window) + return + } + + page := container.NewVBox(title, backButton, widget.NewSeparator(), sendMessageContent) + + setPageContent(page, window) +} + + +func setChatStatisticsPage(window fyne.Window, identityType string, 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(){setChatStatisticsPage(window, identityType, previousPage)} + + title := getPageTitleCentered(identityType + " Chat Statistics") + + backButton := getBackButtonCentered(previousPage) + + subtitle := getPageSubtitleCentered("My " + identityType + " Chat Statistics") + + numberOfInboxMessagesBinding := binding.NewString() + numberOfConversationsBinding := binding.NewString() + numberOfUnreadConversationsBinding := binding.NewString() + + numberOfInboxMessagesTitle := widget.NewLabel("Number Of Inbox Messages:") + numberOfInboxMessagesLabel := widget.NewLabelWithData(numberOfInboxMessagesBinding) + numberOfInboxMessagesLabel.TextStyle = getFyneTextStyle_Bold() + numberOfInboxMessagesRow := container.NewHBox(layout.NewSpacer(), numberOfInboxMessagesTitle, numberOfInboxMessagesLabel, layout.NewSpacer()) + + numberOfConversationsTitle := widget.NewLabel("Number Of Conversations:") + numberOfConversationsLabel := widget.NewLabelWithData(numberOfConversationsBinding) + numberOfConversationsLabel.TextStyle = getFyneTextStyle_Bold() + numberOfConversationsRow := container.NewHBox(layout.NewSpacer(), numberOfConversationsTitle, numberOfConversationsLabel, layout.NewSpacer()) + + numberOfUnreadConversationsTitle := widget.NewLabel("Number Of Unread Conversations:") + numberOfUnreadConversationsLabel := widget.NewLabelWithData(numberOfUnreadConversationsBinding) + numberOfUnreadConversationsLabel.TextStyle = getFyneTextStyle_Bold() + numberOfUnreadConversationsRow := container.NewHBox(layout.NewSpacer(), numberOfUnreadConversationsTitle, numberOfUnreadConversationsLabel, layout.NewSpacer()) + + appNetworkType, err := getAppNetworkType.GetAppNetworkType() + if (err != nil) { + setErrorEncounteredPage(window, err, previousPage) + return + } + + updateBindingsFunction := func(){ + + err := myChatConversations.StartUpdatingMyConversations(identityType, appNetworkType) + if (err != nil){ + setErrorEncounteredPage(window, err, previousPage) + return + } + + var resultsReadyBoolMutex sync.RWMutex + resultsReadyBool := false + + updateLoadingProgress := func(){ + + secondsElapsed := 0 + + for { + + resultsReadyBoolMutex.RLock() + resultsReady := resultsReadyBool + resultsReadyBoolMutex.RUnlock() + + if (resultsReady == true){ + return + } + + pageHasChanged := checkIfPageHasChangedFunction() + if (pageHasChanged == true){ + return + } + + if (secondsElapsed%3 == 0){ + numberOfInboxMessagesBinding.Set("Loading.") + numberOfConversationsBinding.Set("Loading.") + numberOfUnreadConversationsBinding.Set("Loading.") + } else if (secondsElapsed %3 == 1){ + numberOfInboxMessagesBinding.Set("Loading..") + numberOfConversationsBinding.Set("Loading..") + numberOfUnreadConversationsBinding.Set("Loading..") + } else { + numberOfInboxMessagesBinding.Set("Loading...") + numberOfConversationsBinding.Set("Loading...") + numberOfUnreadConversationsBinding.Set("Loading...") + } + + time.Sleep(time.Second) + secondsElapsed += 1 + } + } + + go updateLoadingProgress() + + numberOfInboxMessages, err := myChatMessages.GetNumberOfMessagesInMyInbox(identityType, appNetworkType) + if (err != nil){ + setErrorEncounteredPage(window, err, previousPage) + return + } + numberOfInboxMessagesString := helpers.ConvertIntToString(numberOfInboxMessages) + numberOfInboxMessagesBinding.Set(numberOfInboxMessagesString) + + for { + + pageHasChanged := checkIfPageHasChangedFunction() + if (pageHasChanged == true){ + return + } + + conversationsReady, numberOfConversations, err := myChatConversations.GetNumberOfConversations(identityType, appNetworkType) + if (err != nil){ + setErrorEncounteredPage(window, err, previousPage) + return + } + if (conversationsReady == false){ + continue + } + + numberOfConversationsString := helpers.ConvertIntToString(numberOfConversations) + numberOfConversationsBinding.Set(numberOfConversationsString) + + conversationsReady, numberOfUnreadConversations, err := myChatConversations.GetNumberOfUnreadConversations(identityType, appNetworkType) + if (err != nil){ + setErrorEncounteredPage(window, err, previousPage) + return + } + if (conversationsReady == false){ + continue + } + + numberOfUnreadConversationsString := helpers.ConvertIntToString(numberOfUnreadConversations) + + numberOfUnreadConversationsBinding.Set(numberOfUnreadConversationsString) + + resultsReadyBoolMutex.Lock() + resultsReadyBool = true + resultsReadyBoolMutex.Unlock() + + return + } + } + + viewChatFilterStatisticsButton := getWidgetCentered(widget.NewButtonWithIcon("View My Chat Filter Statistics", theme.VisibilityIcon(), func(){ + setViewMyChatFilterStatisticsPage(window, identityType, false, 0, 0, nil, currentPage) + })) + + page := container.NewVBox(title, backButton, widget.NewSeparator(), subtitle, widget.NewSeparator(), numberOfInboxMessagesRow, numberOfConversationsRow, numberOfUnreadConversationsRow, widget.NewSeparator(), viewChatFilterStatisticsButton) + + setPageContent(page, window) + + go updateBindingsFunction() +} + + +func setViewMyChatFilterStatisticsPage(window fyne.Window, myIdentityType string, statisticsReady bool, numberOfConversations int, numberOfFilteredConversations int, statisticsItemsList []myChatFilterStatistics.ChatFilterStatisticsItem, 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 + } + + title := getPageTitleCentered("My " + myIdentityType + " Chat Filter 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...") + + loadingTextLabel := widget.NewLabelWithData(loadingTextBinding) + loadingTextLabel.TextStyle = getFyneTextStyle_Bold() + loadingTextLabelCentered := getWidgetCentered(loadingTextLabel) + + calculateStatisticsAndRefreshPageFunction := func(){ + + var statisticsCompleteBoolMutex sync.RWMutex + statisticsCompleteBool := false + + updateLoadingBindingFunction := func(){ + + secondsElapsed := 0 + + for { + + if (secondsElapsed%3 == 0){ + loadingTextBinding.Set("Loading.") + } else if (secondsElapsed%3 == 1){ + loadingTextBinding.Set("Loading..") + } else { + loadingTextBinding.Set("Loading...") + } + + pageHasChanged := checkIfPageHasChangedFunction() + if (pageHasChanged == true){ + return + } + + statisticsCompleteBoolMutex.RLock() + statisticsComplete := statisticsCompleteBool + statisticsCompleteBoolMutex.RUnlock() + + if (statisticsComplete == true){ + return + } + + time.Sleep(time.Second) + secondsElapsed += 1 + } + } + + go updateLoadingBindingFunction() + + numberOfConversations, numberOfFilteredConversations, myChatFilterStatisticsItemsList, err := myChatFilterStatistics.GetAllMyChatFilterStatistics(myIdentityType, appNetworkType) + if (err != nil) { + + statisticsCompleteBoolMutex.Lock() + statisticsCompleteBool = true + statisticsCompleteBoolMutex.Unlock() + + pageHasChanged := checkIfPageHasChangedFunction() + if (pageHasChanged == false){ + setErrorEncounteredPage(window, err, previousPage) + } + return + } + + statisticsCompleteBoolMutex.Lock() + statisticsCompleteBool = true + statisticsCompleteBoolMutex.Unlock() + + pageHasChanged := checkIfPageHasChangedFunction() + if (pageHasChanged == false){ + setViewMyChatFilterStatisticsPage(window, myIdentityType, true, numberOfConversations, numberOfFilteredConversations, myChatFilterStatisticsItemsList, previousPage) + } + } + + page := container.NewVBox(title, backButton, widget.NewSeparator(), loadingTextLabelCentered) + + setPageContent(page, window) + + go calculateStatisticsAndRefreshPageFunction() + + return + } + + myEnabledChatFiltersList, err := myChatFilters.GetAllMyEnabledChatFiltersList(myIdentityType) + if (err != nil) { + setErrorEncounteredPage(window, err, previousPage) + return + } + + numberOfConversationsString := helpers.ConvertIntToString(numberOfConversations) + numberOfFilteredConversationsString := helpers.ConvertIntToString(numberOfFilteredConversations) + + if (len(myEnabledChatFiltersList) == 0){ + + noFiltersEnabledDescriptionA := getBoldLabelCentered("You have not enabled any chat filters.") + noFiltersEnabledDescriptionB := getLabelCentered("All " + numberOfConversationsString + " of your conversations will be shown.") + + page := container.NewVBox(title, backButton, widget.NewSeparator(), noFiltersEnabledDescriptionA, noFiltersEnabledDescriptionB) + + setPageContent(page, window) + return + } + + description1 := getLabelCentered("Below are the percentages of conversations that pass your chat filters.") + description2 := getLabelCentered("Filtered Conversations divides by the number of conversations you would see without the filter.") + + getStatisticsDisplayContainer := func()(*fyne.Container, error){ + + statisticsDisplayContainer := container.NewVBox() + + for _, chatFilterStatisticsItem := range statisticsItemsList{ + + filterName := chatFilterStatisticsItem.FilterName + numberOfRecipientsWhoPassFilter := chatFilterStatisticsItem.NumberOfRecipientsWhoPassFilter + percentageOfRecipientsWhoPassFilter := chatFilterStatisticsItem.PercentageOfRecipientsWhoPassFilter + numberOfFilterExcludedRecipients := chatFilterStatisticsItem.NumberOfFilterExcludedRecipients + percentageOfFilterExcludedRecipientsWhoPassFilter := chatFilterStatisticsItem.PercentageOfFilterExcludedRecipientsWhoPassFilter + + filterIsEnabled := slices.Contains(myEnabledChatFiltersList, filterName) + if (filterIsEnabled == false){ + // If the filter is not enabled, all conversations will pass the filter. + // We will not show the filter. + continue + } + + getFilterTitle := func()string{ + if (filterName == "ShowMyContactsOnly"){ + return "Only show conversations with my contacts." + } + if (filterName == "ShowMyMatchesOnly"){ + return "Only show conversations with my matches." + } + if (filterName == "ShowHasMessagedMeOnly"){ + return "Only show conversations with users who have messaged me." + } + if (filterName == "OnlyShowLikedUsers"){ + return "Only show conversations with users I have liked." + } + if (filterName == "HideIgnoredUsers"){ + return "Hide conversations with users I have ignored." + } + return filterName + } + + filterTitle := getFilterTitle() + + filterTitleLabel := getItalicLabelCentered(filterTitle) + + allConversationsLabel := getBoldLabel("All Conversations") + filteredConversationsLabel := getBoldLabel("Filtered Conversations") + + numberOfRecipientsWhoPassFilterString := helpers.ConvertIntToString(numberOfRecipientsWhoPassFilter) + percentageOfRecipientsWhoPassFilterString := helpers.ConvertFloat64ToStringRounded(percentageOfRecipientsWhoPassFilter, 2) + numberOfFilterExcludedRecipientsString := helpers.ConvertIntToString(numberOfFilterExcludedRecipients) + percentageOfFilterExcludedRecipientsWhoPassFilterString := helpers.ConvertFloat64ToStringRounded(percentageOfFilterExcludedRecipientsWhoPassFilter, 2) + + allConversationsPercentageLabel := getLabelCentered(numberOfRecipientsWhoPassFilterString + "/" + numberOfConversationsString + " = " + percentageOfRecipientsWhoPassFilterString + "%") + + filteredConversationsPercentageLabel := getLabelCentered(numberOfFilteredConversationsString + "/" + numberOfFilterExcludedRecipientsString + " = " + percentageOfFilterExcludedRecipientsWhoPassFilterString + "%") + + filterStatisticsDisplayGrid := getContainerCentered(container.NewGridWithColumns(2, allConversationsLabel, filteredConversationsLabel, allConversationsPercentageLabel, filteredConversationsPercentageLabel)) + + statisticsDisplayContainer.Add(filterTitleLabel) + statisticsDisplayContainer.Add(filterStatisticsDisplayGrid) + statisticsDisplayContainer.Add(widget.NewSeparator()) + } + + return statisticsDisplayContainer, nil + } + + statisticsDisplayContainer, err := getStatisticsDisplayContainer() + if (err != nil){ + setErrorEncounteredPage(window, err, previousPage) + return + } + + statisticsDisplayContainerCentered := statisticsDisplayContainer + + page := container.NewVBox(title, backButton, widget.NewSeparator(), description1, description2, widget.NewSeparator(), statisticsDisplayContainerCentered) + setPageContent(page, window) +} + + diff --git a/gui/contactsGui.go b/gui/contactsGui.go new file mode 100644 index 0000000..654ac85 --- /dev/null +++ b/gui/contactsGui.go @@ -0,0 +1,1089 @@ +package gui + +// contactsGui.go implements pages to view and manage a user's contacts + +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/dialog" +import "fyne.io/fyne/v2/data/binding" +import "fyne.io/fyne/v2/layout" +import "fyne.io/fyne/v2/canvas" + +import "seekia/internal/appMemory" +import "seekia/internal/encoding" +import "seekia/internal/helpers" +import "seekia/internal/identity" +import "seekia/internal/imagery" +import "seekia/internal/myContacts" +import "seekia/internal/myIdentity" +import "seekia/internal/mySettings" +import "seekia/internal/network/appNetworkType/getAppNetworkType" +import "seekia/internal/profiles/viewableProfiles" + +import "image" +import "strings" +import "slices" +import "errors" + + +func setMyContactsPage(window fyne.Window, identityType string, previousPage func()){ + + appMemory.SetMemoryEntry("CurrentViewedPage", "MyContacts") + + if (identityType != "Mate" && identityType != "Moderator" && identityType != "Host"){ + setErrorEncounteredPage(window, errors.New("setMyContactsPage called with invalid identityType: " + identityType), previousPage) + return + } + + currentPage := func(){ setMyContactsPage(window, identityType, previousPage) } + + title := getPageTitleCentered("My " + identityType + " Contacts") + + backButton := getBackButtonCentered(previousPage) + + addContactButton := getWidgetCentered(widget.NewButtonWithIcon("Add Contact", theme.ContentAddIcon(), func(){ + setAddContactPage(window, currentPage, currentPage) + })) + + createACategoryButton := widget.NewButtonWithIcon("Create A Category", theme.ContentAddIcon(), func(){ + setCreateAContactCategoryPage(window, identityType, currentPage, currentPage) + }) + + deleteACategoryButton := widget.NewButtonWithIcon("Delete A Category", theme.DeleteIcon(), func(){ + setDeleteACategoryPage(window, identityType, currentPage, currentPage) + }) + + buttonsRow := container.NewHBox(layout.NewSpacer(), addContactButton, createACategoryButton, deleteACategoryButton, layout.NewSpacer()) + + //TODO: Add change identity type ability? + + myContactsMapList, err := myContacts.GetMyContactsMapList(identityType) + if (err != nil){ + setErrorEncounteredPage(window, err, previousPage) + return + } + + if (len(myContactsMapList) == 0){ + + noContactsExistLabel := getBoldLabelCentered("No " + identityType + " contacts exist.") + + page := container.NewVBox(title, backButton, widget.NewSeparator(), addContactButton, widget.NewSeparator(), noContactsExistLabel) + + setPageContent(page, window) + return + } + + allContactCategoriesList, err := myContacts.GetAllMyContactCategories(identityType) + if (err != nil){ + setErrorEncounteredPage(window, err, previousPage) + return + } + + helpers.SortStringListToUnicodeOrder(allContactCategoriesList) + + allCategoryOptionsList := []string{"All Contacts"} + allCategoryOptionsList = append(allCategoryOptionsList, allContactCategoriesList...) + allCategoryOptionsList = append(allCategoryOptionsList, "No Category") + + getCurrentCategory := func()(string, error){ + + exists, currentCategory, err := mySettings.GetSetting("My" + identityType + "ContactsPageViewedCategory") + if (err != nil) { return "", err } + if (exists == false){ + return "All Contacts", nil + } + + isACategory := slices.Contains(allCategoryOptionsList, currentCategory) + if (isACategory == false){ + // Category must have been deleted + return "All Contacts", nil + } + + return currentCategory, nil + } + + currentCategory, err := getCurrentCategory() + if (err != nil){ + setErrorEncounteredPage(window, err, previousPage) + return + } + + categoryLabel := getBoldLabelCentered("Category:") + + categorySelector := widget.NewSelect(allCategoryOptionsList, func(newCategory string){ + + err := mySettings.SetSetting("My" + identityType + "ContactsPageViewedCategory", newCategory) + if (err != nil){ + setErrorEncounteredPage(window, err, currentPage) + return + } + currentPage() + }) + + categorySelector.Selected = currentCategory + + selectCategoryRow := container.NewHBox(layout.NewSpacer(), categoryLabel, categorySelector, layout.NewSpacer()) + + getContactsContainer := func()(*fyne.Container, error){ + + appNetworkType, err := getAppNetworkType.GetAppNetworkType() + if (err != nil) { return nil, err } + + anyContactsFound := false + + contactsContainer := container.NewVBox() + + for _, contactMap := range myContactsMapList{ + + checkIfContactBelongsToCurrentCategory := func()(bool, error){ + + if (currentCategory == "All Contacts"){ + return true, nil + } + + contactCategoriesBase64ListString, exists := contactMap["Categories"] + if (exists == false){ + if (currentCategory == "No Category"){ + return true, nil + } + return false, nil + } + + contactCategoriesBase64List := strings.Split(contactCategoriesBase64ListString, "+") + + for _, categoryNameBase64 := range contactCategoriesBase64List{ + + categoryNameString, err := encoding.DecodeBase64StringToUnicodeString(categoryNameBase64) + if (err != nil) { + return false, errors.New("Malformed MyContacts map list: Category name not Base64: " + categoryNameBase64) + } + + if (categoryNameString == currentCategory){ + return true, nil + } + } + + return false, nil + } + + contactBelongsToCurrentCategory, err := checkIfContactBelongsToCurrentCategory() + if (err != nil) { return nil, err } + if (contactBelongsToCurrentCategory == false){ + continue + } + + anyContactsFound = true + + contactIdentityHashString, exists := contactMap["IdentityHash"] + if (exists == false) { + return nil, errors.New("Contact map malformed: Missing IdentityHash") + } + + contactIdentityHash, _, err := identity.ReadIdentityHashString(contactIdentityHashString) + if (err != nil){ + return nil, errors.New("Contact map malformed: contains invalid IdentityHash: " + contactIdentityHashString) + } + + contactName, exists := contactMap["Name"] + if (exists == false) { + return nil, errors.New("Contact map malformed: Missing Name") + } + + getContactAvatarOrImage := func()(image.Image, error){ + + getAllowUnknownViewableStatusBool := func()bool{ + + if (identityType == "Mate"){ + return false + } + return true + } + + allowUnknownViewableStatusBool := getAllowUnknownViewableStatusBool() + + profileFound, _, retrieveAnyUserProfileAttributeFunction, err := viewableProfiles.GetRetrieveAnyNewestViewableUserProfileAttributeFunction(contactIdentityHash, appNetworkType, true, allowUnknownViewableStatusBool, true) + if (err != nil) { return nil, err } + if (profileFound == false){ + + emojiImageObject, err := getEmojiImageObject(2929) + if (err != nil) { return nil, err } + + return emojiImageObject, nil + } + + attributeExists, _, photosAttributeValue, err := retrieveAnyUserProfileAttributeFunction("Photos") + if (err != nil) { return nil, err } + if (attributeExists == true){ + + base64PhotosList := strings.Split(photosAttributeValue, "+") + firstPhotoBase64 := base64PhotosList[0] + + userImageObject, err := imagery.ConvertWebpBase64StringToImageObject(firstPhotoBase64) + if (err != nil) { + return nil, errors.New("Database corrupt: Contains profile with invalid photos attribute.") + } + + return userImageObject, nil + } + + getContactEmojiIdentifier := func()(int, error){ + + attributeExists, _, avatarAttributeValue, err := retrieveAnyUserProfileAttributeFunction("Avatar") + if (err != nil) { return 0, err } + if (attributeExists == false){ + return 2929, nil + } + + userEmojiIdentifier, err := helpers.ConvertStringToInt(avatarAttributeValue) + if (err != nil) { + return 0, errors.New("Database corrupt: Contains profile with invalid Avatar attribute: " + avatarAttributeValue) + } + + return userEmojiIdentifier, nil + } + + contactEmojiIdentifier, err := getContactEmojiIdentifier() + if (err != nil) { return nil, err } + + emojiImageObject, err := getEmojiImageObject(contactEmojiIdentifier) + if (err != nil) { return nil, err } + + return emojiImageObject, nil + } + + contactImageObject, err := getContactAvatarOrImage() + if (err != nil) { return nil, err } + + contactFyneImage := canvas.NewImageFromImage(contactImageObject) + contactFyneImage.FillMode = canvas.ImageFillContain + imageSize := getCustomFyneSize(10) + contactFyneImage.SetMinSize(imageSize) + imageBoxed := getFyneImageBoxed(contactFyneImage) + + trimmedIdentityHash, _, err := helpers.TrimAndFlattenString(contactIdentityHashString, 20) + if (err != nil) { return nil, err } + contactIdentityHashLabel := widget.NewLabel(trimmedIdentityHash) + contactNameLabel := getBoldLabel(contactName) + + contactNameIdentityHashColumn := getContainerBoxed(container.NewVBox(contactNameLabel, contactIdentityHashLabel)) + + viewProfileButton := widget.NewButtonWithIcon("Profile", theme.VisibilityIcon(), func(){ + setViewPeerProfilePageFromIdentityHash(window, contactIdentityHash, currentPage) + }) + chatButton := widget.NewButtonWithIcon("Chat", theme.MailComposeIcon(), func(){ + + myIdentityExists, myIdentityHash, err := myIdentity.GetMyIdentityHash(identityType) + if (err != nil){ + setErrorEncounteredPage(window, err, currentPage) + return + } + if (myIdentityExists == true && contactIdentityHash == myIdentityHash){ + dialogTitle := translate("Identity Hash Is Self.") + dialogMessageA := getLabelCentered("This contact is your own identity.") + dialogMessageB := getLabelCentered("You cannot chat with yourself.") + dialogContent := container.NewVBox(dialogMessageA, dialogMessageB) + dialog.ShowCustom(dialogTitle, translate("Close"), dialogContent, window) + return + } + + setViewAConversationPage(window, contactIdentityHash, true, currentPage) + }) + + manageButton := widget.NewButtonWithIcon("Manage", theme.DocumentCreateIcon(), func(){ + setManageContactPage(window, contactIdentityHash, currentPage) + }) + + row := container.NewHBox(imageBoxed, contactNameIdentityHashColumn, viewProfileButton, chatButton, manageButton) + rowCentered := getContainerCentered(row) + + contactRow := getContainerBoxed(rowCentered) + + contactsContainer.Add(contactRow) + } + + if (anyContactsFound == false){ + + description1 := getBoldLabelCentered("No contacts belong to this category.") + + categoryNameLabel := getLabelCentered("Category Name:") + + currentCategoryLabel := getBoldLabelCentered(currentCategory) + + categoryNameRow := container.NewHBox(layout.NewSpacer(), categoryNameLabel, currentCategoryLabel, layout.NewSpacer()) + + noContactsExistContainer := container.NewVBox(description1, categoryNameRow) + + return noContactsExistContainer, nil + } + + return contactsContainer, nil + } + + contactsContainer, err := getContactsContainer() + if (err != nil){ + setErrorEncounteredPage(window, err, previousPage) + return + } + + page := container.NewVBox(title, backButton, widget.NewSeparator(), buttonsRow, widget.NewSeparator(), selectCategoryRow, widget.NewSeparator(), contactsContainer) + + setPageContent(page, window) +} + + +func setAddContactPage(window fyne.Window, previousPage func(), nextPage func()){ + + currentPage := func(){setAddContactPage(window, previousPage, nextPage)} + + title := getPageTitleCentered("Add Contact") + + backButton := getBackButtonCentered(previousPage) + + description := getLabelCentered("Enter the identity hash of your new contact below.") + + identityHashEntry := widget.NewEntry() + identityHashEntry.SetPlaceHolder("Enter Identity Hash.") + identityHashEntryBoxed := getWidgetBoxed(identityHashEntry) + + identityHashEntryWithDescriptionGrid := getContainerCentered(container.NewGridWithColumns(1, description, identityHashEntryBoxed)) + + nextButton := getWidgetCentered(widget.NewButtonWithIcon("Next", theme.NavigateNextIcon(), func(){ + + userIdentityHashString := identityHashEntry.Text + + userIdentityHash, userIdentityType, err := identity.ReadIdentityHashString(userIdentityHashString) + if (err != nil){ + dialogTitle := translate("Invalid Identity Hash") + dialogMessageA := getLabelCentered("The identity hash you entered is invalid.") + dialogContent := container.NewVBox(dialogMessageA) + dialog.ShowCustom(dialogTitle, translate("Close"), dialogContent, window) + return + } + + myIdentityExists, myIdentityHash, err := myIdentity.GetMyIdentityHash(userIdentityType) + if (err != nil){ + setErrorEncounteredPage(window, err, currentPage) + return + } + if (myIdentityExists == true && myIdentityHash == userIdentityHash){ + dialogTitle := translate("Identity Hash Is Self") + dialogMessageA := getLabelCentered("The identity hash you entered is your own.") + dialogMessageB := getLabelCentered("You cannot add yourself as a contact.") + dialogContent := container.NewVBox(dialogMessageA, dialogMessageB) + dialog.ShowCustom(dialogTitle, translate("Close"), dialogContent, window) + return + } + + setAddContactFromIdentityHashPage(window, userIdentityHash, currentPage, nextPage) + })) + + page := container.NewVBox(title, backButton, widget.NewSeparator(), identityHashEntryWithDescriptionGrid, nextButton) + + setPageContent(page, window) +} + +func setAddContactFromIdentityHashPage(window fyne.Window, userIdentityHash [16]byte, previousPage func(), nextPage func()){ + + userIdentityType, err := identity.GetIdentityTypeFromIdentityHash(userIdentityHash) + if (err != nil) { + userIdentityHashHex := encoding.EncodeBytesToHexString(userIdentityHash[:]) + setErrorEncounteredPage(window, errors.New("setAddContactFromIdentityHashPage called with invalid identity hash: " + userIdentityHashHex), previousPage) + return + } + + currentPage := func(){setAddContactFromIdentityHashPage(window, userIdentityHash, previousPage, nextPage)} + + title := getPageTitleCentered("Add Contact") + + backButton := getBackButtonCentered(previousPage) + + isMyContact, err := myContacts.CheckIfUserIsMyContact(userIdentityHash) + if (err != nil){ + setErrorEncounteredPage(window, err, previousPage) + return + } + if (isMyContact == true){ + + description1 := getBoldLabelCentered("Cannot add contact.") + description2 := getLabelCentered("User is already a contact.") + + page := container.NewVBox(title, backButton, widget.NewSeparator(), description1, description2) + setPageContent(page, window) + return + } + + enterNameLabel := getBoldLabelCentered("Enter contact name:") + + nameEntry := widget.NewEntry() + nameEntry.SetPlaceHolder("Enter contact name...") + nameEntryBoxed := getWidgetBoxed(nameEntry) + + wideText := " " + nameEntryWidenerA := widget.NewLabel(wideText) + nameEntryWidenerB := widget.NewLabel(wideText) + + enterNameEntryWidened := getContainerCentered(container.NewGridWithColumns(3, nameEntryWidenerA, nameEntryBoxed, nameEntryWidenerB)) + + enterDescriptionLabel := getBoldLabelCentered("Enter contact description:") + + descriptionEntry := widget.NewMultiLineEntry() + descriptionEntry.Wrapping = 3 + descriptionEntry.SetPlaceHolder("Enter description....") + descriptionEntryBoxed := getWidgetBoxed(descriptionEntry) + + descriptionEntryWidenerA := widget.NewLabel(wideText) + descriptionEntryWidenerB := widget.NewLabel(wideText) + + enterDescriptionEntryWidened := getContainerCentered(container.NewGridWithColumns(3, descriptionEntryWidenerA, descriptionEntryBoxed, descriptionEntryWidenerB)) + + page := container.NewVBox(title, backButton, widget.NewSeparator(), enterNameLabel, enterNameEntryWidened, widget.NewSeparator(), enterDescriptionLabel, enterDescriptionEntryWidened, widget.NewSeparator()) + + allContactCategories, err := myContacts.GetAllMyContactCategories(userIdentityType) + if (err != nil){ + setErrorEncounteredPage(window, err, previousPage) + return + } + + contactCategoriesListBinding := binding.NewStringList() + + if (len(allContactCategories) != 0){ + + selectCategoriesLabel := getBoldLabelCentered("Select categories for the contact:") + + getNumberOfGridColumns := func()int{ + numberOfCategories := len(allContactCategories) + if (numberOfCategories <= 2){ + return numberOfCategories + } + return 3 + } + + numberOfGridColumns := getNumberOfGridColumns() + + categoriesGrid := container.NewGridWithColumns(numberOfGridColumns) + + for index, categoryName := range allContactCategories{ + + categoryNameTrimmed, _, err := helpers.TrimAndFlattenString(categoryName, 7) + if (err != nil){ + setErrorEncounteredPage(window, err, previousPage) + return + } + + onCheckedFunction := func(isChecked bool){ + currentContactCategoriesList, err := contactCategoriesListBinding.Get() + if (err != nil){ + setErrorEncounteredPage(window, err, previousPage) + return + } + if (isChecked == true){ + newContactCategoriesList := helpers.AddItemToStringListAndAvoidDuplicate(currentContactCategoriesList, categoryName) + contactCategoriesListBinding.Set(newContactCategoriesList) + } else { + newContactCategoriesList, _ := helpers.DeleteAllMatchingItemsFromStringList(currentContactCategoriesList, categoryName) + contactCategoriesListBinding.Set(newContactCategoriesList) + } + } + + categoryCheck := widget.NewCheck(categoryNameTrimmed, onCheckedFunction) + categoryCheckBoxed := getWidgetBoxed(categoryCheck) + categoriesGrid.Add(categoryCheckBoxed) + + if (index > 15){ + break + } + } + + categoriesGridBoxed := getContainerCentered(getContainerBoxed(categoriesGrid)) + + page.Add(selectCategoriesLabel) + page.Add(categoriesGridBoxed) + } + + addContactButton := getWidgetCentered(widget.NewButtonWithIcon(translate("Add Contact"), theme.ConfirmIcon(), func(){ + + newContactName := nameEntry.Text + if (newContactName == ""){ + + dialogTitle := translate("Name Is Empty") + dialogMessageA := getLabelCentered(translate("You have not entered a contact name.")) + dialogMessageB := getLabelCentered(translate("Enter a name and retry.")) + dialogContent := container.NewVBox(dialogMessageA, dialogMessageB) + dialog.ShowCustom(dialogTitle, translate("Close"), dialogContent, window) + return + } + + newContactDescription := descriptionEntry.Text + + contactCategoriesList, err := contactCategoriesListBinding.Get() + if (err != nil) { + setErrorEncounteredPage(window, err, currentPage) + return + } + + alreadyExists, err := myContacts.AddContact(userIdentityHash, newContactName, contactCategoriesList, newContactDescription) + if (err != nil) { + setErrorEncounteredPage(window, err, currentPage) + return + } + if (alreadyExists == true){ + setErrorEncounteredPage(window, errors.New("Contact already exists after checking already."), currentPage) + return + } + + nextPage() + })) + + page.Add(addContactButton) + + setPageContent(page, window) +} + + +func setManageContactPage(window fyne.Window, contactIdentityHash [16]byte, previousPage func()){ + + currentPage := func(){setManageContactPage(window, contactIdentityHash, previousPage)} + + title := getPageTitleCentered("Manage Contact") + + backButton := getBackButtonCentered(previousPage) + + contactExists, contactName, contactAddedTime, contactCategoriesList, contactDescription, err := myContacts.GetMyContactDetails(contactIdentityHash) + if (err != nil) { + setErrorEncounteredPage(window, err, previousPage) + return + } + if (contactExists == false){ + setErrorEncounteredPage(window, errors.New("setManageContactPage called with missing contact."), previousPage) + return + } + + contactIdentityHashString, _, err := identity.EncodeIdentityHashBytesToString(contactIdentityHash) + if (err != nil){ + contactIdentityHashHex := encoding.EncodeBytesToHexString(contactIdentityHash[:]) + setErrorEncounteredPage(window, errors.New("GetMyContactDetails not verifying identity hash: " + contactIdentityHashHex), previousPage) + return + } + + identityHashLabel := widget.NewLabel("Identity Hash:") + identityHashText := getBoldLabel(contactIdentityHashString) + viewIdentityHashButton := widget.NewButtonWithIcon("", theme.VisibilityIcon(), func(){ + setViewIdentityHashPage(window, contactIdentityHash, currentPage) + }) + contactIdentityHashRow := container.NewHBox(layout.NewSpacer(), identityHashLabel, identityHashText, viewIdentityHashButton, layout.NewSpacer()) + + nameLabel := widget.NewLabel("Name:") + contactNameLabel := getBoldLabel(contactName) + contactNameRow := container.NewHBox(layout.NewSpacer(), nameLabel, contactNameLabel, layout.NewSpacer()) + + contactCategoriesLabel := getLabelCentered("Categories:") + + getContactCategoriesListString := func()(string, error){ + + if (len(contactCategoriesList) == 0){ + return "None", nil + } + + contactCategoriesListString := strings.Join(contactCategoriesList, ", ") + + contactCategoriesListString, _, err := helpers.TrimAndFlattenString(contactCategoriesListString, 20) + if (err != nil) { return "", err } + + return contactCategoriesListString, nil + } + + contactCategoriesListString, err := getContactCategoriesListString() + if (err != nil) { + setErrorEncounteredPage(window, err, previousPage) + return + } + + contactCategoriesListLabel := getBoldLabel(contactCategoriesListString) + contactCategoriesRow := container.NewHBox(layout.NewSpacer(), contactCategoriesLabel, contactCategoriesListLabel, layout.NewSpacer()) + + addedAgoString, err := helpers.ConvertUnixTimeToTimeAgoTranslated(contactAddedTime, false) + addedTimeAgoLabel := getItalicLabelCentered("Contact added " + addedAgoString + ".") + + getContactDescriptionRow := func()(*fyne.Container, error){ + + descriptionLabel := widget.NewLabel("Description:") + + if (contactDescription == ""){ + noneLabel := getBoldLabel("None") + descriptionRow := container.NewHBox(layout.NewSpacer(), descriptionLabel, noneLabel, layout.NewSpacer()) + return descriptionRow, nil + } + + trimmedDescription, changesOccurred, err := helpers.TrimAndFlattenString(contactDescription, 20) + if (err != nil) { return nil, err } + if (changesOccurred == false){ + contactDescriptionLabel := getBoldLabel(contactDescription) + descriptionRow := container.NewHBox(layout.NewSpacer(), descriptionLabel, contactDescriptionLabel, layout.NewSpacer()) + return descriptionRow, nil + } + + contactDescriptionTextLabel := getBoldLabel(trimmedDescription) + viewFullDescriptionButton := widget.NewButtonWithIcon("", theme.VisibilityIcon(), func(){ + setViewTextPage(window, "Viewing Contact Description", contactDescription, false, currentPage) + }) + contactDescriptionRow := container.NewHBox(layout.NewSpacer(), descriptionLabel, contactDescriptionTextLabel, viewFullDescriptionButton, layout.NewSpacer()) + + return contactDescriptionRow, nil + } + + contactDescriptionRow, err := getContactDescriptionRow() + if (err != nil){ + setErrorEncounteredPage(window, err, previousPage) + return + } + + renameButton := widget.NewButtonWithIcon("Edit Name", theme.DocumentCreateIcon(), func(){ + setRenameContactPage(window, contactIdentityHash, currentPage, currentPage) + }) + editCategoriesButton := widget.NewButtonWithIcon("Edit Categories", theme.ListIcon(), func(){ + setEditContactCategoriesPage(window, contactIdentityHash, currentPage, currentPage) + }) + editDescriptionButton := widget.NewButtonWithIcon("Edit Description", theme.DocumentCreateIcon(), func(){ + setEditContactDescriptionPage(window, contactIdentityHash, currentPage, currentPage) + }) + deleteButton := widget.NewButtonWithIcon("Delete", theme.DeleteIcon(), func(){ + setConfirmDeleteContactPage(window, contactIdentityHash, currentPage, previousPage) + }) + + buttonsGrid := getContainerCentered(container.NewGridWithColumns(1, renameButton, editCategoriesButton, editDescriptionButton, deleteButton)) + + page := container.NewVBox(title, backButton, widget.NewSeparator(), contactIdentityHashRow, contactNameRow, contactCategoriesRow, contactDescriptionRow, addedTimeAgoLabel, widget.NewSeparator(), buttonsGrid) + + setPageContent(page, window) +} + +func setRenameContactPage(window fyne.Window, contactIdentityHash [16]byte, previousPage func(), nextPage func()){ + + title := getPageTitleCentered("Rename Contact") + + backButton := getBackButtonCentered(previousPage) + + contactExists, contactName, _, currentContactCategoriesList, contactDescription, err := myContacts.GetMyContactDetails(contactIdentityHash) + if (err != nil) { + setErrorEncounteredPage(window, err, previousPage) + return + } + if (contactExists == false){ + setErrorEncounteredPage(window, errors.New("Trying to rename contact that does not exist."), previousPage) + return + } + + currentNameLabelA := getLabelCentered("Current Contact Name:") + currentNameLabelB := getBoldLabelCentered(contactName) + + enterNameLabel := getLabelCentered("Enter new name:") + + enterNameEntry := widget.NewEntry() + enterNameEntry.SetPlaceHolder("Enter new name.") + enterNameEntryBoxed := getWidgetBoxed(enterNameEntry) + + changeNameButton := getWidgetCentered(widget.NewButtonWithIcon("Change Name", theme.ConfirmIcon(), func(){ + + newName := enterNameEntry.Text + + if (newName == ""){ + + dialogTitle := translate("Name Is Empty") + dialogMessageA := getLabelCentered("You must enter a new name.") + dialogContent := container.NewVBox(dialogMessageA) + dialog.ShowCustom(dialogTitle, translate("Close"), dialogContent, window) + return + } + + err := myContacts.EditContact(contactIdentityHash, newName, currentContactCategoriesList, contactDescription) + if (err != nil) { + setErrorEncounteredPage(window, err, previousPage) + return + } + nextPage() + })) + + widener := widget.NewLabel(" ") + + enterNameEntryWithButton := getContainerCentered(container.NewGridWithColumns(1, enterNameEntryBoxed, changeNameButton, widener)) + + page := container.NewVBox(title, backButton, widget.NewSeparator(), currentNameLabelA, currentNameLabelB, widget.NewSeparator(), enterNameLabel, enterNameEntryWithButton) + + setPageContent(page, window) +} + + +func setEditContactCategoriesPage(window fyne.Window, contactIdentityHash [16]byte, previousPage func(), nextPage func()){ + + currentPage := func(){setEditContactCategoriesPage(window, contactIdentityHash, previousPage, nextPage)} + + contactIdentityType, err := identity.GetIdentityTypeFromIdentityHash(contactIdentityHash) + if (err != nil){ + setErrorEncounteredPage(window, err, previousPage) + return + } + + contactExists, contactName, _, currentContactCategoriesList, contactDescription, err := myContacts.GetMyContactDetails(contactIdentityHash) + if (err != nil) { + setErrorEncounteredPage(window, err, previousPage) + return + } + if (contactExists == false){ + setErrorEncounteredPage(window, errors.New("Trying to rename contact that does not exist."), previousPage) + return + } + + allMyContactCategoriesList, err := myContacts.GetAllMyContactCategories(contactIdentityType) + if (err != nil){ + setErrorEncounteredPage(window, err, previousPage) + return + } + + title := getPageTitleCentered("Edit Contact Categories") + + backButton := getBackButtonCentered(previousPage) + + createACategoryButton := getWidgetCentered(widget.NewButtonWithIcon("Create A Category", theme.ContentAddIcon(), func(){ + setCreateAContactCategoryPage(window, contactIdentityType, currentPage, currentPage) + })) + + if (len(allMyContactCategoriesList) == 0){ + + noCategoriesExistLabel := getBoldLabelCentered("No categories exist.") + + page := container.NewVBox(title, backButton, widget.NewSeparator(), noCategoriesExistLabel, createACategoryButton) + setPageContent(page, window) + return + } + + description1 := getLabelCentered("Select the categories the contact should be a member of.") + + getNumberOfColumns := func()int{ + if (len(allMyContactCategoriesList) == 1){ + return 1 + } + return 2 + } + + numberOfColumns := getNumberOfColumns() + + categoryChecksGrid := container.NewGridWithColumns(numberOfColumns) + + // We sort the categories so they show up in the same order every time. + + helpers.SortStringListToUnicodeOrder(allMyContactCategoriesList) + + for _, categoryName := range allMyContactCategoriesList{ + + handleCheckFunction := func(response bool){ + + getNewCategoriesList := func()[]string{ + + if (response == true){ + newCategoriesList := append(currentContactCategoriesList, categoryName) + return newCategoriesList + } + + newCategoriesList, _ := helpers.DeleteAllMatchingItemsFromStringList(currentContactCategoriesList, categoryName) + return newCategoriesList + } + + newCategoriesList := getNewCategoriesList() + + err := myContacts.EditContact(contactIdentityHash, contactName, newCategoriesList, contactDescription) + if (err != nil){ + setErrorEncounteredPage(window, err, previousPage) + return + } + + currentPage() + } + + categoryCheck := widget.NewCheck(categoryName, handleCheckFunction) + + contactIsInCategory := slices.Contains(currentContactCategoriesList, categoryName) + if (contactIsInCategory == true){ + categoryCheck.Checked = true + } + + categoryCheckBoxed := getWidgetBoxed(categoryCheck) + + categoryChecksGrid.Add(categoryCheckBoxed) + } + + categoryChecksGridCentered := getContainerCentered(categoryChecksGrid) + + page := container.NewVBox(title, backButton, widget.NewSeparator(), description1, widget.NewSeparator(), createACategoryButton, widget.NewSeparator(), categoryChecksGridCentered) + + setPageContent(page, window) +} + +func setEditContactDescriptionPage(window fyne.Window, contactIdentityHash [16]byte, previousPage func(), nextPage func()){ + + currentPage := func(){setEditContactDescriptionPage(window, contactIdentityHash, previousPage, nextPage)} + + title := getPageTitleCentered("Edit Contact Description") + + backButton := getBackButtonCentered(previousPage) + + contactExists, contactName, _, contactCategoriesList, currentContactDescription, err := myContacts.GetMyContactDetails(contactIdentityHash) + if (err != nil) { + setErrorEncounteredPage(window, err, previousPage) + return + } + if (contactExists == false){ + setErrorEncounteredPage(window, errors.New("Trying to rename contact that does not exist."), previousPage) + return + } + + editDescriptionDescription := getLabelCentered("Edit your contact description below.") + + enterDescriptionEntry := widget.NewMultiLineEntry() + enterDescriptionEntry.Wrapping = 3 + if (currentContactDescription == ""){ + enterDescriptionEntry.SetPlaceHolder("Enter description.") + } else { + enterDescriptionEntry.SetText(currentContactDescription) + } + enterDescriptionEntryBoxed := getWidgetBoxed(enterDescriptionEntry) + + entryHeightenerA := container.NewVBox(widget.NewLabel(""), widget.NewLabel(""), widget.NewLabel(""), widget.NewLabel(""), widget.NewLabel("")) + + entryHeightenerB := container.NewVBox(widget.NewLabel(""), widget.NewLabel(""), widget.NewLabel(""), widget.NewLabel(""), widget.NewLabel("")) + + changeDescriptionButton := getWidgetCentered(widget.NewButtonWithIcon("Change Description", theme.ConfirmIcon(), func(){ + + newContactDescription := enterDescriptionEntry.Text + + err := myContacts.EditContact(contactIdentityHash, contactName, contactCategoriesList, newContactDescription) + if (err != nil) { + setErrorEncounteredPage(window, err, currentPage) + return + } + nextPage() + })) + + enterDescriptionEntryWithButton := container.NewBorder(nil, changeDescriptionButton, entryHeightenerA, entryHeightenerB, enterDescriptionEntryBoxed) + + page := container.NewVBox(title, backButton, widget.NewSeparator(), editDescriptionDescription, enterDescriptionEntryWithButton) + + setPageContent(page, window) +} + +func setConfirmDeleteContactPage(window fyne.Window, contactIdentityHash [16]byte, previousPage func(), nextPage func()){ + + currentPage := func(){setConfirmDeleteContactPage(window, contactIdentityHash, previousPage, nextPage)} + + contactExists, contactName, contactAddedTime, _, contactDescription, err := myContacts.GetMyContactDetails(contactIdentityHash) + if (err != nil) { + setErrorEncounteredPage(window, err, previousPage) + return + } + if (contactExists == false){ + setErrorEncounteredPage(window, errors.New("setConfirmDeleteContactPage called with non-contact identity hash"), previousPage) + return + } + + title := getPageTitleCentered("Confirm Delete Contact") + + backButton := getBackButtonCentered(previousPage) + + description1 := getBoldLabelCentered("Delete contact?") + + description2 := getLabelCentered("The user will be removed from your contacts.") + + contactIdentityHashString, _, err := identity.EncodeIdentityHashBytesToString(contactIdentityHash) + if (err != nil){ + contactIdentityHashHex := encoding.EncodeBytesToHexString(contactIdentityHash[:]) + setErrorEncounteredPage(window, errors.New("setConfirmDeleteContactPage not verifying identity hash: " + contactIdentityHashHex), previousPage) + } + + identityHashTitle := widget.NewLabel("Identity Hash:") + identityHashLabel := getBoldLabel(contactIdentityHashString) + identityHashRow := container.NewHBox(layout.NewSpacer(), identityHashTitle, identityHashLabel, layout.NewSpacer()) + + contactNameLabel := widget.NewLabel("Contact Name:") + contactNameText := getBoldLabel(contactName) + contactNameRow := container.NewHBox(layout.NewSpacer(), contactNameLabel, contactNameText, layout.NewSpacer()) + + page := container.NewVBox(title, backButton, widget.NewSeparator(), description1, description2, widget.NewSeparator(), identityHashRow, contactNameRow) + + if (contactDescription != ""){ + contactDescriptionLabel := widget.NewLabel("Contact Description:") + contactDescriptionTrimmed, changesOccurred, err := helpers.TrimAndFlattenString(contactDescription, 20) + if (err != nil){ + setErrorEncounteredPage(window, err, previousPage) + return + } + contactDescriptionTextLabel := getBoldLabel(contactDescriptionTrimmed) + if (changesOccurred == false){ + + contactDescriptionRow := container.NewHBox(layout.NewSpacer(), contactDescriptionLabel, contactDescriptionTextLabel, layout.NewSpacer()) + + page.Add(contactDescriptionRow) + } else { + viewFullDescriptionButton := widget.NewButtonWithIcon("", theme.VisibilityIcon(), func(){ + setViewTextPage(window, "Viewing Contact Description", contactDescription, false, currentPage) + }) + + contactDescriptionRow := container.NewHBox(layout.NewSpacer(), contactDescriptionLabel, contactDescriptionTextLabel, viewFullDescriptionButton, layout.NewSpacer()) + + page.Add(contactDescriptionRow) + } + } + + addedAgoString, err := helpers.ConvertUnixTimeToTimeAgoTranslated(contactAddedTime, false) + addedTimeAgoLabel := getItalicLabelCentered("Contact added " + addedAgoString + ".") + page.Add(addedTimeAgoLabel) + + deleteContactButton := getWidgetCentered(widget.NewButtonWithIcon("Confirm Delete", theme.DeleteIcon(), func(){ + err := myContacts.DeleteContact(contactIdentityHash) + if (err != nil){ + setErrorEncounteredPage(window, err, currentPage) + return + } + nextPage() + })) + page.Add(deleteContactButton) + + setPageContent(page, window) +} + + +func setCreateAContactCategoryPage(window fyne.Window, identityType string, previousPage func(), nextPage func()){ + + currentPage := func(){setCreateAContactCategoryPage(window, identityType, previousPage, nextPage)} + + title := getPageTitleCentered("Create " + identityType + " Contact Category") + + backButton := getBackButtonCentered(previousPage) + + description1 := getLabelCentered("Create a " + identityType + " contact category.") + + enterNameLabel := getBoldLabelCentered("Enter category name:") + + descriptionsContainer := container.NewVBox(description1, enterNameLabel) + + enterNameEntry := widget.NewEntry() + enterNameEntry.SetPlaceHolder("Enter category name.") + enterNameEntryBoxed := getWidgetBoxed(enterNameEntry) + + enterNameEntryWithDescriptions := getContainerCentered(container.NewGridWithColumns(1, descriptionsContainer, enterNameEntryBoxed)) + + contactCategoriesList, err := myContacts.GetAllMyContactCategories(identityType) + if (err != nil) { + setErrorEncounteredPage(window, err, previousPage) + return + } + + createCategoryButton := getWidgetCentered(widget.NewButtonWithIcon("Create Category", theme.ConfirmIcon(), func(){ + categoryName := enterNameEntry.Text + if (categoryName == ""){ + dialogTitle := translate("Name Is Empty") + dialogMessageA := getLabelCentered("You must enter a category name.") + dialogContent := container.NewVBox(dialogMessageA) + dialog.ShowCustom(dialogTitle, translate("Close"), dialogContent, window) + return + } + categoryAlreadyExists := slices.Contains(contactCategoriesList, categoryName) + if (categoryAlreadyExists == true){ + dialogTitle := translate("Category Already Exists") + dialogMessageA := getLabelCentered("The category you are trying to create already exists.") + dialogContent := container.NewVBox(dialogMessageA) + dialog.ShowCustom(dialogTitle, translate("Close"), dialogContent, window) + return + } + err := myContacts.AddContactCategory(identityType, categoryName) + if (err != nil){ + setErrorEncounteredPage(window, err, currentPage) + return + } + nextPage() + })) + + page := container.NewVBox(title, backButton, widget.NewSeparator(), enterNameEntryWithDescriptions, createCategoryButton) + + if (len(contactCategoriesList) != 0){ + + page.Add(widget.NewSeparator()) + + existingCategoriesLabel := getItalicLabelCentered("Existing Categories:") + page.Add(existingCategoriesLabel) + + for _, categoryName := range contactCategoriesList{ + categoryNameLabel := getBoldLabelCentered(categoryName) + page.Add(categoryNameLabel) + } + } + + setPageContent(page, window) +} + + +func setDeleteACategoryPage(window fyne.Window, identityType string, previousPage func(), nextPage func()){ + + currentPage := func(){setDeleteACategoryPage(window, identityType, previousPage, nextPage)} + + title := getPageTitleCentered("Delete " + identityType + " Contact Category") + + backButton := getBackButtonCentered(previousPage) + + myContactCategoriesList, err := myContacts.GetAllMyContactCategories(identityType) + if (err != nil){ + setErrorEncounteredPage(window, err, previousPage) + return + } + + if (len(myContactCategoriesList) == 0){ + + description1 := getBoldLabelCentered("No " + identityType + " contact categories exist.") + + page := container.NewVBox(title, backButton, widget.NewSeparator(), description1) + + setPageContent(page, window) + return + } + + description := getLabelCentered("Select the contact category to delete.") + + // We sort the categories so they show up in the same order every time. + + helpers.SortStringListToUnicodeOrder(myContactCategoriesList) + + categorySelector := widget.NewSelect(myContactCategoriesList, nil) + categorySelector.PlaceHolder = "Select Category..." + + categorySelectorCentered := getWidgetCentered(categorySelector) + + deleteButton := getWidgetCentered(widget.NewButtonWithIcon("Delete Category", theme.DeleteIcon(), func(){ + + categoryToDeleteIndex := categorySelector.SelectedIndex() + if (categoryToDeleteIndex == -1){ + + dialogTitle := translate("No Category Selected") + dialogMessageA := getLabelCentered("You must select a category to delete.") + dialogContent := container.NewVBox(dialogMessageA) + dialog.ShowCustom(dialogTitle, translate("Close"), dialogContent, window) + return + } + + categoryToDelete := categorySelector.Selected + + err := myContacts.DeleteContactCategory(identityType, categoryToDelete) + if (err != nil){ + setErrorEncounteredPage(window, err, currentPage) + return + } + nextPage() + })) + + page := container.NewVBox(title, backButton, widget.NewSeparator(), description, widget.NewSeparator(), categorySelectorCentered, deleteButton) + + setPageContent(page, window) +} + + + + diff --git a/gui/createIdentityGui.go b/gui/createIdentityGui.go new file mode 100644 index 0000000..7d533e6 --- /dev/null +++ b/gui/createIdentityGui.go @@ -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() +} + + + + + diff --git a/gui/desireStatisticsGui.go b/gui/desireStatisticsGui.go new file mode 100644 index 0000000..3e9b8e6 --- /dev/null +++ b/gui/desireStatisticsGui.go @@ -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) +} + + diff --git a/gui/desiresGui_General.go b/gui/desiresGui_General.go new file mode 100644 index 0000000..8592a48 --- /dev/null +++ b/gui/desiresGui_General.go @@ -0,0 +1,2267 @@ +package gui + +//desiresGui_General.go implements pages to view and manage a user's General mate desires, view all their desires, and manage their download desires + +import "fyne.io/fyne/v2" +import "fyne.io/fyne/v2/container" +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/resources/worldLanguages" +import "seekia/resources/worldLocations" + +import "seekia/internal/allowedText" +import "seekia/internal/appMemory" +import "seekia/internal/desires/mateDesires" +import "seekia/internal/desires/myLocalDesires" +import "seekia/internal/encoding" +import "seekia/internal/globalSettings" +import "seekia/internal/helpers" +import "seekia/internal/network/myMateCriteria" +import "seekia/internal/profiles/attributeDisplay" + +import "strings" +import "errors" +import "slices" + +func setDesiresPage(window fyne.Window){ + + appMemory.SetMemoryEntry("CurrentViewedPage", "Desires") + + currentPage := func(){setDesiresPage(window)} + + title := getPageTitleCentered(translate("My Mate Desires")) + + description1 := getLabelCentered(translate("Select your Mate desires.")) + description2 := getLabelCentered(translate("Your desires are used to generate your matches.")) + description3 := getLabelCentered(translate("These desires not shared on your profile.")) + + generalIcon, err := getFyneImageIcon("General") + if (err != nil){ + setErrorEncounteredPage(window, err, func(){setHomePage(window)}) + return + } + generalButton := widget.NewButton(translate("General"), func(){ + setChooseDesiresCategoryPage_General(window, currentPage) + }) + generalButtonWithIcon := container.NewGridWithRows(2, generalIcon, generalButton) + + physicalIcon, err := getFyneImageIcon("Person") + if (err != nil){ + setErrorEncounteredPage(window, err, func(){setHomePage(window)}) + return + } + physicalButton := widget.NewButton(translate("Physical"), func(){ + setChooseDesiresCategoryPage_Physical(window, currentPage) + }) + physicalButtonWithIcon := container.NewGridWithRows(2, physicalIcon, physicalButton) + + lifestyleIcon, err := getFyneImageIcon("Lifestyle") + if (err != nil){ + setErrorEncounteredPage(window, err, func(){setHomePage(window)}) + return + } + lifestyleButton := widget.NewButton(translate("Lifestyle"), func(){ + setChooseDesiresCategoryPage_Lifestyle(window, currentPage) + }) + lifestyleButtonWithIcon := container.NewGridWithRows(2, lifestyleIcon, lifestyleButton) + + mentalIcon, err := getFyneImageIcon("Mental") + if (err != nil){ + setErrorEncounteredPage(window, err, func(){setHomePage(window)}) + return + } + mentalButton := widget.NewButton(translate("Mental"), func(){ + setChooseDesiresCategoryPage_Mental(window, currentPage) + }) + mentalButtonWithIcon := container.NewGridWithRows(2, mentalIcon, mentalButton) + + categoriesRow := getContainerCentered(container.NewGridWithRows(1, generalButtonWithIcon, physicalButtonWithIcon, lifestyleButtonWithIcon, mentalButtonWithIcon)) + + allDesiresIcon, err := getFyneImageIcon("Choice") + if (err != nil){ + setErrorEncounteredPage(window, err, func(){setHomePage(window)}) + return + } + + viewAllDesiresButton := widget.NewButton("View All Desires", func(){ + setViewAllMyDesiresPage(window, currentPage) + }) + + viewAllDesiresButtonWithIcon := container.NewGridWithRows(2, allDesiresIcon, viewAllDesiresButton) + + statisticsIcon, err := getFyneImageIcon("Stats") + if (err != nil){ + setErrorEncounteredPage(window, err, func(){setHomePage(window)}) + return + } + + viewAllStatisticsButton := widget.NewButton("View All Statistics", func(){ + setViewAllMyDesireStatisticsPage(window, false, 0, 0, nil, currentPage) + }) + + viewAllStatisticsButtonWithIcon := container.NewGridWithColumns(1, statisticsIcon, viewAllStatisticsButton) + + viewAllButtonsRow := getContainerCentered(container.NewGridWithRows(1, viewAllDesiresButtonWithIcon, viewAllStatisticsButtonWithIcon)) + + downloadDesiresIcon, err := getFyneImageIcon("Host") + if (err != nil){ + setErrorEncounteredPage(window, err, func(){setHomePage(window)}) + return + } + downloadDesiresButton := widget.NewButton("Download Desires", func(){ + setManageMateDownloadDesiresPage(window, currentPage) + }) + + downloadDesiresButtonWithIcon := getContainerCentered(container.NewGridWithRows(2, downloadDesiresIcon, downloadDesiresButton)) + + page := container.NewVBox(title, widget.NewSeparator(), description1, description2, description3, widget.NewSeparator(), categoriesRow, widget.NewSeparator(), viewAllButtonsRow, widget.NewSeparator(), downloadDesiresButtonWithIcon) + + setPageContent(page, window) +} + +func setChooseDesiresCategoryPage_General(window fyne.Window, previousPage func()){ + + currentPage := func(){setChooseDesiresCategoryPage_General(window, previousPage)} + + title := getPageTitleCentered(translate("My Mate Desires - General")) + + backButton := getBackButtonCentered(previousPage) + + profileLanguageButton := widget.NewButton("Profile Language", func(){ + setChooseDesiresPage_ProfileLanguage(window, currentPage) + }) + + distanceButton := widget.NewButton("Distance", func(){ + setChooseDesiresPage_Distance(window, currentPage) + }) + + countryButton := widget.NewButton("Country", func(){ + setChooseDesiresPage_Country(window, currentPage) + }) + + sexualityButton := widget.NewButton("Sexuality", func(){ + setChooseDesiresPage_Sexuality(window, currentPage) + }) + + searchTermsButton := widget.NewButton("Search Terms", func(){ + setChooseDesiresPage_SearchTerms(window, currentPage) + }) + + chatButton := widget.NewButton("Chat", func(){ + setChooseDesiresPage_Chat(window, currentPage) + }) + + likedUsersButton := widget.NewButton("Liked Users", func(){ + setChooseDesiresPage_LikedUsers(window, currentPage) + }) + + ignoredUsersButton := widget.NewButton("Ignored Users", func(){ + setChooseDesiresPage_IgnoredUsers(window, currentPage) + }) + + contactsButton := widget.NewButton("Contacts", func(){ + setChooseDesiresPage_Contacts(window, currentPage) + }) + + buttonsGrid := getContainerCentered(container.NewGridWithColumns(1, profileLanguageButton, distanceButton, countryButton, sexualityButton, searchTermsButton, chatButton, likedUsersButton, ignoredUsersButton, contactsButton)) + + buttonsGridPadded := container.NewPadded(buttonsGrid) + + page := container.NewVBox(title, backButton, widget.NewSeparator(), buttonsGridPadded) + + setPageContent(page, window) +} + + +func setChooseDesiresPage_ProfileLanguage(window fyne.Window, previousPage func()){ + + currentPage := func(){setChooseDesiresPage_ProfileLanguage(window, previousPage)} + + title := getPageTitleCentered("My Mate Desires - General") + + backButton := getBackButtonCentered(previousPage) + + subtitle := getPageSubtitleCentered("Profile Language") + + description1 := getLabelCentered("Choose your profile language desires.") + description2 := getLabelCentered("A profile language is the language that a profile is written in.") + description3 := getLabelCentered("Choose all of the languages you can read.") + + getSelectedLanguagesSection := func()(*fyne.Container, error){ + + getCurrentDesiredChoicesList := func()([]string, error){ + + currentChoicesListExists, currentChoicesList, err := myLocalDesires.GetDesire("ProfileLanguage") + 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 + 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(){ + + submitLanguageFunction := func(newLanguageIdentifier int, _ string)error{ + + newLanguageIdentifierString := helpers.ConvertIntToString(newLanguageIdentifier) + + newLanguageIdentifierBase64 := encoding.EncodeBytesToBase64String([]byte(newLanguageIdentifierString)) + + newLanguagesList := helpers.AddItemToStringListAndAvoidDuplicate(currentDesiredChoicesList, newLanguageIdentifierBase64) + + newDesireString := strings.Join(newLanguagesList, "+") + + err = myLocalDesires.SetDesire("ProfileLanguage", newDesireString) + if (err != nil){ return err } + + return nil + } + + setChooseDesiresPage_AddLanguage(window, submitLanguageFunction, currentPage, currentPage) + })) + + if (len(currentDesiredChoicesList) == 0){ + + noLanguagesLabel := getBoldLabelCentered("No Languages Selected.") + + selectedLanguagesSection := container.NewVBox(noLanguagesLabel, addLanguageButton) + + return selectedLanguagesSection, nil + } + + myDesiredLanguagesLabel := getItalicLabelCentered("My Desired Languages:") + + languageNameColumn := container.NewVBox(widget.NewSeparator()) + + deleteButtonsColumn := container.NewVBox(widget.NewSeparator()) + + for _, languageIdentifierBase64 := range currentDesiredChoicesList{ + + languageIdentifierString, err := encoding.DecodeBase64StringToUnicodeString(languageIdentifierBase64) + if (err != nil){ + return nil, errors.New("My current profile language desire is malformed: Contains invalid language: " + languageIdentifierBase64) + } + + languageIdentifier, err := helpers.ConvertStringToInt(languageIdentifierString) + if (err != nil){ + return nil, errors.New("My current profile language desire is malformed: Contains invalid language: " + languageIdentifierString) + } + + languageObject, err := worldLanguages.GetLanguageObjectFromLanguageIdentifier(languageIdentifier) + if (err != nil){ + return nil, errors.New("My current profile language desire is malformed: Contains unknown language identifier: " + languageIdentifierString) + } + + languageNamesList := languageObject.NamesList + + languageDescription := helpers.TranslateAndJoinStringListItems(languageNamesList, "/") + + languageDescriptionTrimmed, _, err := helpers.TrimAndFlattenString(languageDescription, 20) + if (err != nil) { return nil, err } + + languageNameLabel := getBoldLabelCentered(languageDescriptionTrimmed) + + deleteLanguageButton := widget.NewButtonWithIcon("", theme.DeleteIcon(), func(){ + + newDesiredLanguagesList, _ := helpers.DeleteAllMatchingItemsFromStringList(currentDesiredChoicesList, languageIdentifierBase64) + + if (len(newDesiredLanguagesList) == 0){ + + err := myLocalDesires.DeleteDesire("ProfileLanguage") + if (err != nil){ + setErrorEncounteredPage(window, err, currentPage) + return + } + currentPage() + return + } + + newDesireValueString := strings.Join(newDesiredLanguagesList, "+") + + err := myLocalDesires.SetDesire("ProfileLanguage", 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()) + + selectedLanguagesSection := container.NewVBox(addLanguageButton, widget.NewSeparator(), myDesiredLanguagesLabel, languagesGrid) + + return selectedLanguagesSection, nil + } + + selectedLanguagesSection, err := getSelectedLanguagesSection() + if (err != nil){ + setErrorEncounteredPage(window, err, previousPage) + return + } + + filterOptionsSection, err := getDesireEditorFilterOptionsSection(window, currentPage, "ProfileLanguage", true) + if (err != nil){ + setErrorEncounteredPage(window, err, previousPage) + return + } + + viewStatisticsButton := getWidgetCentered(widget.NewButtonWithIcon("View Statistics", theme.InfoIcon(), func(){ + setViewMyMateDesireStatisticsPage(window, "Profile Language", "ProfileLanguage", true, "Bar", "ProfileLanguage", 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) +} + +func setChooseDesiresPage_Distance(window fyne.Window, previousPage func()){ + + currentPage := func(){setChooseDesiresPage_Distance(window, previousPage)} + + title := getPageTitleCentered(translate("My Mate Desires - General")) + + backButton := getBackButtonCentered(previousPage) + + subtitle := getPageSubtitleCentered(translate("Distance")) + + description1 := getLabelCentered("Choose your desired user distance.") + description2 := getLabelCentered("You must add a primary location to your profile to calculate distance.") + + metricOrImperialSwitchButton, err := getMetricImperialSwitchButton(window, currentPage) + if (err != nil){ + setErrorEncounteredPage(window, err, previousPage) + return + } + + currentUnitsLabel := getItalicLabel("Current Units:") + + currentUnitsRow := container.NewHBox(layout.NewSpacer(), currentUnitsLabel, metricOrImperialSwitchButton, layout.NewSpacer()) + + getMyMetricOrImperial := func()(string, error){ + + exists, metricOrImperial, err := globalSettings.GetSetting("MetricOrImperial") + if (err != nil) { return "", err } + if (exists == false){ + return "Metric", nil + } + if (metricOrImperial != "Metric" && metricOrImperial != "Imperial"){ + return "", errors.New("Malformed globalSettings: Invalid metricOrImperial: " + metricOrImperial) + } + + return metricOrImperial, nil + } + + myMetricOrImperial, err := getMyMetricOrImperial() + if (err != nil){ + setErrorEncounteredPage(window, err, previousPage) + return + } + + getCurrentUnits := func()string{ + if (myMetricOrImperial == "Metric"){ + return "kilometers" + } + return "miles" + } + + currentUnits := getCurrentUnits() + + // Distance is normally represented in metric + // Metric is the standard, and Imperial is custom + + getUnitsAreCustomBool := func()bool{ + + if (myMetricOrImperial == "Metric"){ + return false + } + return true + } + + unitsAreCustomBool := getUnitsAreCustomBool() + + convertToMetricFunction := helpers.ConvertMilesToKilometers + + convertToImperialFunction := helpers.ConvertKilometersToMiles + + desireEditor, err := getDesireEditor_Numeric(window, currentPage, "Distance", 0, 20000, currentUnits, 2, unitsAreCustomBool, convertToMetricFunction, convertToImperialFunction, false, "", "", nil) + if (err != nil) { + setErrorEncounteredPage(window, err, previousPage) + return + } + + viewStatisticsButton := getWidgetCentered(widget.NewButtonWithIcon("View Statistics", theme.InfoIcon(), func(){ + setViewMyMateDesireStatisticsPage(window, "Distance", "Distance", true, "Bar", "Distance", currentPage) + })) + + page := container.NewVBox(title, backButton, widget.NewSeparator(), subtitle, widget.NewSeparator(), description1, description2, widget.NewSeparator(), currentUnitsRow, widget.NewSeparator(), desireEditor, widget.NewSeparator(), viewStatisticsButton) + + setPageContent(page, window) +} + + +func setChooseDesiresPage_Country(window fyne.Window, previousPage func()){ + + currentPage := func(){setChooseDesiresPage_Country(window, previousPage)} + + title := getPageTitleCentered("My Mate Desires - General") + + backButton := getBackButtonCentered(previousPage) + + subtitle := getPageSubtitleCentered("Country") + + description1 := getLabelCentered("Choose your country desires.") + description2 := getLabelCentered("Select the countries where you want your matches to live in.") + description3 := getLabelCentered("Most users should only use this desire if they live near a border.") + description4 := getLabelCentered("Most users should only need to use the Distance desire.") + + getSelectedCountriesSection := func()(*fyne.Container, error){ + + getCurrentDesiredChoicesList := func()([]string, error){ + + currentChoicesListExists, currentChoicesList, err := myLocalDesires.GetDesire("PrimaryLocationCountry") + 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 + currentDesiredChoicesList := strings.Split(currentChoicesList, "+") + + return currentDesiredChoicesList, nil + } + + currentDesiredChoicesList, err := getCurrentDesiredChoicesList() + if (err != nil) { return nil, err } + + addCountryButton := getWidgetCentered(widget.NewButtonWithIcon("Add Country", theme.ContentAddIcon(), func(){ + + setChooseMateDesiresPage_ChooseCountry(window, currentPage, currentPage) + })) + + if (len(currentDesiredChoicesList) == 0){ + + noCountriesLabel := getBoldLabelCentered("No Countries Selected.") + + selectedCountriesSection := container.NewVBox(noCountriesLabel, addCountryButton) + + return selectedCountriesSection, nil + } + + myDesiredCountriesLabel := getItalicLabelCentered("My Desired Countries:") + + countryNameColumn := container.NewVBox(widget.NewSeparator()) + + deleteButtonsColumn := container.NewVBox(widget.NewSeparator()) + + for _, countryIdentifierBase64 := range currentDesiredChoicesList{ + + countryIdentifierString, err := encoding.DecodeBase64StringToUnicodeString(countryIdentifierBase64) + if (err != nil){ + return nil, errors.New("My current profile country desire is malformed: Contains invalid country: " + countryIdentifierBase64) + } + + countryIdentifier, err := helpers.ConvertStringToInt(countryIdentifierString) + if (err != nil){ + return nil, errors.New("My current profile country desire is malformed: Contains invalid country: " + countryIdentifierString) + } + + countryObject, err := worldLocations.GetCountryObjectFromCountryIdentifier(countryIdentifier) + if (err != nil){ + return nil, errors.New("My current profile country is malformed: Contains unknown country identifier: " + countryIdentifierString) + } + + countryNamesList := countryObject.NamesList + + countryDescription := helpers.TranslateAndJoinStringListItems(countryNamesList, "/") + + countryNameLabel := getBoldLabelCentered(countryDescription) + + deleteCountryButton := widget.NewButtonWithIcon("", theme.DeleteIcon(), func(){ + + newDesiredCountriesList, _ := helpers.DeleteAllMatchingItemsFromStringList(currentDesiredChoicesList, countryIdentifierBase64) + + if (len(newDesiredCountriesList) == 0){ + + err := myLocalDesires.DeleteDesire("PrimaryLocationCountry") + if (err != nil){ + setErrorEncounteredPage(window, err, currentPage) + return + } + currentPage() + return + } + + newDesireValueString := strings.Join(newDesiredCountriesList, "+") + + err := myLocalDesires.SetDesire("PrimaryLocationCountry", newDesireValueString) + if (err != nil){ + setErrorEncounteredPage(window, err, currentPage) + return + } + currentPage() + }) + + countryNameColumn.Add(countryNameLabel) + deleteButtonsColumn.Add(deleteCountryButton) + + countryNameColumn.Add(widget.NewSeparator()) + deleteButtonsColumn.Add(widget.NewSeparator()) + } + + countriesGrid := container.NewHBox(layout.NewSpacer(), countryNameColumn, deleteButtonsColumn, layout.NewSpacer()) + + selectedCountriesSection := container.NewVBox(addCountryButton, widget.NewSeparator(), myDesiredCountriesLabel, countriesGrid) + + return selectedCountriesSection, nil + } + + selectedCountriesSection, err := getSelectedCountriesSection() + if (err != nil){ + setErrorEncounteredPage(window, err, previousPage) + return + } + + filterOptionsSection, err := getDesireEditorFilterOptionsSection(window, currentPage, "PrimaryLocationCountry", true) + if (err != nil){ + setErrorEncounteredPage(window, err, previousPage) + return + } + + viewStatisticsButton := getWidgetCentered(widget.NewButtonWithIcon("View Statistics", theme.InfoIcon(), func(){ + setViewMyMateDesireStatisticsPage(window, "Country", "PrimaryLocationCountry", true, "Donut", "PrimaryLocationCountry", currentPage) + })) + + page := container.NewVBox(title, backButton, widget.NewSeparator(), subtitle, widget.NewSeparator(), description1, description2, description3, description4, widget.NewSeparator(), selectedCountriesSection, widget.NewSeparator(), filterOptionsSection, widget.NewSeparator(), viewStatisticsButton) + + setPageContent(page, window) +} + +func setChooseMateDesiresPage_ChooseCountry(window fyne.Window, previousPage func(), nextPage func()){ + + currentPage := func(){setChooseMateDesiresPage_ChooseCountry(window, previousPage, nextPage)} + + title := getPageTitleCentered(translate("My Mate Desires - General")) + + backButton := getBackButtonCentered(previousPage) + + subtitle := getPageSubtitleCentered(translate("Add Country")) + + description1 := getLabelCentered("Choose the country to add.") + + allCountryObjectsList, err := worldLocations.GetAllCountryObjectsList() + if (err != nil){ + setErrorEncounteredPage(window, err, previousPage) + return + } + + allTranslatedCountriesList := make([]string, 0, len(allCountryObjectsList)) + + // Map Structure: Country Description -> Country identifier + countryIdentifiersMap := make(map[string]int) + + for _, countryObject := range allCountryObjectsList{ + + countryIdentifier := countryObject.Identifier + countryNamesList := countryObject.NamesList + + countryDescription := helpers.TranslateAndJoinStringListItems(countryNamesList, "/") + + countryIdentifiersMap[countryDescription] = countryIdentifier + allTranslatedCountriesList = append(allTranslatedCountriesList, countryDescription) + } + + helpers.SortStringListToUnicodeOrder(allTranslatedCountriesList) + + onSelectedFunction := func(selectedCountryIndex int) { + + selectedCountryDescription := allTranslatedCountriesList[selectedCountryIndex] + + selectedCountryIdentifier, exists := countryIdentifiersMap[selectedCountryDescription] + if (exists == false){ + setErrorEncounteredPage(window, errors.New("countryIdentifiersMap missing country description."), currentPage) + return + } + + selectedCountryIdentifierString := helpers.ConvertIntToString(selectedCountryIdentifier) + + newCountryIdentifierBase64 := encoding.EncodeBytesToBase64String([]byte(selectedCountryIdentifierString)) + + getNewDesiredAttributeList := func()([]string, error){ + + currentChoicesListExists, currentChoicesList, err := myLocalDesires.GetDesire("PrimaryLocationCountry") + if (err != nil) { return nil, err } + if (currentChoicesListExists == false){ + + newList := []string{newCountryIdentifierBase64} + return newList, nil + } + //currentChoicesList is a "+" separated list of choices + // Each choice option is encoded in base64 + currentDesiredChoicesList := strings.Split(currentChoicesList, "+") + + newCountriesList := helpers.AddItemToStringListAndAvoidDuplicate(currentDesiredChoicesList, newCountryIdentifierBase64) + + return newCountriesList, nil + } + + newDesiredAttributeList, err := getNewDesiredAttributeList() + if (err != nil){ + setErrorEncounteredPage(window, err, currentPage) + return + } + + newDesireString := strings.Join(newDesiredAttributeList, "+") + + err = myLocalDesires.SetDesire("PrimaryLocationCountry", newDesireString) + if (err != nil){ + setErrorEncounteredPage(window, err, currentPage) + return + } + + nextPage() + } + + widgetList, err := getFyneWidgetListFromStringList(allTranslatedCountriesList, onSelectedFunction) + if (err != nil){ + setErrorEncounteredPage(window, err, previousPage) + return + } + + header := container.NewVBox(title, backButton, widget.NewSeparator(), subtitle, widget.NewSeparator(), description1, widget.NewSeparator()) + + page := container.NewBorder(header, nil, nil, nil, widgetList) + + setPageContent(page, window) +} + +func setChooseDesiresPage_Sexuality(window fyne.Window, previousPage func()){ + + currentPage := func(){setChooseDesiresPage_Sexuality(window, previousPage)} + + title := getPageTitleCentered("My Mate Desires - General") + + backButton := getBackButtonCentered(previousPage) + + subtitle := getPageSubtitleCentered(translate("Sexuality")) + + description1 := getLabelCentered("Choose the user sexualities that you desire.") + description2 := getLabelCentered("This refers to which sexes the users are interested in.") + description3 := getLabelCentered("For example, if you are Male, you should at least select Male.") + + option1Translated := translate("Male") + option2Translated := translate("Female") + option3Translated := translate("Male And Female") + + optionTitlesList := []string{option1Translated, option2Translated, option3Translated} + + optionNamesMap := map[string][]string{ + option1Translated: []string{"Male"}, + option2Translated: []string{"Female"}, + option3Translated: []string{"Male And Female"}, + } + + desireEditor, err := getDesireEditor_Choice(window, currentPage, "Sexuality", optionTitlesList, optionNamesMap, false, true, 1) + if (err != nil){ + setErrorEncounteredPage(window, err, previousPage) + return + } + + viewStatisticsButton := getWidgetCentered(widget.NewButtonWithIcon("View Statistics", theme.InfoIcon(), func(){ + setViewMyMateDesireStatisticsPage(window, "Sexuality", "Sexuality", true, "Donut", "Sexuality", 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_SearchTerms(window fyne.Window, previousPage func()){ + + currentPage := func(){setChooseDesiresPage_SearchTerms(window, previousPage)} + + title := getPageTitleCentered(translate("My Mate Desires - General")) + + backButton := getBackButtonCentered(previousPage) + + subtitle := getPageSubtitleCentered("Search Terms") + + description1 := getLabelCentered("Choose your desired search terms.") + description2 := getLabelCentered("Seekia will search through each user's profile for your terms.") + description3 := getLabelCentered("Each user's description, tags, hobbies, and beliefs will be searched.") + description4 := getLabelCentered("A user's profile must contain at least 1 of your terms to pass the desire.") + + getSelectedSearchTermsSection := func()(*fyne.Container, error){ + + getCurrentDesiredChoicesList := func()([]string, error){ + + currentChoicesListExists, currentChoicesList, err := myLocalDesires.GetDesire("SearchTerms") + 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 + currentDesiredChoicesList := strings.Split(currentChoicesList, "+") + + return currentDesiredChoicesList, nil + } + + currentDesiredChoicesList, err := getCurrentDesiredChoicesList() + if (err != nil) { return nil, err } + + addTermEntry := widget.NewEntry() + addTermEntry.SetPlaceHolder("Enter term name...") + + addTermButton := getWidgetCentered(widget.NewButtonWithIcon("Add Term", theme.ContentAddIcon(), func(){ + + newTermName := addTermEntry.Text + + if (newTermName == ""){ + return + } + isAllowed := allowedText.VerifyStringIsAllowed(newTermName) + if (isAllowed == false){ + title := translate("Invalid Term Name") + dialogMessageA := getLabelCentered(translate("Your search term contains an invalid character.")) + dialogMessageB := getLabelCentered(translate("It must be encoded in UTF-8.")) + dialogMessageC := getLabelCentered(translate("Remove this character and resubmit.")) + dialogContent := container.NewVBox(dialogMessageA, dialogMessageB, dialogMessageC) + dialog.ShowCustom(title, translate("Close"), dialogContent, window) + return + } + + containsTabsOrNewlines := helpers.CheckIfStringContainsTabsOrNewlines(newTermName) + if (containsTabsOrNewlines == true){ + title := translate("Invalid Term Name") + dialogMessageA := getLabelCentered(translate("Your search term contains a tab or a newline.")) + dialogMessageB := getLabelCentered(translate("Remove the tab or newline and resubmit.")) + dialogContent := container.NewVBox(dialogMessageA, dialogMessageB) + dialog.ShowCustom(title, translate("Close"), dialogContent, window) + return + } + + termNameBase64 := encoding.EncodeBytesToBase64String([]byte(newTermName)) + + newDesireList := helpers.AddItemToStringListAndAvoidDuplicate(currentDesiredChoicesList, termNameBase64) + newDesireAttributeValue := strings.Join(newDesireList, "+") + + err := myLocalDesires.SetDesire("SearchTerms", newDesireAttributeValue) + if (err != nil){ + setErrorEncounteredPage(window, err, currentPage) + return + } + + currentPage() + })) + + addTermRow := getContainerCentered(container.NewGridWithRows(1, addTermEntry, addTermButton)) + + if (len(currentDesiredChoicesList) == 0){ + + noTermsExistLabel := getBoldLabelCentered("No Terms Exist.") + + selectedSearchTermsSection := container.NewVBox(addTermRow, widget.NewSeparator(), noTermsExistLabel) + + return selectedSearchTermsSection, nil + } + + myDesiredTermsLabel := getItalicLabelCentered("My Desired Terms:") + + termNameColumn := container.NewVBox(widget.NewSeparator()) + + deleteButtonsColumn := container.NewVBox(widget.NewSeparator()) + + for _, termNameBase64 := range currentDesiredChoicesList{ + + termName, err := encoding.DecodeBase64StringToUnicodeString(termNameBase64) + if (err != nil){ + return nil, errors.New("My current search terms desire is malformed: Contains invalid term: " + termNameBase64) + } + + termNameLabel := getBoldLabelCentered(termName) + + deleteTermButton := widget.NewButtonWithIcon("", theme.DeleteIcon(), func(){ + + newDesiredTermsList, _ := helpers.DeleteAllMatchingItemsFromStringList(currentDesiredChoicesList, termNameBase64) + + if (len(newDesiredTermsList) == 0){ + + err := myLocalDesires.DeleteDesire("SearchTerms") + if (err != nil){ + setErrorEncounteredPage(window, err, currentPage) + return + } + currentPage() + return + } + + newDesireValueString := strings.Join(newDesiredTermsList, "+") + + err := myLocalDesires.SetDesire("SearchTerms", newDesireValueString) + if (err != nil){ + setErrorEncounteredPage(window, err, currentPage) + return + } + currentPage() + }) + + termNameColumn.Add(termNameLabel) + deleteButtonsColumn.Add(deleteTermButton) + + termNameColumn.Add(widget.NewSeparator()) + deleteButtonsColumn.Add(widget.NewSeparator()) + } + + termsGrid := container.NewHBox(layout.NewSpacer(), termNameColumn, deleteButtonsColumn, layout.NewSpacer()) + + selectedTermsSection := container.NewVBox(addTermRow, widget.NewSeparator(), myDesiredTermsLabel, termsGrid) + + return selectedTermsSection, nil + } + + selectedTermsSection, err := getSelectedSearchTermsSection() + if (err != nil){ + setErrorEncounteredPage(window, err, previousPage) + return + } + + filterOptionsSection, err := getDesireEditorFilterOptionsSection(window, currentPage, "SearchTerms", false) + if (err != nil){ + setErrorEncounteredPage(window, err, previousPage) + return + } + + viewStatisticsButton := getWidgetCentered(widget.NewButtonWithIcon("View Statistics", theme.InfoIcon(), func(){ + setViewMyMateDesireStatisticsPage(window, "Search Terms", "SearchTerms", true, "Bar", "SearchTermsCount", currentPage) + })) + + page := container.NewVBox(title, backButton, widget.NewSeparator(), subtitle, widget.NewSeparator(), description1, description2, description3, description4, widget.NewSeparator(), selectedTermsSection, widget.NewSeparator(), filterOptionsSection, widget.NewSeparator(), viewStatisticsButton) + + setPageContent(page, window) +} + +func setChooseDesiresPage_Chat(window fyne.Window, previousPage func()){ + + currentPage := func(){setChooseDesiresPage_Chat(window, previousPage)} + + title := getPageTitleCentered(translate("My Mate Desires - General")) + + backButton := getBackButtonCentered(previousPage) + + subtitle := getPageSubtitleCentered("Chat Desires") + + description1 := getLabelCentered("Choose your chat desires.") + + hasMessagedMeButton := widget.NewButton("Has Messaged Me", func(){ + setChooseDesiresPage_HasMessagedMe(window, currentPage) + }) + + iHaveMessagedButton := widget.NewButton("I Have Messaged", func(){ + setChooseDesiresPage_IHaveMessaged(window, currentPage) + }) + + hasRejectedMeButton := widget.NewButton("Has Rejected Me", func(){ + setChooseDesiresPage_HasRejectedMe(window, currentPage) + }) + + hasAnsweredQuestionnaireButton := widget.NewButton("Has Answered Questionnaire", func(){ + //TODO + // We need to include the functionality to only show users who responded in a specified way + // For example, if the user's question is: Do you like french fries?, and the user has 2 options: Yes/No, + // the user should be able to filter users who answered with yes/no + showUnderConstructionDialog(window) + }) + + buttonsGrid := getContainerCentered(container.NewGridWithColumns(1, hasMessagedMeButton, iHaveMessagedButton, hasRejectedMeButton, hasAnsweredQuestionnaireButton)) + + page := container.NewVBox(title, backButton, widget.NewSeparator(), subtitle, widget.NewSeparator(), description1, widget.NewSeparator(), buttonsGrid) + + setPageContent(page, window) +} + +func setChooseDesiresPage_HasMessagedMe(window fyne.Window, previousPage func()){ + + currentPage := func(){setChooseDesiresPage_HasMessagedMe(window, previousPage)} + + title := getPageTitleCentered("My Mate Desires - General") + + backButton := getBackButtonCentered(previousPage) + + subtitle := getPageSubtitleCentered("Has Messaged Me") + + description1 := getLabelCentered("Use this desire to show or hide users who have messaged you.") + description2 := getLabelCentered("Select the kind of users to include in your matches.") + + //TODO: Change this to HasGreetedMe? Currently, a user who has rejected us will be included by this desire + + option1Translated := translate("Has Messaged Me") + option2Translated := translate("Has Not Messaged Me") + + optionTitlesList := []string{option1Translated, option2Translated} + + optionNamesMap := map[string][]string{ + option1Translated: []string{"Yes"}, + option2Translated: []string{"No"}, + } + + desireEditor, err := getDesireEditor_Choice(window, currentPage, "HasMessagedMe", optionTitlesList, optionNamesMap, false, false, 1) + if (err != nil){ + setErrorEncounteredPage(window, err, previousPage) + return + } + + viewStatisticsButton := getWidgetCentered(widget.NewButtonWithIcon("View Statistics", theme.InfoIcon(), func(){ + setViewMyMateDesireStatisticsPage(window, "Has Messaged Me", "HasMessagedMe", true, "Donut", "HasMessagedMe", currentPage) + })) + + page := container.NewVBox(title, backButton, widget.NewSeparator(), subtitle, widget.NewSeparator(), description1, description2, widget.NewSeparator(), desireEditor, widget.NewSeparator(), viewStatisticsButton) + + setPageContent(page, window) +} + +func setChooseDesiresPage_IHaveMessaged(window fyne.Window, previousPage func()){ + + currentPage := func(){setChooseDesiresPage_IHaveMessaged(window, previousPage)} + + title := getPageTitleCentered("My Mate Desires - General") + + backButton := getBackButtonCentered(previousPage) + + subtitle := getPageSubtitleCentered("I Have Messaged") + + description1 := getLabelCentered("Use this desire to show or hide users whom you have messaged.") + description2 := getLabelCentered("Select the kind of users to include in your matches.") + + option1Translated := translate("I Have Messaged") + option2Translated := translate("I Have Not Messaged") + + optionTitlesList := []string{option1Translated, option2Translated} + + optionNamesMap := map[string][]string{ + option1Translated: []string{"Yes"}, + option2Translated: []string{"No"}, + } + + desireEditor, err := getDesireEditor_Choice(window, currentPage, "IHaveMessaged", optionTitlesList, optionNamesMap, false, false, 1) + if (err != nil){ + setErrorEncounteredPage(window, err, previousPage) + return + } + + viewStatisticsButton := getWidgetCentered(widget.NewButtonWithIcon("View Statistics", theme.InfoIcon(), func(){ + setViewMyMateDesireStatisticsPage(window, "I Have Messaged", "IHaveMessaged", true, "Donut", "IHaveMessaged", currentPage) + })) + + page := container.NewVBox(title, backButton, widget.NewSeparator(), subtitle, widget.NewSeparator(), description1, description2, widget.NewSeparator(), desireEditor, widget.NewSeparator(), viewStatisticsButton) + + setPageContent(page, window) +} + + +func setChooseDesiresPage_HasRejectedMe(window fyne.Window, previousPage func()){ + + currentPage := func(){setChooseDesiresPage_HasRejectedMe(window, previousPage)} + + title := getPageTitleCentered(translate("My Mate Desires - General")) + + backButton := getBackButtonCentered(previousPage) + + subtitle := getPageSubtitleCentered("Has Rejected Me") + + description1 := getLabelCentered("Use this desire to show or hide users who have rejected you.") + description2 := getLabelCentered("These are users who sent you a message that they are not interested.") + description3 := getLabelCentered("Select the kind of users to include in your matches.") + + option1Translated := translate("Has Rejected Me") + option2Translated := translate("Has Not Rejected Me") + + optionTitlesList := []string{option1Translated, option2Translated} + + optionNamesMap := map[string][]string{ + option1Translated: []string{"Yes"}, + option2Translated: []string{"No"}, + } + + desireEditor, err := getDesireEditor_Choice(window, currentPage, "HasRejectedMe", optionTitlesList, optionNamesMap, false, false, 1) + if (err != nil){ + setErrorEncounteredPage(window, err, previousPage) + return + } + + viewStatisticsButton := getWidgetCentered(widget.NewButtonWithIcon("View Statistics", theme.InfoIcon(), func(){ + setViewMyMateDesireStatisticsPage(window, "Has Rejected Me", "HasRejectedMe", true, "Donut", "HasRejectedMe", 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_LikedUsers(window fyne.Window, previousPage func()){ + + currentPage := func(){setChooseDesiresPage_LikedUsers(window, previousPage)} + + title := getPageTitleCentered(translate("My Mate Desires - General")) + + backButton := getBackButtonCentered(previousPage) + + subtitle := getPageSubtitleCentered("Liked Users") + + description1 := getLabelCentered("Use this desire to show or hide liked users.") + description2 := getLabelCentered("A liked user is a user that you have added to your likes.") + description3 := getLabelCentered("Select the kind of users to include in your matches.") + + option1Translated := translate("Liked") + option2Translated := translate("Not Liked") + + optionTitlesList := []string{option1Translated, option2Translated} + + optionNamesMap := map[string][]string{ + option1Translated: []string{"Yes"}, + option2Translated: []string{"No"}, + } + + desireEditor, err := getDesireEditor_Choice(window, currentPage, "IsLiked", optionTitlesList, optionNamesMap, false, false, 1) + if (err != nil){ + setErrorEncounteredPage(window, err, previousPage) + return + } + + viewStatisticsButton := getWidgetCentered(widget.NewButtonWithIcon("View Statistics", theme.InfoIcon(), func(){ + setViewMyMateDesireStatisticsPage(window, "Is Liked", "IsLiked", true, "Donut", "IsLiked", 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_IgnoredUsers(window fyne.Window, previousPage func()){ + + currentPage := func(){setChooseDesiresPage_IgnoredUsers(window, previousPage)} + + title := getPageTitleCentered(translate("My Mate Desires - General")) + + backButton := getBackButtonCentered(previousPage) + + subtitle := getPageSubtitleCentered("Ignored Users") + + description1 := getLabelCentered("Use this desire to show or hide ignored users.") + description2 := getLabelCentered("An Ignored user is a user that you have ignored.") + description3 := getLabelCentered("Select the kind of users to include in your matches.") + + option1Translated := translate("Ignored") + option2Translated := translate("Not Ignored") + + optionTitlesList := []string{option1Translated, option2Translated} + + optionNamesMap := map[string][]string{ + option1Translated: []string{"Yes"}, + option2Translated: []string{"No"}, + } + + desireEditor, err := getDesireEditor_Choice(window, currentPage, "IsIgnored", optionTitlesList, optionNamesMap, false, false, 1) + if (err != nil){ + setErrorEncounteredPage(window, err, previousPage) + return + } + + viewStatisticsButton := getWidgetCentered(widget.NewButtonWithIcon("View Statistics", theme.InfoIcon(), func(){ + setViewMyMateDesireStatisticsPage(window, "Is Ignored", "IsIgnored", true, "Donut", "IsIgnored", 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_Contacts(window fyne.Window, previousPage func()){ + + currentPage := func(){setChooseDesiresPage_Contacts(window, previousPage)} + + title := getPageTitleCentered(translate("My Mate Desires - General")) + + backButton := getBackButtonCentered(previousPage) + + subtitle := getPageSubtitleCentered("Contacts") + + description1 := getLabelCentered("Use this desire to show or hide your contacts.") + description2 := getLabelCentered("These are users you have added to your contacts.") + description3 := getLabelCentered("Select the kind of users to include in your matches.") + + option1Translated := translate("Is My Contact") + option2Translated := translate("Is Not My Contact") + + optionTitlesList := []string{option1Translated, option2Translated} + + optionNamesMap := map[string][]string{ + option1Translated: []string{"Yes"}, + option2Translated: []string{"No"}, + } + + desireEditor, err := getDesireEditor_Choice(window, currentPage, "IsMyContact", optionTitlesList, optionNamesMap, false, false, 1) + if (err != nil){ + setErrorEncounteredPage(window, err, previousPage) + return + } + + viewStatisticsButton := getWidgetCentered(widget.NewButtonWithIcon("View Statistics", theme.InfoIcon(), func(){ + setViewMyMateDesireStatisticsPage(window, "Is My Contact", "IsMyContact", true, "Donut", "IsMyContact", currentPage) + })) + + page := container.NewVBox(title, backButton, widget.NewSeparator(), subtitle, widget.NewSeparator(), description1, description2, description3, widget.NewSeparator(), desireEditor, widget.NewSeparator(), viewStatisticsButton) + + setPageContent(page, window) +} + + +// This function provides the editor to edit a choice desire +// A choice desire is one where there are a some canonical choices available, and the potential for an "Other" choice +// Inputs: +// -fyne.Window +// -func(): Current page +// -string: Desire name +// -[]string: Option titles list: What each check will show. Example: "1/5", "2/5", "3/5", "4/5", "5/5") +// -map[string][]string: Option Title -> Option Names list (Example ("1/5" -> "1", "2/5" -> "2"...) +// -bool: Show other choice: True if we want to show the "Other" option +// -bool: Show the "Require Response" button (false for attributes which do not allow that option) +// -int: Number of columns for the grid +// Outputs: +// -*fyne.Container +// -error +func getDesireEditor_Choice(window fyne.Window, currentPage func(), desireName string, optionTitlesList []string, optionsNamesMap map[string][]string, showOtherChoice bool, showRequireResponseButton bool, numberOfGridColumns int)(*fyne.Container, error){ + + getCurrentDesiredChoicesList := func()([]string, error){ + + currentChoicesListExists, currentChoicesList, err := myLocalDesires.GetDesire(desireName) + 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 } + + selectButtonsGrid := container.NewGridWithColumns(numberOfGridColumns) + + for _, optionTitle := range optionTitlesList{ + + optionNamesList, exists := optionsNamesMap[optionTitle] + if (exists == false){ + return nil, errors.New("getDesireEditor_Choice called with optionsNamesMap missing optionTitle: " + optionTitle) + } + + optionBase64NamesList := make([]string, 0, len(optionNamesList)) + + for _, optionName := range optionNamesList{ + + optionNameBase64 := encoding.EncodeBytesToBase64String([]byte(optionName)) + + optionBase64NamesList = append(optionBase64NamesList, optionNameBase64) + } + + handleSelectFunction := func(selection bool){ + + //Outputs: + // -[]string: New desire attribute list (list of base64 option names) + getNewDesireAttributeList := func()[]string{ + + if (selection == false){ + if (len(currentDesiredChoicesList) == 0){ + // This should not happen, because the option was already selected + // The list should have had at least 1 item before + emptyList := make([]string, 0) + return emptyList + } + + newAttributeList := make([]string, 0) + + for _, optionNameBase64 := range currentDesiredChoicesList{ + // We see if this option name belongs to the current optionTitle + // If it does, we remove it from our selected attributes list + shouldDelete := slices.Contains(optionBase64NamesList, optionNameBase64) + if (shouldDelete == false){ + newAttributeList = append(newAttributeList, optionNameBase64) + } + } + + return newAttributeList + } + //selection == true + + newDesiredChoicesList := slices.Concat(currentDesiredChoicesList, optionBase64NamesList) + + duplicatesRemovedList := helpers.RemoveDuplicatesFromStringList(newDesiredChoicesList) + + return duplicatesRemovedList + } + + newDesireAttributeList := getNewDesireAttributeList() + if (len(newDesireAttributeList) == 0){ + + err := myLocalDesires.DeleteDesire(desireName) + if (err != nil){ + setErrorEncounteredPage(window, err, currentPage) + return + } + currentPage() + return + } + + newDesireAttribute := strings.Join(newDesireAttributeList, "+") + + err := myLocalDesires.SetDesire(desireName, newDesireAttribute) + if (err != nil){ + setErrorEncounteredPage(window, err, currentPage) + return + } + currentPage() + } + + optionSelectCheck := widget.NewCheck(optionTitle, handleSelectFunction) + + getChoiceIsSelectedBool := func()bool{ + + // Every option name associated with this choice must be selected for this choice to be considered selected + + for _, optionNameBase64 := range optionBase64NamesList{ + listContainsItem := slices.Contains(currentDesiredChoicesList, optionNameBase64) + if (listContainsItem == false){ + return false + } + } + return true + } + choiceIsSelected := getChoiceIsSelectedBool() + if (choiceIsSelected == true){ + optionSelectCheck.Checked = true + } + + selectButtonsGrid.Add(optionSelectCheck) + } + + if (showOtherChoice == true){ + + handleOtherSelectFunction := func(selection bool){ + + //Outputs: + // -[]string: New desire attribute list (list of base64 option names) + getNewDesireAttributeList := func()[]string{ + + if (selection == false){ + if (len(currentDesiredChoicesList) == 0){ + // This should not happen, because "Other" had to have been selected for it to be deselected + emptyList := make([]string, 0) + return emptyList + } + + newAttributeList, _ := helpers.DeleteAllMatchingItemsFromStringList(currentDesiredChoicesList, "Other") + + return newAttributeList + } + //selection == true + + newAttributeList := helpers.AddItemToStringListAndAvoidDuplicate(currentDesiredChoicesList, "Other") + + return newAttributeList + } + + newDesireAttributeList := getNewDesireAttributeList() + if (len(newDesireAttributeList) == 0){ + + err := myLocalDesires.DeleteDesire(desireName) + if (err != nil){ + setErrorEncounteredPage(window, err, currentPage) + return + } + currentPage() + return + } + newDesireAttribute := strings.Join(newDesireAttributeList, "+") + + err := myLocalDesires.SetDesire(desireName, newDesireAttribute) + if (err != nil){ + setErrorEncounteredPage(window, err, currentPage) + return + } + currentPage() + } + + otherSelectCheck := widget.NewCheck("Other", handleOtherSelectFunction) + + otherIsSelected := slices.Contains(currentDesiredChoicesList, "Other") + if (otherIsSelected == true){ + otherSelectCheck.Checked = true + } + + selectButtonsGrid.Add(otherSelectCheck) + } + + selectButtonsGridCentered := getContainerCentered(selectButtonsGrid) + + filterOptionsSection, err := getDesireEditorFilterOptionsSection(window, currentPage, desireName, showRequireResponseButton) + if (err != nil){ return nil, err } + + desireEditor := container.NewVBox(selectButtonsGridCentered, widget.NewSeparator(), filterOptionsSection) + + return desireEditor, nil +} + +// This function provides the editor to edit a numeric attribute +// The user can choose a minimum and maximum allowed value +// Units are shown for each minimum/maximum value, and a secondary row of alternate units can optionally be shown +func getDesireEditor_Numeric( + window fyne.Window, + currentPage func(), + desireName string, + minimumAllowed int64, + maximumAllowed int64, + currentUnits string, + unitsRoundingPrecision int, + unitsAreCustom bool, // This will be true if we need to convert the shown units + convertToStandardFunction func(float64)(float64, error), + convertToCustomFunction func(float64)(float64, error), + showSecondaryForm bool, // Secondary form is an alternate representation of the bound. Example: Offspring variants + secondaryFormTitle string, + secondaryFormPrefix string, // A prefix to add to the secondary form representation. Example: "~" for offspring variants + getSecondaryFormFunction func(float64)(float64, error))(*fyne.Container, error){ + + getBoundColumn := func(minimumOrMaximum string)(*fyne.Container, error){ + + if (minimumOrMaximum != "Minimum" && minimumOrMaximum != "Maximum"){ + return nil, errors.New("getBoundColumn called with invalid minimumOrMaximum: " + minimumOrMaximum) + } + + desireBoundName := desireName + "_" + minimumOrMaximum + + boundLabel := getBoldLabelCentered(minimumOrMaximum) + + myBoundExists, myCurrentBound, err := myLocalDesires.GetDesire(desireBoundName) + if (err != nil) { return nil, err } + + getMyBoundSection := func()(*fyne.Container, error){ + + if (myBoundExists == false){ + noneLabel := getBoldItalicLabelCentered(translate("None")) + + if (showSecondaryForm == false){ + return noneLabel, nil + } + + noneLabel2 := getBoldItalicLabelCentered(translate("None")) + + secondaryFormTitle := getItalicLabelCentered(secondaryFormTitle + ":") + + myBoundSection := container.NewVBox(noneLabel, widget.NewSeparator(), secondaryFormTitle, noneLabel2) + + return myBoundSection, nil + } + + // This will convert the bound if unitsAreCustom is true + getMyCurrentBound := func()(float64, error){ + + myBoundFloat64, err := helpers.ConvertStringToFloat64(myCurrentBound) + if (err != nil){ + return 0, errors.New("My " + desireName + " desire malformed: not float64: " + myCurrentBound) + } + + if (unitsAreCustom == false){ + return myBoundFloat64, nil + } + + myBoundCustom, err := convertToCustomFunction(myBoundFloat64) + if (err != nil){ + return 0, errors.New("My " + desireName + " desire malformed: is negative: " + myCurrentBound) + } + + return myBoundCustom, nil + } + + myCurrentBound, err := getMyCurrentBound() + if (err != nil){ return nil, err } + + myCurrentBoundString := helpers.ConvertFloat64ToStringRounded(myCurrentBound, unitsRoundingPrecision) + + myBoundLabelText := myCurrentBoundString + " " + translate(currentUnits) + myBoundLabel := getBoldLabelCentered(myBoundLabelText) + + if (showSecondaryForm == false){ + + return myBoundLabel, nil + } + + secondaryFormBound, err := getSecondaryFormFunction(myCurrentBound) + if (err != nil){ return nil, err } + + secondaryFormBoundString := helpers.ConvertFloat64ToStringRounded(secondaryFormBound, unitsRoundingPrecision) + + secondaryFormTitle := getItalicLabelCentered(secondaryFormTitle + ":") + + mySecondaryFormBoundLabelText := secondaryFormPrefix + secondaryFormBoundString + " " + translate(currentUnits) + mySecondaryFormBoundLabel := getBoldItalicLabelCentered(mySecondaryFormBoundLabelText) + + myBoundSection := container.NewVBox(myBoundLabel, widget.NewSeparator(), secondaryFormTitle, mySecondaryFormBoundLabel) + + return myBoundSection, nil + } + + myBoundSection, err := getMyBoundSection() + if (err != nil) { return nil, err } + + minimumOrMaximumLowercase := strings.ToLower(minimumOrMaximum) + + boundEntry := widget.NewEntry() + + if (myBoundExists == true){ + + getMyCurrentBoundString := func()(string, error){ + + myCurrentBoundFloat64, err := helpers.ConvertStringToFloat64(myCurrentBound) + if (err != nil) { + return "", errors.New("My desires malformed: Contains invalid numeric desire: " + desireName) + } + if (myCurrentBoundFloat64 < float64(minimumAllowed) || myCurrentBoundFloat64 > float64(maximumAllowed)) { + return "", errors.New("My desires malformed: Contains invalid numeric desire: " + desireName) + } + + if (unitsAreCustom == false){ + myCurrentBoundRounded := helpers.ConvertFloat64ToStringRounded(myCurrentBoundFloat64, unitsRoundingPrecision) + return myCurrentBoundRounded, nil + } + + myCurrentBoundCustom, err := convertToCustomFunction(myCurrentBoundFloat64) + if (err != nil){ + return "", errors.New("My desires malformed: Contains invalid numeric desire: " + desireName) + } + + myCurrentBoundCustomString := helpers.ConvertFloat64ToStringRounded(myCurrentBoundCustom, unitsRoundingPrecision) + + return myCurrentBoundCustomString, nil + } + + myCurrentBoundString, err := getMyCurrentBoundString() + if (err != nil) { return nil, err } + + boundEntry.SetText(myCurrentBoundString) + + } else { + boundEntry.SetPlaceHolder(translate("Enter " + minimumOrMaximumLowercase + "...")) + } + + saveBoundButton := widget.NewButtonWithIcon(translate("Save"), theme.ConfirmIcon(), func(){ + + newBound := boundEntry.Text + + if (newBound == ""){ + + err := myLocalDesires.DeleteDesire(desireBoundName) + if (err != nil){ + setErrorEncounteredPage(window, err, currentPage) + return + } + currentPage() + return + } + + //TODO: If min is greater than max, or max is greater than min, show dialog and don't allow change + + newBoundFloat64, err := helpers.ConvertStringToFloat64(newBound) + if (err != nil){ + + title := translate("Invalid Bound") + dialogMessage := getLabelCentered(translate("Your bound must be a number.")) + dialogContent := container.NewVBox(dialogMessage) + dialog.ShowCustom(title, translate("Close"), dialogContent, window) + return + } + + getNewDesireBoundConverted := func()(float64, error){ + + if (unitsAreCustom == false){ + return newBoundFloat64, nil + } + + boundInStandardUnits, err := convertToStandardFunction(newBoundFloat64) + if (err != nil){ + return 0, errors.New("Invalid new numeric bound: " + err.Error()) + } + + return boundInStandardUnits, nil + } + + newDesireBoundConverted, err := getNewDesireBoundConverted() + if (err != nil){ + setErrorEncounteredPage(window, err, currentPage) + return + } + + if (newDesireBoundConverted < float64(minimumAllowed) || newDesireBoundConverted > float64(maximumAllowed)){ + + minimumAllowedString := helpers.ConvertInt64ToString(minimumAllowed) + maximumAllowedString := helpers.ConvertInt64ToString(maximumAllowed) + + dialogTitle := translate("Invalid Desire") + dialogMessage := getLabelCentered(translate("Your " + minimumOrMaximumLowercase + " must be a number between " + minimumAllowedString + " and " + maximumAllowedString + ".")) + dialogContent := container.NewVBox(dialogMessage) + dialog.ShowCustom(dialogTitle, translate("Close"), dialogContent, window) + return + } + + newDesireBoundString := helpers.ConvertFloat64ToStringRounded(newDesireBoundConverted, unitsRoundingPrecision) + + err = myLocalDesires.SetDesire(desireBoundName, newDesireBoundString) + if (err != nil){ + setErrorEncounteredPage(window, err, currentPage) + return + } + + currentPage() + }) + + saveBoundButtonWithEntry := container.NewGridWithRows(1, boundEntry, saveBoundButton) + + deleteBoundButton := widget.NewButtonWithIcon(translate("No Preference"), theme.CancelIcon(), func(){ + + if (myBoundExists == false){ + return + } + + err := myLocalDesires.DeleteDesire(desireBoundName) + if (err != nil){ + setErrorEncounteredPage(window, err, currentPage) + return + } + currentPage() + }) + + boundColumn := getContainerCentered(container.NewVBox(boundLabel, widget.NewSeparator(), myBoundSection, widget.NewSeparator(), saveBoundButtonWithEntry, deleteBoundButton)) + return boundColumn, nil + } + + minimumColumn, err := getBoundColumn("Minimum") + if (err != nil){ return nil, err } + + maximumColumn, err := getBoundColumn("Maximum") + if (err != nil){ return nil, err } + + boundColumns := getContainerCentered(container.NewGridWithRows(1, minimumColumn, maximumColumn)) + + filterOptionsSection, err := getDesireEditorFilterOptionsSection(window, currentPage, desireName, true) + if (err != nil){ return nil, err } + + editorContent := container.NewVBox(boundColumns, widget.NewSeparator(), filterOptionsSection) + + return editorContent, nil +} + +func getDesireEditorFilterOptionsSection(window fyne.Window, currentPage func(), desireName string, showRequireResponseButton bool)(*fyne.Container, error){ + + configureFiltersDescription := widget.NewLabel("Configure your desire filter options.") + + desireFilterOptionsHelpButton := widget.NewButtonWithIcon("", theme.QuestionIcon(), func(){ + setDesireFilterOptionsExplainerPage(window, currentPage) + }) + + desireFilterOptionsDescriptionRow := container.NewHBox(layout.NewSpacer(), configureFiltersDescription, desireFilterOptionsHelpButton, layout.NewSpacer()) + + getFilterAllButton := func()(fyne.Widget, error){ + + filterAllExists, currentFilterAllResponse, err := myLocalDesires.GetDesire(desireName + "_FilterAll") + if (err != nil) { return nil, err } + + if (filterAllExists == true && currentFilterAllResponse == "Yes"){ + button := widget.NewButtonWithIcon(translate("Filter All"), theme.CheckButtonCheckedIcon(), func(){ + _ = myLocalDesires.SetDesire(desireName + "_FilterAll", "No") + currentPage() + }) + return button, nil + } + button := widget.NewButtonWithIcon(translate("Filter All"), theme.CheckButtonIcon(), func(){ + _ = myLocalDesires.SetDesire(desireName + "_FilterAll", "Yes") + currentPage() + }) + return button, nil + } + + filterAllButton, err := getFilterAllButton() + if (err != nil) { return nil, err } + + if (showRequireResponseButton == false){ + + filterAllButtonCentered := getWidgetCentered(filterAllButton) + + filterOptionsSection := container.NewVBox(desireFilterOptionsDescriptionRow, filterAllButtonCentered) + + return filterOptionsSection, nil + } + + getRequireResponseButton := func()(fyne.Widget, error){ + + requireResponseExists, currentRequireResponse, err := myLocalDesires.GetDesire(desireName + "_RequireResponse") + if (err != nil) { return nil, err } + + if (requireResponseExists == true && currentRequireResponse == "Yes"){ + button := widget.NewButtonWithIcon(translate("Require Response"), theme.CheckButtonCheckedIcon(), func(){ + _ = myLocalDesires.SetDesire(desireName + "_RequireResponse", "No") + currentPage() + }) + return button, nil + } + button := widget.NewButtonWithIcon(translate("Require Response"), theme.CheckButtonIcon(), func(){ + _ = myLocalDesires.SetDesire(desireName + "_RequireResponse", "Yes") + currentPage() + }) + return button, nil + } + + requireResponseButton, err := getRequireResponseButton() + if (err != nil) { return nil, err } + + filterSettingsButtonsGrid := getContainerCentered(container.NewGridWithColumns(1, filterAllButton, requireResponseButton)) + + filterOptionsSection := container.NewVBox(desireFilterOptionsDescriptionRow, filterSettingsButtonsGrid) + + return filterOptionsSection, nil +} + + +// This is a page to view all of a user's desires +func setViewAllMyDesiresPage(window fyne.Window, previousPage func()){ + + setLoadingScreen(window, "My Mate Desires", "Loading My Mate Desires...") + + appMemory.SetMemoryEntry("CurrentViewedPage", "ViewAllMyDesires") + + currentPage := func(){setViewAllMyDesiresPage(window, previousPage)} + + title := getPageTitleCentered("My Mate Desires") + + backButton := getBackButtonCentered(previousPage) + + description := getLabelCentered("Below are all of your mate desires.") + + getAllDesiresGrid := func()(*fyne.Container, error){ + + desireButtonsColumn := container.NewVBox(widget.NewSeparator()) + desireNameAndDataColumn := container.NewVBox(widget.NewSeparator()) + filterOptionChecksColumn := container.NewVBox(widget.NewSeparator()) + + getFilterOptionCheck := func(desireName string, optionType string)(fyne.Widget, error){ + + if (optionType != "FilterAll" && optionType != "RequireResponse"){ + return nil, errors.New("getFilterOptionCheck called with invalid optionType: " + optionType) + } + + filterOptionDesireName := desireName + "_" + optionType + + getOptionTitle := func()string{ + + if (optionType == "FilterAll"){ + result := translate("Filter All") + return result + } + + result := translate("Require Response") + return result + } + + optionTitle := getOptionTitle() + + filterOptionCheck := widget.NewCheck(optionTitle, func(newSelection bool){ + + newSelectionString := helpers.ConvertBoolToYesOrNoString(newSelection) + _ = myLocalDesires.SetDesire(filterOptionDesireName, newSelectionString) + }) + + exists, currentValue, err := myLocalDesires.GetDesire(filterOptionDesireName) + if (err != nil) { return nil, err } + if (exists == true && currentValue == "Yes"){ + filterOptionCheck.Checked = true + } + + return filterOptionCheck, nil + } + + addDesireRow_Numerical := func(desireTitle string, desireName string, editPage func(), barOrDonutChart string, attributeName string)error{ + + editDesireButton := widget.NewButtonWithIcon("Edit", theme.DocumentCreateIcon(), editPage) + + desireTitleLabel := getBoldLabelCentered(desireTitle) + + getDesireValueLabel := func() (*fyne.Container, error){ + + minimumExists, desireMinimum, err := myLocalDesires.GetDesire(desireName + "_Minimum") + if (err != nil) { return nil, err } + + maximumExists, desireMaximum, err := myLocalDesires.GetDesire(desireName + "_Maximum") + if (err != nil) { return nil, err } + + if (minimumExists == false && maximumExists == false){ + noPreferenceLabel := getItalicLabelCentered("No Preference") + return noPreferenceLabel, nil + } + + // We may have to format the desire value + // For example: converting distance units from kilometers to miles + + _, attributeIsNumerical, formatDesireValuesFunction, desireValueUnits, _, err := attributeDisplay.GetProfileAttributeDisplayInfo(attributeName) + if (err != nil) { return nil, err } + if (attributeIsNumerical == false){ + return nil, errors.New("addDesireRow_Numerical called with non-numerical attribute: " + attributeName) + } + + getDesireRangeFormatted := func()(string, error){ + + if (minimumExists == true && maximumExists == false){ + + desireMinimumFormatted, err := formatDesireValuesFunction(desireMinimum) + if (err != nil) { return "", err } + + result := desireMinimumFormatted + "+" + + return result, nil + } + if (minimumExists == false && maximumExists == true){ + + desireMaximumFormatted, err := formatDesireValuesFunction(desireMaximum) + if (err != nil) { return "", err } + + result := "<" + desireMaximumFormatted + + return result, nil + } + + desireMinimumFormatted, err := formatDesireValuesFunction(desireMinimum) + if (err != nil) { return "", err } + + desireMaximumFormatted, err := formatDesireValuesFunction(desireMaximum) + if (err != nil) { return "", err } + + result := desireMinimumFormatted + " - " + desireMaximumFormatted + + return result, nil + } + + desireRangeFormatted, err := getDesireRangeFormatted() + if (err != nil) { return nil, err } + + labelText := desireRangeFormatted + desireValueUnits + + minMaxLabel := getBoldItalicLabelCentered(labelText) + + return minMaxLabel, nil + } + + desireValueLabel, err := getDesireValueLabel() + if (err != nil) { return err } + + filterAllCheck, err := getFilterOptionCheck(desireName, "FilterAll") + if (err != nil) { return err } + + requireResponseCheck, err := getFilterOptionCheck(desireName, "RequireResponse") + if (err != nil) { return err } + + viewStatisticsButton := widget.NewButtonWithIcon("Stats", theme.InfoIcon(), func(){ + + setViewMyMateDesireStatisticsPage(window, desireTitle, desireName, true, barOrDonutChart, attributeName, currentPage) + }) + + desireButtonsColumn.Add(editDesireButton) + desireButtonsColumn.Add(viewStatisticsButton) + + desireNameAndDataColumn.Add(desireTitleLabel) + desireNameAndDataColumn.Add(desireValueLabel) + + filterOptionChecksColumn.Add(filterAllCheck) + filterOptionChecksColumn.Add(requireResponseCheck) + + desireButtonsColumn.Add(widget.NewSeparator()) + desireNameAndDataColumn.Add(widget.NewSeparator()) + filterOptionChecksColumn.Add(widget.NewSeparator()) + + return nil + } + + addDesireRow_Choice := func(desireTitle string, desireName string, editPage func(), barOrDonutChart string, attributeName string)error{ + + editDesireButton := widget.NewButtonWithIcon("Edit", theme.DocumentCreateIcon(), editPage) + + desireTitleLabel := getBoldLabelCentered(desireTitle) + + getChoiceValueLabel := func()(*fyne.Container, error){ + + desireExists, desireChoicesList, err := myLocalDesires.GetDesire(desireName) + if (err != nil) { return nil, err } + + if (desireExists == false){ + noPreferenceLabel := getItalicLabelCentered(translate("No Preference")) + return noPreferenceLabel, nil + } + + + // We may have to format the desired choices for readability + // For example: Converting "1" -> "1/6" + + _, _, formatDesireValuesFunction, desireValueUnits, _, err := attributeDisplay.GetProfileAttributeDisplayInfo(attributeName) + if (err != nil) { return nil, err } + + getLabelText := func()(string, error){ + + base64ChoicesList := strings.Split(desireChoicesList, "+") + + choicesList := make([]string, 0, len(base64ChoicesList)) + + for _, base64Choice := range base64ChoicesList{ + + choiceString, err := encoding.DecodeBase64StringToUnicodeString(base64Choice) + if (err != nil) { + return "", errors.New("My Desire choices list is malformed: Contains non-base64 choice: " + base64Choice) + } + + choiceFormatted, err := formatDesireValuesFunction(choiceString) + if (err != nil) { return "", err } + + choiceFormattedWithUnits := choiceFormatted + desireValueUnits + + choicesList = append(choicesList, choiceFormattedWithUnits) + } + + choicesListJoined := strings.Join(choicesList, ", ") + choicesListTrimmed, _, err := helpers.TrimAndFlattenString(choicesListJoined, 35) + if (err != nil) { return "", err } + + return choicesListTrimmed, nil + } + + labelText, err := getLabelText() + if (err != nil) { return nil, err } + + desireValueLabel := getBoldItalicLabelCentered(labelText) + + return desireValueLabel, nil + } + + choiceValueLabel, err := getChoiceValueLabel() + if (err != nil) { return err } + + filterAllCheck, err := getFilterOptionCheck(desireName, "FilterAll") + if (err != nil) { return err } + + requireResponseCheck, err := getFilterOptionCheck(desireName, "RequireResponse") + if (err != nil) { return err } + + viewStatisticsButton := widget.NewButtonWithIcon("Stats", theme.InfoIcon(), func(){ + + setViewMyMateDesireStatisticsPage(window, desireTitle, desireName, true, barOrDonutChart, attributeName, currentPage) + }) + + desireButtonsColumn.Add(editDesireButton) + desireButtonsColumn.Add(viewStatisticsButton) + + desireNameAndDataColumn.Add(desireTitleLabel) + desireNameAndDataColumn.Add(choiceValueLabel) + + filterOptionChecksColumn.Add(filterAllCheck) + filterOptionChecksColumn.Add(requireResponseCheck) + + desireButtonsColumn.Add(widget.NewSeparator()) + desireNameAndDataColumn.Add(widget.NewSeparator()) + filterOptionChecksColumn.Add(widget.NewSeparator()) + + return nil + } + + // This function is used for desires which are not choice/numerical, so their value cannot be displayed automatically + //TODO: Add a function to format the desire value to something readable + addDesireRow_Custom := func(desireTitle string, desireName string, editPage func())error{ + + editDesireButton := widget.NewButtonWithIcon("Edit", theme.DocumentCreateIcon(), editPage) + + desireTitleLabel := getBoldLabelCentered(desireTitle) + + getDesireValueLabel := func() (*fyne.Container, error){ + + desireExists, _, err := myLocalDesires.GetDesire(desireName) + if (err != nil) { return nil, err } + if (desireExists == false){ + noPreferenceLabel := getItalicLabelCentered("No Preference") + return noPreferenceLabel, nil + } + + customLabel := getBoldItalicLabelCentered("Custom") + + return customLabel, nil + } + + desireValueLabel, err := getDesireValueLabel() + if (err != nil) { return err } + + filterAllCheck, err := getFilterOptionCheck(desireName, "FilterAll") + if (err != nil) { return err } + + requireResponseCheck, err := getFilterOptionCheck(desireName, "RequireResponse") + if (err != nil) { return err } + + desireButtonsColumn.Add(editDesireButton) + desireButtonsColumn.Add(widget.NewLabel("")) + + desireNameAndDataColumn.Add(desireTitleLabel) + desireNameAndDataColumn.Add(desireValueLabel) + + filterOptionChecksColumn.Add(filterAllCheck) + filterOptionChecksColumn.Add(requireResponseCheck) + + desireButtonsColumn.Add(widget.NewSeparator()) + desireNameAndDataColumn.Add(widget.NewSeparator()) + filterOptionChecksColumn.Add(widget.NewSeparator()) + + return nil + } + + editAgeDesirePage := func(){setChooseDesiresPage_Age(window, currentPage)} + err := addDesireRow_Numerical("Age", "Age", editAgeDesirePage, "Bar", "Age") + if (err != nil){ return nil, err } + + editHeightDesirePage := func(){setChooseDesiresPage_Height(window, currentPage)} + err = addDesireRow_Numerical("Height", "Height", editHeightDesirePage, "Bar", "Height") + if (err != nil){ return nil, err } + + editWealthDesirePage := func(){setChooseDesiresPage_Wealth(window, currentPage)} + err = addDesireRow_Numerical("Wealth", "Wealth", editWealthDesirePage, "Bar", "WealthInGold") + if (err != nil){ return nil, err } + + editSexDesirePage := func(){setChooseDesiresPage_Sex(window, currentPage)} + err = addDesireRow_Choice("Sex", "Sex", editSexDesirePage, "Donut", "Sex") + if (err != nil){ return nil, err } + + editProfileLanguageDesirePage := func(){setChooseDesiresPage_ProfileLanguage(window, currentPage)} + err = addDesireRow_Choice("Profile Language", "ProfileLanguage", editProfileLanguageDesirePage, "Bar", "ProfileLanguage") + if (err != nil){ return nil, err } + + editCountryDesirePage := func(){setChooseDesiresPage_Country(window, currentPage)} + err = addDesireRow_Choice("Country", "PrimaryLocationCountry", editCountryDesirePage, "Donut", "PrimaryLocationCountry") + if (err != nil){ return nil, err } + + editSearchTermsDesirePage := func(){setChooseDesiresPage_SearchTerms(window, currentPage)} + err = addDesireRow_Choice("Search Terms", "SearchTerms", editSearchTermsDesirePage, "Donut", "SearchTermsCount") + if (err != nil){ return nil, err } + + editHasMessagedMeDesirePage := func(){setChooseDesiresPage_HasMessagedMe(window, currentPage)} + err = addDesireRow_Choice("Has Messaged Me", "HasMessagedMe", editHasMessagedMeDesirePage, "Donut", "HasMessagedMe") + if (err != nil){ return nil, err } + + editIHaveMessagedDesirePage := func(){setChooseDesiresPage_IHaveMessaged(window, currentPage)} + err = addDesireRow_Choice("I Have Messaged", "IHaveMessaged", editIHaveMessagedDesirePage, "Donut", "IHaveMessaged") + if (err != nil){ return nil, err } + + editHasRejectedMeDesirePage := func(){setChooseDesiresPage_HasRejectedMe(window, currentPage)} + err = addDesireRow_Choice("Has Rejected Me", "HasRejectedMe", editHasRejectedMeDesirePage, "Donut", "HasRejectedMe") + if (err != nil){ return nil, err } + + editIsLikedDesirePage := func(){setChooseDesiresPage_LikedUsers(window, currentPage)} + err = addDesireRow_Choice("Is Liked", "IsLiked", editIsLikedDesirePage, "Donut", "IsLiked") + if (err != nil){ return nil, err } + + editIsIgnoredDesirePage := func(){setChooseDesiresPage_IgnoredUsers(window, currentPage)} + err = addDesireRow_Choice("Is Ignored", "IsIgnored", editIsIgnoredDesirePage, "Donut", "IsIgnored") + if (err != nil){ return nil, err } + + editIsMyContactDesirePage := func(){setChooseDesiresPage_Contacts(window, currentPage)} + err = addDesireRow_Choice("Is My Contact", "IsMyContact", editIsMyContactDesirePage, "Donut", "IsMyContact") + if (err != nil){ return nil, err } + + editDistanceDesirePage := func(){setChooseDesiresPage_Distance(window, currentPage)} + err = addDesireRow_Numerical("Distance", "Distance", editDistanceDesirePage, "Bar", "Distance") + if (err != nil){ return nil, err } + + editSexualityDesirePage := func(){setChooseDesiresPage_Sexuality(window, currentPage)} + err = addDesireRow_Choice("Sexuality", "Sexuality", editSexualityDesirePage, "Donut", "Sexuality") + if (err != nil){ return nil, err } + + editBodyFatDesirePage := func(){setChooseDesiresPage_BodyFat(window, currentPage)} + err = addDesireRow_Choice("Body Fat", "BodyFat", editBodyFatDesirePage, "Donut", "BodyFat") + if (err != nil){ return nil, err } + + editBodyMuscleDesirePage := func(){setChooseDesiresPage_BodyMuscle(window, currentPage)} + err = addDesireRow_Choice("Body Muscle", "BodyMuscle", editBodyMuscleDesirePage, "Donut", "BodyMuscle") + if (err != nil){ return nil, err } + + editEyeColorDesirePage := func(){setChooseDesiresPage_EyeColor(window, currentPage)} + err = addDesireRow_Choice("Eye Color", "EyeColor", editEyeColorDesirePage, "Donut", "EyeColor") + if (err != nil){ return nil, err } + + editSkinColorDesirePage := func(){setChooseDesiresPage_SkinColor(window, currentPage)} + err = addDesireRow_Choice("Skin Color", "SkinColor", editSkinColorDesirePage, "Donut", "SkinColor") + if (err != nil){ return nil, err } + + editHairColorDesirePage := func(){setChooseDesiresPage_HairColor(window, currentPage)} + err = addDesireRow_Choice("Hair Color", "HairColor", editHairColorDesirePage, "Donut", "HairColor") + if (err != nil){ return nil, err } + + editHairTextureDesirePage := func(){setChooseDesiresPage_HairTexture(window, currentPage)} + err = addDesireRow_Choice("Hair Texture", "HairTexture", editHairTextureDesirePage, "Donut", "HairTexture") + if (err != nil){ return nil, err } + + editHasHIVDesirePage := func(){setChooseDesiresPage_Infections(window, currentPage)} + err = addDesireRow_Choice("Has HIV", "HasHIV", editHasHIVDesirePage, "Donut", "HasHIV") + if (err != nil){ return nil, err } + + editHasGenitalHerpesDesirePage := func(){setChooseDesiresPage_Infections(window, currentPage)} + err = addDesireRow_Choice("Has Genital Herpes", "HasGenitalHerpes", editHasGenitalHerpesDesirePage, "Donut", "HasGenitalHerpes") + if (err != nil){ return nil, err } + + foodNamesList := []string{ + "Fruit", + "Vegetables", + "Nuts", + "Grains", + "Dairy", + "Seafood", + "Beef", + "Pork", + "Poultry", + "Eggs", + "Beans", + } + + for _, foodName := range foodNamesList{ + + foodRatingDesireTitle := foodName + " Rating" + foodRatingDesireName := foodName + "Rating" + + editFoodRatingDesirePage := func(){setChooseDesiresPage_FoodRating(window, foodName, currentPage)} + err = addDesireRow_Choice(foodRatingDesireTitle, foodRatingDesireName, editFoodRatingDesirePage, "Bar", foodRatingDesireName) + if (err != nil){ return nil, err } + } + + editFameDesirePage := func(){setChooseDesiresPage_Fame(window, currentPage)} + err = addDesireRow_Choice("Fame", "Fame", editFameDesirePage, "Bar", "Fame") + if (err != nil){ return nil, err } + + editAlcoholFrequencyDesirePage := func(){setChooseDesiresPage_DrugFrequency(window, "Alcohol", currentPage)} + err = addDesireRow_Choice("Alcohol Frequency", "AlcoholFrequency", editAlcoholFrequencyDesirePage, "Bar", "AlcoholFrequency") + if (err != nil){ return nil, err } + + editTobaccoFrequencyDesirePage := func(){setChooseDesiresPage_DrugFrequency(window, "Tobacco", currentPage)} + err = addDesireRow_Choice("Tobacco Frequency", "TobaccoFrequency", editTobaccoFrequencyDesirePage, "Bar", "TobaccoFrequency") + if (err != nil){ return nil, err } + + editCannabisFrequencyDesirePage := func(){setChooseDesiresPage_DrugFrequency(window, "Cannabis", currentPage)} + err = addDesireRow_Choice("Cannabis Frequency", "CannabisFrequency", editCannabisFrequencyDesirePage, "Bar", "CannabisFrequency") + if (err != nil){ return nil, err } + + editGenderIdentityDesirePage := func(){setChooseDesiresPage_GenderIdentity(window, currentPage)} + err = addDesireRow_Choice("Gender Identity", "GenderIdentity", editGenderIdentityDesirePage, "Donut", "GenderIdentity") + if (err != nil){ return nil, err } + + editPetsRatingDesirePage := func(){setChooseDesiresPage_Pets(window, currentPage)} + err = addDesireRow_Choice("Pets Rating", "PetsRating", editPetsRatingDesirePage, "Bar", "PetsRating") + if (err != nil){ return nil, err } + + editDogsRatingDesirePage := func(){setChooseDesiresPage_Pets(window, currentPage)} + err = addDesireRow_Choice("Dogs Rating", "DogsRating", editDogsRatingDesirePage, "Bar", "DogsRating") + if (err != nil){ return nil, err } + + editCatsRatingDesirePage := func(){setChooseDesiresPage_Pets(window, currentPage)} + err = addDesireRow_Choice("Cats Rating", "CatsRating", editCatsRatingDesirePage, "Bar", "CatsRating") + if (err != nil){ return nil, err } + + editOffspringProbabilityOfAnyMonogenicDiseaseDesirePage := func(){setChooseDesiresPage_MonogenicDiseases(window, currentPage)} + err = addDesireRow_Numerical("Offspring Probability Of Any Monogenic Disease", "OffspringProbabilityOfAnyMonogenicDisease", editOffspringProbabilityOfAnyMonogenicDiseaseDesirePage, "Donut", "OffspringProbabilityOfAnyMonogenicDisease") + if (err != nil){ return nil, err } + + edit23andMeMaternalHaplogroupDesirePage := func(){setChooseDesiresPage_23andMe_Haplogroup(window, "Maternal", currentPage)} + err = addDesireRow_Choice("23andMe Maternal Haplogroup", "23andMe_MaternalHaplogroup", edit23andMeMaternalHaplogroupDesirePage, "Bar", "23andMe_MaternalHaplogroup") + if (err != nil){ return nil, err } + + edit23andMePaternalHaplogroupDesirePage := func(){setChooseDesiresPage_23andMe_Haplogroup(window, "Paternal", currentPage)} + err = addDesireRow_Choice("23andMe Paternal Haplogroup", "23andMe_PaternalHaplogroup", edit23andMePaternalHaplogroupDesirePage, "Bar", "23andMe_PaternalHaplogroup") + if (err != nil){ return nil, err } + + edit23andMeNeanderthalVariantsDesirePage := func(){setChooseDesiresPage_23andMe_NeanderthalVariants(window, currentPage)} + err = addDesireRow_Numerical("23andMe Neanderthal Variants", "23andMe_NeanderthalVariants", edit23andMeNeanderthalVariantsDesirePage, "Donut", "23andMe_NeanderthalVariants") + if (err != nil){ return nil, err } + + edit23andMeAncestryCompositionDesirePage := func(){setChooseDesiresPage_23andMe_AncestryComposition(window, "User", currentPage)} + err = addDesireRow_Custom("23andMe Ancestry Composition", "23andMe_AncestryComposition", edit23andMeAncestryCompositionDesirePage) + if (err != nil){ return nil, err } + + editLanguageDesirePage := func(){setChooseDesiresPage_Language(window, currentPage)} + err = addDesireRow_Custom("Language", "Language", editLanguageDesirePage) + if (err != nil){ return nil, err } + + allDesiresGrid := container.NewHBox(layout.NewSpacer(), widget.NewSeparator(), desireButtonsColumn, widget.NewSeparator(), desireNameAndDataColumn, widget.NewSeparator(), filterOptionChecksColumn, widget.NewSeparator(), layout.NewSpacer()) + + return allDesiresGrid, nil + } + + allDesiresGrid, err := getAllDesiresGrid() + if (err != nil) { + setErrorEncounteredPage(window, err, previousPage) + return + } + + page := container.NewVBox(title, backButton, widget.NewSeparator(), description, widget.NewSeparator(), allDesiresGrid) + + setPageContent(page, window) +} + + +func setManageMateDownloadDesiresPage(window fyne.Window, previousPage func()){ + + currentPage := func(){setManageMateDownloadDesiresPage(window, previousPage)} + + title := getPageTitleCentered("My Mate Download Desires") + + backButton := getBackButtonCentered(previousPage) + + subtitle := getPageSubtitleCentered("My Mate Download Desires") + + description1 := getLabelCentered("Seekia downloads mate profiles from network hosts.") + description2 := getLabelCentered("These are servers run by volunteers.") + description3 := getLabelCentered("Your download desires are shared to hosts when you download profiles.") + description4 := getLabelCentered("These desires limit the profiles you download.") + description5 := getLabelCentered("You can choose how much information to share.") + description6 := getLabelCentered("The more desires you share, the less time it will take to download profiles.") + description7 := getLabelCentered("The hosts could be tracking this information, so more desires will reduce your privacy.") + description8 := getLabelCentered("All profiles are downloaded over the Tor anonymity network.") + + chooseDesiresButton := getWidgetCentered(widget.NewButtonWithIcon("Choose Desires", theme.NavigateNextIcon(), func(){ + setChooseMateDownloadDesiresPage(window, currentPage) + })) + + page := container.NewVBox(title, backButton, widget.NewSeparator(), subtitle, widget.NewSeparator(), description1, description2, description3, description4, description5, description6, description7, description8, chooseDesiresButton) + + setPageContent(page, window) +} + + + +func setChooseMateDownloadDesiresPage(window fyne.Window, previousPage func()){ + + setLoadingScreen(window, "Choose Mate Download Desires", "Loading Download Desires...") + + currentPage := func(){setChooseMateDownloadDesiresPage(window, previousPage)} + + title := getPageTitleCentered("Choose Mate Download Desires") + + backButton := getBackButtonCentered(previousPage) + + description1 := getLabelCentered("You will share the selected desires with Hosts when you download profiles.") + description2 := getLabelCentered("These desires will not be displayed on your profile.") + description3 := getLabelCentered("Each desire will only be shared if you have enabled Filter All or Require Response") + description4 := getLabelCentered("Select each desire you are comfortable sharing with hosts.") + + //TODO: Add Help buttons to explain this better + // User should understand that the hosts will not necessarily know who they are. + // They will make requests over Tor, and their requestor fingerprint will be defined by these desires + // Each shared desire will increase the entropy of a requestor's fingerprint + // Most requestors will probably have a very unique fingerprint + // + // The challenge for malicious hosts will be to link a requestor fingerprint to a requestor's mate identity + // This may be difficult or easy, depending on how similar the requestor's desires are to their own profile + + getDesiresGrid := func()(*fyne.Container, error){ + + desireTitlesColumn := container.NewVBox() + desireSelectChecksColumn := container.NewVBox() + + desireNameLabel := getItalicLabelCentered("Desire Name:") + shareDesireLabel := getItalicLabelCentered("Share Desire?") + + desireTitlesColumn.Add(desireNameLabel) + desireSelectChecksColumn.Add(shareDesireLabel) + + desireTitlesColumn.Add(widget.NewSeparator()) + desireSelectChecksColumn.Add(widget.NewSeparator()) + + allMyDownloadDesiresList := myMateCriteria.GetAllMyMateDownloadDesiresList() + + for _, desireName := range allMyDownloadDesiresList{ + + desireTitle, err := mateDesires.GetDesireTitleFromDesireName(desireName) + if (err != nil) { return nil, err } + + desireTitleLabel := getBoldLabelCentered(translate(desireTitle)) + + getDesireSelectCheck := func()(fyne.Widget, error){ + + selectCheck := widget.NewCheck("", func(selected bool){ + + err := myMateCriteria.SetShareMyDesireStatus(desireName, selected) + if (err != nil){ + setErrorEncounteredPage(window, err, currentPage) + return + } + }) + + isSelected, err := myMateCriteria.GetShareMyDesireStatus(desireName) + if (err != nil){ return nil, err } + if (isSelected == true){ + selectCheck.Checked = true + } + + return selectCheck, nil + } + + desireSelectCheck, err := getDesireSelectCheck() + if (err != nil) { return nil, err } + + desireSelectCheckCentered := getWidgetCentered(desireSelectCheck) + + desireTitlesColumn.Add(desireTitleLabel) + desireSelectChecksColumn.Add(desireSelectCheckCentered) + + desireTitlesColumn.Add(widget.NewSeparator()) + desireSelectChecksColumn.Add(widget.NewSeparator()) + } + + desiresGrid := container.NewHBox(layout.NewSpacer(), desireTitlesColumn, desireSelectChecksColumn, layout.NewSpacer()) + + return desiresGrid, nil + } + + desiresGrid, err := getDesiresGrid() + if (err != nil){ + setErrorEncounteredPage(window, err, previousPage) + return + } + + page := container.NewVBox(title, backButton, widget.NewSeparator(), description1, description2, description3, description4, widget.NewSeparator(), desiresGrid) + + setPageContent(page, window) +} + + diff --git a/gui/desiresGui_Lifestyle.go b/gui/desiresGui_Lifestyle.go new file mode 100644 index 0000000..a95ec17 --- /dev/null +++ b/gui/desiresGui_Lifestyle.go @@ -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) +} + + + diff --git a/gui/desiresGui_Mental.go b/gui/desiresGui_Mental.go new file mode 100644 index 0000000..5872422 --- /dev/null +++ b/gui/desiresGui_Mental.go @@ -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) +} + + + diff --git a/gui/desiresGui_Physical.go b/gui/desiresGui_Physical.go new file mode 100644 index 0000000..a59143c --- /dev/null +++ b/gui/desiresGui_Physical.go @@ -0,0 +1,2325 @@ +package gui + +// desiresGui_Physical.go implements pages to manage a user's Physical mate desires + +import "fyne.io/fyne/v2" +import "fyne.io/fyne/v2/canvas" +import "fyne.io/fyne/v2/container" +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/allowedText" +import "seekia/internal/desires/mateDesires" +import "seekia/internal/desires/myLocalDesires" +import "seekia/internal/encoding" +import "seekia/internal/genetics/companyAnalysis" +import "seekia/internal/globalSettings" +import "seekia/internal/helpers" +import "seekia/internal/profiles/myLocalProfiles" + +import "strings" +import "errors" +import "slices" + + +func setChooseDesiresCategoryPage_Physical(window fyne.Window, previousPage func()){ + + currentPage := func(){setChooseDesiresCategoryPage_Physical(window, previousPage)} + + title := getPageTitleCentered(translate("My Mate Desires - Physical")) + + backButton := getBackButtonCentered(previousPage) + + sexButton := widget.NewButton(translate("Sex"), func(){ + setChooseDesiresPage_Sex(window, currentPage) + }) + + ageButton := widget.NewButton(translate("Age"), func(){ + setChooseDesiresPage_Age(window, currentPage) + }) + + ancestryCompositionButton := widget.NewButton("Ancestry Composition", func(){ + setChooseDesiresPage_23andMe_AncestryComposition(window, "User", currentPage) + }) + + neanderthalVariantsButton := widget.NewButton("Neanderthal Variants", func(){ + setChooseDesiresPage_23andMe_NeanderthalVariants(window, currentPage) + }) + + maternalHaplogroupButton := widget.NewButton("Maternal Haplogroup", func(){ + setChooseDesiresPage_23andMe_Haplogroup(window, "Maternal", currentPage) + }) + + paternalHaplogroupButton := widget.NewButton("Paternal Haplogroup", func(){ + setChooseDesiresPage_23andMe_Haplogroup(window, "Paternal", currentPage) + }) + + monogenicDiseasesButton := widget.NewButton("Monogenic Diseases", func(){ + setChooseDesiresPage_MonogenicDiseases(window, currentPage) + }) + + polygenicDiseasesButton := widget.NewButton("Polygenic Diseases", func(){ + //TODO: Filter based on minimum variants tested + // Users will be able to sort matches based on lowest overall disease risk + showUnderConstructionDialog(window) + }) + geneticTraitsButton := widget.NewButton("Genetic Traits", func(){ + //TODO: Filter based on number of variants tested + // Users will be able to sort matches based on highest probability of blue eyes, brown eyes, straight hair, curly hair, etc.. + // Also, users should be able to filter based on outcome points for each trait outcome + showUnderConstructionDialog(window) + }) + + heightButton := widget.NewButton(translate("Height"), func(){ + setChooseDesiresPage_Height(window, currentPage) + }) + + bodyFatButton := widget.NewButton(translate("Body Fat"), func(){ + setChooseDesiresPage_BodyFat(window, currentPage) + }) + bodyMuscleButton := widget.NewButton(translate("Body Muscle"), func(){ + setChooseDesiresPage_BodyMuscle(window, currentPage) + }) + + eyeColorButton := widget.NewButton(translate("Eye Color"), func(){ + setChooseDesiresPage_EyeColor(window, currentPage) + }) + + hairColorButton := widget.NewButton(translate("Hair Color"), func(){ + setChooseDesiresPage_HairColor(window, currentPage) + }) + + hairTextureButton := widget.NewButton(translate("Hair Texture"), func(){ + setChooseDesiresPage_HairTexture(window, currentPage) + }) + + skinColorButton := widget.NewButton(translate("Skin Color"), func(){ + setChooseDesiresPage_SkinColor(window, currentPage) + }) + + infectionsButton := widget.NewButton(translate("Infections"), func(){ + setChooseDesiresPage_Infections(window, currentPage) + }) + + buttonsGrid := getContainerCentered(container.NewGridWithColumns(1, sexButton, ageButton, ancestryCompositionButton, neanderthalVariantsButton, maternalHaplogroupButton, paternalHaplogroupButton, monogenicDiseasesButton, polygenicDiseasesButton, geneticTraitsButton, heightButton, bodyFatButton, bodyMuscleButton, eyeColorButton, hairColorButton, hairTextureButton, skinColorButton, infectionsButton)) + + buttonsGridPadded := container.NewPadded(buttonsGrid) + + page := container.NewVBox(title, backButton, widget.NewSeparator(), buttonsGridPadded) + + setPageContent(page, window) +} + +func setChooseDesiresPage_Sex(window fyne.Window, previousPage func()){ + + currentPage := func(){setChooseDesiresPage_Sex(window, previousPage)} + + title := getPageTitleCentered(translate("My Mate Desires - Physical")) + + backButton := getBackButtonCentered(previousPage) + + subtitle := getPageSubtitleCentered(translate("Sex")) + + descriptionText := widget.NewLabel("Choose the sex(es) that you desire.") + + sexHelpButton := widget.NewButtonWithIcon("", theme.QuestionIcon(), func(){ + setSexExplainerPage(window, currentPage) + }) + + descriptionRow := container.NewHBox(layout.NewSpacer(), descriptionText, sexHelpButton, layout.NewSpacer()) + + optionTitlesList := []string{translate("Male"), translate("Female"), translate("Intersex Male"), translate("Intersex Female"), translate("Intersex")} + + optionNamesMap := make(map[string][]string) + + optionNamesMap[translate("Male")] = []string{"Male"} + optionNamesMap[translate("Female")] = []string{"Female"} + optionNamesMap[translate("Intersex Male")] = []string{"Intersex Male"} + optionNamesMap[translate("Intersex Female")] = []string{"Intersex Female"} + optionNamesMap[translate("Intersex")] = []string{"Intersex"} + + desireEditor, err := getDesireEditor_Choice(window, currentPage, "Sex", optionTitlesList, optionNamesMap, false, true, 2) + if (err != nil){ + setErrorEncounteredPage(window, err, previousPage) + return + } + + viewStatisticsButton := getWidgetCentered(widget.NewButtonWithIcon("View Statistics", theme.InfoIcon(), func(){ + setViewMyMateDesireStatisticsPage(window, "Sex", "Sex", true, "Donut", "Sex", currentPage) + })) + + page := container.NewVBox(title, backButton, widget.NewSeparator(), subtitle, widget.NewSeparator(), descriptionRow, widget.NewSeparator(), desireEditor, widget.NewSeparator(), viewStatisticsButton) + + setPageContent(page, window) +} + +func setChooseDesiresPage_23andMe_AncestryComposition(window fyne.Window, userOrOffspring string, previousPage func()){ + + currentPage := func(){setChooseDesiresPage_23andMe_AncestryComposition(window, userOrOffspring, previousPage)} + + title := getPageTitleCentered(translate("My Mate Desires - Physical")) + + backButton := getBackButtonCentered(previousPage) + + subtitle := getPageSubtitleCentered("Ancestry Composition") + + description1 := getLabelCentered("Choose your ancestry composition desires.") + description2 := getLabelCentered("Each location is represented by a desired percentage range.") + + getRestrictiveModeIsEnabledBool := func()(bool, error){ + + myDesireExists, restrictiveModeEnabled, err := myLocalDesires.GetDesire("23andMe_AncestryComposition_RestrictiveModeEnabled") + if (err != nil) { return false, err } + + if (myDesireExists == true && restrictiveModeEnabled == "Yes"){ + return true, nil + } + + return false, nil + } + + restrictiveModeIsEnabled, err := getRestrictiveModeIsEnabledBool() + if (err != nil){ + setErrorEncounteredPage(window, err, previousPage) + return + } + + restrictiveModeCheck := widget.NewCheck("Restrictive Mode", func(response bool){ + + newValue := helpers.ConvertBoolToYesOrNoString(response) + + err := myLocalDesires.SetDesire("23andMe_AncestryComposition_RestrictiveModeEnabled", newValue) + if (err != nil){ + setErrorEncounteredPage(window, err, currentPage) + return + } + + currentPage() + }) + restrictiveModeCheck.Checked = restrictiveModeIsEnabled + + restrictiveModeHelpButton := widget.NewButtonWithIcon("", theme.QuestionIcon(), func(){ + setAncestryCompositionDesireRestrictiveModeExplainerPage(window, currentPage) + }) + + restrictiveModeRow := container.NewHBox(layout.NewSpacer(), restrictiveModeCheck, restrictiveModeHelpButton, layout.NewSpacer()) + + chooseLocationDesiresButton := getWidgetCentered(widget.NewButtonWithIcon("Choose Location Desires", theme.NavigateNextIcon(), func(){ + setChooseDesiresPage_23andMe_ViewAncestryComposition(window, restrictiveModeIsEnabled, "User", currentPage) + })) + + getDesireName := func()string{ + if (restrictiveModeIsEnabled == false){ + return "23andMe_AncestryComposition" + } + return "23andMe_AncestryComposition_Restrictive" + } + + desireName := getDesireName() + + filterOptionsSection, err := getDesireEditorFilterOptionsSection(window, currentPage, desireName, true) + if (err != nil){ + setErrorEncounteredPage(window, err, previousPage) + return + } + + getDesireTitle := func()string{ + + if (restrictiveModeIsEnabled == false){ + return "23andMe Ancestry Composition" + } + return "23andMe Ancestry Composition (Restrictive)" + } + + desireTitle := getDesireTitle() + + viewStatisticsButton := getWidgetCentered(widget.NewButtonWithIcon("View Statistics", theme.InfoIcon(), func(){ + setViewMyMateDesireStatisticsPage(window, desireTitle, desireName, false, "", "", currentPage) + })) + + page := container.NewVBox(title, backButton, widget.NewSeparator(), subtitle, widget.NewSeparator(), description1, description2, widget.NewSeparator(), restrictiveModeRow, widget.NewSeparator(), chooseLocationDesiresButton, widget.NewSeparator(), filterOptionsSection, widget.NewSeparator(), viewStatisticsButton) + + setPageContent(page, window) +} + +func setChooseDesiresPage_23andMe_ViewAncestryComposition(window fyne.Window, restrictiveModeIsEnabled bool, userOrOffspring string, previousPage func()){ + + currentPage := func(){setChooseDesiresPage_23andMe_ViewAncestryComposition(window, restrictiveModeIsEnabled, userOrOffspring, previousPage)} + + title := getPageTitleCentered(translate("My Mate Desires - Physical")) + + backButton := getBackButtonCentered(previousPage) + + getSubtitleText := func()string{ + if (restrictiveModeIsEnabled == false){ + return "23andMe - Ancestry Composition" + } + return "23andMe - Ancestry Composition (Restrictive)" + } + + subtitleText := getSubtitleText() + + subtitle := getPageSubtitleCentered(subtitleText) + + description1 := getLabelCentered("You can toggle between viewing your user or offspring desires.") + description2Label := widget.NewLabel("Your offspring desires are calculated using your profile ancestry composition.") + + offspringAncestryCompositionHelpButton := widget.NewButtonWithIcon("", theme.QuestionIcon(), func(){ + setOffspringAncestryCompositionExplainerPage(window, currentPage) + }) + description2Row := container.NewHBox(layout.NewSpacer(), description2Label, offspringAncestryCompositionHelpButton, layout.NewSpacer()) + + userOrOffspringList := []string{"User", "Offspring"} + userOrOffspringSelector := widget.NewSelect(userOrOffspringList, func(newUserOrOffspring string){ + setChooseDesiresPage_23andMe_ViewAncestryComposition(window, restrictiveModeIsEnabled, newUserOrOffspring, previousPage) + }) + userOrOffspringSelector.Selected = userOrOffspring + + userOrOffspringSelectorCentered := getWidgetCentered(userOrOffspringSelector) + + getDesireName := func()string{ + if (restrictiveModeIsEnabled == false){ + return "23andMe_AncestryComposition" + } + return "23andMe_AncestryComposition_Restrictive" + } + + desireName := getDesireName() + + modifyDesiresButton := getWidgetCentered(widget.NewButtonWithIcon("Modify Desires", theme.DocumentCreateIcon(), func(){ + setChooseDesiresPage_23andMe_EditAncestryComposition(window, restrictiveModeIsEnabled, false, "", false, "", false, "", currentPage) + })) + + myDesireExists, myDesiredAncestryComposition, err := myLocalDesires.GetDesire(desireName) + if (err != nil){ + setErrorEncounteredPage(window, err, previousPage) + return + } + if (myDesireExists == false){ + + noLocationsAddedLabel := getBoldLabelCentered("No locations added.") + + page := container.NewVBox(title, backButton, widget.NewSeparator(), subtitle, widget.NewSeparator(), description1, description2Row, widget.NewSeparator(), userOrOffspringSelectorCentered, widget.NewSeparator(), noLocationsAddedLabel, modifyDesiresButton) + + setPageContent(page, window) + return + } + + //Outputs: + // -bool: My ancestry composition exists + // -map[string]float64: My continent percentages map + // -map[string]float64: My region percentages map + // -map[string]float64: My subregion percentages map + // -error + getMyAncestryCompositionMaps := func()(bool, map[string]float64, map[string]float64, map[string]float64, error){ + + myAncestryCompositionExists, myCurrentAncestryComposition, err := myLocalProfiles.GetProfileData("Mate", "23andMe_AncestryComposition") + if (err != nil){ return false, nil, nil, nil, err } + if (myAncestryCompositionExists == false){ + return false, nil, nil, nil, nil + } + + attributeIsValid, myContinentPercentagesMap, myRegionPercentagesMap, mySubregionPercentagesMap, err := companyAnalysis.ReadAncestryCompositionAttribute_23andMe(true, myCurrentAncestryComposition) + if (err != nil) { return false, nil, nil, nil, err } + if (attributeIsValid == false){ + return false, nil, nil, nil, errors.New("MyLocalProfiles contains invalid 23andMe ancestry composition attribute: " + myCurrentAncestryComposition) + } + + mapsAreValid, myFilledContinentPercentagesMap, myFilledRegionPercentagesMap, myFilledSubregionPercentagesMap, err := companyAnalysis.AddMissingParentsToAncestryCompositionMaps_23andMe(myContinentPercentagesMap, myRegionPercentagesMap, mySubregionPercentagesMap) + if (err != nil){ return false, nil, nil, nil, err } + if (mapsAreValid == false){ + return false, nil, nil, nil, errors.New("ReadAncestryCompositionAttribute_23andMe not verifying maps.") + } + + return true, myFilledContinentPercentagesMap, myFilledRegionPercentagesMap, myFilledSubregionPercentagesMap, nil + } + + myAncestryCompositionExists, myContinentPercentagesMap, myRegionPercentagesMap, mySubregionPercentagesMap, err := getMyAncestryCompositionMaps() + if (err != nil) { + setErrorEncounteredPage(window, err, previousPage) + return + } + + if (userOrOffspring == "Offspring" && myAncestryCompositionExists == false){ + + description3 := getBoldLabelCentered("Your ancestry composition does not exist.") + description4 := getLabelCentered("To view your offspring desires, you must add your ancestry composition.") + description5 := getLabelCentered("Add it on the Build Profile - Physical - Race - 23andMe - Ancestry Composition page.") + + page := container.NewVBox(title, backButton, widget.NewSeparator(), subtitle, widget.NewSeparator(), description1, description2Row, widget.NewSeparator(), modifyDesiresButton, widget.NewSeparator(), userOrOffspringSelectorCentered, widget.NewSeparator(), description3, description4, description5) + + setPageContent(page, window) + return + } + + //Outputs: + // -map[string][]string: Location description -> List of sub-locations descriptions + // -error + getDesiredLocationTreeMap := func()(map[string][]string, error){ + + myContinentMinimumBoundsMap, myContinentMaximumBoundsMap, myRegionMinimumBoundsMap, myRegionMaximumBoundsMap, mySubregionMinimumBoundsMap, mySubregionMaximumBoundsMap, err := mateDesires.ReadAncestryCompositionDesire_23andMe(myDesiredAncestryComposition) + if (err != nil){ + return nil, errors.New("MyLocalDesires contains invalid ancestry composition desire: " + err.Error()) + } + + // This function will convert the range to an offspring range (if userOrOffspring == "Offspring") + getLocationDescription := func(locationType string, locationName string, userMinimumDesiredBound float64, userMaximumDesiredBound float64)(string, error){ + + if (locationType != "Continent" && locationType != "Region" && locationType != "Subregion"){ + return "", errors.New("getLocationDescription called with invalid locationType: " + locationType) + } + + getMinimumAndMaximumBounds := func()(float64, float64){ + + if (userOrOffspring == "User"){ + return userMinimumDesiredBound, userMaximumDesiredBound + } + + // We calculate offspring range + + getMyLocationPercentage := func()float64{ + + if (locationType == "Continent"){ + myPercentage, exists := myContinentPercentagesMap[locationName] + if (exists == false){ + return 0 + } + return myPercentage + } + if (locationType == "Region"){ + myPercentage, exists := myRegionPercentagesMap[locationName] + if (exists == false){ + return 0 + } + return myPercentage + } + // locationType == "Subregion + myPercentage, exists := mySubregionPercentagesMap[locationName] + if (exists == false){ + return 0 + } + return myPercentage + } + + myLocationPercentage := getMyLocationPercentage() + + offspringMinimumBound := (userMinimumDesiredBound + myLocationPercentage)/2 + offspringMaximumBound := (userMaximumDesiredBound + myLocationPercentage)/2 + + return offspringMinimumBound, offspringMaximumBound + } + + minimumDesiredBound, maximumDesiredBound := getMinimumAndMaximumBounds() + + minimumDesiredBoundString := helpers.ConvertFloat64ToStringRounded(minimumDesiredBound, 1) + maximumDesiredBoundString := helpers.ConvertFloat64ToStringRounded(maximumDesiredBound, 1) + + if (minimumDesiredBoundString == maximumDesiredBoundString){ + locationDescription := locationName + ": " + minimumDesiredBoundString + "%" + return locationDescription, nil + } + + locationDescription := locationName + ": " + minimumDesiredBoundString + "-" + maximumDesiredBoundString + "%" + + return locationDescription, nil + } + + // We now build the tree map + // This is used to display a fyne tree widget + // Each item is mapped to a list of child items + // The "" item represents the root of the tree + + treeMap := make(map[string][]string) + + // This list will store the desired continent descriptions + desiredContinentsList := make([]string, 0) + + allContinentsList := companyAnalysis.GetAncestryContinentsList_23andMe() + + for _, continentName := range allContinentsList{ + + continentRegionsList, err := companyAnalysis.GetAncestryContinentRegionsList_23andMe(continentName) + if (err != nil){ + return nil, errors.New("GetAncestryContinentRegionsList_23andMe missing continent regions: " + continentName) + } + + if (len(continentRegionsList) == 0){ + // Continent has no sublocations + // We see if we desire it + minimumDesiredRange, exists := myContinentMinimumBoundsMap[continentName] + if (exists == false){ + // We do not desire this continent + continue + } + maximumDesiredRange, exists := myContinentMaximumBoundsMap[continentName] + if (exists == false){ + return nil, errors.New("myContinentMinimumBoundsMap contains continent, myContinentMaximumBoundsMap does not.") + } + + continentDescription, err := getLocationDescription("Continent", continentName, minimumDesiredRange, maximumDesiredRange) + if (err != nil) { return nil, err } + + desiredContinentsList = append(desiredContinentsList, continentDescription) + continue + } + + // These represent the minimum and maximum desired ranges for this continent + // For example, if the user desires Japanese between 0-50%, and Korean between 50-100%, we say they desire East Asian from 0-100% + + // This list will store the desired region descriptions for this continent + desiredContinentRegionsList := make([]string, 0) + + continentMinimumDesiredBound := float64(100) + continentMaximumDesiredBound := float64(0) + + for _, regionName := range continentRegionsList{ + + regionSubregionsList, err := companyAnalysis.GetAncestryRegionSubregionsList_23andMe(continentName, regionName) + if (err != nil){ + return nil, errors.New("GetAncestryRegionSubregionsList_23andMe missing region subregions: " + regionName) + } + + if (len(regionSubregionsList) == 0){ + + // This region has no sublocations + // Check if we desire it + + regionMinimumDesiredBound, exists := myRegionMinimumBoundsMap[regionName] + if (exists == false){ + // We do not desire this region + continue + } + + regionMaximumDesiredBound, exists := myRegionMaximumBoundsMap[regionName] + if (exists == false){ + return nil, errors.New("myRegionMinimumBoundsMap contains region name, myRegionMaximumBoundsMap does not.") + } + + if (regionMinimumDesiredBound < continentMinimumDesiredBound){ + continentMinimumDesiredBound = regionMinimumDesiredBound + } + + if (regionMaximumDesiredBound > continentMaximumDesiredBound){ + continentMaximumDesiredBound = regionMaximumDesiredBound + } + + regionDescription, err := getLocationDescription("Region", regionName, regionMinimumDesiredBound, regionMaximumDesiredBound) + if (err != nil) { return nil, err } + + desiredContinentRegionsList = append(desiredContinentRegionsList, regionDescription) + + continue + } + + // This list will store the descriptions for our desired subregions of this region + desiredRegionSubregionsList := make([]string, 0) + + regionMinimumDesiredBound := float64(100) + regionMaximumDesiredBound := float64(0) + + for _, subregionName := range regionSubregionsList{ + + // All subregions have no sublocations + // We see if we desire this subregion + + subregionMinimumDesiredBound, exists := mySubregionMinimumBoundsMap[subregionName] + if (exists == false){ + // We do not desire this subregion + continue + } + + subregionMaximumDesiredBound, exists := mySubregionMaximumBoundsMap[subregionName] + if (exists == false){ + return nil, errors.New("subregionMinimumBoundsMap contains subregion name, subregionMaximumBoundsMap does not.") + } + + if (subregionMinimumDesiredBound < continentMinimumDesiredBound){ + continentMinimumDesiredBound = subregionMinimumDesiredBound + } + + if (subregionMaximumDesiredBound > continentMaximumDesiredBound){ + continentMaximumDesiredBound = subregionMaximumDesiredBound + } + + if (subregionMinimumDesiredBound < regionMinimumDesiredBound){ + regionMinimumDesiredBound = subregionMinimumDesiredBound + } + + if (subregionMaximumDesiredBound > regionMaximumDesiredBound){ + regionMaximumDesiredBound = subregionMaximumDesiredBound + } + + subregionDescription, err := getLocationDescription("Subregion", subregionName, subregionMinimumDesiredBound, subregionMaximumDesiredBound) + if (err != nil) { return nil, err } + + desiredRegionSubregionsList = append(desiredRegionSubregionsList, subregionDescription) + } + + if (len(desiredRegionSubregionsList) != 0){ + + regionDescription, err := getLocationDescription("Region", regionName, regionMinimumDesiredBound, regionMaximumDesiredBound) + if (err != nil) { return nil, err } + + desiredContinentRegionsList = append(desiredContinentRegionsList, regionDescription) + treeMap[regionDescription] = desiredRegionSubregionsList + } + } + + if (len(desiredContinentRegionsList) != 0){ + + continentDescription, err := getLocationDescription("Continent", continentName, continentMinimumDesiredBound, continentMaximumDesiredBound) + if (err != nil) { return nil, err } + + desiredContinentsList = append(desiredContinentsList, continentDescription) + treeMap[continentDescription] = desiredContinentRegionsList + } + } + + treeMap[""] = desiredContinentsList + + return treeMap, nil + } + + desiredLocationTreeMap, err := getDesiredLocationTreeMap() + if (err != nil){ + setErrorEncounteredPage(window, err, previousPage) + return + } + + locationDesireWidgetTree := widget.NewTreeWithStrings(desiredLocationTreeMap) + locationDesireWidgetTree.OpenAllBranches() + + header := container.NewVBox(title, backButton, widget.NewSeparator(), subtitle, widget.NewSeparator(), description1, description2Row, widget.NewSeparator(), modifyDesiresButton, widget.NewSeparator(), userOrOffspringSelectorCentered, widget.NewSeparator()) + + page := container.NewBorder(header, nil, nil, nil, locationDesireWidgetTree) + + setPageContent(page, window) +} + +func setChooseDesiresPage_23andMe_EditAncestryComposition( + window fyne.Window, + restrictiveModeIsEnabled bool, + continentProvided bool, + currentContinent string, + regionProvided bool, + currentRegion string, + subregionProvided bool, + currentSubregion string, + previousPage func()){ + + currentPage := func(){setChooseDesiresPage_23andMe_EditAncestryComposition(window, restrictiveModeIsEnabled, continentProvided, currentContinent, regionProvided, currentRegion, subregionProvided, currentSubregion, previousPage)} + + title := getPageTitleCentered("My Mate Desires - Physical") + + backButton := getBackButtonCentered(previousPage) + + getSubtitleText := func()string{ + if (restrictiveModeIsEnabled == true){ + return "23andMe Ancestry Composition (Restrictive)" + } + + return "23andMe Ancestry Composition" + } + + subtitleText := getSubtitleText() + + subtitle := getPageSubtitleCentered(subtitleText) + + description := getLabelCentered("Choose the percentage range for each location you desire.") + + getPageContent := func()(*fyne.Container, error){ + + getRangeEditor := func(locationType string)(*fyne.Container, error){ + + if (locationType != "Continent" && locationType != "Region" && locationType != "Subregion"){ + return nil, errors.New("getRangeEditor called with invalid locationType: " + locationType) + } + + getDesireName := func()string{ + if (restrictiveModeIsEnabled == false){ + return "23andMe_AncestryComposition" + } + return "23andMe_AncestryComposition_Restrictive" + } + + desireName := getDesireName() + + //Outputs: + // -map[string]float64: Continent minimum bounds map + // -map[string]float64: Continent maximum bounds map + // -map[string]float64: Region minimum bounds map + // -map[string]float64: Region maximum bounds map + // -map[string]float64: Subregion minimum bounds map + // -map[string]float64: Subregion maximum bounds map + // -error + getMyDesiredAncestryCompositionMaps := func()(map[string]float64, map[string]float64, map[string]float64, map[string]float64, map[string]float64, map[string]float64, error){ + + myDesireExists, myDesiredAncestryComposition, err := myLocalDesires.GetDesire(desireName) + if (err != nil){ return nil, nil, nil, nil, nil, nil, err } + if (myDesireExists == false){ + emptyMap1 := make(map[string]float64) + emptyMap2 := make(map[string]float64) + emptyMap3 := make(map[string]float64) + emptyMap4 := make(map[string]float64) + emptyMap5 := make(map[string]float64) + emptyMap6 := make(map[string]float64) + return emptyMap1, emptyMap2, emptyMap3, emptyMap4, emptyMap5, emptyMap6, nil + } + + myContinentMinimumBoundsMap, myContinentMaximumBoundsMap, myRegionMinimumBoundsMap, myRegionMaximumBoundsMap, mySubregionMinimumBoundsMap, mySubregionMaximumBoundsMap, err := mateDesires.ReadAncestryCompositionDesire_23andMe(myDesiredAncestryComposition) + if (err != nil){ + return nil, nil, nil, nil, nil, nil, errors.New("MyLocalDesires contains invalid " + desireName + " desire: " + err.Error()) + } + + return myContinentMinimumBoundsMap, myContinentMaximumBoundsMap, myRegionMinimumBoundsMap, myRegionMaximumBoundsMap, mySubregionMinimumBoundsMap, mySubregionMaximumBoundsMap, nil + } + + myContinentMinimumBoundsMap, myContinentMaximumBoundsMap, myRegionMinimumBoundsMap, myRegionMaximumBoundsMap, mySubregionMinimumBoundsMap, mySubregionMaximumBoundsMap, err := getMyDesiredAncestryCompositionMaps() + if (err != nil){ return nil, err } + + // Outputs: + // -bool: Range bounds exist (any desire exists for current location) + // -float64: Minimum bound + // -float64: Maximum bound + // -error + getMyDesiredRangeBounds := func()(bool, float64, float64, error){ + + if (locationType == "Continent"){ + + continentMinimumBound, exists := myContinentMinimumBoundsMap[currentContinent] + if (exists == false){ + return false, 0, 0, nil + } + + continentMaximumBound, exists := myContinentMaximumBoundsMap[currentContinent] + if (exists == false){ + return false, 0, 0, errors.New("myContinentMinimumBoundsMap contains continent, maximumBoundsMap does not: " + currentContinent) + } + + return true, continentMinimumBound, continentMaximumBound, nil + } + if (locationType == "Region"){ + + regionMinimumBound, exists := myRegionMinimumBoundsMap[currentRegion] + if (exists == false){ + return false, 0, 0, nil + } + + regionMaximumBound, exists := myRegionMaximumBoundsMap[currentRegion] + if (exists == false){ + return false, 0, 0, errors.New("myRegionMinimumBoundsMap contains region, maximumBoundsMap does not: " + currentRegion) + } + + return true, regionMinimumBound, regionMaximumBound, nil + } + // locationType == "Subregion" + + subregionMinimumBound, exists := mySubregionMinimumBoundsMap[currentSubregion] + if (exists == false){ + return false, 0, 0, nil + } + + subregionMaximumBound, exists := mySubregionMaximumBoundsMap[currentSubregion] + if (exists == false){ + return false, 0, 0, errors.New("mySubregionMinimumBoundsMap contains subregion, maximumBoundsMap does not: " + currentSubregion) + } + + return true, subregionMinimumBound, subregionMaximumBound, nil + } + + myDesireExists, myMinimumBound, myMaximumBound, err := getMyDesiredRangeBounds() + if (err != nil){ return nil, err } + + getMyDesireDisplaySection := func()(*fyne.Container, error){ + + myDesireTitle := getItalicLabelCentered("My Desire:") + + getMyDesireText := func()string{ + + if (myDesireExists == false){ + return "None" + } + myMinimumBoundString := helpers.ConvertFloat64ToStringRounded(myMinimumBound, 1) + myMaximumBoundString := helpers.ConvertFloat64ToStringRounded(myMaximumBound, 1) + + if (myMinimumBoundString == myMaximumBoundString){ + result := myMinimumBoundString + "%" + return result + } + + result := myMinimumBoundString + "-" + myMaximumBoundString + "%" + return result + } + + myDesireText := getMyDesireText() + + myDesireLabel := getBoldLabelCentered(myDesireText) + + if (myDesireExists == false){ + + displaySection := container.NewVBox(myDesireTitle, myDesireLabel) + return displaySection, nil + } + + // We check to see if we should display offspring range + myAncestryCompositionExists, myCurrentAncestryComposition, err := myLocalProfiles.GetProfileData("Mate", "23andMe_AncestryComposition") + if (err != nil){ return nil, err } + if (myAncestryCompositionExists == false){ + + // We have not added our ancestry composition + // Thus, we cannot determine the offspring location composition + + displaySection := container.NewVBox(myDesireTitle, myDesireLabel) + return displaySection, nil + } + + // This returns the percentage of the location that we are, as described in our profile + getMyLocationPercentage := func()(float64, error){ + + attributeIsValid, myContinentPercentagesMap, myRegionPercentagesMap, mySubregionPercentagesMap, err := companyAnalysis.ReadAncestryCompositionAttribute_23andMe(true, myCurrentAncestryComposition) + if (err != nil) { return 0, err } + if (attributeIsValid == false){ + return 0, errors.New("MyLocalProfiles contains invalid 23andMe ancestry composition attribute: " + myCurrentAncestryComposition) + } + + if (locationType == "Continent"){ + myPercentage, exists := myContinentPercentagesMap[currentContinent] + if (exists == false){ + return 0, nil + } + return myPercentage, nil + } + if (locationType == "Region"){ + myPercentage, exists := myRegionPercentagesMap[currentRegion] + if (exists == false){ + return 0, nil + } + return myPercentage, nil + } + // LocationType == "Subregion" + myPercentage, exists := mySubregionPercentagesMap[currentSubregion] + if (exists == false){ + return 0, nil + } + return myPercentage, nil + } + + myLocationPercentage, err := getMyLocationPercentage() + if (err != nil){ return nil, err } + + offspringMinimumBound := (myLocationPercentage + myMinimumBound)/2 + offspringMaximumBound := (myLocationPercentage + myMaximumBound)/2 + + offspringMinimumBoundString := helpers.ConvertFloat64ToStringRounded(offspringMinimumBound, 1) + offspringMaximumBoundString := helpers.ConvertFloat64ToStringRounded(offspringMaximumBound, 1) + + getOffspringDesiredRangeString := func()string{ + + if (offspringMinimumBoundString == offspringMaximumBoundString){ + result := offspringMinimumBoundString + "%" + return result + } + + result := offspringMinimumBoundString + "-" + offspringMaximumBoundString + "%" + return result + } + + offspringDesiredRangeString := getOffspringDesiredRangeString() + + offspringRangeLabel := getItalicLabelCentered("(My Offspring: ~" + offspringDesiredRangeString + ")") + + displaySection := container.NewVBox(myDesireTitle, myDesireLabel, offspringRangeLabel) + return displaySection, nil + } + + myDesireDisplaySection, err := getMyDesireDisplaySection() + if (err != nil) { return nil, err } + + minimumLabel := getBoldLabelCentered("Minimum:") + minimumEntry := widget.NewEntry() + if (myDesireExists == true){ + myMinimumBoundString := helpers.ConvertFloat64ToStringRounded(myMinimumBound, 1) + minimumEntry.Text = myMinimumBoundString + } else { + minimumEntry.SetPlaceHolder("Enter Minimum...") + } + minimumEntryBoxed := getWidgetBoxed(minimumEntry) + minimumEntryColumn := container.NewGridWithColumns(1, minimumLabel, minimumEntryBoxed) + + maximumLabel := getBoldLabelCentered("Maximum:") + maximumEntry := widget.NewEntry() + if (myDesireExists == true){ + myMaximumBoundString := helpers.ConvertFloat64ToStringRounded(myMaximumBound, 1) + maximumEntry.Text = myMaximumBoundString + } else { + maximumEntry.SetPlaceHolder("Enter Maximum...") + } + maximumEntryBoxed := getWidgetBoxed(maximumEntry) + maximumEntryColumn := container.NewGridWithColumns(1, maximumLabel, maximumEntryBoxed) + + minimumMaximumEntryColumnsRow := getContainerCentered(container.NewGridWithRows(1, minimumEntryColumn, maximumEntryColumn)) + + submitButton := getWidgetCentered(widget.NewButtonWithIcon("Submit", theme.ConfirmIcon(), func(){ + + newMinimum := minimumEntry.Text + newMaximum := maximumEntry.Text + + newMinimumBoundFloat64, err := helpers.ConvertStringToFloat64(newMinimum) + if (err != nil) { + dialogTitle := translate("Invalid Minimum Bound") + dialogMessageA := getLabelCentered(translate("Your minimum bound is invalid.")) + dialogMessageB := getLabelCentered(translate("It must be a number between 0 and 100.")) + dialogContent := container.NewVBox(dialogMessageA, dialogMessageB) + dialog.ShowCustom(dialogTitle, translate("Close"), dialogContent, window) + return + } + newMaximumBoundFloat64, err := helpers.ConvertStringToFloat64(newMaximum) + if (err != nil) { + dialogTitle := translate("Invalid Maximum Bound") + dialogMessageA := getLabelCentered(translate("Your maximum bound is invalid.")) + dialogMessageB := getLabelCentered(translate("It must be a number between 0 and 100.")) + dialogContent := container.NewVBox(dialogMessageA, dialogMessageB) + dialog.ShowCustom(dialogTitle, translate("Close"), dialogContent, window) + return + } + if (newMinimumBoundFloat64 < 0 || newMinimumBoundFloat64 > 100){ + dialogTitle := translate("Invalid Minimum Bound") + dialogMessageA := getLabelCentered(translate("Your minimum bound is invalid.")) + dialogMessageB := getLabelCentered(translate("It must be a number between 0 and 100.")) + dialogContent := container.NewVBox(dialogMessageA, dialogMessageB) + dialog.ShowCustom(dialogTitle, translate("Close"), dialogContent, window) + return + } + if (newMaximumBoundFloat64 < 0 || newMaximumBoundFloat64 > 100){ + dialogTitle := translate("Invalid Maximum Bound") + dialogMessageA := getLabelCentered(translate("Your maximum bound is invalid.")) + dialogMessageB := getLabelCentered(translate("It must be a number between 0 and 100.")) + dialogContent := container.NewVBox(dialogMessageA, dialogMessageB) + dialog.ShowCustom(dialogTitle, translate("Close"), dialogContent, window) + return + } + + if (newMinimumBoundFloat64 > newMaximumBoundFloat64){ + dialogTitle := translate("Invalid Range") + dialogMessageA := getLabelCentered(translate("Your range is invalid.")) + dialogMessageB := getLabelCentered(translate("The minimum must be less than the maximum.")) + dialogContent := container.NewVBox(dialogMessageA, dialogMessageB) + dialog.ShowCustom(dialogTitle, translate("Close"), dialogContent, window) + return + } + + if (newMinimumBoundFloat64 == 0 && newMaximumBoundFloat64 == 100){ + + dialogTitle := translate("Invalid Range") + dialogMessageA := getLabelCentered(translate("Your range is invalid.")) + dialogMessageB := getLabelCentered(translate("You cannot set a desired range of 0-100%.")) + dialogMessageC := getLabelCentered(translate("All users will pass this range.")) + dialogContent := container.NewVBox(dialogMessageA, dialogMessageB, dialogMessageC) + dialog.ShowCustom(dialogTitle, translate("Close"), dialogContent, window) + return + } + + if (locationType == "Continent"){ + + myContinentMinimumBoundsMap[currentContinent] = newMinimumBoundFloat64 + myContinentMaximumBoundsMap[currentContinent] = newMaximumBoundFloat64 + + } else if (locationType == "Region"){ + + myRegionMinimumBoundsMap[currentRegion] = newMinimumBoundFloat64 + myRegionMaximumBoundsMap[currentRegion] = newMaximumBoundFloat64 + + } else { + + // locationType == "Subregion" + mySubregionMinimumBoundsMap[currentSubregion] = newMinimumBoundFloat64 + mySubregionMaximumBoundsMap[currentSubregion] = newMaximumBoundFloat64 + } + + newDesireValue, err := mateDesires.CreateAncestryCompositionDesire_23andMe(myContinentMinimumBoundsMap, myContinentMaximumBoundsMap, myRegionMinimumBoundsMap, myRegionMaximumBoundsMap, mySubregionMinimumBoundsMap, mySubregionMaximumBoundsMap) + if (err != nil){ + setErrorEncounteredPage(window, err, currentPage) + return + } + + err = myLocalDesires.SetDesire(desireName, newDesireValue) + if (err != nil){ + setErrorEncounteredPage(window, err, currentPage) + return + } + currentPage() + })) + + noDesireButton := getWidgetCentered(widget.NewButtonWithIcon("No Desire", theme.CancelIcon(), func(){ + + if (locationType == "Continent"){ + + delete(myContinentMinimumBoundsMap, currentContinent) + delete(myContinentMaximumBoundsMap, currentContinent) + + } else if (locationType == "Region"){ + + delete(myRegionMinimumBoundsMap, currentRegion) + delete(myRegionMaximumBoundsMap, currentRegion) + } else { + + // locationType == "Subregion" + + delete(mySubregionMinimumBoundsMap, currentSubregion) + delete(mySubregionMaximumBoundsMap, currentSubregion) + } + + if (len(myContinentMinimumBoundsMap) == 0 && len(myContinentMaximumBoundsMap) == 0 && len(myRegionMinimumBoundsMap) == 0 && len(myRegionMaximumBoundsMap) == 0 && len(mySubregionMinimumBoundsMap) == 0 && len(mySubregionMaximumBoundsMap) == 0){ + + err := myLocalDesires.DeleteDesire(desireName) + if (err != nil){ + setErrorEncounteredPage(window, err, currentPage) + return + } + currentPage() + return + } + + newDesireValue, err := mateDesires.CreateAncestryCompositionDesire_23andMe(myContinentMinimumBoundsMap, myContinentMaximumBoundsMap, myRegionMinimumBoundsMap, myRegionMaximumBoundsMap, mySubregionMinimumBoundsMap, mySubregionMaximumBoundsMap) + if (err != nil){ + setErrorEncounteredPage(window, err, currentPage) + return + } + + err = myLocalDesires.SetDesire(desireName, newDesireValue) + if (err != nil){ + setErrorEncounteredPage(window, err, currentPage) + return + } + + currentPage() + })) + + rangeEditor := container.NewVBox(myDesireDisplaySection, widget.NewSeparator(), minimumMaximumEntryColumnsRow, submitButton, noDesireButton) + + return rangeEditor, nil + } + + continentLabel := getBoldLabel("Continent:") + + allContinentsList := companyAnalysis.GetAncestryContinentsList_23andMe() + + handleContinentSelectFunction := func(newContinent string){ + if (continentProvided == true && newContinent == currentContinent){ + return + } + + setChooseDesiresPage_23andMe_EditAncestryComposition(window, restrictiveModeIsEnabled, true, newContinent, false, "", false, "", previousPage) + } + + continentSelector := widget.NewSelect(allContinentsList, handleContinentSelectFunction) + if (continentProvided == true){ + continentSelector.Selected = currentContinent + } + + continentRow := container.NewHBox(layout.NewSpacer(), continentLabel, continentSelector, layout.NewSpacer()) + + if (continentProvided == false){ + + pageContent := container.NewVBox(continentRow) + return pageContent, nil + } + + regionsList, err := companyAnalysis.GetAncestryContinentRegionsList_23andMe(currentContinent) + if (err != nil) { return nil, err } + + if (len(regionsList) == 0){ + + rangeEditor, err := getRangeEditor("Continent") + if (err != nil) { return nil, err } + + pageContent := container.NewVBox(continentRow, widget.NewSeparator(), rangeEditor) + + return pageContent, nil + } + + regionLabel := getBoldLabel("Region:") + + handleRegionSelectFunction := func(newRegion string){ + + if (regionProvided == true && currentRegion == newRegion){ + return + } + + setChooseDesiresPage_23andMe_EditAncestryComposition(window, restrictiveModeIsEnabled, true, currentContinent, true, newRegion, false, "", previousPage) + } + + regionSelector := widget.NewSelect(regionsList, handleRegionSelectFunction) + + if (regionProvided == true){ + regionSelector.Selected = currentRegion + } + + regionRow := container.NewHBox(layout.NewSpacer(), regionLabel, regionSelector, layout.NewSpacer()) + + if (regionProvided == false){ + + pageContent := container.NewVBox(continentRow, widget.NewSeparator(), regionRow) + + return pageContent, nil + } + + subregionsList, err := companyAnalysis.GetAncestryRegionSubregionsList_23andMe(currentContinent, currentRegion) + if (err != nil) { return nil, err } + + if (len(subregionsList) == 0){ + + rangeEditor, err := getRangeEditor("Region") + if (err != nil) { return nil, err } + + pageContent := container.NewVBox(continentRow, widget.NewSeparator(), regionRow, widget.NewSeparator(), rangeEditor) + + return pageContent, nil + } + + subregionLabel := getBoldLabel("Subregion:") + + handleSubregionSelectFunction := func(newSubregion string){ + + if (subregionProvided == true && currentSubregion == newSubregion){ + return + } + + setChooseDesiresPage_23andMe_EditAncestryComposition(window, restrictiveModeIsEnabled, true, currentContinent, true, currentRegion, true, newSubregion, previousPage) + } + + subregionSelector := widget.NewSelect(subregionsList, handleSubregionSelectFunction) + if (subregionProvided == true){ + subregionSelector.Selected = currentSubregion + } + + subregionRow := container.NewHBox(layout.NewSpacer(), subregionLabel, subregionSelector, layout.NewSpacer()) + + if (subregionProvided == false){ + + pageContent := container.NewVBox(continentRow, widget.NewSeparator(), regionRow, widget.NewSeparator(), subregionRow) + + return pageContent, nil + } + + rangeEditor, err := getRangeEditor("Subregion") + if (err != nil) { return nil, err } + + pageContent := container.NewVBox(continentRow, widget.NewSeparator(), regionRow, widget.NewSeparator(), subregionRow, widget.NewSeparator(), rangeEditor) + + return pageContent, nil + } + + pageContent, err := getPageContent() + if (err != nil){ + setErrorEncounteredPage(window, err, previousPage) + return + } + + page := container.NewVBox(title, backButton, widget.NewSeparator(), subtitle, widget.NewSeparator(), description, widget.NewSeparator(), pageContent) + + setPageContent(page, window) +} + + +func setChooseDesiresPage_23andMe_NeanderthalVariants(window fyne.Window, previousPage func()){ + + currentPage := func(){setChooseDesiresPage_23andMe_NeanderthalVariants(window, previousPage)} + + title := getPageTitleCentered(translate("My Mate Desires - Physical")) + + backButton := getBackButtonCentered(previousPage) + + subtitle := getPageSubtitleCentered("Neanderthal Variants") + + description1 := getLabelCentered("Choose your neanderthal variant desires.") + description2 := getLabelCentered("Enter the neanderthal variant count you desire in your matches.") + description3 := getLabelCentered("The data is provided by 23andMe.") + description4 := getLabelCentered("More companies will be added soon.") + + myVariantCountExists, myCurrentNeanderthalVariants, err := myLocalProfiles.GetProfileData("Mate", "23andMe_NeanderthalVariants") + if (err != nil){ + setErrorEncounteredPage(window, err, previousPage) + return + } + + getOffspringVariantsCount := func(userVariantsCount float64)(float64, error){ + + if (myVariantCountExists == false){ + return 0, errors.New("Trying to get offspring neanderthal variants count when my count does not exist.") + } + myVariantsCountFloat64, err := helpers.ConvertStringToFloat64(myCurrentNeanderthalVariants) + if (err != nil) { + return 0, errors.New("MyLocalProfile neanderthal variants count is invalid: " + myCurrentNeanderthalVariants) + } + + offspringVariantsCount := (userVariantsCount + myVariantsCountFloat64)/2 + + return offspringVariantsCount, nil + } + + desireEditor, err := getDesireEditor_Numeric(window, currentPage, "23andMe_NeanderthalVariants", 0, 7462, "variants", 0, false, nil, nil, myVariantCountExists, "My Offspring", "~", getOffspringVariantsCount) + if (err != nil){ + setErrorEncounteredPage(window, err, previousPage) + return + } + + viewStatisticsButton := getWidgetCentered(widget.NewButtonWithIcon("View Statistics", theme.InfoIcon(), func(){ + setViewMyMateDesireStatisticsPage(window, "23andMe Neanderthal Variants", "23andMe_NeanderthalVariants", true, "Bar", "23andMe_NeanderthalVariants", 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_23andMe_Haplogroup(window fyne.Window, maternalOrPaternal string, previousPage func()){ + + if (maternalOrPaternal != "Maternal" && maternalOrPaternal != "Paternal"){ + setErrorEncounteredPage(window, errors.New("setChooseDesiresPage_23andMe_Haplogroup called with invalid maternalOrPaternal: " + maternalOrPaternal), previousPage) + return + } + + currentPage := func(){setChooseDesiresPage_23andMe_Haplogroup(window, maternalOrPaternal, previousPage)} + + title := getPageTitleCentered(translate("My Mate Desires - Physical")) + + backButton := getBackButtonCentered(previousPage) + + subtitle := getPageSubtitleCentered(maternalOrPaternal + " Haplogroup") + + maternalOrPaternalLowercase := strings.ToLower(maternalOrPaternal) + + description1 := getLabelCentered("Choose your " + maternalOrPaternalLowercase + " haplogroup desires.") + description2 := getLabelCentered("Add each " + maternalOrPaternalLowercase + " haplogroup you desire.") + description3 := getLabelCentered("You can choose from the known haplogroups, or enter ones that are not listed.") + description4 := getLabelCentered("The haplogroups are provided by 23andMe. More companies will be added soon.") + + desireName := "23andMe_" + maternalOrPaternal + "Haplogroup" + + getSelectedHaplogroupsSection := func()(*fyne.Container, error){ + + getCurrentDesiredChoicesList := func()([]string, error){ + + currentChoicesListExists, currentChoicesList, err := myLocalDesires.GetDesire(desireName) + 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 + currentDesiredChoicesList := strings.Split(currentChoicesList, "+") + + return currentDesiredChoicesList, nil + } + + currentDesiredChoicesList, err := getCurrentDesiredChoicesList() + if (err != nil) { return nil, err } + + getKnownHaplogroupsList := func()[]string{ + + if (maternalOrPaternal == "Maternal"){ + maternalHaplogroupsList := companyAnalysis.GetKnownMaternalHaplogroupsList_23andMe() + return maternalHaplogroupsList + } + + paternalHaplogroupsList := companyAnalysis.GetKnownPaternalHaplogroupsList_23andMe() + return paternalHaplogroupsList + } + + knownHaplogroupsList := getKnownHaplogroupsList() + + haplogroupEntry := widget.NewSelectEntry(knownHaplogroupsList) + haplogroupEntry.SetPlaceHolder("Enter haplogroup...") + + addHaplogroupButton := getWidgetCentered(widget.NewButtonWithIcon("Add Haplogroup", theme.ContentAddIcon(), func(){ + + newHaplogroupName := haplogroupEntry.Text + + if (newHaplogroupName == ""){ + return + } + isAllowed := allowedText.VerifyStringIsAllowed(newHaplogroupName) + if (isAllowed == false){ + dialogTitle := translate("Invalid Haplogroup Name") + dialogMessageA := getLabelCentered(translate("Your haplogroup contains an invalid character.")) + dialogMessageB := getLabelCentered(translate("It must be encoded in UTF-8.")) + dialogMessageC := getLabelCentered(translate("Remove this character and resubmit.")) + dialogContent := container.NewVBox(dialogMessageA, dialogMessageB, dialogMessageC) + dialog.ShowCustom(dialogTitle, translate("Close"), dialogContent, window) + return + } + + containsTabsOrNewlines := helpers.CheckIfStringContainsTabsOrNewlines(newHaplogroupName) + if (containsTabsOrNewlines == true){ + dialogTitle := translate("Invalid Haplogroup Name") + dialogMessageA := getLabelCentered(translate("Your haplogroup contains a tab or a newline.")) + dialogMessageB := getLabelCentered(translate("Remove the tab or newline and resubmit.")) + dialogContent := container.NewVBox(dialogMessageA, dialogMessageB) + dialog.ShowCustom(dialogTitle, translate("Close"), dialogContent, window) + return + } + if (len(newHaplogroupName) > 25){ + + currentBytesLengthString := helpers.ConvertIntToString(len(newHaplogroupName)) + + dialogTitle := translate("Invalid Haplogroup Name") + dialogMessageA := getLabelCentered(translate("Your haplogroup name is too long.")) + dialogMessageB := getLabelCentered(translate("It cannot exceed 25 bytes.")) + dialogMessageC := getLabelCentered(translate("Current Length:") + " " + currentBytesLengthString) + dialogContent := container.NewVBox(dialogMessageA, dialogMessageB, dialogMessageC) + dialog.ShowCustom(dialogTitle, translate("Close"), dialogContent, window) + return + } + + haplogroupNameBase64 := encoding.EncodeBytesToBase64String([]byte(newHaplogroupName)) + + newDesireList := helpers.AddItemToStringListAndAvoidDuplicate(currentDesiredChoicesList, haplogroupNameBase64) + newDesireAttributeValue := strings.Join(newDesireList, "+") + + err := myLocalDesires.SetDesire(desireName, newDesireAttributeValue) + if (err != nil){ + setErrorEncounteredPage(window, err, currentPage) + return + } + + currentPage() + })) + + addHaplogroupRow := getContainerCentered(container.NewGridWithRows(1, haplogroupEntry, addHaplogroupButton)) + + otherSelectCheck := widget.NewCheck("Allow Other", func(selection bool){ + + //Outputs: + // -[]string: New desire attribute list (list of base64 option names) + getNewDesireAttributeList := func()[]string{ + + if (selection == false){ + if (len(currentDesiredChoicesList) == 0){ + // This should not happen, because "Other" had to have been selected for it to be deselected + emptyList := make([]string, 0) + return emptyList + } + + newAttributeList, _ := helpers.DeleteAllMatchingItemsFromStringList(currentDesiredChoicesList, "Other") + + return newAttributeList + } + //selection == true + + newAttributeList := helpers.AddItemToStringListAndAvoidDuplicate(currentDesiredChoicesList, "Other") + + return newAttributeList + } + + newDesireAttributeList := getNewDesireAttributeList() + if (len(newDesireAttributeList) == 0){ + + err := myLocalDesires.DeleteDesire(desireName) + if (err != nil){ + setErrorEncounteredPage(window, err, currentPage) + return + } + currentPage() + return + } + newDesireAttribute := strings.Join(newDesireAttributeList, "+") + + err := myLocalDesires.SetDesire(desireName, newDesireAttribute) + if (err != nil){ + setErrorEncounteredPage(window, err, currentPage) + return + } + currentPage() + }) + + otherIsSelected := slices.Contains(currentDesiredChoicesList, "Other") + if (otherIsSelected == true){ + otherSelectCheck.Checked = true + } + + allowOtherHelpButton := widget.NewButtonWithIcon("", theme.QuestionIcon(), func(){ + setAllowOtherExplainerPage(window, currentPage) + }) + + otherSelectCheckRow := container.NewHBox(layout.NewSpacer(), otherSelectCheck, allowOtherHelpButton, layout.NewSpacer()) + + checkIfAnyHaplogroupsAreSelected := func()bool{ + + if (len(currentDesiredChoicesList) == 0){ + return false + } + if (len(currentDesiredChoicesList) == 1){ + onlySelection := currentDesiredChoicesList[0] + if (onlySelection == "Other"){ + return false + } + } + return true + } + + anyHaplogroupsAreSelected := checkIfAnyHaplogroupsAreSelected() + if (anyHaplogroupsAreSelected == false){ + + noHaplogroupsExistLabel := getBoldLabelCentered("No Haplogroups Chosen.") + + selectedHaplogroupsSection := container.NewVBox(addHaplogroupRow, widget.NewSeparator(), noHaplogroupsExistLabel, widget.NewSeparator(), otherSelectCheckRow) + + return selectedHaplogroupsSection, nil + } + + myDesiredHaplogroupsLabel := getItalicLabelCentered("My Desired Haplogroups:") + + haplogroupNameColumn := container.NewVBox(widget.NewSeparator()) + + deleteButtonsColumn := container.NewVBox(widget.NewSeparator()) + + for _, haplogroupNameBase64 := range currentDesiredChoicesList{ + + if (haplogroupNameBase64 == "Other"){ + continue + } + + haplogroupName, err := encoding.DecodeBase64StringToUnicodeString(haplogroupNameBase64) + if (err != nil){ + return nil, errors.New("My current profile " + maternalOrPaternal + " haplogroup desire is malformed: Contains invalid haplogroup: " + haplogroupNameBase64) + } + + haplogroupNameLabel := getBoldLabelCentered(haplogroupName) + + deleteHaplogroupButton := widget.NewButtonWithIcon("", theme.DeleteIcon(), func(){ + + newDesiredHaplogroupsList, _ := helpers.DeleteAllMatchingItemsFromStringList(currentDesiredChoicesList, haplogroupNameBase64) + + if (len(newDesiredHaplogroupsList) == 0){ + + err := myLocalDesires.DeleteDesire(desireName) + if (err != nil){ + setErrorEncounteredPage(window, err, currentPage) + return + } + currentPage() + return + } + + newDesireValueString := strings.Join(newDesiredHaplogroupsList, "+") + + err := myLocalDesires.SetDesire(desireName, newDesireValueString) + if (err != nil){ + setErrorEncounteredPage(window, err, currentPage) + return + } + currentPage() + }) + + haplogroupNameColumn.Add(haplogroupNameLabel) + deleteButtonsColumn.Add(deleteHaplogroupButton) + + haplogroupNameColumn.Add(widget.NewSeparator()) + deleteButtonsColumn.Add(widget.NewSeparator()) + } + + haplogroupsGrid := container.NewHBox(layout.NewSpacer(), haplogroupNameColumn, deleteButtonsColumn, layout.NewSpacer()) + + selectedHaplogroupsSection := container.NewVBox(addHaplogroupRow, widget.NewSeparator(), myDesiredHaplogroupsLabel, haplogroupsGrid, widget.NewSeparator(), otherSelectCheckRow) + + return selectedHaplogroupsSection, nil + } + + selectedHaplogroupsSection, err := getSelectedHaplogroupsSection() + if (err != nil){ + setErrorEncounteredPage(window, err, previousPage) + return + } + + filterOptionsSection, err := getDesireEditorFilterOptionsSection(window, currentPage, desireName, true) + if (err != nil){ + setErrorEncounteredPage(window, err, previousPage) + return + } + + viewStatisticsButton := getWidgetCentered(widget.NewButtonWithIcon("View Statistics", theme.InfoIcon(), func(){ + setViewMyMateDesireStatisticsPage(window, "23andMe " + maternalOrPaternal + " Haplogroup", desireName, true, "Bar", desireName, currentPage) + })) + + page := container.NewVBox(title, backButton, widget.NewSeparator(), subtitle, widget.NewSeparator(), description1, description2, description3, description4, widget.NewSeparator(), selectedHaplogroupsSection, widget.NewSeparator(), filterOptionsSection, widget.NewSeparator(), viewStatisticsButton) + + setPageContent(page, window) +} + + +func setChooseDesiresPage_Age(window fyne.Window, previousPage func()){ + + currentPage := func(){setChooseDesiresPage_Age(window, previousPage)} + + pageTitle := getPageTitleCentered(translate("My Mate Desires - Physical")) + + backButton := getBackButtonCentered(previousPage) + + subtitle := getPageSubtitleCentered(translate("Age")) + + description := getLabelCentered("Choose your age desires.") + + desireEditor, err := getDesireEditor_Numeric(window, currentPage, "Age", 18, 200, "Years", 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, "Age", "Age", true, "Bar", "Age", currentPage) + })) + + page := container.NewVBox(pageTitle, backButton, widget.NewSeparator(), subtitle, widget.NewSeparator(), description, widget.NewSeparator(), desireEditor, widget.NewSeparator(), viewStatisticsButton) + + setPageContent(page, window) +} + +func setChooseDesiresPage_MonogenicDiseases(window fyne.Window, previousPage func()){ + + currentPage := func(){setChooseDesiresPage_MonogenicDiseases(window, previousPage)} + + title := getPageTitleCentered(translate("My Mate Desires - Physical")) + + backButton := getBackButtonCentered(previousPage) + + subtitle := getPageSubtitleCentered("My Offspring Monogenic Disease Desires") + + description1 := getLabelCentered("Choose your offspring monogenic disease probability desires.") + description2 := getLabelCentered("You must link your genome person on the Build Profile - Genetic Analysis page.") + description3 := getLabelCentered("This desire will filter users based upon the probability that your offspring will have any monogenic disease.") + description4 := widget.NewLabel("If you use genetic testing, you can ensure your offspring will not have any monogenic diseases.") + + geneticTestingHelpButton := widget.NewButtonWithIcon("", theme.QuestionIcon(), func(){ + setGeneticTestingExplainerPage(window, currentPage) + }) + + description4Row := container.NewHBox(layout.NewSpacer(), description4, geneticTestingHelpButton, layout.NewSpacer()) + + description5 := getBoldLabel("Choose the monogenic disease probability for your offspring that you desire.") + desireHelpButton := widget.NewButtonWithIcon("", theme.QuestionIcon(), func(){ + setOffspringMonogenicDiseaseProbabilityDesireExplainerPage(window, currentPage) + }) + + description5Row := container.NewHBox(layout.NewSpacer(), description5, desireHelpButton, layout.NewSpacer()) + + description6 := getLabelCentered("Be aware that if you select 0%, all users with dominant monogenic diseases will be filtered.") + description7 := getLabelCentered("You should not select 0% if you have a dominant monogenic disease.") + + getCurrentDesireStatus := func()(string, error){ + + currentValueExists, currentValue, err := myLocalDesires.GetDesire("OffspringProbabilityOfAnyMonogenicDisease_Maximum") + if (err != nil){ return "", err } + if (currentValueExists == false || currentValue == "100"){ + + result := translate("0-100% (No Preference)") + return result, nil + } + + if (currentValue == "99"){ + return "0-99%", nil + } + if (currentValue != "0"){ + return "", errors.New("MyLocalDesires malformed: Invalid OffspringProbabilityOfAnyMonogenicDisease_Maximum: " + currentValue) + } + + return "0%", nil + } + + currentDesireStatus, err := getCurrentDesireStatus() + if (err != nil){ + setErrorEncounteredPage(window, err, previousPage) + return + } + + myDesireLabel := widget.NewLabel("My Desire:") + + currentStatusText := getBoldLabel(currentDesireStatus) + + currentStatusRow := container.NewHBox(layout.NewSpacer(), myDesireLabel, currentStatusText, layout.NewSpacer()) + + desireOptionsList := []string{"0%", "0-99%", "0-100%"} + + desireSelector := widget.NewSelect(desireOptionsList, func(newSelection string){ + + if (newSelection == "0-100%"){ + err = myLocalDesires.DeleteDesire("OffspringProbabilityOfAnyMonogenicDisease_Maximum") + if (err != nil){ + setErrorEncounteredPage(window, err, currentPage) + return + } + currentPage() + return + } + + getNewMaximumBound := func()string{ + if (newSelection == "0%"){ + return "0" + } + // newSelection == "0-99%" + return "99" + } + + newMaximumBound := getNewMaximumBound() + + err = myLocalDesires.SetDesire("OffspringProbabilityOfAnyMonogenicDisease_Maximum", newMaximumBound) + if (err != nil){ + setErrorEncounteredPage(window, err, currentPage) + return + } + currentPage() + }) + + if (currentDesireStatus == translate("0-100% (No Preference)")){ + desireSelector.Selected = "0-100%" + } else { + desireSelector.Selected = currentDesireStatus + } + + desireSelectorCentered := getWidgetCentered(desireSelector) + + filterOptionsSection, err := getDesireEditorFilterOptionsSection(window, currentPage, "OffspringProbabilityOfAnyMonogenicDisease", false) + if (err != nil){ + setErrorEncounteredPage(window, err, previousPage) + return + } + + viewStatisticsButton := getWidgetCentered(widget.NewButtonWithIcon("View Statistics", theme.InfoIcon(), func(){ + setViewMyMateDesireStatisticsPage(window, "Offspring Has Any Monogenic Disease Probability", "OffspringProbabilityOfAnyMonogenicDisease", true, "Donut", "OffspringProbabilityOfAnyMonogenicDisease", currentPage) + })) + + page := container.NewVBox(title, backButton, widget.NewSeparator(), subtitle, widget.NewSeparator(), description1, description2, description3, description4Row, widget.NewSeparator(), description5Row, description6, description7, widget.NewSeparator(), currentStatusRow, desireSelectorCentered, widget.NewSeparator(), filterOptionsSection, widget.NewSeparator(), viewStatisticsButton) + + setPageContent(page, window) +} + + +func setChooseDesiresPage_Height(window fyne.Window, previousPage func()){ + + currentPage := func(){setChooseDesiresPage_Height(window, previousPage)} + + title := getPageTitleCentered(translate("My Mate Desires - Physical")) + + backButton := getBackButtonCentered(previousPage) + + subtitle := getPageSubtitleCentered("Height") + + description := getLabelCentered("Choose your desired height.") + + metricOrImperialSwitchButton, err := getMetricImperialSwitchButton(window, currentPage) + if (err != nil){ + setErrorEncounteredPage(window, err, previousPage) + return + } + + currentUnitsLabel := getItalicLabel("Current Units:") + + currentUnitsRow := container.NewHBox(layout.NewSpacer(), currentUnitsLabel, metricOrImperialSwitchButton, layout.NewSpacer()) + + getMyMetricOrImperial := func()(string, error){ + + exists, metricOrImperial, err := globalSettings.GetSetting("MetricOrImperial") + if (err != nil) { return "", err } + if (exists == false){ + return "Metric", nil + } + if (metricOrImperial != "Metric" && metricOrImperial != "Imperial"){ + return "", errors.New("Malformed globalSettings: Invalid metricOrImperial: " + metricOrImperial) + } + + return metricOrImperial, nil + } + + myMetricOrImperial, err := getMyMetricOrImperial() + if (err != nil){ + setErrorEncounteredPage(window, err, previousPage) + return + } + + getBoundColumn := func(minimumOrMaximum string)(*fyne.Container, error){ + + if (minimumOrMaximum != "Minimum" && minimumOrMaximum != "Maximum"){ + return nil, errors.New("getBoundColumn called with invalid minimumOrMaximum: " + minimumOrMaximum) + } + + desireBoundName := "Height_" + minimumOrMaximum + + boundLabel := getBoldLabelCentered(minimumOrMaximum) + + myBoundExists, myCurrentBound, err := myLocalDesires.GetDesire(desireBoundName) + if (err != nil) { return nil, err } + + getMyBoundLabel := func()(*fyne.Container, error){ + + if (myBoundExists == false){ + noneLabel := getBoldItalicLabelCentered(translate("None")) + return noneLabel, nil + } + + myCurrentBoundFloat64, err := helpers.ConvertStringToFloat64(myCurrentBound) + if (err != nil) { + return nil, errors.New("My current height " + minimumOrMaximum + " desire is invalid: " + myCurrentBound) + } + + if (myMetricOrImperial == "Metric"){ + + myCurrentBoundRounded := helpers.ConvertFloat64ToStringRounded(myCurrentBoundFloat64, 2) + + myBoundLabel := getBoldLabelCentered(myCurrentBoundRounded + " " + translate("centimeters")) + return myBoundLabel, nil + } + + feetInchesString, err := helpers.ConvertCentimetersToFeetInchesTranslatedString(myCurrentBoundFloat64) + if (err != nil) { return nil, err } + + myBoundLabel := getBoldLabelCentered(feetInchesString) + return myBoundLabel, nil + } + + myBoundLabel, err := getMyBoundLabel() + if (err != nil){ return nil, err } + + minimumOrMaximumLowercase := strings.ToLower(minimumOrMaximum) + + //Outputs: + // -fyne.Container: Bound entry (either metric or imperial) + // -fyne.Widget: Submit button + // -error + getBoundEntryAndSubmitButton := func()(*fyne.Container, fyne.Widget, error){ + + if (myMetricOrImperial == "Metric"){ + + boundEntry := widget.NewEntry() + + boundEntryBoxed := getWidgetBoxed(boundEntry) + + if (myBoundExists == true){ + currentBoundFloat64, err := helpers.ConvertStringToFloat64(myCurrentBound) + if (err != nil) { return nil, nil, err } + if (currentBoundFloat64 < 30 || currentBoundFloat64 > 400) { + return nil, nil, errors.New("My desires malformed: Contains invalid height desire: " + myCurrentBound) + } + + myCurrentBoundRounded := helpers.ConvertFloat64ToStringRounded(currentBoundFloat64, 2) + + boundEntry.SetText(myCurrentBoundRounded) + } else { + boundEntry.SetPlaceHolder(translate("Enter " + minimumOrMaximumLowercase + "...")) + } + + saveBoundButton := widget.NewButtonWithIcon(translate("Save"), theme.ConfirmIcon(), func(){ + + newHeightBound := boundEntry.Text + + if (newHeightBound == ""){ + + err := myLocalDesires.DeleteDesire(desireBoundName) + if (err != nil){ + setErrorEncounteredPage(window, err, currentPage) + return + } + currentPage() + return + } + + //TODO: If min is greater than max, delete min + + newHeightBoundFloat64, err := helpers.ConvertStringToFloat64(newHeightBound) + if (err != nil){ + + title := translate("Invalid " + minimumOrMaximum + " Height") + dialogMessage := getLabelCentered(translate("Your height bound must be a number.")) + dialogContent := container.NewVBox(dialogMessage) + dialog.ShowCustom(title, translate("Close"), dialogContent, window) + return + } + if (newHeightBoundFloat64 < 30 || newHeightBoundFloat64 > 400){ + + title := translate("Invalid " + minimumOrMaximum + " Height") + dialogMessage := getLabelCentered(translate("Your height desire must be a number between 30 and 400.")) + dialogContent := container.NewVBox(dialogMessage) + dialog.ShowCustom(title, translate("Close"), dialogContent, window) + return + } + + err = myLocalDesires.SetDesire(desireBoundName, newHeightBound) + if (err != nil){ + setErrorEncounteredPage(window, err, currentPage) + return + } + + currentPage() + }) + + return boundEntryBoxed, saveBoundButton, nil + } + + feetEntry := widget.NewEntry() + inchesEntry := widget.NewEntry() + + feetLabel := getBoldLabel(translate("feet")) + inchesLabel := getBoldLabel(translate("inches")) + + entryRow := container.NewGridWithRows(1, feetEntry, feetLabel, inchesEntry, inchesLabel) + + if (myBoundExists == true){ + currentBoundFloat64, err := helpers.ConvertStringToFloat64(myCurrentBound) + if (err != nil) { return nil, nil, err } + if (currentBoundFloat64 < 30 || currentBoundFloat64 > 400) { + return nil, nil, errors.New("My desires malformed: Contains invalid height desire: " + myCurrentBound) + } + + feetInt, inchesFloat, err := helpers.ConvertCentimetersToFeetInches(currentBoundFloat64) + if (err != nil) { + return nil, nil, errors.New("My desires malformed: Contains invalid height desire: " + myCurrentBound) + } + + feetString := helpers.ConvertIntToString(feetInt) + inchesString := helpers.ConvertFloat64ToStringRounded(inchesFloat, 1) + + feetEntry.SetText(feetString) + inchesEntry.SetText(inchesString) + } else { + feetEntry.SetPlaceHolder(translate("Enter " + minimumOrMaximumLowercase + "...")) + inchesEntry.SetPlaceHolder(translate("Enter " + minimumOrMaximumLowercase + "...")) + } + + saveBoundButton := widget.NewButtonWithIcon(translate("Save"), theme.ConfirmIcon(), func(){ + + newHeightFeetBound := feetEntry.Text + newHeightInchesBound := inchesEntry.Text + + if (newHeightFeetBound == "" && newHeightInchesBound == ""){ + + err := myLocalDesires.DeleteDesire(desireBoundName) + if (err != nil){ + setErrorEncounteredPage(window, err, currentPage) + return + } + currentPage() + return + } + + newFeetBoundInt, err := helpers.ConvertStringToInt(newHeightFeetBound) + if (err != nil){ + title := translate("Invalid " + minimumOrMaximum + " Feet") + dialogMessage := getLabelCentered(translate("Your feet bound must be a number.")) + dialogContent := container.NewVBox(dialogMessage) + dialog.ShowCustom(title, translate("Close"), dialogContent, window) + return + } + + newInchesBoundFloat64, err := helpers.ConvertStringToFloat64(newHeightInchesBound) + if (err != nil){ + title := translate("Invalid " + minimumOrMaximum + " Inches") + dialogMessage := getLabelCentered(translate("Your inches bound must be a number.")) + dialogContent := container.NewVBox(dialogMessage) + dialog.ShowCustom(title, translate("Close"), dialogContent, window) + return + } + + newCentimetersBound, err := helpers.ConvertFeetInchesToCentimeters(newFeetBoundInt, newInchesBoundFloat64) + if (err != nil){ + + title := translate("Invalid " + minimumOrMaximum + " Height") + dialogMessage := getLabelCentered(translate("Your height bound must be a positive number.")) + dialogContent := container.NewVBox(dialogMessage) + dialog.ShowCustom(title, translate("Close"), dialogContent, window) + return + } + + if (newCentimetersBound < 30 || newCentimetersBound > 400){ + + title := translate("Invalid " + minimumOrMaximum + " Height") + dialogMessage := getLabelCentered(translate("Your height bound must be between 30 and 400 centimeters.")) + dialogContent := container.NewVBox(dialogMessage) + dialog.ShowCustom(title, translate("Close"), dialogContent, window) + return + } + + newCentimetersBoundString := helpers.ConvertFloat64ToString(newCentimetersBound) + + err = myLocalDesires.SetDesire(desireBoundName, newCentimetersBoundString) + if (err != nil){ + setErrorEncounteredPage(window, err, currentPage) + return + } + + currentPage() + }) + + return entryRow, saveBoundButton, nil + } + + boundEntryRow, saveBoundButton, err := getBoundEntryAndSubmitButton() + if (err != nil){ return nil, err } + + deleteBoundButton := widget.NewButtonWithIcon(translate("No Preference"), theme.CancelIcon(), func(){ + + if (myBoundExists == false){ + return + } + + err := myLocalDesires.DeleteDesire(desireBoundName) + if (err != nil){ + setErrorEncounteredPage(window, err, currentPage) + return + } + currentPage() + }) + + boundColumn := getContainerCentered(container.NewVBox(boundLabel, widget.NewSeparator(), myBoundLabel, widget.NewSeparator(), boundEntryRow, saveBoundButton, deleteBoundButton)) + return boundColumn, nil + } + + minimumColumn, err := getBoundColumn("Minimum") + if (err != nil){ + setErrorEncounteredPage(window, err, previousPage) + return + } + + maximumColumn, err := getBoundColumn("Maximum") + if (err != nil){ + setErrorEncounteredPage(window, err, previousPage) + return + } + + boundColumns := getContainerCentered(container.NewGridWithRows(1, minimumColumn, maximumColumn)) + + filterOptionsSection, err := getDesireEditorFilterOptionsSection(window, currentPage, "Height", true) + if (err != nil){ + setErrorEncounteredPage(window, err, previousPage) + return + } + + viewStatisticsButton := getWidgetCentered(widget.NewButtonWithIcon("View Statistics", theme.InfoIcon(), func(){ + setViewMyMateDesireStatisticsPage(window, "Height", "Height", true, "Bar", "Height", currentPage) + })) + + page := container.NewVBox(title, backButton, widget.NewSeparator(), subtitle, widget.NewSeparator(), description, widget.NewSeparator(), currentUnitsRow, widget.NewSeparator(), boundColumns, widget.NewSeparator(), filterOptionsSection, widget.NewSeparator(), viewStatisticsButton) + + setPageContent(page, window) +} + +func setChooseDesiresPage_BodyFat(window fyne.Window, previousPage func()){ + + currentPage := func(){setChooseDesiresPage_BodyFat(window, previousPage)} + + title := getPageTitleCentered(translate("My Mate Desires - Physical")) + + backButton := getBackButtonCentered(previousPage) + + subtitle := getPageSubtitleCentered("Body Fat") + + description1 := getLabelCentered("Choose your body fat desires.") + description2 := getLabelCentered("Select each rating which you desire.") + description3 := getLabelCentered("1/4 = Least body fat, 4/4 = Most body fat.") + + optionTitlesList := []string{"1/4", "2/4", "3/4", "4/4"} + + optionNamesMap := map[string][]string{ + "1/4": []string{"1"}, + "2/4": []string{"2"}, + "3/4": []string{"3"}, + "4/4": []string{"4"}, + } + + desireEditor, err := getDesireEditor_Choice(window, currentPage, "BodyFat", optionTitlesList, optionNamesMap, false, true, 1) + if (err != nil){ + setErrorEncounteredPage(window, err, previousPage) + return + } + + viewStatisticsButton := getWidgetCentered(widget.NewButtonWithIcon("View Statistics", theme.InfoIcon(), func(){ + setViewMyMateDesireStatisticsPage(window, "Body Fat", "BodyFat", true, "Donut", "BodyFat", 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_BodyMuscle(window fyne.Window, previousPage func()){ + + currentPage := func(){setChooseDesiresPage_BodyMuscle(window, previousPage)} + + title := getPageTitleCentered(translate("My Mate Desires - Physical")) + + backButton := getBackButtonCentered(previousPage) + + subtitle := getPageSubtitleCentered("Body Muscle") + + description1 := getLabelCentered("Choose your body muscle desires.") + description2 := getLabelCentered("Select each rating which you desire.") + description3 := getLabelCentered("1/4 = Least body muscle, 4/4 = Most body muscle.") + + optionTitlesList := []string{"1/4", "2/4", "3/4", "4/4"} + + optionNamesMap := map[string][]string{ + "1/4": []string{"1"}, + "2/4": []string{"2"}, + "3/4": []string{"3"}, + "4/4": []string{"4"}, + } + + desireEditor, err := getDesireEditor_Choice(window, currentPage, "BodyMuscle", optionTitlesList, optionNamesMap, false, true, 1) + if (err != nil){ + setErrorEncounteredPage(window, err, previousPage) + return + } + + viewStatisticsButton := getWidgetCentered(widget.NewButtonWithIcon("View Statistics", theme.InfoIcon(), func(){ + setViewMyMateDesireStatisticsPage(window, "Body Muscle", "BodyMuscle", true, "Donut", "BodyMuscle", 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_EyeColor(window fyne.Window, previousPage func()){ + + currentPage := func(){setChooseDesiresPage_EyeColor(window, previousPage)} + + title := getPageTitleCentered(translate("My Mate Desires - Physical")) + + backButton := getBackButtonCentered(previousPage) + + subtitle := getPageSubtitleCentered("Eye Color") + + description1 := getLabelCentered("Choose your eye color desires.") + description2 := getLabelCentered("Select each eye color that you desire.") + + optionTitlesList := []string{"Blue", "Green", "Brown", "Amber", "Blue+Green", "Blue+Brown", "Blue+Amber", "Amber+Brown", "Green+Brown", "Green+Amber", "Green+Amber+Brown", "Blue+Amber+Brown", "Blue+Green+Brown", "Blue+Green+Amber", "Blue+Green+Amber+Brown"} + + optionNamesMap := make(map[string][]string) + + optionNamesMap["Blue"] = []string{"Blue"} + optionNamesMap["Green"] = []string{"Green"} + optionNamesMap["Brown"] = []string{"Brown"} + optionNamesMap["Amber"] = []string{"Amber"} + optionNamesMap["Blue+Green"] = []string{"Blue+Green", "Green+Blue"} + optionNamesMap["Blue+Brown"] = []string{"Blue+Brown", "Brown+Blue"} + optionNamesMap["Blue+Amber"] = []string{"Blue+Amber", "Amber+Blue"} + optionNamesMap["Amber+Brown"] = []string{"Amber+Brown", "Brown+Amber"} + optionNamesMap["Green+Brown"] = []string{"Green+Brown", "Brown+Green"} + optionNamesMap["Green+Amber"] = []string{"Green+Amber", "Amber+Green"} + + optionNamesMap["Green+Amber+Brown"] = []string{"Green+Amber+Brown", "Green+Brown+Amber", "Amber+Green+Brown", "Amber+Brown+Green", "Brown+Amber+Green", "Brown+Green+Amber"} + + optionNamesMap["Blue+Amber+Brown"] = []string{"Blue+Amber+Brown", "Blue+Brown+Amber", "Amber+Blue+Brown", "Amber+Brown+Blue", "Brown+Amber+Blue", "Brown+Blue+Amber"} + + optionNamesMap["Blue+Green+Brown"] = []string{"Blue+Green+Brown", "Blue+Brown+Green", "Green+Blue+Brown", "Green+Brown+Blue", "Brown+Green+Blue", "Brown+Blue+Green"} + + optionNamesMap["Blue+Green+Amber"] = []string{"Blue+Green+Amber", "Blue+Amber+Green", "Green+Blue+Amber", "Green+Amber+Blue", "Amber+Green+Blue", "Amber+Blue+Green"} + + optionNamesMap["Blue+Green+Amber+Brown"] = []string{"Blue+Green+Brown+Amber", "Blue+Green+Amber+Brown", "Blue+Brown+Green+Amber", "Blue+Brown+Amber+Green", "Blue+Amber+Green+Brown", "Blue+Amber+Brown+Green", "Green+Blue+Brown+Amber", "Green+Blue+Amber+Brown", "Green+Brown+Blue+Amber", "Green+Brown+Amber+Blue", "Green+Amber+Blue+Brown", "Green+Amber+Brown+Blue", "Brown+Blue+Green+Amber", "Brown+Blue+Amber+Green", "Brown+Green+Blue+Amber", "Brown+Green+Amber+Blue", "Brown+Amber+Blue+Green", "Brown+Amber+Green+Blue", "Amber+Blue+Green+Brown", "Amber+Blue+Brown+Green", "Amber+Green+Blue+Brown", "Amber+Green+Brown+Blue", "Amber+Brown+Blue+Green", "Amber+Brown+Green+Blue"} + + desireEditor, err := getDesireEditor_Choice(window, currentPage, "EyeColor", optionTitlesList, optionNamesMap, false, true, 2) + if (err != nil){ + setErrorEncounteredPage(window, err, previousPage) + return + } + + viewStatisticsButton := getWidgetCentered(widget.NewButtonWithIcon("View Statistics", theme.InfoIcon(), func(){ + setViewMyMateDesireStatisticsPage(window, "Eye Color", "EyeColor", true, "Donut", "EyeColor", currentPage) + })) + + page := container.NewVBox(title, backButton, widget.NewSeparator(), subtitle, widget.NewSeparator(), description1, description2, widget.NewSeparator(), desireEditor, widget.NewSeparator(), viewStatisticsButton) + + setPageContent(page, window) +} + + +func setChooseDesiresPage_HairColor(window fyne.Window, previousPage func()){ + + currentPage := func(){setChooseDesiresPage_HairColor(window, previousPage)} + + title := getPageTitleCentered(translate("My Mate Desires - Physical")) + + backButton := getBackButtonCentered(previousPage) + + subtitle := getPageSubtitleCentered("Hair Color") + + description1 := getLabelCentered("Choose your natural hair color desires.") + description2 := getLabelCentered("Select each natural hair color that you desire.") + + optionTitlesList := []string{"Black", "Brown", "Blonde", "Orange", "Black+Brown", "Black+Blonde", "Black+Orange", "Brown+Blonde", "Brown+Orange", "Blonde+Orange"} + + //TODO: Add color guide page, so users can see the colors + // We already have this on the Build Profile page for hair/eye color + + optionNamesMap := make(map[string][]string) + optionNamesMap["Black"] = []string{"Black"} + optionNamesMap["Brown"] = []string{"Brown"} + optionNamesMap["Blonde"] = []string{"Blonde"} + optionNamesMap["Orange"] = []string{"Orange"} + optionNamesMap["Black+Brown"] = []string{"Black+Brown", "Brown+Black"} + optionNamesMap["Black+Blonde"] = []string{"Black+Blonde", "Blonde+Black"} + optionNamesMap["Black+Orange"] = []string{"Black+Orange", "Orange+Black"} + optionNamesMap["Brown+Blonde"] = []string{"Brown+Blonde", "Blonde+Brown"} + optionNamesMap["Brown+Orange"] = []string{"Brown+Orange", "Orange+Brown"} + optionNamesMap["Blonde+Orange"] = []string{"Blonde+Orange", "Orange+Blonde"} + + desireEditor, err := getDesireEditor_Choice(window, currentPage, "HairColor", optionTitlesList, optionNamesMap, false, true, 2) + if (err != nil){ + setErrorEncounteredPage(window, err, previousPage) + return + } + + viewStatisticsButton := getWidgetCentered(widget.NewButtonWithIcon("View Statistics", theme.InfoIcon(), func(){ + setViewMyMateDesireStatisticsPage(window, "Hair Color", "HairColor", true, "Donut", "HairColor", currentPage) + })) + + page := container.NewVBox(title, backButton, widget.NewSeparator(), subtitle, widget.NewSeparator(), description1, description2, widget.NewSeparator(), desireEditor, widget.NewSeparator(), viewStatisticsButton) + + setPageContent(page, window) +} + +func setChooseDesiresPage_HairTexture(window fyne.Window, previousPage func()){ + + currentPage := func(){setChooseDesiresPage_HairTexture(window, previousPage)} + + title := getPageTitleCentered(translate("My Mate Desires - Physical")) + + backButton := getBackButtonCentered(previousPage) + + subtitle := getPageSubtitleCentered("Hair Texture") + + description1 := getLabelCentered("Choose your hair texture desires.") + description2 := getLabelCentered("Select each hair texture that you desire.") + + // These descriptions are taken from 23andMe. + + option1Translated := translate("1 - Straight Hair") + option2Translated := translate("2 - Slightly Wavy Hair") + option3Translated := translate("3 - Wavy Hair") + option4Translated := translate("4 - Big Curls") + option5Translated := translate("5 - Small Curls") + option6Translated := translate("6 - Very Tight Curls") + + optionTitlesList := []string{option1Translated, option2Translated, option3Translated, option4Translated, option5Translated, option6Translated} + + optionNamesMap := map[string][]string{ + option1Translated: []string{"1"}, + option2Translated: []string{"2"}, + option3Translated: []string{"3"}, + option4Translated: []string{"4"}, + option5Translated: []string{"5"}, + option6Translated: []string{"6"}, + } + + desireEditor, err := getDesireEditor_Choice(window, currentPage, "HairTexture", optionTitlesList, optionNamesMap, false, true, 1) + if (err != nil){ + setErrorEncounteredPage(window, err, previousPage) + return + } + + viewStatisticsButton := getWidgetCentered(widget.NewButtonWithIcon("View Statistics", theme.InfoIcon(), func(){ + setViewMyMateDesireStatisticsPage(window, "Hair Texture", "HairTexture", true, "Donut", "HairTexture", currentPage) + })) + + page := container.NewVBox(title, backButton, widget.NewSeparator(), subtitle, widget.NewSeparator(), description1, description2, widget.NewSeparator(), desireEditor, widget.NewSeparator(), viewStatisticsButton) + + setPageContent(page, window) +} + +func setChooseDesiresPage_SkinColor(window fyne.Window, previousPage func()){ + + currentPage := func(){setChooseDesiresPage_SkinColor(window, previousPage)} + + title := getPageTitleCentered(translate("My Mate Desires - Physical")) + + backButton := getBackButtonCentered(previousPage) + + subtitle := getPageSubtitleCentered("Skin Color") + + description1 := getLabelCentered("Choose your skin color desires.") + description2 := getLabelCentered("Select each skin color that you desire.") + description3 := getLabelCentered("Skin color can change under different conditions.") + description4 := getLabelCentered("You may want to select multiple options to account for this.") + + getSkinColorExampleColumn := func(colorIdentifier string, colorCode string)(*fyne.Container, error){ + + colorSquare, err := getColorSquareAsFyneImage(colorCode) + if (err != nil){ return nil, err } + + colorSquare.FillMode = canvas.ImageFillStretch + + colorIdentifierLabel := getBoldLabelCentered(colorIdentifier) + + colorColumn := container.NewGridWithColumns(1, colorSquare, colorIdentifierLabel) + + return colorColumn, nil + } + + skinColorColumn_1, err := getSkinColorExampleColumn("1", "f4e3da") + if (err != nil){ + setErrorEncounteredPage(window, err, previousPage) + return + } + + skinColorColumn_2, err := getSkinColorExampleColumn("2", "f5d6b9") + if (err != nil){ + setErrorEncounteredPage(window, err, previousPage) + return + } + + skinColorColumn_3, err := getSkinColorExampleColumn("3", "dabe91") + if (err != nil){ + setErrorEncounteredPage(window, err, previousPage) + return + } + + skinColorColumn_4, err := getSkinColorExampleColumn("4", "ba9175") + if (err != nil){ + setErrorEncounteredPage(window, err, previousPage) + return + } + + skinColorColumn_5, err := getSkinColorExampleColumn("5", "916244") + if (err != nil){ + setErrorEncounteredPage(window, err, previousPage) + return + } + + skinColorColumn_6, err := getSkinColorExampleColumn("6", "744d2d") + if (err != nil){ + setErrorEncounteredPage(window, err, previousPage) + return + } + + skinColorExamplesRow := container.NewHBox(layout.NewSpacer(), skinColorColumn_1, skinColorColumn_2, skinColorColumn_3, skinColorColumn_4, skinColorColumn_5, skinColorColumn_6, layout.NewSpacer()) + + optionTitlesList := []string{"1", "2", "3", "4", "5", "6"} + + optionNamesMap := map[string][]string{ + "1": []string{"1"}, + "2": []string{"2"}, + "3": []string{"3"}, + "4": []string{"4"}, + "5": []string{"5"}, + "6": []string{"6"}, + } + + desireEditor, err := getDesireEditor_Choice(window, currentPage, "SkinColor", optionTitlesList, optionNamesMap, false, true, 6) + if (err != nil){ + setErrorEncounteredPage(window, err, previousPage) + return + } + + viewStatisticsButton := getWidgetCentered(widget.NewButtonWithIcon("View Statistics", theme.InfoIcon(), func(){ + setViewMyMateDesireStatisticsPage(window, "Skin Color", "SkinColor", true, "Donut", "SkinColor", currentPage) + })) + + page := container.NewVBox(title, backButton, widget.NewSeparator(), subtitle, widget.NewSeparator(), description1, description2, description3, description4, widget.NewSeparator(), skinColorExamplesRow, widget.NewSeparator(), desireEditor, widget.NewSeparator(), viewStatisticsButton) + + setPageContent(page, window) +} + +func setChooseDesiresPage_Infections(window fyne.Window, previousPage func()){ + + currentPage := func(){setChooseDesiresPage_Infections(window, previousPage)} + + title := getPageTitleCentered(translate("My Mate Desires - Physical")) + + backButton := getBackButtonCentered(previousPage) + + subtitle := getPageSubtitleCentered("Infections") + + description1 := getLabelCentered("Choose your infection desires.") + + hivButton := widget.NewButton("HIV", func(){ + setChooseDesiresPage_HIV(window, currentPage) + }) + + genitalHerpesButton := widget.NewButton("Genital Herpes", func(){ + setChooseDesiresPage_GenitalHerpes(window, currentPage) + }) + + buttonsGrid := getContainerCentered(container.NewGridWithColumns(1, hivButton, genitalHerpesButton)) + + page := container.NewVBox(title, backButton, widget.NewSeparator(), subtitle, widget.NewSeparator(), description1, widget.NewSeparator(), buttonsGrid) + + setPageContent(page, window) +} + +func setChooseDesiresPage_HIV(window fyne.Window, previousPage func()){ + + currentPage := func(){setChooseDesiresPage_HIV(window, previousPage)} + + title := getPageTitleCentered(translate("My Mate Desires - Physical")) + + backButton := getBackButtonCentered(previousPage) + + subtitle := getPageSubtitleCentered("HIV") + + description1 := getLabelCentered("Choose your HIV desires.") + description2 := getLabelCentered("Select each option which you desire.") + description3 := getLabelCentered("Positive = Has disease, Negative = Does not have disease.") + + optionTitlesList := []string{"HIV Positive", "HIV Negative"} + + optionNamesMap := map[string][]string{ + "HIV Positive": []string{"Yes"}, + "HIV Negative": []string{"No"}, + } + + desireEditor, err := getDesireEditor_Choice(window, currentPage, "HasHIV", optionTitlesList, optionNamesMap, false, true, 1) + if (err != nil){ + setErrorEncounteredPage(window, err, previousPage) + return + } + + viewStatisticsButton := getWidgetCentered(widget.NewButtonWithIcon("View Statistics", theme.InfoIcon(), func(){ + setViewMyMateDesireStatisticsPage(window, "Has HIV", "HasHIV", true, "Donut", "HasHIV", 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_GenitalHerpes(window fyne.Window, previousPage func()){ + + currentPage := func(){setChooseDesiresPage_GenitalHerpes(window, previousPage)} + + title := getPageTitleCentered(translate("My Mate Desires - Physical")) + + backButton := getBackButtonCentered(previousPage) + + subtitle := getPageSubtitleCentered("Genital Herpes") + + description1 := getLabelCentered("Choose your Genital Herpes desires.") + description2 := getLabelCentered("Select each option which you desire.") + description3 := getLabelCentered("Positive = Has disease, Negative = Does not have disease.") + + optionTitlesList := []string{"Genital Herpes Positive", "Genital Herpes Negative"} + + optionNamesMap := map[string][]string{ + "Genital Herpes Positive": []string{"Yes"}, + "Genital Herpes Negative": []string{"No"}, + } + + desireEditor, err := getDesireEditor_Choice(window, currentPage, "HasGenitalHerpes", optionTitlesList, optionNamesMap, false, true, 1) + if (err != nil){ + setErrorEncounteredPage(window, err, previousPage) + return + } + + viewStatisticsButton := getWidgetCentered(widget.NewButtonWithIcon("View Statistics", theme.InfoIcon(), func(){ + setViewMyMateDesireStatisticsPage(window, "Has Genital Herpes", "HasGenitalHerpes", true, "Donut", "HasGenitalHerpes", currentPage) + })) + + page := container.NewVBox(title, backButton, widget.NewSeparator(), subtitle, widget.NewSeparator(), description1, description2, description3, widget.NewSeparator(), desireEditor, widget.NewSeparator(), viewStatisticsButton) + + setPageContent(page, window) +} + + diff --git a/gui/downloadGui.go b/gui/downloadGui.go new file mode 100644 index 0000000..e0b6b0a --- /dev/null +++ b/gui/downloadGui.go @@ -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() +} + + + + diff --git a/gui/gui.go b/gui/gui.go new file mode 100644 index 0000000..8d2cad5 --- /dev/null +++ b/gui/gui.go @@ -0,0 +1,1259 @@ +package gui + +// gui.go provides miscellaneous GUI functions + +import "fyne.io/fyne/v2" +import "fyne.io/fyne/v2/widget" +import "fyne.io/fyne/v2/layout" +import "fyne.io/fyne/v2/dialog" +import "fyne.io/fyne/v2/theme" +import "fyne.io/fyne/v2/canvas" +import "fyne.io/fyne/v2/container" + +import "seekia/internal/appMemory" +import "seekia/internal/appUsers" +import "seekia/internal/encoding" +import "seekia/internal/helpers" +import "seekia/internal/identity" +import "seekia/internal/mySettings" +import "seekia/internal/translation" + +import "errors" + +func getCustomFyneSize(shift int)fyne.Size{ + + normalStyle := fyne.TextStyle{ + Bold: false, + Italic: false, + Monospace: false, + } + + customSize := float32(30 + shift) + + size := fyne.MeasureText("Standard", customSize, normalStyle) + + return size +} + +func getBoldLabel(text string) fyne.Widget{ + + titleStyle := fyne.TextStyle{ + Bold: true, + Italic: false, + Monospace: false, + } + + boldLabel := widget.NewLabelWithStyle(text, fyne.TextAlign(fyne.TextAlignCenter), titleStyle) + + return boldLabel +} + +func getBoldItalicLabel(text string) fyne.Widget{ + + titleStyle := fyne.TextStyle{ + Bold: true, + Italic: true, + Monospace: false, + } + + boldItalicLabel := widget.NewLabelWithStyle(text, fyne.TextAlign(fyne.TextAlignCenter), titleStyle) + + return boldItalicLabel +} + +func getItalicLabel(text string) fyne.Widget{ + + italicTextStyle := fyne.TextStyle{ + Bold: false, + Italic: true, + Monospace: false, + } + + italicLabel := widget.NewLabelWithStyle(text, fyne.TextAlign(fyne.TextAlignCenter), italicTextStyle) + + return italicLabel +} + +func getFyneTextStyle_Standard()fyne.TextStyle{ + + standardStyle := fyne.TextStyle{ + Bold: false, + Italic: false, + Monospace: false, + } + + return standardStyle +} + +func getFyneTextStyle_Bold()fyne.TextStyle{ + + boldStyle := fyne.TextStyle{ + Bold: true, + Italic: false, + Monospace: false, + } + + return boldStyle +} + +func getFyneTextStyle_Italic()fyne.TextStyle{ + + italicStyle := fyne.TextStyle{ + Bold: false, + Italic: true, + Monospace: false, + } + + return italicStyle +} + + +func getLabelCentered(text string) *fyne.Container{ + + label := widget.NewLabel(text) + labelCentered := container.NewHBox(layout.NewSpacer(), label, layout.NewSpacer()) + + return labelCentered +} + + +func getBoldLabelCentered(inputText string)*fyne.Container{ + + boldLabel := getBoldLabel(inputText) + + boldLabelCentered := container.NewHBox(layout.NewSpacer(), boldLabel, layout.NewSpacer()) + + return boldLabelCentered +} + +func getItalicLabelCentered(inputText string)*fyne.Container{ + + italicLabel := getItalicLabel(inputText) + + italicLabelCentered := container.NewHBox(layout.NewSpacer(), italicLabel, layout.NewSpacer()) + + return italicLabelCentered +} + +func getBoldItalicLabelCentered(inputText string)*fyne.Container{ + + boldItalicLabel := getBoldItalicLabel(inputText) + + boldItalicLabelCentered := container.NewHBox(layout.NewSpacer(), boldItalicLabel, layout.NewSpacer()) + return boldItalicLabelCentered +} + +func getWidgetCentered(widget fyne.Widget)*fyne.Container{ + + widgetCentered := container.NewHBox(layout.NewSpacer(), widget, layout.NewSpacer()) + + return widgetCentered +} + + +func getContainerCentered(inputContainer *fyne.Container)*fyne.Container{ + + containerCentered := container.NewHBox(layout.NewSpacer(), inputContainer, layout.NewSpacer()) + + return containerCentered +} + + +func getFyneImageCentered(inputImage *canvas.Image) *fyne.Container{ + + imageCentered := container.NewHBox(layout.NewSpacer(), inputImage, layout.NewSpacer()) + + return imageCentered +} + +func getPageTitleCentered(title string)(*fyne.Container){ + + textStyle := getFyneTextStyle_Bold() + + currentApp := fyne.CurrentApp() + + currentThemeVariant := currentApp.Settings().ThemeVariant() + + currentThemeObject := currentApp.Settings().Theme() + + // The .Color function is used to retrieve any specified color + // In this case, we are retrieving the current foreground color of the theme + // We have to include the theme variant to retrieve the color + textColor := currentThemeObject.Color(theme.ColorNameForeground, currentThemeVariant) + + label := canvas.NewText(title, textColor) + label.TextSize = 17 + label.TextStyle = textStyle + + increasedPadding := container.NewPadded(label) + increasedPadding2 := container.NewPadded(increasedPadding) + + pageTitleCentered := container.NewHBox(layout.NewSpacer(), increasedPadding2, layout.NewSpacer()) + + return pageTitleCentered +} + +func getPageSubtitleCentered(subtitle string) *fyne.Container{ + + textStyle := getFyneTextStyle_Bold() + + currentApp := fyne.CurrentApp() + + currentThemeVariant := currentApp.Settings().ThemeVariant() + + currentThemeObject := currentApp.Settings().Theme() + + // The .Color function is used to retrieve any specified color + // In this case, we are retrieving the current foreground color of the theme + // We have to include the theme variant to retrieve the color + textColor := currentThemeObject.Color(theme.ColorNameForeground, currentThemeVariant) + + label := canvas.NewText(subtitle, textColor) + label.TextSize = 16 + label.TextStyle = textStyle + + labelPadded1 := container.NewPadded(label) + labelPadded2 := container.NewPadded(labelPadded1) + + pageSubtitleCentered := container.NewHBox(layout.NewSpacer(), labelPadded2, layout.NewSpacer()) + + return pageSubtitleCentered +} + +func getBackButtonCentered(previousPage func())*fyne.Container{ + + backButton := getWidgetCentered(widget.NewButtonWithIcon(translate("Go Back"), theme.NavigateBackIcon(), previousPage)) + + return backButton +} + +func getFyneImageBoxed(inputImage *canvas.Image) *fyne.Container{ + + boxedImage := container.NewBorder(widget.NewSeparator(), widget.NewSeparator(), widget.NewSeparator(), widget.NewSeparator(), inputImage) + + return boxedImage +} + +func getContainerBoxed(inputContainer *fyne.Container) *fyne.Container{ + + boxedContainer := container.NewBorder(widget.NewSeparator(), widget.NewSeparator(), widget.NewSeparator(), widget.NewSeparator(), inputContainer) + + return boxedContainer +} + +func getAppTabsBoxed(inputTabs *container.AppTabs) *fyne.Container{ + + boxedTabs := container.NewBorder(widget.NewSeparator(), widget.NewSeparator(), widget.NewSeparator(), widget.NewSeparator(), inputTabs) + + return boxedTabs +} + +func getScrollContainerBoxed(inputScrollContainer *container.Scroll) *fyne.Container{ + + boxedContainer := container.NewBorder(widget.NewSeparator(), widget.NewSeparator(), widget.NewSeparator(), widget.NewSeparator(), inputScrollContainer) + + return boxedContainer +} + +func getWidgetBoxed(inputWidget fyne.Widget) *fyne.Container{ + + boxedWidget := container.NewBorder(widget.NewSeparator(), widget.NewSeparator(), widget.NewSeparator(), widget.NewSeparator(), inputWidget) + + return boxedWidget +} + +func translate(text string) string{ + + result := translation.TranslateTextFromEnglishToMyLanguage(text) + + return result +} + +func translateWithContext(text string, context string)string{ + + //TODO: We will use this to translate text that belongs to a specific context + // An example is 23andMe, which will have its own translations for text + // For this reason, we may end up having 2 identical texts that have different translations + + return text +} + +//TODO: Swap content and window parameters +func setPageContent(content *fyne.Container, window fyne.Window){ + + navbar, navbarLocation, err := getNavigationBar(window) + if (err != nil){ + title := getPageTitleCentered("Failed to read from filesystem.") + description := getLabelCentered("Contact the Seekia developers to report this error.") + + errorString := err.Error() + errorLabel := getLabelCentered(errorString) + + page := container.NewVBox(title, description, errorLabel) + window.SetContent(page) + return + } + + contentScrollable := container.NewVScroll(content) + + getPageContentWithNavbar := func()*fyne.Container{ + if (navbarLocation == "Bottom"){ + content := container.NewBorder(nil, navbar, nil, nil, contentScrollable) + return content + } + if (navbarLocation == "Left"){ + content := container.NewBorder(nil, nil, navbar, nil, contentScrollable) + return content + } + if (navbarLocation == "Right"){ + content := container.NewBorder(nil, nil, nil, navbar, contentScrollable) + return content + } + content := container.NewBorder(navbar, nil, nil, nil, contentScrollable) + return content + } + + pageContentWithNavbar := getPageContentWithNavbar() + + window.SetContent(pageContentWithNavbar) +} + +//Outputs: +// -*fyne.Container: Navigation Bar +// -string: Navigation location ("Top"/"Bottom"/"Left"/"Right") +// -error +func getNavigationBar(window fyne.Window) (*fyne.Container, string, error){ + + geneticsIcon, err := getFyneImageIcon("Genome") + if (err != nil) { return nil, "", err } + + hostIcon, err := getFyneImageIcon("Host") + if (err != nil) { return nil, "", err } + + desiresIcon, err := getFyneImageIcon("Desires") + if (err != nil) { return nil, "", err } + + matchesIcon, err := getFyneImageIcon("Mate") + if (err != nil) { return nil, "", err } + + homeIcon, err := getFyneImageIcon("Home") + if (err != nil) { return nil, "", err } + + profileIcon, err := getFyneImageIcon("Profile") + if (err != nil) { return nil, "", err } + + moderateIcon, err := getFyneImageIcon("Moderate") + if (err != nil) { return nil, "", err } + + chatIcon, err := getFyneImageIcon("Chat") + if (err != nil) { return nil, "", err } + + settingsIcon, err := getFyneImageIcon("Settings") + if (err != nil) { return nil, "", err } + + geneticsButton := widget.NewButton(translate("Genetics"), func(){ + setGeneticsPage(window) + }) + desiresButton := widget.NewButton(translate("Desires"), func(){ + setDesiresPage(window) + }) + matchesButton := widget.NewButton(translate("Matches"), func(){ + setMatchesPage(window) + }) + homeButton := widget.NewButton(translate("Home"), func(){ + setHomePage(window) + }) + profileButton := widget.NewButton(translate("Profile"), func(){ + setProfilePage(window, false, "", false, nil) + }) + hostButton := widget.NewButton(translate("Host"), func(){ + setHostPage(window, false, nil) + }) + chatButton := widget.NewButton(translate("Chat"), func(){ + setChatPage(window) + }) + moderateButton := widget.NewButton(translate("Moderate"), func(){ + setModeratePage(window, false, nil) + }) + settingsButton := widget.NewButton(translate("Settings"), func(){ + setSettingsPage(window) + }) + + geneticsButtonWithIcon := container.NewGridWithRows(2, geneticsIcon, geneticsButton) + desiresButtonWithIcon := container.NewGridWithRows(2, desiresIcon, desiresButton) + matchesButtonWithIcon := container.NewGridWithRows(2, matchesIcon, matchesButton) + homeButtonWithIcon := container.NewGridWithRows(2, homeIcon, homeButton) + profileButtonWithIcon := container.NewGridWithRows(2, profileIcon, profileButton) + hostButtonWithIcon := container.NewGridWithRows(2, hostIcon, hostButton) + chatButtonWithIcon := container.NewGridWithRows(2, chatIcon, chatButton) + moderateButtonWithIcon := container.NewGridWithRows(2, moderateIcon, moderateButton) + settingsButtonWithIcon := container.NewGridWithRows(2, settingsIcon, settingsButton) + + getNavigationLocation := func()(string, error){ + + exists, navigationLocation, err := mySettings.GetSetting("NavigationBarLocation") + if (err != nil) { return "", err } + if (exists == false){ + return "Top", nil + } + if (navigationLocation != "Top" && navigationLocation != "Bottom" && navigationLocation != "Left" && navigationLocation != "Right"){ + return "", errors.New("Invalid NavigationBarLocation: " + navigationLocation) + } + return navigationLocation, nil + } + + navigationLocation, err := getNavigationLocation() + if (err != nil) { return nil, "", err } + + getNavbarButtonsList := func()([]*fyne.Container, error){ + + _, showHostButton, err := mySettings.GetSetting("ShowHostButtonNavigation") + if (err != nil) { return nil, err } + + _, showModerateButton, err := mySettings.GetSetting("ShowModerateButtonNavigation") + if (err != nil) { return nil, err } + + if (showHostButton == "Yes" && showModerateButton == "Yes"){ + + itemsList := []*fyne.Container{homeButtonWithIcon, geneticsButtonWithIcon, profileButtonWithIcon, desiresButtonWithIcon, matchesButtonWithIcon, chatButtonWithIcon, hostButtonWithIcon, moderateButtonWithIcon, settingsButtonWithIcon} + + return itemsList, nil + + } + if (showHostButton != "Yes" && showModerateButton == "Yes"){ + + itemsList := []*fyne.Container{homeButtonWithIcon, geneticsButtonWithIcon, profileButtonWithIcon, desiresButtonWithIcon, matchesButtonWithIcon, chatButtonWithIcon, moderateButtonWithIcon, settingsButtonWithIcon } + + return itemsList, nil + } + if (showHostButton == "Yes" && showModerateButton != "Yes"){ + + itemsList := []*fyne.Container{homeButtonWithIcon, geneticsButtonWithIcon, profileButtonWithIcon, desiresButtonWithIcon, matchesButtonWithIcon, chatButtonWithIcon, hostButtonWithIcon, settingsButtonWithIcon } + + return itemsList, nil + } + + // showHostButton != "Yes" && showModerateButton != "Yes" + + itemsList := []*fyne.Container{homeButtonWithIcon, geneticsButtonWithIcon, profileButtonWithIcon, desiresButtonWithIcon, matchesButtonWithIcon, chatButtonWithIcon, settingsButtonWithIcon } + + return itemsList, nil + } + + navbarButtonsList, err := getNavbarButtonsList() + if (err != nil) { return nil, "", err } + + if (navigationLocation == "Top" || navigationLocation == "Bottom"){ + + navbar := container.NewHBox() + navbar.Add(layout.NewSpacer()) + + for _, element := range navbarButtonsList{ + navbar.Add(element) + } + navbar.Add(layout.NewSpacer()) + + navbarScrollable := container.NewHScroll(navbar) + + if (navigationLocation == "Top"){ + navbarWithSeparator := container.NewVBox(navbarScrollable, widget.NewSeparator()) + + return navbarWithSeparator, navigationLocation, nil + } + + navbarWithSeparator := container.NewVBox(widget.NewSeparator(), navbarScrollable) + + return navbarWithSeparator, navigationLocation, nil + } + if (navigationLocation == "Left" || navigationLocation == "Right"){ + + navbar := container.NewVBox() + navbar.Add(layout.NewSpacer()) + + for _, element := range navbarButtonsList{ + navbar.Add(element) + } + navbar.Add(layout.NewSpacer()) + + navbarScrollable := container.NewVScroll(navbar) + + if (navigationLocation == "Left"){ + navbarWithSeparator := container.NewHBox(navbarScrollable, widget.NewSeparator()) + + return navbarWithSeparator, navigationLocation, nil + } + + navbarWithSeparator := container.NewHBox(widget.NewSeparator(), navbarScrollable) + + return navbarWithSeparator, navigationLocation, nil + } + + return nil, "", errors.New("Invalid Navigation bar location: " + navigationLocation) +} + +// This page is used when a new GUI page is being loaded, and concurrency is not being used +// When concurrency is being used, we give the user a GUI page where they can exit the page instead of waiting for the process to complete +// This screen should only be used when the time it takes to load will only be a few seconds +func setLoadingScreen(window fyne.Window, pageTitle string, loadingText string){ + + title := getPageTitleCentered(pageTitle) + + loadingLabel := getWidgetCentered(getItalicLabel(loadingText)) + progressBar := getWidgetCentered(widget.NewProgressBarInfinite()) + + pageContent := container.NewVBox(title, loadingLabel, progressBar) + + page := container.NewCenter(pageContent) + + window.SetContent(page) +} + + + +// This will only be used within the startup gui +func setErrorEncounteredPage_NoNavBar(window fyne.Window, err error, showBackButton bool, previousPage func()){ + + title := getPageTitleCentered("Error Encountered") + + header := container.NewVBox(title) + + if (showBackButton == true){ + backButton := getBackButtonCentered(previousPage) + header.Add(backButton) + } + + header.Add(widget.NewSeparator()) + + description1 := getLabelCentered("Something went wrong. Report this error to Seekia developers.") + description2 := getBoldLabelCentered("Be aware that this error may contain sensitive information.") + description3 := getLabelCentered("Censor any sensitive information manually before sharing the error.") + + header.Add(description1) + header.Add(description2) + header.Add(description3) + header.Add(widget.NewSeparator()) + + getErrorString := func()string{ + if (err == nil){ + return "No nav bar error encountered page called with nil error." + } + errorString := err.Error() + return errorString + } + + errorString := getErrorString() + + errorLabel := widget.NewLabel(errorString) + errorLabel.Wrapping = 3 + errorLabel.Alignment = 1 + errorLabel.TextStyle = getFyneTextStyle_Bold() + + //TODO: Add copyable toggle + + page := container.NewBorder(header, nil, nil, nil, errorLabel) + + window.SetContent(page) +} + +func setErrorEncounteredPage(window fyne.Window, err error, previousPage func()){ + + appMemory.SetMemoryEntry("CurrentViewedPage", "ErrorEncounteredPage") + + currentPage := func(){setErrorEncounteredPage(window, err, previousPage)} + + title := getPageTitleCentered("Error Encountered") + + backButton := getBackButtonCentered(previousPage) + + description1 := getLabelCentered("Something went wrong. Report this error to Seekia developers.") + description2 := getBoldLabelCentered("Be aware that this error may contain sensitive information.") + description3 := getLabelCentered("Censor any sensitive information manually before sharing the error.") + + getErrorString := func()string{ + if (err == nil){ + return "Error encountered page called with nil error." + } + errorString := err.Error() + return errorString + } + + errorString := getErrorString() + + errorStringTrimmed, _, _ := helpers.TrimAndFlattenString(errorString, 40) + + errorLabel := getBoldLabelCentered(errorStringTrimmed) + + viewTextButton := widget.NewButtonWithIcon("", theme.VisibilityIcon(), func(){ + setViewTextPage(window, "Viewing Error", errorString, false, currentPage) + }) + + errorRow := container.NewHBox(layout.NewSpacer(), errorLabel, viewTextButton, layout.NewSpacer()) + + page := container.NewVBox(title, backButton, widget.NewSeparator(), description1, description2, description3, widget.NewSeparator(), errorRow, widget.NewSeparator()) + + setPageContent(page, window) +} + +func showUnderConstructionDialog(window fyne.Window){ + + dialogTitle := translate("Under Construction") + dialogMessageA := getLabelCentered(translate("Seekia is under construction.")) + dialogMessageB := getLabelCentered(translate("This page/feature needs to be built.")) + dialogContent := container.NewVBox(dialogMessageA, dialogMessageB) + dialog.ShowCustom(dialogTitle, translate("Close"), dialogContent, window) +} + +func setHomePage(window fyne.Window){ + + appMemory.SetMemoryEntry("CurrentViewedPage", "Home") + + currentPage := func(){setHomePage(window)} + + title := getPageTitleCentered("Home") + + welcomeTitle := getBoldLabelCentered("Welcome to Seekia!") + + welcomeMessage := getLabelCentered("Seekia is a race aware mate discovery network.") + + exists, currentUserName := appUsers.GetCurrentAppUserName() + if (exists == false){ + setErrorEncounteredPage(window, errors.New("setHomePage called with no signed-in user."), func(){setChooseAppUserPage(window)}) + return + } + currentUserLabel := getBoldItalicLabel("Current User:") + + changeUserButton := getWidgetCentered(widget.NewButtonWithIcon(currentUserName, theme.AccountIcon(), func(){ + + setLoadingScreen(window, "Signing Out", "Signing out...") + + err := appUsers.SignOutOfAppUser() + if (err != nil){ + setErrorEncounteredPage_NoNavBar(window, err, true, func(){setChooseAppUserPage(window)}) + return + } + + setChooseAppUserPage(window) + })) + + currentUserRow := container.NewHBox(layout.NewSpacer(), currentUserLabel, changeUserButton, layout.NewSpacer()) + + helpIcon, err := getFyneImageIcon("Info") + if (err != nil){ + setErrorEncounteredPage(window, err, currentPage) + return + } + helpButton := widget.NewButton(translate("Help"), func(){ + setHelpPage(window, currentPage) + }) + helpButtonWithIcon := container.NewGridWithColumns(1, helpIcon, helpButton) + + rulesIcon, err := getFyneImageIcon("Questionnaire") + if (err != nil){ + setErrorEncounteredPage(window, err, currentPage) + return + } + + rulesButton := widget.NewButton("Rules", func(){ + setViewSeekiaRulesPage(window, currentPage) + }) + rulesButtonWithIcon := container.NewGridWithColumns(1, rulesIcon, rulesButton) + + syncIcon, err := getFyneImageIcon("Sync") + if (err != nil){ + setErrorEncounteredPage(window, err, currentPage) + return + } + + syncButton := widget.NewButton(translate("Sync"), func(){ + setSyncPage(window, currentPage) + }) + syncButtonWithIcon := container.NewGridWithRows(2, syncIcon, syncButton) + + buttonsRow := getContainerCentered(container.NewGridWithRows(1, helpButtonWithIcon, rulesButtonWithIcon, syncButtonWithIcon)) + + description1 := getBoldLabelCentered("Learn how to use Seekia on the Help page.") + description2 := getLabelCentered("View the Seekia rules on the Rules page.") + description3 := getLabelCentered("Manage your connection to the Seekia network on the Sync page.") + + seekiaLinkTitle := widget.NewLabel("Stay updated at:") + + //TODO: Retrieve URL from parameters + // URL may need to be changed if it is lost or stolen + // Also add a page that shows .onion and .eth URLs + seekiaLink := getBoldLabel("Seekia.net") + + seekiaVersion := getLabelCentered("Seekia Version 0.50") + + seekiaLinkWithTitle := container.NewHBox(layout.NewSpacer(), seekiaLinkTitle, seekiaLink, layout.NewSpacer()) + + page := container.NewVBox(title, widget.NewSeparator(), welcomeTitle, welcomeMessage, widget.NewSeparator(), currentUserRow, widget.NewSeparator(), buttonsRow, widget.NewSeparator(), description1, description2, description3, widget.NewSeparator(), seekiaLinkWithTitle, seekiaVersion) + + setPageContent(page, window) +} + +func setSelectLanguagePage(window fyne.Window, showNavigationBar bool, previousPage func()){ + + title := getPageTitleCentered("Select Language") + + backButton := getBackButtonCentered(previousPage) + + description := getLabelCentered("Choose your language.") + + currentLanguage := translation.GetMyLanguage() + + currentLanguageLabel := getBoldLabelCentered("Current Language:") + currentLanguageTextLabel := getLabelCentered(currentLanguage) + + //TODO + + comingSoonLabel := getBoldLabelCentered("More languages coming soon.") + + page := container.NewVBox(title, backButton, widget.NewSeparator(), description, widget.NewSeparator(), currentLanguageLabel, currentLanguageTextLabel, widget.NewSeparator(), comingSoonLabel) + + if (showNavigationBar == true){ + setPageContent(page, window) + } else { + window.SetContent(page) + } +} + +func setViewSeekiaRulesPage(window fyne.Window, previousPage func()){ + + title := getPageTitleCentered("Seekia Rules") + + backButton := getBackButtonCentered(previousPage) + + description1 := getBoldLabelCentered("Unruleful content will be banned by the Seekia moderators.") + description2 := getLabelCentered("You can report rulebreaking content within the app.") + description3 := getLabelCentered("Identities, profiles and messages can be reported and banned.") + + //TODO: Add more rules, and improve these rules. + // There should be a more descriptive page for moderators that has specific examples + + spamDescription := widget.NewLabel("Selling or trading. Advertising or soliciting anything except yourself.") + spamAccordionItem := widget.NewAccordionItem("Spam", spamDescription) + + nudityDescription := widget.NewLabel("Nudity and pornography.") + nudityAccordionItem := widget.NewAccordionItem("Nudity", nudityDescription) + + dangerousDescription := widget.NewLabel("Threats, harrasment, and other dangerous content.") + dangerousAccordionItem := widget.NewAccordionItem("Dangerous Content", dangerousDescription) + + illegalDescription := widget.NewLabel("Prostitution, drug trading, copyrighted content, and other unlawful content.") + illegalAccordionItem := widget.NewAccordionItem("Illegal Content", illegalDescription) + + rulesAccordion := getContainerCentered(getWidgetBoxed(widget.NewAccordion(spamAccordionItem, nudityAccordionItem, dangerousAccordionItem, illegalAccordionItem))) + + page := container.NewVBox(title, backButton, widget.NewSeparator(), description1, description2, description3, rulesAccordion) + + setPageContent(page, window) +} + +// If profileTypeProvided == true, there is no ability to navigate between profile types +// If profileTypeProvided == false, you can navigate between profile types, and the result is saved in ProfilePageProfileType +func setProfilePage(window fyne.Window, profileTypeProvided bool, profileType string, previousPageProvided bool, previousPage func()){ + + currentPage := func(){setProfilePage(window, profileTypeProvided, profileType, previousPageProvided, previousPage)} + + appMemory.SetMemoryEntry("CurrentViewedPage", "Profile") + + getModeratorModeEnabledStatus := 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 + } + + getHostModeEnabledStatus := 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 + } + + moderatorModeEnabled, err := getModeratorModeEnabledStatus() + if (err != nil) { + setErrorEncounteredPage(window, err, func(){setHomePage(window)}) + return + } + + hostModeEnabled, err := getHostModeEnabledStatus() + if (err != nil) { + setErrorEncounteredPage(window, err, func(){setHomePage(window)}) + return + } + + getMyProfileType := func()(string, error){ + + if (profileTypeProvided == true){ + + if (profileType != "Mate" && profileType != "Host" && profileType != "Moderator"){ + return "", errors.New("setProfilePage called with invalid profileType: " + profileType) + } + return profileType, nil + } + + exists, myProfileType, err := mySettings.GetSetting("ProfilePageProfileType") + if (err != nil) { return "", err } + if (exists == false){ + return "Mate", nil + } + if (myProfileType != "Mate" && myProfileType != "Host" && myProfileType != "Moderator"){ + return "", errors.New("mySettings malformed: invalid ProfilePageProfileType: " + myProfileType) + } + + if (myProfileType == "Moderator" && moderatorModeEnabled == false){ + return "Mate", nil + } + if (myProfileType == "Host" && hostModeEnabled == false){ + return "Mate", nil + } + + return myProfileType, nil + } + + myProfileType, err := getMyProfileType() + if (err != nil) { + setErrorEncounteredPage(window, err, func(){setHomePage(window)}) + return + } + + title := getPageTitleCentered("My Profile") + + page := container.NewVBox(title) + + if (previousPageProvided == true){ + backButton := getBackButtonCentered(previousPage) + page.Add(backButton) + } + + profileTypeIcon, err := getIdentityTypeIcon(myProfileType, -10) + if (err != nil){ + setErrorEncounteredPage(window, err, func(){setHomePage(window)}) + return + } + + getChangeProfileTypeButtonOrText := func()(fyne.Widget, error){ + + if (profileTypeProvided == true){ + // The page does not offer a button to switch between profile types. + profileTypeLabel := getBoldLabel(myProfileType + " Profile") + return profileTypeLabel, nil + } + + if (moderatorModeEnabled == false && hostModeEnabled == false){ + + mateLabel := getBoldLabel("Mate Profile") + return mateLabel, nil + } + + changeProfileTypeButton := widget.NewButton(myProfileType + " Profile", func(){ + getNextProfileType := func()string{ + if (myProfileType == "Mate"){ + + if (moderatorModeEnabled == true){ + return "Moderator" + } + + return "Host" + } + if (myProfileType == "Moderator"){ + if (hostModeEnabled == true){ + return "Host" + } + return "Mate" + } + + return "Mate" + } + + nextProfileType := getNextProfileType() + + err := mySettings.SetSetting("ProfilePageProfileType", nextProfileType) + if (err != nil){ + setErrorEncounteredPage(window, err, func(){setHomePage(window)}) + return + } + + currentPage() + }) + + return changeProfileTypeButton, nil + } + + changeProfileTypeButtonOrText, err := getChangeProfileTypeButtonOrText() + if (err != nil){ + setErrorEncounteredPage(window, err, func(){setHomePage(window)}) + return + } + + changeProfileTypeButtonWithIcon := getContainerCentered(container.NewGridWithRows(2, profileTypeIcon, changeProfileTypeButtonOrText)) + + getDescriptionSection := func()*fyne.Container{ + + description1 := getLabelCentered("Manage your " + myProfileType + " profile.") + + if (myProfileType == "Mate"){ + description2 := getLabelCentered("Start by building your profile.") + description3 := getLabelCentered("Broadcast your profile once it is ready.") + description4 := getLabelCentered("You must rebroadcast your profile whenever you make changes.") + + descriptionSection := container.NewVBox(description1, description2, description3, description4) + return descriptionSection + } + if (myProfileType == "Moderator"){ + + description2 := getLabelCentered("Building your Moderator profile is optional.") + description3 := getLabelCentered("You must rebroadcast your profile whenever you make changes.") + + descriptionSection := container.NewVBox(description1, description2, description3) + return descriptionSection + } + + // myProfileType = "Host" + description2 := getLabelCentered("Building your Host profile is optional.") + description3 := getLabelCentered("Your Host profile will be broadcast automatically.") + + descriptionSection := container.NewVBox(description1, description2, description3) + return descriptionSection + } + + descriptionSection := getDescriptionSection() + + buildProfileIcon, err := getFyneImageIcon("Check") + if (err != nil) { + setErrorEncounteredPage(window, err, func(){setHomePage(window)}) + return + } + + profileIcon, err := getFyneImageIcon("Profile") + if (err != nil) { + setErrorEncounteredPage(window, err, func(){setHomePage(window)}) + return + } + + broadcastIcon, err := getFyneImageIcon("Broadcast") + if (err != nil) { + setErrorEncounteredPage(window, err, func(){setHomePage(window)}) + return + } + + buildProfileButton := widget.NewButton("Build", func(){ + if (myProfileType == "Mate"){ + setBuildMyMateProfilePage(window, currentPage) + return + } + if (myProfileType == "Moderator"){ + setBuildMyModeratorProfilePage(window, currentPage) + return + } + setBuildMyHostProfilePage(window, currentPage) + }) + buildProfileButtonWithIcon := container.NewGridWithRows(2, buildProfileIcon, buildProfileButton) + + broadcastButton := widget.NewButton(translate("Broadcast"), func(){ + if (myProfileType == "Host"){ + //TODO: Add a custom broadcast page for Host where you can monitor the last time it was broadcast and deactivate + showUnderConstructionDialog(window) + return + } + + setBroadcastPage(window, myProfileType, currentPage) + }) + broadcastButtonWithIcon := container.NewGridWithRows(2, broadcastIcon, broadcastButton) + + viewProfileButton := widget.NewButton(translate("View"), func(){ + setViewMyLocalOrPublicProfilePage(window, myProfileType, currentPage) + }) + viewProfileButtonWithIcon := container.NewGridWithRows(2, profileIcon, viewProfileButton) + + actionsGrid := container.NewGridWithRows(1, buildProfileButtonWithIcon, broadcastButtonWithIcon, viewProfileButtonWithIcon) + actionsSection := getContainerCentered(actionsGrid) + + page.Add(widget.NewSeparator()) + page.Add(changeProfileTypeButtonWithIcon) + page.Add(widget.NewSeparator()) + page.Add(descriptionSection) + page.Add(widget.NewSeparator()) + page.Add(actionsSection) + + if (myProfileType == "Moderator"){ + + page.Add(widget.NewSeparator()) + + scoreIcon, err := getFyneImageIcon("Score") + if (err != nil){ + setErrorEncounteredPage(window, err, func(){setHomePage(window)}) + return + } + + myScoreButton := widget.NewButton("My Score", func(){ + setViewMyModeratorScorePage(window, currentPage) + }) + myScoreButtonWithIcon := container.NewGridWithColumns(1, scoreIcon, myScoreButton) + + reviewsIcon, err := getFyneImageIcon("Choice") + if (err != nil){ + setErrorEncounteredPage(window, err, func(){setHomePage(window)}) + return + } + + myReviewsButton := widget.NewButton("My Reviews", func(){ + setViewMyReviewsPage(window, "Profile", currentPage) + }) + myReviewsButtonWithIcon := container.NewGridWithColumns(1, reviewsIcon, myReviewsButton) + + moderatorButtonsRow := getContainerCentered(container.NewGridWithRows(1, myScoreButtonWithIcon, myReviewsButtonWithIcon)) + + page.Add(moderatorButtonsRow) + page.Add(widget.NewSeparator()) + } + + setPageContent(page, window) +} + +// This is a page to view an identity hash in its entirety, and a text entry to copy it +func setViewIdentityHashPage(window fyne.Window, identityHash [16]byte, previousPage func()){ + + title := getPageTitleCentered("Viewing Identity Hash") + + backButton := getBackButtonCentered(previousPage) + + identityHashString, _, err := identity.EncodeIdentityHashBytesToString(identityHash) + if (err != nil){ + identityHashHex := encoding.EncodeBytesToHexString(identityHash[:]) + setErrorEncounteredPage(window, errors.New("setViewIdentityHashPage called with invalid identity hash: " + identityHashHex), previousPage) + } + + identityHashLabel := getBoldLabel(identityHashString) + identityHashLabelPadded := container.NewPadded(container.NewPadded(identityHashLabel)) + + identityHashEntry := widget.NewMultiLineEntry() + identityHashEntry.SetText(identityHashString) + identityHashEntry.OnChanged = func(_ string){ + identityHashEntry.SetText(identityHashString) + } + identityHashEntryBoxed := getWidgetBoxed(identityHashEntry) + + widener := widget.NewLabel(" ") + identityHashEntryWidened := getContainerCentered(container.NewGridWithColumns(1, identityHashEntryBoxed, widener)) + + page := container.NewVBox(title, backButton, widget.NewSeparator(), identityHashLabelPadded, identityHashEntryWidened) + + setPageContent(page, window) +} + +// This is a page to view an message/profile hash in its entirety, and a text entry to copy it +func setViewContentHashPage(window fyne.Window, hashType string, contentHash []byte, previousPage func()){ + + title := getPageTitleCentered("Viewing " + hashType + " Hash") + + backButton := getBackButtonCentered(previousPage) + + contentHashHex := encoding.EncodeBytesToHexString(contentHash) + + contentHashLabel := getBoldLabel(contentHashHex) + contentHashLabelPadded := container.NewPadded(container.NewPadded(contentHashLabel)) + + contentHashEntry := widget.NewMultiLineEntry() + contentHashEntry.SetText(contentHashHex) + contentHashEntry.OnChanged = func(_ string){ + contentHashEntry.SetText(contentHashHex) + } + contentHashEntry.Wrapping = 3 + + contentHashEntryBoxed := getWidgetBoxed(contentHashEntry) + + widener := widget.NewLabel(" ") + contentHashEntryWidened := getContainerCentered(container.NewGridWithColumns(1, contentHashEntryBoxed, widener)) + + page := container.NewVBox(title, backButton, widget.NewSeparator(), contentHashLabelPadded, contentHashEntryWidened) + + setPageContent(page, window) +} + +//TODO: Make this page able to handle large amounts of text without crashing +// In the meantime, trim text that is too long to render +func setViewTextPage(window fyne.Window, pageTitle string, textToView string, viewCopyable bool, previousPage func()){ + + title := getPageTitleCentered(pageTitle) + + backButton := getBackButtonCentered(previousPage) + + header := container.NewVBox(title, backButton, widget.NewSeparator()) + + getTextViewWidget := func()fyne.Widget{ + + if (viewCopyable == true){ + + textEntry := widget.NewMultiLineEntry() + textEntry.SetText(textToView) + textEntry.OnChanged = func(_ string){ + textEntry.SetText(textToView) + } + textEntry.Wrapping = 3 + + return textEntry + } + + textViewWidget := widget.NewLabel(textToView) + textViewWidget.Wrapping = 3 + textViewWidget.Alignment = 1 + + return textViewWidget + } + + textViewWidget := getTextViewWidget() + + getChangeDisplayTypeButton := func()fyne.Widget{ + + if (viewCopyable == false){ + + viewCopyableButton := widget.NewButtonWithIcon("View Copyable", theme.ContentPasteIcon(), func(){ + setViewTextPage(window, pageTitle, textToView, true, previousPage) + }) + return viewCopyableButton + } + viewUncopyableButton := widget.NewButtonWithIcon("View Uncopyable", theme.VisibilityIcon(), func(){ + setViewTextPage(window, pageTitle, textToView, false, previousPage) + }) + return viewUncopyableButton + } + + changeDisplayTypeButton := getChangeDisplayTypeButton() + + changeDisplayTypeButtonCentered := getWidgetCentered(changeDisplayTypeButton) + + page := container.NewBorder(header, changeDisplayTypeButtonCentered, nil, nil, textViewWidget) + + setPageContent(page, window) +} + +func setViewLinkPage(window fyne.Window, pageTitle string, textToView string, previousPage func()){ + + title := getPageTitleCentered(pageTitle) + + backButton := getBackButtonCentered(previousPage) + + header := container.NewVBox(title, backButton, widget.NewSeparator()) + + textEntry := widget.NewMultiLineEntry() + textEntry.SetText(textToView) + textEntry.OnChanged = func(_ string){ + textEntry.SetText(textToView) + } + textEntry.Wrapping = 3 + + page := container.NewBorder(header, nil, nil, nil, textEntry) + + setPageContent(page, window) +} + + +func getCryptocurrencyAddressLabelWithCopyAndQRButtons(window fyne.Window, cryptocurrency string, cryptoAddress string, currentPage func())(*fyne.Container, error){ + + if (cryptocurrency != "Ethereum" && cryptocurrency != "Cardano"){ + return nil, errors.New("getCryptocurrencyAddressLabelWithCopyAndQRButtons called with invalid cryptocurrency: " + cryptocurrency) + } + + addressLabel := widget.NewMultiLineEntry() + addressLabel.SetText(cryptoAddress) + addressLabel.OnChanged = func(_ string){ + addressLabel.SetText(cryptoAddress) + } + addressBox := getWidgetBoxed(addressLabel) + + qrCodeImage, err := getFyneImageIcon("QR") + if (err != nil) { return nil, err } + + qrCodeButton := widget.NewButton("QR Code", func(){ + + title := translate(cryptocurrency + " QR Code") + + //TODO: Add QR Code generation + dialogMessage := getLabelCentered("Under Construction") + + dialog.ShowCustom(title, translate("Close"), dialogMessage, window) + }) + + qrCodeButtonWithIcon := getContainerBoxed(container.NewGridWithColumns(1, qrCodeImage, qrCodeButton)) + + clipboardIcon, err := getFyneImageIcon("Clipboard") + if (err != nil){ return nil, err } + + currentClipboard := window.Clipboard() + + getClipboardButtonText := func()string{ + + currentContent := currentClipboard.Content() + if (currentContent == cryptoAddress){ + return "Copied!" + } + return "Copy" + } + + clipboardButtonText := getClipboardButtonText() + copyToClipboardButton := widget.NewButton(clipboardButtonText, func(){ + + currentClipboard.SetContent(cryptoAddress) + currentPage() + }) + + clipboardButtonWithIcon := getContainerBoxed(container.NewGridWithColumns(1, clipboardIcon, copyToClipboardButton)) + + addressBoxWithClipboardQRRow := getContainerCentered(container.NewGridWithRows(1, qrCodeButtonWithIcon, addressBox, clipboardButtonWithIcon)) + + return addressBoxWithClipboardQRRow, nil +} + + +// This function creates a widget list from an input list of strings +// The onClickedFunction is called if any of the items are selected +//Inputs: +// -[]string: A list of the items +// -func(itemIndex int): The function that is called when an item is selected +// Outputs: +// -fyne.Widget: Widget list +// -error +func getFyneWidgetListFromStringList(inputList []string, onClickedFunction func(int))(fyne.Widget, error){ + + listLength := len(inputList) + + if (listLength == 0){ + return nil, errors.New("getFyneWidgetListFromStringList called with empty list.") + } + + getListLength := func()int{ + return listLength + } + + createListItemFunction := func()fyne.CanvasObject{ + listItem := container.NewHBox(layout.NewSpacer(), widget.NewLabel(""), layout.NewSpacer()) + return listItem + } + updateListItemFunction := func(id widget.ListItemID, item fyne.CanvasObject){ + itemName := inputList[id] + + itemObjectsList := item.(*fyne.Container).Objects + + itemLabel := itemObjectsList[1].(*widget.Label) + + itemLabel.SetText(itemName) + } + + widgetList := widget.NewList(getListLength, createListItemFunction, updateListItemFunction) + + widgetList.OnSelected = func(itemIndex widget.ListItemID) { + onClickedFunction(itemIndex) + } + + return widgetList, nil +} + + diff --git a/gui/helpGui.go b/gui/helpGui.go new file mode 100644 index 0000000..6dcae3f --- /dev/null +++ b/gui/helpGui.go @@ -0,0 +1,1101 @@ +package gui + +// helpGui.go implements pages to explain Seekia's features and functionality to users + +import "fyne.io/fyne/v2" +import "fyne.io/fyne/v2/container" +import "fyne.io/fyne/v2/theme" +import "fyne.io/fyne/v2/widget" + +import "seekia/resources/geneticReferences/monogenicDiseases" + +//TODO: Improve many of these descriptions + +func setHelpPage(window fyne.Window, previousPage func()){ + + title := getPageTitleCentered("Help") + + backButton := getBackButtonCentered(previousPage) + + subtitle := getPageSubtitleCentered("Help") + + description1 := getLabelCentered("Select your desires on the Desires page.") + description2 := getLabelCentered("View matches on the Matches page.") + description3 := getLabelCentered("Build and broadcast your profile on the Profile page.") + description4 := getLabelCentered("You can view matches without broadcasting a profile.") + + description5 := getBoldLabelCentered("More help pages are coming soon.") + + //TODO: Add buttons and navigation to view more help pages. + + page := container.NewVBox(title, backButton, widget.NewSeparator(), subtitle, widget.NewSeparator(), description1, description2, description3, description4, description5) + + setPageContent(page, window) +} + +func setMateProfileExpirationExplainerPage(window fyne.Window, previousPage func()){ + + title := getPageTitleCentered("Help - Mate Profile Expiration") + + backButton := getBackButtonCentered(previousPage) + + subtitle := getPageSubtitleCentered("Mate Profile Expiration") + + //TODO: Retrieve the expiration time from the parameters + description1 := getLabelCentered("All mate profiles expire after 3 months.") + description2 := getLabelCentered("You must broadcast a new profile at least this often.") + description3 := getLabelCentered("Your new profile does not need to have any changes.") + description4 := getLabelCentered("The Broadcast page will tell you when your profile will expire.") + description5 := getLabelCentered("This is seperate from your identity balance, which also expires.") + description6 := getLabelCentered("Your identity balance must also be funded to broadcast any profiles.") + + page := container.NewVBox(title, backButton, widget.NewSeparator(), subtitle, widget.NewSeparator(), description1, description2, description3, description4, widget.NewSeparator(), description5, description6, widget.NewSeparator()) + + setPageContent(page, window) +} + +func setGreetAndRejectExplainerPage(window fyne.Window, previousPage func()){ + + title := getPageTitleCentered("Help - Greet And Reject") + + backButton := getBackButtonCentered(previousPage) + + subtitle := getPageSubtitleCentered("Greet And Reject") + + description1 := getLabelCentered("Seekia users can send Greet and Reject messages.") + description2 := getLabelCentered("Greet messages are sent to indicate interest in a user.") + description3 := getLabelCentered("Reject messages are sent to tell a user they are not interested.") + description4 := getLabelCentered("You can filter your matches to only show users who have greeted or contacted you.") + description5 := getLabelCentered("You can also hide users who have rejected you.") + description6 := getLabelCentered("You can undo a rejection by sending a greeting.") + + page := container.NewVBox(title, backButton, widget.NewSeparator(), subtitle, widget.NewSeparator(), description1, description2, description3, description4, description5, description6, widget.NewSeparator()) + + setPageContent(page, window) +} + +func setSexExplainerPage(window fyne.Window, previousPage func()){ + + title := getPageTitleCentered("Help - Sex") + + backButton := getBackButtonCentered(previousPage) + + subtitle := getPageSubtitleCentered("Sex") + + description1 := getLabelCentered("Sex refers to a person's physical sex.") + description2 := getLabelCentered("Male: XY chromosomes, born with male anatomy.") + description3 := getLabelCentered("Female: XX chromosomes, born with female anatomy.") + description4 := getLabelCentered("Intersex Male: Not either of the above categories, but closer to Male.") + description5 := getLabelCentered("Intersex Female: Not any of the above categories, but closer to Female.") + description6 := getLabelCentered("Intersex: Not any of the above categories.") + + page := container.NewVBox(title, backButton, widget.NewSeparator(), subtitle, widget.NewSeparator(), description1, description2, description3, description4, description5, description6) + + setPageContent(page, window) +} + +func setMateProfileAttributeVisibilityExplainerPage(window fyne.Window, previousPage func()){ + + title := getPageTitleCentered("Help - Profile Attribute Visibility") + + backButton := getBackButtonCentered(previousPage) + + subtitle := getPageSubtitleCentered("Profile Attribute Visibility") + + description1 := getLabelCentered("Some Mate profile attributes can have their visibility turned on or off.") + description2 := getLabelCentered("If you turn an attribute's visibility off, it will not show up in your profile.") + description3 := getLabelCentered("This feature is needed to be able to calculate desires privately.") + description4 := getLabelCentered("For example, if you want to calculate distance, you must provide your location.") + description5 := getLabelCentered("If you don't want your profile to contain your location, you can turn off its visibility.") + description6 := getLabelCentered("This way, you can still calculate distance without sharing your location publicly.") + + page := container.NewVBox(title, backButton, widget.NewSeparator(), subtitle, widget.NewSeparator(), description1, description2, description3, description4, description5, description6) + + setPageContent(page, window) +} + +func setProfileGenomeInfoExplainerPage(window fyne.Window, previousPage func()){ + + title := getPageTitleCentered("Help - Profile Genome Info") + + backButton := getBackButtonCentered(previousPage) + + subtitle := getPageSubtitleCentered("Profile Genome Information") + + description1 := getLabelCentered("Seekia allows users to share their genome information.") + description2 := getLabelCentered("Users can learn about the monogenic disease, polygenic disease, and trait information for offspring with each user.") + description3 := getLabelCentered("Users can learn the probability of their offspring having a specific disease.") + description4 := getLabelCentered("Users can use this information to reduce the probability of their offspring having diseases.") + description5 := getLabelCentered("Users can choose their mate to influence their offspring's trait and disease risk.") + description6 := getLabelCentered("Only specific locations of a user's genome are shared.") + + page := container.NewVBox(title, backButton, widget.NewSeparator(), subtitle, widget.NewSeparator(), description1, description2, description3, description4, description5, description6) + + setPageContent(page, window) +} + +func setDesireFilterOptionsExplainerPage(window fyne.Window, previousPage func()){ + + title := getPageTitleCentered("Help - Desire Filter Options") + + backButton := getBackButtonCentered(previousPage) + + subtitle := getPageSubtitleCentered("Desire Filter Options") + + description1 := getLabelCentered("Each desire has 1 or 2 filter options to configure.") + + description2 := getBoldLabelCentered("Filter All") + description3 := getLabelCentered("When enabled, only users who fulfill the desire will be shown in your matches.") + description4 := getLabelCentered("This does not filter users who have not responded.") + + description5 := getBoldLabelCentered("Require Response") + description6 := getLabelCentered("When enabled, only users who have responded to the attribute will be shown.") + description7 := getLabelCentered("This does not filter users who responded, but do not fulfill the desire.") + + description8 := getLabelCentered("If you disable both options, the desire will only effect match score calculations.") + + page := container.NewVBox(title, backButton, widget.NewSeparator(), subtitle, widget.NewSeparator(), description1, widget.NewSeparator(), description2, description3, description4, widget.NewSeparator(), description5, description6, description7, widget.NewSeparator(), description8) + + setPageContent(page, window) +} + +func setAncestryCompositionDesireRestrictiveModeExplainerPage(window fyne.Window, previousPage func()){ + + title := getPageTitleCentered("Help - Restrictive Mode") + + backButton := getBackButtonCentered(previousPage) + + subtitle := getPageSubtitleCentered("Ancestry Composition Desire - Restrictive Mode") + + description1 := getLabelCentered("The ancestry composition desire has 2 modes: Restrictive and Non-Restrictive.") + description2 := getLabelCentered("In restrictive mode, all ancestral groups which you desire must be described.") + description3 := getLabelCentered("For example, let's say you add 80-100% Melanesian as your only location desire.") + description4 := getLabelCentered("In non-restrictive mode, anyone who is 80-100% Melanesian will fulfill the desire.") + description5 := getLabelCentered("In restrictive mode, only users whom are 100% Melanesian will fulfill the desire.") + description6 := getLabelCentered("This is because in restrictive mode, you must define all racial amounts you will tolerate.") + description7 := getLabelCentered("In restrictive mode, if you will tolerate 0-2% for all locations, you must add 0-2% for all locations.") + + page := container.NewVBox(title, backButton, widget.NewSeparator(), subtitle, widget.NewSeparator(), description1, description2, description3, description4, description5, description6, description7) + + setPageContent(page, window) +} + +func setOffspringAncestryCompositionExplainerPage(window fyne.Window, previousPage func()){ + + title := getPageTitleCentered("Help - Offspring Ancestry Composition") + + backButton := getBackButtonCentered(previousPage) + + subtitle := getPageSubtitleCentered("Offspring Ancestry Composition") + + description1 := getLabelCentered("Seekia is able to calculate the ancestry composition of an offspring between two users.") + description2 := getLabelCentered("Any conceived offspring's ancestry composition may vary slightly from the prediction.") + description3 := getLabelCentered("Offspring from the same parents will not always inherit the same genetic sections from each parent.") + description4 := getLabelCentered("Consequently, siblings range between being ~40%-60% genetically identical.") + description5 := getLabelCentered("However, sometimes the offspring ancestry composition prediction is ~100% accurate.") + description6 := getLabelCentered("For example, if both parents are 100% Melanesian, their offspring will always be ~100% Melanesian.") + description7 := getLabelCentered("This rule applies for all ancestral locations, not just Melanesian.") + + page := container.NewVBox(title, backButton, widget.NewSeparator(), subtitle, widget.NewSeparator(), description1, description2, description3, description4, description5, description6, description7) + + setPageContent(page, window) +} + +func setGenomePhasingExplainerPage(window fyne.Window, previousPage func()){ + + title := getPageTitleCentered("Help - Genome Phasing") + + backButton := getBackButtonCentered(previousPage) + + subtitle := getPageSubtitleCentered("Genome Phasing") + + description1 := getLabelCentered("A raw genome file is either Phased or Unphased") + description2 := getLabelCentered("A phased genome file describes which parent each gene was inherited from.") + description3 := getLabelCentered("This allows Seekia to more accurately diagnose genetic diseases.") + description4 := getLabelCentered("Only some companies provide phased genome sequencing.") + + page := container.NewVBox(title, backButton, widget.NewSeparator(), subtitle, widget.NewSeparator(), description1, description2, description3, description4) + + setPageContent(page, window) +} + +func setGenomeSNPCountExplainerPage(window fyne.Window, previousPage func()){ + + title := getPageTitleCentered("Help - Genome SNP Count") + + backButton := getBackButtonCentered(previousPage) + + subtitle := getPageSubtitleCentered("Genome SNP Count") + + description1 := getLabelCentered("Each raw genome file has an SNP count.") + description2 := getLabelCentered("This is the number of SNPs, or locations, sequenced.") + description3 := getLabelCentered("The higher this number, the more information your genome file contains.") + + page := container.NewVBox(title, backButton, widget.NewSeparator(), subtitle, widget.NewSeparator(), description1, description2, description3) + + setPageContent(page, window) +} + + +func setCombinedGenomesExplainerPage(window fyne.Window, previousPage func()){ + + title := getPageTitleCentered("Help - Combined Genomes") + + backButton := getBackButtonCentered(previousPage) + + subtitle := getPageSubtitleCentered("Combined Genomes") + + description1 := getLabelCentered("Seekia combines multiple genome files into two different genomes.") + description2 := getLabelCentered("Genome sequencing can be inaccurate, so multiple imported genome files will sometimes disagree.") + description3 := getLabelCentered("Each genome file also contains different recorded locations.") + description4 := getLabelCentered("The Only Exclude Conflicts genome will include genome locations that only 1 genome has reported.") + description5 := getLabelCentered("The Only Include Shared genome only includes genome locations where at least 2 genomes agree.") + description6 := getLabelCentered("The Only Include Shared genome is the most accurate, but may contain less information.") + description7 := getLabelCentered("You should study all imported genomes to see what they report.") + + page := container.NewVBox(title, backButton, widget.NewSeparator(), subtitle, widget.NewSeparator(), description1, description2, description3, description4, description5, description6, description7) + + setPageContent(page, window) +} + +func setGenomeHasMonogenicDiseaseVariantMutationExplainerPage(window fyne.Window, previousPage func()){ + + title := getPageTitleCentered("Help - Genome Has Mutation") + + backButton := getBackButtonCentered(previousPage) + + subtitle := getPageSubtitleCentered("Genome Has Mutation") + + description1 := getLabelCentered("A monogenic disease variant is a specific defect in a gene that causes the disease.") + description2 := getLabelCentered("Each genome can either have 0, 1, or 2 mutations for each variant.") + description3 := getLabelCentered("If a genome has a variant mutation, then either 1 or 2 mutations exist.") + description4 := getLabelCentered("For a recessive monogenic disease, you need 2 mutations, one on each chromosome, to have the disease.") + description5 := getLabelCentered("For a dominant monogenic disease, you only need at least 1 mutation to have the disease.") + description6 := getLabelCentered("Unless your genome is phased, Seekia does not know which chromosome contains each mutation.") + description7 := getLabelCentered("Seekia calculates your probabilities of having and passing the disease automatically.") + + page := container.NewVBox(title, backButton, widget.NewSeparator(), subtitle, widget.NewSeparator(), description1, description2, description3, description4, description5, description6, description7) + + setPageContent(page, window) +} + +func setMonogenicDiseaseVariantsExplainerPage(window fyne.Window, previousPage func()){ + + title := getPageTitleCentered("Help - Monogenic Disease Variants") + + backButton := getBackButtonCentered(previousPage) + + subtitle := getPageSubtitleCentered("Monogenic Disease Variants") + + description1 := getLabelCentered("A monogenic disease variant is a specific defect in a gene that causes the disease.") + description2 := getLabelCentered("Each genome can either have 0, 1, or 2 mutations for each variant.") + description3 := getLabelCentered("For a recessive monogenic disease, you at least 2 mutations, one on each chromosome, to have the disease.") + description4 := getLabelCentered("For a dominant monogenic disease, you only need 1 mutation to have the disease.") + description5 := getLabelCentered("Unless your genome is phased, Seekia does not know which chromosome contains each mutation.") + description6 := getLabelCentered("Seekia calculates your probabilities of having and passing the disease automatically.") + + page := container.NewVBox(title, backButton, widget.NewSeparator(), subtitle, widget.NewSeparator(), description1, description2, description3, description4, description5, description6) + + setPageContent(page, window) +} + +func setVariantEffectIsMildExplainerPage(window fyne.Window, previousPage func()){ + + title := getPageTitleCentered("Help - Effect Is Mild") + + backButton := getBackButtonCentered(previousPage) + + subtitle := getPageSubtitleCentered("Variant Effect Is Mild") + + description1 := getLabelCentered("Some monogenic disease variants are known to be mild.") + description2 := getLabelCentered("They are known to only cause mild disease symptoms for the affected person.") + description3 := getLabelCentered("This may not be true for all cases.") + description4 := getLabelCentered("A defective gene is affected by all of its variants.") + description5 := getLabelCentered("If you have multiple variants, you will usually have a more severe disease.") + + page := container.NewVBox(title, backButton, widget.NewSeparator(), subtitle, widget.NewSeparator(), description1, description2, description3, description4, description5) + + setPageContent(page, window) +} + + +func setNumberOfTestedVariantsExplainerPage(window fyne.Window, previousPage func()){ + + title := getPageTitleCentered("Help - Tested Variants") + + backButton := getBackButtonCentered(previousPage) + + subtitle := getPageSubtitleCentered("Number of Tested Variants") + + description1 := getLabelCentered("Each monogenic disease has a set of variants Seekia tests for.") + description2 := getLabelCentered("These variants are specific mutations that cause the monogenic disease.") + description3 := getLabelCentered("The greater the number of tested variants is, the more accurate the results will be.") + description4 := getLabelCentered("Seekia cannot test for variants whose location your genome file does not contain.") + description5 := getLabelCentered("Seekia also does not test for many very rare disease variants.") + + page := container.NewVBox(title, backButton, widget.NewSeparator(), subtitle, widget.NewSeparator(), description1, description2, description3, description4, description5) + + setPageContent(page, window) +} + + +func setCoupleGenomePairExplainerPage(window fyne.Window, previousPage func()){ + + title := getPageTitleCentered("Help - Couple Genome Pair") + + backButton := getBackButtonCentered(previousPage) + + subtitle := getPageSubtitleCentered("Couple Genome Pair") + + description1 := getLabelCentered("A couple analysis analyzes either 1 or 2 genome pairs.") + description2 := getLabelCentered("Each genome pair contains 1 genome from each person.") + description3 := getLabelCentered("The results from each genome pair may differ.") + description4 := getLabelCentered("You should view which genomes were analyzed for each genome pair.") + + page := container.NewVBox(title, backButton, widget.NewSeparator(), subtitle, widget.NewSeparator(), description1, description2, description3, description4) + + setPageContent(page, window) +} + + +func setPersonProbabilityOfPassingVariantExplainerPage(window fyne.Window, previousPage func()){ + + title := getPageTitleCentered("Help - Probability Of Passing Variant") + + backButton := getBackButtonCentered(previousPage) + + subtitle := getPageSubtitleCentered("Probability Of Passing A Variant") + + description1 := getLabelCentered("This is the probability that a person will pass a monogenic disease variant to their offspring.") + description2 := getLabelCentered("If the disease is recessive, both parents need to pass a variant for the offspring to have the disease.") + description3 := getLabelCentered("If the disease is dominant, only one parent needs to pass a variant for the offspring to have the disease.") + description4 := getLabelCentered("This probability becomes more accurate if more variants are tested.") + description5 := getLabelCentered("The person's imported genome may not contain the locations for some variants.") + description6 := getLabelCentered("Seekia also does not test for many rare monogenic disease variants.") + + page := container.NewVBox(title, backButton, widget.NewSeparator(), subtitle, widget.NewSeparator(), description1, description2, description3, description4, description5, description6) + + setPageContent(page, window) +} + +func setPersonProbabilityOfHavingMonogenicDiseaseExplainerPage(window fyne.Window, previousPage func()){ + + title := getPageTitleCentered("Help - Person Monogenic Disease Probability") + + backButton := getBackButtonCentered(previousPage) + + subtitle := getPageSubtitleCentered("Probability Of Having Monogenic Disease") + + description1 := getLabelCentered("This is the probability that the analyzed person has this monogenic disease.") + description2 := getLabelCentered("This probability becomes more accurate if more variants are tested.") + description3 := getLabelCentered("The person's imported genomes may not contain locations for some variants.") + description4 := getLabelCentered("Seekia also does not test for many rare disease variants.") + + page := container.NewVBox(title, backButton, widget.NewSeparator(), subtitle, widget.NewSeparator(), description1, description2, description3, description4) + + setPageContent(page, window) +} + +func setOffspringProbabilityOfHavingMonogenicDiseaseExplainerPage(window fyne.Window, previousPage func()){ + + title := getPageTitleCentered("Help - Offspring Monogenic Disease Probability") + + backButton := getBackButtonCentered(previousPage) + + subtitle := getPageSubtitleCentered("Offspring Probability Of Having Monogenic Disease") + + description1 := getLabelCentered("This is the probability that an offspring from this couple will have the disease.") + description2 := getLabelCentered("This probability is more accurate if more variants are tested.") + description3 := getLabelCentered("The couple's imported genomes may not contain locations for some variants.") + description4 := getLabelCentered("Seekia also does not test for many rare disease variants.") + + page := container.NewVBox(title, backButton, widget.NewSeparator(), subtitle, widget.NewSeparator(), description1, description2, description3, description4) + + setPageContent(page, window) +} + + +func setOffspringProbabilityOfHavingVariantExplainerPage(window fyne.Window, previousPage func()){ + + title := getPageTitleCentered("Help - Offspring Variant Probability") + + backButton := getBackButtonCentered(previousPage) + + subtitle := getPageSubtitleCentered("Offspring Probability Of Having Variant") + + description1 := getLabelCentered("This is the probability that an offspring from this couple will have a variant for a monogenic disease.") + description2 := getLabelCentered("If the disease is dominant, an offspring with any variants will have the disease.") + description3 := getLabelCentered("If the disease is recessive, an offspring having a variant will not necessarily have the disease.") + description4 := getLabelCentered("They could be a disease carrier who does not experience any symptoms of the disease.") + description5 := getLabelCentered("Couples only need to try to avoid the offspring having the disease.") + description6 := getBoldLabelCentered("This probability is only shown to satisfy curiosity.") + description7 := getLabelCentered("This probability is more accurate if more variants are tested.") + description8 := getLabelCentered("The couple's imported genomes may not contain locations for some variants.") + description9 := getLabelCentered("Seekia also does not test for many rare disease variants.") + + page := container.NewVBox(title, backButton, widget.NewSeparator(), subtitle, widget.NewSeparator(), description1, description2, description3, description4, description5, description6, description7, description8, description9) + + setPageContent(page, window) +} + +func setPersonGeneticAnalysisConflictExistsExplainerPage(window fyne.Window, previousPage func()){ + + title := getPageTitleCentered("Help - Conflict Exists") + + backButton := getBackButtonCentered(previousPage) + + subtitle := getPageSubtitleCentered("Genome Conflict Exists") + + description1 := getLabelCentered("If a person has multiple genome files, the contents from each file may differ.") + description2 := getLabelCentered("This is because the sequencing process recorded different values.") + description3 := getLabelCentered("All genomes should contain the same sequence, so one of the genomes must have an error.") + description4 := getLabelCentered("These genomes may have conflicting analysis results.") + description5 := getLabelCentered("If they have conflicting results, you should view the results for each genome.") + description6 := getLabelCentered("Import more genomes to get more accurate results.") + + page := container.NewVBox(title, backButton, widget.NewSeparator(), subtitle, widget.NewSeparator(), description1, description2, description3, description4, description5, description6) + + setPageContent(page, window) +} + + +func setCoupleGeneticAnalysisConflictExistsExplainerPage(window fyne.Window, previousPage func()){ + + title := getPageTitleCentered("Help - Conflict Exists") + + backButton := getBackButtonCentered(previousPage) + + subtitle := getPageSubtitleCentered("Conflict Exists") + + description1 := getLabelCentered("If either person has multiple genomes, multiple genome pairs will be created.") + description2 := getLabelCentered("These genome pairs may have conflicting analysis results.") + description3 := getLabelCentered("These conflicts are caused by errors in the genome sequencing process.") + description4 := getLabelCentered("If they have conflicting results, you should view both genome pair results.") + + page := container.NewVBox(title, backButton, widget.NewSeparator(), subtitle, widget.NewSeparator(), description1, description2, description3, description4) + + setPageContent(page, window) +} + +func setPolygenicDiseaseAverageLifetimeRiskExplainerPage(window fyne.Window, previousPage func()){ + + title := getPageTitleCentered("Help - Lifetime Polygenic Disease Risk") + + backButton := getBackButtonCentered(previousPage) + + subtitle := getPageSubtitleCentered("Lifetime Polygenic Disease Risk") + + description1 := getLabelCentered("Each polygenic disease has a chart describing its lifetime risk.") + description2 := getLabelCentered("This represents the probability that any person will have the disease.") + description3 := getLabelCentered("For example, lets say the lifetime risk for a disease is 10%, if you are a Male between 50-60.") + description4 := getLabelCentered("This means that Males between the ages of 50-60 have a 10% probability of suffering from the disease.") + + page := container.NewVBox(title, backButton, widget.NewSeparator(), subtitle, widget.NewSeparator(), description1, description2, description3, description4) + + setPageContent(page, window) +} + +func setPolygenicDiseaseRiskScoreExplainerPage(window fyne.Window, previousPage func()){ + + title := getPageTitleCentered("Help - Polygenic Disease Risk Score") + + backButton := getBackButtonCentered(previousPage) + + subtitle := getPageSubtitleCentered("Polygenic Disease Risk Score") + + description1 := getLabelCentered("Person genetic analyses contain a person's risk score for each polygenic disease.") + description2 := getLabelCentered("This score describes their overall risk for the disease.") + description3 := getLabelCentered("0/10 = Lowest risk, 10/10 = Highest risk.") + description4 := getLabelCentered("The more locations that are tested, the more accurate the score is.") + + page := container.NewVBox(title, backButton, widget.NewSeparator(), subtitle, widget.NewSeparator(), description1, description2, description3, description4) + + setPageContent(page, window) +} + +func setOffspringPolygenicDiseaseRiskScoreExplainerPage(window fyne.Window, previousPage func()){ + + title := getPageTitleCentered("Help - Offspring Polygenic Disease Risk Score") + + backButton := getBackButtonCentered(previousPage) + + subtitle := getPageSubtitleCentered("Offspring Polygenic Disease Risk Score") + + description1 := getLabelCentered("Couple genetic analyses contain an offspring's risk score for each polygenic disease.") + description2 := getLabelCentered("This score describes the average risk score for offspring produced by the couple.") + description3 := getLabelCentered("0/10 = Lowest risk, 10/10 = Highest risk.") + description4 := getLabelCentered("The more locations that are tested, the more accurate the score is.") + + page := container.NewVBox(title, backButton, widget.NewSeparator(), subtitle, widget.NewSeparator(), description1, description2, description3, description4) + + setPageContent(page, window) +} + +func setPolygenicDiseaseLociExplainerPage(window fyne.Window, previousPage func()){ + + title := getPageTitleCentered("Help - Polygenic Disease Loci") + + backButton := getBackButtonCentered(previousPage) + + subtitle := getPageSubtitleCentered("Polygenic Disease Loci") + + description1 := getLabelCentered("Each polygenic disease has a set of associated genome loci.") + description2 := getLabelCentered("These are locations on the genome that can be tested to determine disease risk.") + description3 := getLabelCentered("The more loci that your genome contains, the more accurate your disease risk score will be.") + + page := container.NewVBox(title, backButton, widget.NewSeparator(), subtitle, widget.NewSeparator(), description1, description2, description3) + + setPageContent(page, window) +} + +func setPolygenicDiseaseNumberOfLociTestedExplainerPage(window fyne.Window, previousPage func()){ + + title := getPageTitleCentered("Help - Number Of Loci Tested") + + backButton := getBackButtonCentered(previousPage) + + subtitle := getPageSubtitleCentered("Polygenic Disease - Number Of Loci Tested") + + description1 := getLabelCentered("A person's polygenic disease risk score is calculated by testing locations on their genome.") + description2 := getLabelCentered("Each location will represent either an increased risk, decreased risk, or no impact on risk.") + description3 := getLabelCentered("The more locations that are tested, the more accurate the risk score will be.") + description4 := getLabelCentered("Import a full genome sequence to test all locations.") + + page := container.NewVBox(title, backButton, widget.NewSeparator(), subtitle, widget.NewSeparator(), description1, description2, description3, description4) + + setPageContent(page, window) +} + +func setOffspringPolygenicDiseaseNumberOfLociTestedExplainerPage(window fyne.Window, previousPage func()){ + + title := getPageTitleCentered("Help - Number Of Loci Tested") + + backButton := getBackButtonCentered(previousPage) + + subtitle := getPageSubtitleCentered("Polygenic Disease - Number Of Loci Tested") + + description1 := getLabelCentered("An offspring's polygenic disease risk score is calculated by testing locations on their genome.") + description2 := getLabelCentered("Each location will represent either an increased risk, decreased risk, or no impact on risk.") + description3 := getLabelCentered("The more locations that are tested, the more accurate the risk score will be.") + description4 := getLabelCentered("A location will only be considered tested if it exists in both parent's genomes.") + description5 := getLabelCentered("Seekia will test all 4 possible outcomes for each location.") + description6 := getLabelCentered("Import a full genome sequence for both parents to test all locations.") + + page := container.NewVBox(title, backButton, widget.NewSeparator(), subtitle, widget.NewSeparator(), description1, description2, description3, description4, description5, description6) + + setPageContent(page, window) +} + + +func setPolygenicDiseaseLocusRiskWeightExplainerPage(window fyne.Window, previousPage func()){ + + title := getPageTitleCentered("Help - Locus Risk Weight") + + backButton := getBackButtonCentered(previousPage) + + subtitle := getPageSubtitleCentered("Locus Risk Weight") + + description1 := getLabelCentered("A polygenic disease risk score is calculated by testing many locations on a genome.") + description2 := getLabelCentered("A genome will have a risk weight for each locus.") + description3 := getLabelCentered("A negative weight reduces the risk of the disease.") + description4 := getLabelCentered("A positive weight increases the risk of the disease.") + description5 := getLabelCentered("A 0 weight has no effect on the risk.") + + page := container.NewVBox(title, backButton, widget.NewSeparator(), subtitle, widget.NewSeparator(), description1, description2, description3, description4, description5) + + setPageContent(page, window) +} + +func setOffspringPolygenicDiseaseLocusRiskWeightExplainerPage(window fyne.Window, previousPage func()){ + + title := getPageTitleCentered("Help - Locus Risk Weight") + + backButton := getBackButtonCentered(previousPage) + + subtitle := getPageSubtitleCentered("Offspring Locus Risk Weight") + + description1 := getLabelCentered("A polygenic disease risk score is calculated by testing many locations on a genome.") + description2 := getLabelCentered("A genome will have a risk weight for each locus.") + description3 := getLabelCentered("A negative weight reduces the risk of the disease.") + description4 := getLabelCentered("A positive weight increases the risk of the disease.") + description5 := getLabelCentered("A 0 weight has no effect on the risk.") + description6 := getLabelCentered("An offspring's locus risk weight represents the average risk weight for all 4 possible locus outcomes.") + + page := container.NewVBox(title, backButton, widget.NewSeparator(), subtitle, widget.NewSeparator(), description1, description2, description3, description4, description5, description6) + + setPageContent(page, window) +} + +func setPolygenicDiseaseLocusRiskWeightProbabilityExplainerPage(window fyne.Window, previousPage func()){ + + title := getPageTitleCentered("Help - Risk Weight Probability") + + backButton := getBackButtonCentered(previousPage) + + subtitle := getPageSubtitleCentered("Locus Risk Weight Probability") + + description1 := getLabelCentered("A polygenic disease risk score is calculated by testing many locations on a genome.") + description2 := getLabelCentered("A genome will have a risk weight for each locus.") + description3 := getLabelCentered("A risk weight probability describes the probability of having that risk weight.") + description4 := getLabelCentered("For example, lets suppose a risk weight of 2 has a probability of 5%") + description5 := getLabelCentered("This means that 5% of people will have a risk weight of 2 at this locus.") + + page := container.NewVBox(title, backButton, widget.NewSeparator(), subtitle, widget.NewSeparator(), description1, description2, description3, description4, description5) + + setPageContent(page, window) +} + + +func setTraitOutcomeScoresExplainerPage(window fyne.Window, previousPage func()){ + + title := getPageTitleCentered("Help - Outcome Scores") + + backButton := getBackButtonCentered(previousPage) + + subtitle := getPageSubtitleCentered("Trait Outcome Scores") + + description1 := getLabelCentered("Person genetic analyses contain trait analyses.") + description2 := getLabelCentered("Each trait has multiple outcomes, and each outcome has an associated score.") + description3 := getLabelCentered("Points are added to an outcome to represent a higher probability.") + description4 := getLabelCentered("Points are subtracted from an outcome to represent a lower probability.") + description5 := getLabelCentered("The more rules that are tested, the higher the accuracy of the result will be.") + + page := container.NewVBox(title, backButton, widget.NewSeparator(), subtitle, widget.NewSeparator(), description1, description2, description3, description4, description5) + + setPageContent(page, window) +} + +func setOffspringTraitOutcomeScoresExplainerPage(window fyne.Window, previousPage func()){ + + title := getPageTitleCentered("Help - Outcome Scores") + + backButton := getBackButtonCentered(previousPage) + + subtitle := getPageSubtitleCentered("Offspring Trait Outcome Scores") + + description1 := getLabelCentered("Couple genetic analyses contain trait analyses for the offspring.") + description2 := getLabelCentered("Each trait has multiple outcomes, and each outcome has an associated score.") + description3 := getLabelCentered("Points are added to an outcome to represent a higher probability.") + description4 := getLabelCentered("Points are subtracted from an outcome to represent a lower probability.") + description5 := getLabelCentered("The more rules that are tested, the higher the accuracy of the result will be.") + + page := container.NewVBox(title, backButton, widget.NewSeparator(), subtitle, widget.NewSeparator(), description1, description2, description3, description4, description5) + + setPageContent(page, window) +} + +func setTraitRulesExplainerPage(window fyne.Window, previousPage func()){ + + title := getPageTitleCentered("Help - Trait Rules") + + backButton := getBackButtonCentered(previousPage) + + subtitle := getPageSubtitleCentered("Trait Rules") + + description1 := getLabelCentered("Person genetic analyses contain trait analyses.") + description2 := getLabelCentered("Each trait has multiple outcomes, and each outcome has an associated score.") + description3 := getLabelCentered("A higher score represents a higher probability, and a lower score represents the opposite.") + description4 := getLabelCentered("Each outcome's score is determined by trait rules.") + description5 := getLabelCentered("A trait rule will add/subtract values to outcome(s).") + description6 := getLabelCentered("If your genome passes a rule, its effects will be applied to the outcome(s).") + + page := container.NewVBox(title, backButton, widget.NewSeparator(), subtitle, widget.NewSeparator(), description1, description2, description3, description4, description5, description6) + + setPageContent(page, window) +} + +func setOffspringTraitRulesExplainerPage(window fyne.Window, previousPage func()){ + + title := getPageTitleCentered("Help - Trait Rules") + + backButton := getBackButtonCentered(previousPage) + + subtitle := getPageSubtitleCentered("Trait Rules") + + description1 := getLabelCentered("Offspring genetic analyses contain trait analyses for a couple's offspring.") + description2 := getLabelCentered("Each trait has multiple outcomes, and each outcome has an associated score.") + description3 := getLabelCentered("A higher score represents a higher probability, and a lower score represents the opposite.") + description4 := getLabelCentered("Each outcome's score is determined by trait rules.") + description5 := getLabelCentered("A trait rule will add/subtract values to outcome(s).") + description6 := getLabelCentered("The probability that an offspring will pass a rule is multiplied by the rule's effects.") + description7 := getLabelCentered("This effect is then applied to the outcome(s).") + + page := container.NewVBox(title, backButton, widget.NewSeparator(), subtitle, widget.NewSeparator(), description1, description2, description3, description4, description5, description6, description7) + + setPageContent(page, window) +} + +func setTraitNumberOfRulesTestedExplainerPage(window fyne.Window, previousPage func()){ + + title := getPageTitleCentered("Help - Number Of Rules Tested") + + backButton := getBackButtonCentered(previousPage) + + subtitle := getPageSubtitleCentered("Number Of Trait Rules Tested") + + description1 := getLabelCentered("Person genetic analyses contain trait analyses.") + description2 := getLabelCentered("Each trait has multiple outcomes, and each outcome has an associated score.") + description3 := getLabelCentered("A higher score represents a higher probability, and a lower score represent the opposite.") + description4 := getLabelCentered("Each outcome's score is determined by trait rules.") + description5 := getLabelCentered("A trait rule will add/subtract values to outcome(s).") + description6 := getLabelCentered("If your genome passes a rule, its effects will be applied to the outcome(s).") + description7 := getLabelCentered("The more trait rules which are tested, the more accurate the result will be.") + description8 := getLabelCentered("Import a full genome sequence to test all rules.") + + page := container.NewVBox(title, backButton, widget.NewSeparator(), subtitle, widget.NewSeparator(), description1, description2, description3, description4, description5, description6, description7, description8) + + setPageContent(page, window) +} + +func setOffspringTraitNumberOfRulesTestedExplainerPage(window fyne.Window, previousPage func()){ + + title := getPageTitleCentered("Help - Number Of Rules Tested") + + backButton := getBackButtonCentered(previousPage) + + subtitle := getPageSubtitleCentered("Offspring Trait Number Of Rules Tested") + + description1 := getLabelCentered("Offspring genetic analyses contain trait analyses for a couple's offspring.") + description2 := getLabelCentered("Each trait has multiple outcomes, and each outcome has an associated score.") + description3 := getLabelCentered("A higher score represents a higher probability, and a lower score represents the opposite.") + description4 := getLabelCentered("Each outcome's score is determined by trait rules.") + description5 := getLabelCentered("A trait rule will add/subtract values to outcome(s).") + description6 := getLabelCentered("The probability that an offspring will pass a rule is multiplied by the rule's effects.") + description7 := getLabelCentered("This effect is then applied to the outcome(s).") + description8 := getLabelCentered("Both parents must have a location in their genome for the rule to be tested.") + description9 := getLabelCentered("Import a full genome sequence from both parents to test all offspring rules.") + + page := container.NewVBox(title, backButton, widget.NewSeparator(), subtitle, widget.NewSeparator(), description1, description2, description3, description4, description5, description6, description7, description8, description9) + + setPageContent(page, window) +} + +func setOffspringProbabilityOfPassingTraitRuleExplainerPage(window fyne.Window, previousPage func()){ + + title := getPageTitleCentered("Help - Probability Of Passing Rule") + + backButton := getBackButtonCentered(previousPage) + + subtitle := getPageSubtitleCentered("Offspring Probability Of Passing Trait Rule") + + description1 := getLabelCentered("Offspring genetic analyses contain trait analyses for a couple's offspring.") + description2 := getLabelCentered("Each trait has multiple outcomes, and each outcome has an associated score.") + description3 := getLabelCentered("A higher score represents a higher probability, and a lower score represents the opposite.") + description4 := getLabelCentered("Each outcome's score is determined by trait rules.") + description5 := getLabelCentered("A trait rule will add/subtract values to outcome(s).") + description6 := getLabelCentered("There are 4 offspring outcomes for each location.") + description7 := getLabelCentered("The probability that an offspring will pass a rule is multiplied by the rule's effects.") + description8 := getLabelCentered("This effect is then applied to the outcome(s).") + description9 := getLabelCentered("Both parents must have a location in their genome for the rule to be tested.") + + page := container.NewVBox(title, backButton, widget.NewSeparator(), subtitle, widget.NewSeparator(), description1, description2, description3, description4, description5, description6, description7, description8, description9) + + setPageContent(page, window) +} + + +func setPersonPassesTraitRuleExplainerPage(window fyne.Window, previousPage func()){ + + title := getPageTitleCentered("Help - Trait Rules") + + backButton := getBackButtonCentered(previousPage) + + subtitle := getPageSubtitleCentered("Person Passes Trait Rule") + + description1 := getLabelCentered("Person genetic analyses contain trait analyses.") + description2 := getLabelCentered("Each trait has multiple outcomes, and each outcome has an associated score.") + description3 := getLabelCentered("A higher score represents a higher probability, and a lower score represents the opposite.") + description4 := getLabelCentered("Each outcome's score is determined by trait rules.") + description5 := getLabelCentered("A trait rule will add/subtract values to outcome(s).") + description6 := getLabelCentered("If a person passes a rule, its effects will be applied to their outcome(s).") + + page := container.NewVBox(title, backButton, widget.NewSeparator(), subtitle, widget.NewSeparator(), description1, description2, description3, description4, description5, description6) + + setPageContent(page, window) +} + +func setGenomePassesTraitRuleExplainerPage(window fyne.Window, previousPage func()){ + + title := getPageTitleCentered("Help - Genome Passes Rule") + + backButton := getBackButtonCentered(previousPage) + + subtitle := getPageSubtitleCentered("Genome Passes Trait Rule") + + description1 := getLabelCentered("Person genetic analyses contain trait analyses.") + description2 := getLabelCentered("Each trait has multiple outcomes, and each outcome has an associated score.") + description3 := getLabelCentered("A higher score represents a higher probability, and a lower score represents the opposite.") + description4 := getLabelCentered("Each outcome's score is determined by trait rules.") + description5 := getLabelCentered("A trait rule will add/subtract values to outcome(s).") + description6 := getLabelCentered("If a person passes a rule, its effects will be applied to their outcome(s).") + description7 := getLabelCentered("If a person has imported multiple genomes, each genome may or may not pass the rule.") + description8 := getLabelCentered("If one genome passes and another does not, it means that one genome has an invalid value.") + description9 := getLabelCentered("This happens because genome sequencing technology is not perfectly accurate.") + + page := container.NewVBox(title, backButton, widget.NewSeparator(), subtitle, widget.NewSeparator(), description1, description2, description3, description4, description5, description6, description7, description8, description9) + + setPageContent(page, window) +} + +func setTraitRuleOutcomeEffectsExplainerPage(window fyne.Window, previousPage func()){ + + title := getPageTitleCentered("Help - Outcome Effects") + + backButton := getBackButtonCentered(previousPage) + + subtitle := getPageSubtitleCentered("Trait Rule Outcome Effects") + + description1 := getLabelCentered("Each trait rule has an outcome effect.") + description2 := getLabelCentered("The outcome effect describes points that are added or subtracted from outcomes.") + description3 := getLabelCentered("If the rule is passed, the outcome effect will be applied.") + description4 := getLabelCentered("For example, let's suppose a rule has the outcome effect of Blue +2") + description5 := getLabelCentered("This means that 2 points are added to the Blue outcome if the rule is passed.") + + page := container.NewVBox(title, backButton, widget.NewSeparator(), subtitle, widget.NewSeparator(), description1, description2, description3, description4, description5) + + setPageContent(page, window) +} + + +func setWealthOrIncomeIsLowerBoundExplainerPage(window fyne.Window, previousPage func()){ + + title := getPageTitleCentered("Help - Is Lower Bound") + + backButton := getBackButtonCentered(previousPage) + + subtitle := getPageSubtitleCentered("Is Lower Bound") + + description1 := getLabelCentered("If a wealth value is a lower bound, its true value may be higher.") + description2 := getLabelCentered("Select this if you wish to represent a monetary value that is lower that your true value.") + description3 := getLabelCentered("For example, if $5000 is a lower bound, then the user has at least $5000, and may have more.") + description4 := getLabelCentered("This is recommended to protect your privacy.") + + page := container.NewVBox(title, backButton, widget.NewSeparator(), subtitle, widget.NewSeparator(), description1, description2, description3, description4) + + setPageContent(page, window) +} + + +func setMemoExplainerPage(window fyne.Window, previousPage func()){ + + title := getPageTitleCentered("Help - Memo") + + backButton := getBackButtonCentered(previousPage) + + subtitle := getPageSubtitleCentered("Memo") + + description1 := getLabelCentered("A Memo is a message signed with a Seekia identity key.") + description2 := getLabelCentered("You can share a memo anywhere, and others can use Seekia to verify it.") + description3 := getLabelCentered("You can timestamp a memo on the blockchain to verify it existed before a certain time.") + description5 := getLabelCentered("You can include a cryptocurrency block hash in the memo to prove it was created after that block.") + description6 := getLabelCentered("You must verify all Seekia memos using the Seekia app to verify their author's signature.") + + page := container.NewVBox(title, backButton, widget.NewSeparator(), subtitle, widget.NewSeparator(), description1, description2, description3, description5, description6) + + setPageContent(page, window) +} + + +func setAllowOtherExplainerPage(window fyne.Window, previousPage func()){ + + title := getPageTitleCentered("Help - Allow Other") + + backButton := getBackButtonCentered(previousPage) + + subtitle := getPageSubtitleCentered("Allow Other") + + description1 := getLabelCentered("When choosing your desires, you can select the Other category.") + description2 := getLabelCentered("Some attributes allow users to provide a custom value.") + description3 := getLabelCentered("Examples of these are Gender Identity and Language.") + description4 := getLabelCentered("If you select Other, you will be allowing users who provide a custom value.") + + page := container.NewVBox(title, backButton, widget.NewSeparator(), subtitle, widget.NewSeparator(), description1, description2, description3, description4) + + setPageContent(page, window) +} + +func setOffspringMonogenicDiseaseProbabilityDesireExplainerPage(window fyne.Window, previousPage func()){ + + currentPage := func(){setOffspringMonogenicDiseaseProbabilityDesireExplainerPage(window, previousPage)} + + title := getPageTitleCentered("Help - Monogenic Disease Probability Desire") + + backButton := getBackButtonCentered(previousPage) + + subtitle := getPageSubtitleCentered("Offspring Monogenic Disease Probability Desire") + + description1 := getLabelCentered("This desire allows you to filter users based on your offspring's probability of monogenic disease.") + description2 := getLabelCentered("This probability is based on the monogenic diseases that you or the user have been tested for.") + description3 := getLabelCentered("The more diseases and variants that have been tested for, the more accurate the result will be.") + description4 := getLabelCentered("If you sequence your entire genome, Seekia will be able to test for all variants.") + description5 := getLabelCentered("You can sort your matches by how many variants they have tested.") + + viewListOfMonogenicDiseasesButton := getWidgetCentered(widget.NewButtonWithIcon("View List Of Monogenic Diseases", theme.VisibilityIcon(), func(){ + setViewMonogenicDiseasesList(window, currentPage) + })) + + page := container.NewVBox(title, backButton, widget.NewSeparator(), subtitle, widget.NewSeparator(), description1, description2, description3, description4, description5, viewListOfMonogenicDiseasesButton) + + setPageContent(page, window) +} + +func setViewMonogenicDiseasesList(window fyne.Window, previousPage func()){ + + title := getPageTitleCentered("View All Monogenic Diseases") + + backButton := getBackButtonCentered(previousPage) + + description := getLabelCentered("Below are all the monogenic diseases that Seekia can test for.") + + page := container.NewVBox(title, backButton, widget.NewSeparator(), description, widget.NewSeparator()) + + monogenicDiseaseNamesList, err := monogenicDiseases.GetMonogenicDiseaseNamesList() + if (err != nil){ + setErrorEncounteredPage(window, err, previousPage) + return + } + + for _, diseaseName := range monogenicDiseaseNamesList{ + + diseaseLabel := getBoldLabelCentered(translate(diseaseName)) + + page.Add(diseaseLabel) + } + + setPageContent(page, window) +} + +func setGeneticTestingExplainerPage(window fyne.Window, previousPage func()){ + + title := getPageTitleCentered("Help - Genetic Testing") + + backButton := getBackButtonCentered(previousPage) + + subtitle := getPageSubtitleCentered("Offspring Embryo Genetic Testing") + + description1 := getLabelCentered("Using embryo genetic testing, you can ensure your offspring will not have any monogenic disease.") + description2 := getLabelCentered("You can always have healthy children with someone for whom your offspring has a <100% probability of monogenic disease.") + description3 := getLabelCentered("You must find a company offering embryo testing.") + description4 := getLabelCentered("The technology is sometimes called Preimplantation Genetic Testing, or PGT.") + description5 := getLabelCentered("This technology requires the use of in-vitro fertilization.") + description6 := getLabelCentered("Choosing someone for whom your offspring has a 0% probability of monogenic diseases is still valuable.") + description7 := getLabelCentered("You could accidentally have children through natural conception, even if you were planning on using PGT.") + + page := container.NewVBox(title, backButton, widget.NewSeparator(), subtitle, widget.NewSeparator(), description1, description2, description3, description4, description5, description6, description7) + + setPageContent(page, window) +} + + +func setHostingHelpPage(window fyne.Window, previousPage func()){ + + title := getPageTitleCentered("Help - Hosting") + + backButton := getBackButtonCentered(previousPage) + + subtitle := getPageSubtitleCentered("Hosting") + + description1 := getLabelCentered("Be a Seekia host.") + description2 := getLabelCentered("Your computer will seed content to the network.") + description3 := getLabelCentered("You can choose to host as much or as little as you desire.") + + //TODO: Add buttons to show more help + //TODO: Describe how hosting works and associated risks + + page := container.NewVBox(title, backButton, widget.NewSeparator(), subtitle, widget.NewSeparator(), description1, description2, description3) + + setPageContent(page, window) +} + +func setMateDesireStatisticsWarningPage(window fyne.Window, previousPage func()){ + + title := getPageTitleCentered("Help - Desire Statistics Warning") + + backButton := getBackButtonCentered(previousPage) + + subtitle := getPageSubtitleCentered("Mate Desire Statistics Warning") + + description1 := getBoldLabelCentered("Your mate desire statistics may not describe the statistics of the network accurately.") + description2 := getLabelCentered("In Mate mode, you will only download profiles which fulfill your download desires.") + description3 := getLabelCentered("Your download desires are the desires you share with hosts.") + description4 := getLabelCentered("These desires prevent you from having to download all profiles from the network.") + description5 := getLabelCentered("If you are only in Mate mode, you are viewing the statistics of users who fulfill your download desires.") + + description6 := getLabelCentered("To view statistics of the entire network, you should disable Mate/Moderator mode and enable Host mode.") + description7 := getLabelCentered("In Host mode, you will download a random portion of the network, allowing you to see accurate statistics.") + description8 := getLabelCentered("Moderator mode will also skew your statistics.") + description9 := getLabelCentered("In moderator mode, you will only download profiles which are written in your understood language(s).") + + page := container.NewVBox(title, backButton, widget.NewSeparator(), subtitle, widget.NewSeparator(), description1, description2, description3, description4, description5, widget.NewSeparator(), description6, description7, widget.NewSeparator(), description8, description9) + + setPageContent(page, window) +} + +func setNetworkStatisticsInaccuracyWarningPage(window fyne.Window, previousPage func()){ + + title := getPageTitleCentered("Help - Network Statistics Warning") + + backButton := getBackButtonCentered(previousPage) + + //TODO: Improve this page + // We need to make it clear that the statistics may still be accurate, but the numberOfUsers will not reflect the total number of network users + // This will become more obvious when we eventually start to only sample a portion of stored profiles, so results are generated faster + + subtitle := getPageSubtitleCentered("Network Statistics Warning") + + description1 := getBoldLabelCentered("Viewed statistics about users may not describe the statistics of the network accurately.") + description2 := getLabelCentered("To view statistics of the entire network, you should disable Mate/Moderator mode and enable Host mode.") + description3 := getLabelCentered("In Host mode, you will download a random portion of the network, allowing you to see accurate statistics.") + description4 := getLabelCentered("In Mate mode, you will only download profiles which fulfill your download desires.") + description5 := getLabelCentered("Your download desires are the desires you share with hosts.") + description6 := getLabelCentered("These desires prevent you from having to download all profiles from the network.") + description7 := getLabelCentered("If you are only in Mate mode, you are viewing the statistics of users who fulfill your download desires.") + description8 := getLabelCentered("Moderator mode will also skew your statistics.") + description9 := getLabelCentered("In moderator mode, you will only download profiles which are written in your understood language(s).") + + page := container.NewVBox(title, backButton, widget.NewSeparator(), subtitle, widget.NewSeparator(), description1, description2, description3, widget.NewSeparator(), description4, description5, description6, description7, widget.NewSeparator(), description8, description9) + + setPageContent(page, window) +} + + +func setAttributeIsCanonicalExplainerPage(window fyne.Window, previousPage func()){ + + title := getPageTitleCentered("Help - Attribute Is Canonical") + + backButton := getBackButtonCentered(previousPage) + + subtitle := getPageSubtitleCentered("Attribute Is Canonical") + + description1 := getLabelCentered("Profile attributes are either canonical or not.") + description2 := getLabelCentered("Canonical attributes are attributes that do not allow a custom value.") + description3 := getLabelCentered("They only allow predefined values or numerical values.") + description4 := getLabelCentered("An example is Age, which must be a number.") + description5 := getLabelCentered("Canonical Mate attributes only have to be approved if they are banned by at least 1 moderator.") + + page := container.NewVBox(title, backButton, widget.NewSeparator(), subtitle, widget.NewSeparator(), description1, description2, description3, description4, description5) + + setPageContent(page, window) +} + + +func setAnyUnreviewedAttributesExistExplainerPage(window fyne.Window, previousPage func()){ + + title := getPageTitleCentered("Help - Unreviewed Attributes") + + backButton := getBackButtonCentered(previousPage) + + subtitle := getPageSubtitleCentered("Unreviewed Attributes Exist") + + description1 := getLabelCentered("Seekia will serve moderators attributes to review.") + description2 := getLabelCentered("These attributes are served in order of least to most reviewed by other moderators.") + description3 := getLabelCentered("Once you approve/ban/skip an attribute, it is removed from your moderator queue.") + description4 := getLabelCentered("If you have banned a profile or any of its attributes, the rest of the profile's attributes do not need review.") + description5 := getLabelCentered("If you have banned a user, profiles authored by them do not need review.") + description6 := getLabelCentered("You can change this behavior on the Settings page (under construction).") + + page := container.NewVBox(title, backButton, widget.NewSeparator(), subtitle, widget.NewSeparator(), description1, description2, description3, description4, widget.NewSeparator(), description5, description6) + + setPageContent(page, window) +} + diff --git a/gui/hostGui.go b/gui/hostGui.go new file mode 100644 index 0000000..34159d7 --- /dev/null +++ b/gui/hostGui.go @@ -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) +} + + diff --git a/gui/imageGui.go b/gui/imageGui.go new file mode 100644 index 0000000..94dd01d --- /dev/null +++ b/gui/imageGui.go @@ -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) +} + + + + diff --git a/gui/manageGeneticsGui.go b/gui/manageGeneticsGui.go new file mode 100644 index 0000000..5115746 --- /dev/null +++ b/gui/manageGeneticsGui.go @@ -0,0 +1,1821 @@ + +package gui + +// manageGeneticsGui.go implements the pages to manage genome people/couples, and to create genetic analyses +// viewAnalysisGui_Person.go and viewAnalysisGui_Couple.go implement the code to view genetic analyses + +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 "fyne.io/fyne/v2/data/binding" + +import "seekia/internal/genetics/myGenomes" +import "seekia/internal/genetics/myPeople" +import "seekia/internal/genetics/myCouples" +import "seekia/internal/genetics/myAnalyses" +import "seekia/internal/genetics/readRawGenomes" +import "seekia/internal/genetics/sampleAnalyses" +import "seekia/internal/helpers" +import "seekia/internal/appMemory" +import "seekia/internal/localFilesystem" + +import "time" +import "strings" +import "errors" + +func setGeneticsPage(window fyne.Window){ + + currentPage := func(){setGeneticsPage(window)} + + title := getPageTitleCentered("Genetics") + + description1 := getLabelCentered("Seekia provides tools to analyze genomes.") + description2 := getLabelCentered("You can import raw genome files from multiple sequencing companies.") + description3 := getLabelCentered("You can share your genome information on your profile.") + description4 := getLabelCentered("When an analysis is created, the genomes are never sent or shared anywhere.") + + managePeopleIcon, err := getFyneImageIcon("Person") + if (err != nil){ + setErrorEncounteredPage(window, err, func(){setHomePage(window)}) + return + } + managePeopleButton := widget.NewButton("Manage People", func(){ + setManageGenomePeoplePage(window, currentPage) + }) + managePeopleButtonWithIcon := container.NewGridWithColumns(1, managePeopleIcon, managePeopleButton) + + analyzeCoupleIcon, err := getFyneImageIcon("Couple") + if (err != nil){ + setErrorEncounteredPage(window, err, func(){setHomePage(window)}) + return + } + analyzeCoupleButton := widget.NewButton("Analyze Couple", func(){ + setManageCouplesPage(window, currentPage) + }) + analyzeCoupleButtonWithIcon := container.NewGridWithColumns(1, analyzeCoupleIcon, analyzeCoupleButton) + + buttonsRow := container.NewHBox(layout.NewSpacer(), managePeopleButtonWithIcon, analyzeCoupleButtonWithIcon, layout.NewSpacer()) + + viewSampleAnalysesIcon, err := getFyneImageIcon("Questionnaire") + if (err != nil){ + setErrorEncounteredPage(window, err, func(){setHomePage(window)}) + return + } + + viewSampleAnalysesButton := widget.NewButton("View Sample Analyses", func(){setViewSampleGeneticAnalysesPage(window, currentPage)}) + viewSampleAnalysesButtonWithIcon := getContainerCentered(container.NewGridWithColumns(1, viewSampleAnalysesIcon, viewSampleAnalysesButton)) + + page := container.NewVBox(title, widget.NewSeparator(), description1, description2, description3, description4, widget.NewSeparator(), buttonsRow, widget.NewSeparator(), viewSampleAnalysesButtonWithIcon) + + setPageContent(page, window) +} + +func setViewSampleGeneticAnalysesPage(window fyne.Window, previousPage func()){ + + currentPage := func(){setViewSampleGeneticAnalysesPage(window, previousPage)} + + title := getPageTitleCentered("Genetics - View Sample Analyses") + + backButton := getBackButtonCentered(previousPage) + + description1 := getLabelCentered("Seekia can create genetic analyses from imported genome files.") + description2 := getLabelCentered("You can view example analyses on the pages below.") + + personIcon, err := getFyneImageIcon("Person") + if (err != nil){ + setErrorEncounteredPage(window, err, previousPage) + return + } + + personAnalysisButton := widget.NewButton("Person Analysis", func(){ + + personIdentifier := "111111111111111111111111111111" + + analysisMapList, err := sampleAnalyses.GetSamplePerson1Analysis() + if (err != nil) { + setErrorEncounteredPage(window, err, currentPage) + return + } + + setViewPersonGeneticAnalysisPage(window, personIdentifier, analysisMapList, 1, currentPage) + }) + + personAnalysisButtonWithIcon := container.NewGridWithColumns(1, personIcon, personAnalysisButton) + + coupleIcon, err := getFyneImageIcon("Couple") + if (err != nil){ + setErrorEncounteredPage(window, err, previousPage) + return + } + coupleAnalysisButton := widget.NewButton("Couple Analysis", func(){ + + personAIdentifier := "111111111111111111111111111111" + personBIdentifier := "222222222222222222222222222222" + + person1AnalysisMapList, err := sampleAnalyses.GetSamplePerson1Analysis() + if (err != nil) { + setErrorEncounteredPage(window, err, currentPage) + return + } + + person2AnalysisMapList, err := sampleAnalyses.GetSamplePerson2Analysis() + if (err != nil) { + setErrorEncounteredPage(window, err, currentPage) + return + } + + coupleAnalysisMapList, err := sampleAnalyses.GetSampleCoupleAnalysis() + if (err != nil) { + setErrorEncounteredPage(window, err, currentPage) + return + } + + setViewCoupleGeneticAnalysisPage(window, personAIdentifier, personBIdentifier, person1AnalysisMapList, person2AnalysisMapList, coupleAnalysisMapList, 1, 1, currentPage) + }) + + coupleAnalysisButtonWithIcon := container.NewGridWithColumns(1, coupleIcon, coupleAnalysisButton) + + buttonsRow := container.NewHBox(layout.NewSpacer(), personAnalysisButtonWithIcon, coupleAnalysisButtonWithIcon, layout.NewSpacer()) + + page := container.NewVBox(title, backButton, widget.NewSeparator(), description1, description2, widget.NewSeparator(), buttonsRow) + + setPageContent(page, window) +} + +func setManageGenomePeoplePage(window fyne.Window, previousPage func()){ + + currentPage := func(){setManageGenomePeoplePage(window, previousPage)} + + title := getPageTitleCentered("Genetics - Manage People") + + backButton := getBackButtonCentered(previousPage) + + description1 := getLabelCentered("A Person is used to organize genome files.") + description2 := getLabelCentered("You can import multiple genome files for the same person.") + + createPersonButton := getWidgetCentered(widget.NewButtonWithIcon("Create Person", theme.ContentAddIcon(), func(){ + setCreateGenomePersonPage(window, currentPage, currentPage) + })) + + getPeopleContainer := func()(*fyne.Container, error){ + + peopleMapList, err := myPeople.GetMyGenomePeopleMapList() + if (err != nil) { return nil, err } + + if (len(peopleMapList) == 0){ + + noPeopleExistDescription := getBoldLabelCentered("No people found.") + return noPeopleExistDescription, nil + } + + myPeopleLabel := getItalicLabelCentered("My People:") + + manageButtonsGrid := container.NewGridWithColumns(1) + + for _, personMap := range peopleMapList{ + + personNameString, exists := personMap["PersonName"] + if (exists == false) { + return nil, errors.New("Malformed GenomePeople map list: Item missing PersonName.") + } + + personIdentifier, exists := personMap["PersonIdentifier"] + if (exists == false) { + return nil, errors.New("Malformed GenomePeople map list: Item missing PersonIdentifier.") + } + + personNameTrimmed, _, err := helpers.TrimAndFlattenString(personNameString, 15) + if (err != nil) { return nil, err } + + managePersonButton := widget.NewButton(personNameTrimmed, func(){ + setManageGenomePersonPage(window, personIdentifier, currentPage) + }) + + manageButtonsGrid.Add(managePersonButton) + } + + buttonsGridCentered := getContainerCentered(manageButtonsGrid) + peopleContainer := container.NewVBox(myPeopleLabel, buttonsGridCentered) + + return peopleContainer, nil + } + + peopleContainer, err := getPeopleContainer() + if (err != nil){ + setErrorEncounteredPage(window, err, previousPage) + return + } + + page := container.NewVBox(title, backButton, widget.NewSeparator(), description1, description2, widget.NewSeparator(), createPersonButton, widget.NewSeparator(), peopleContainer) + + setPageContent(page, window) +} + + + +func setCreateGenomePersonPage(window fyne.Window, previousPage func(), nextPage func()){ + + currentPage := func(){setCreateGenomePersonPage(window, previousPage, nextPage)} + + title := getPageTitleCentered(translate("Genetics - Create Person")) + + backButton := getBackButtonCentered(previousPage) + + description1 := getLabelCentered("Create a Person to manage genome files.") + + enterNameDescription := getBoldLabelCentered("Enter Person Name:") + + enterNameEntry := widget.NewEntry() + enterNameEntry.SetPlaceHolder(translate("Enter Name...")) + enterNameEntryBoxed := getWidgetBoxed(enterNameEntry) + + enterNameDescriptionWithEntry := getContainerCentered(container.NewGridWithColumns(1, enterNameDescription, enterNameEntryBoxed)) + + option1Translated := translate("Male") + option2Translated := translate("Female") + option3Translated := translate("Intersex") + + untranslatedOptionsMap := map[string]string{ + option1Translated: "Male", + option2Translated: "Female", + option3Translated: "Intersex", + } + + sexSelectorOptions := []string{option1Translated, option2Translated, option3Translated} + + selectSexLabel := getBoldLabelCentered(translate("Select Sex:")) + sexSelector := widget.NewSelect(sexSelectorOptions, nil) + sexSelector.SetSelectedIndex(0) + sexSelectorCentered := getWidgetCentered(sexSelector) + + createPersonButton := getWidgetCentered(widget.NewButtonWithIcon("Create Person", theme.ConfirmIcon(), func(){ + + newPersonName := enterNameEntry.Text + if (newPersonName == ""){ + dialogTitle := translate("Missing Person Name.") + dialogMessageA := getLabelCentered("You must enter a name for this person.") + dialogContent := container.NewVBox(dialogMessageA) + dialog.ShowCustom(dialogTitle, translate("Close"), dialogContent, window) + return + } + if (len(newPersonName) > 40){ + dialogTitle := translate("Name Is Too Long.") + dialogMessageA := getLabelCentered("You must enter a name that is less than 40 characters.") + dialogContent := container.NewVBox(dialogMessageA) + dialog.ShowCustom(dialogTitle, translate("Close"), dialogContent, window) + return + } + + personSexTranslated := sexSelector.Selected + + personSex, exists := untranslatedOptionsMap[personSexTranslated] + if (exists == false) { + setErrorEncounteredPage(window, errors.New("untranslatedOptionsMap missing personSexTranslated: " + personSexTranslated), currentPage) + return + } + + duplicateNameExists, err := myPeople.AddPerson(newPersonName, personSex) + if (err != nil) { + setErrorEncounteredPage(window, err, currentPage) + return + } + if (duplicateNameExists == true){ + dialogTitle := translate("Invalid Person Name.") + dialogMessageA := getLabelCentered("A Person with this name already exists.") + dialogMessageB := getLabelCentered("Enter a different name for the Person.") + dialogContent := container.NewVBox(dialogMessageA, dialogMessageB) + dialog.ShowCustom(dialogTitle, translate("Close"), dialogContent, window) + return + } + + nextPage() + })) + + page := container.NewVBox(title, backButton, widget.NewSeparator(), description1, widget.NewSeparator(), enterNameDescriptionWithEntry, selectSexLabel, sexSelectorCentered, createPersonButton) + + setPageContent(page, window) +} + +// This is a page to manage a genome Person +func setManageGenomePersonPage(window fyne.Window, personIdentifier string, previousPage func()){ + + currentPage := func(){setManageGenomePersonPage(window, personIdentifier, previousPage)} + + title := getPageTitleCentered("Genetics - Manage Person") + + backButton := getBackButtonCentered(previousPage) + + personFound, personName, _, personSex, err := myPeople.GetPersonInfo(personIdentifier) + if (err != nil){ + setErrorEncounteredPage(window, err, previousPage) + return + } + if (personFound == false){ + setErrorEncounteredPage(window, errors.New("setManageGenomePersonPage called with missing personIdentifier"), previousPage) + return + } + + personNameLabel := getLabelCentered("Person Name:") + personNameText := getBoldLabelCentered(personName) + + personSexLabel := getLabelCentered("Person Sex:") + personSexText := getBoldLabelCentered(personSex) + + analyzeIcon, err := getFyneImageIcon("Stats") + if (err != nil){ + setErrorEncounteredPage(window, err, previousPage) + return + } + analyzeGeneticsButton := widget.NewButton("Analyze Genetics", func(){ + setAnalyzePersonGeneticsPage(window, personIdentifier, currentPage) + }) + analyzeGeneticsButtonWithIcon := getContainerCentered(container.NewGridWithColumns(1, analyzeIcon, analyzeGeneticsButton)) + + genomesIcon, err := getFyneImageIcon("Genome") + manageGenomesButton := widget.NewButton("Manage Genomes", func(){ + setManagePersonGenomesPage(window, personIdentifier, currentPage) + }) + manageGenomesButtonWithIcon := getContainerCentered(container.NewGridWithColumns(1, genomesIcon, manageGenomesButton)) + + renamePersonButton := widget.NewButtonWithIcon("Rename", theme.DocumentCreateIcon(), func(){ + setRenameGenomePersonPage(window, personIdentifier, currentPage, currentPage) + }) + changeSexButton := widget.NewButtonWithIcon("Change Sex", theme.DocumentCreateIcon(), func(){ + //TODO + showUnderConstructionDialog(window) + }) + deletePersonButton := widget.NewButtonWithIcon("Delete", theme.DeleteIcon(), func(){ + setDeleteGenomePersonPage(window, personIdentifier, currentPage, previousPage) + }) + + buttonsGrid := getContainerCentered(container.NewGridWithColumns(1, renamePersonButton, changeSexButton, deletePersonButton)) + + page := container.NewVBox(title, backButton, widget.NewSeparator(), personNameLabel, personNameText, widget.NewSeparator(), personSexLabel, personSexText, widget.NewSeparator(), analyzeGeneticsButtonWithIcon, manageGenomesButtonWithIcon, widget.NewSeparator(), buttonsGrid) + + setPageContent(page, window) +} + +func setRenameGenomePersonPage(window fyne.Window, personIdentifier string, previousPage func(), nextPage func()){ + + currentPage := func(){setRenameGenomePersonPage(window, personIdentifier, previousPage, nextPage)} + + title := getPageTitleCentered("Genetics - Rename Person") + + backButton := getBackButtonCentered(previousPage) + + subtitle := getPageSubtitleCentered("Rename Person") + + personFound, personName, _, personSex, err := myPeople.GetPersonInfo(personIdentifier) + if (err != nil){ + setErrorEncounteredPage(window, err, previousPage) + return + } + if (personFound == false){ + setErrorEncounteredPage(window, errors.New("setRenameGenomePersonPage called with person who does not exist."), previousPage) + return + } + + currentNameLabel := widget.NewLabel("Current Name:") + currentNameText := getBoldLabel(personName) + + currentNameRow := container.NewHBox(layout.NewSpacer(), currentNameLabel, currentNameText, layout.NewSpacer()) + + enterNameDescription := widget.NewLabel("Enter new name:") + + newNameEntry := widget.NewEntry() + newNameEntry.SetPlaceHolder("Enter name...") + + enterNameEntryWithDescription := getContainerCentered(container.NewGridWithColumns(1, enterNameDescription, newNameEntry)) + + renameButton := getWidgetCentered(widget.NewButtonWithIcon("Rename", theme.ConfirmIcon(), func(){ + newName := newNameEntry.Text + if (newName == ""){ + dialogTitle := translate("No Name Provided.") + dialogMessageA := getLabelCentered("You must enter a new name.") + dialogContent := container.NewVBox(dialogMessageA) + dialog.ShowCustom(dialogTitle, translate("Close"), dialogContent, window) + return + } + if (len(newName) > 40){ + dialogTitle := translate("Name Is Too Long.") + dialogMessageA := getLabelCentered("You must enter a name that is less than 40 characters.") + dialogContent := container.NewVBox(dialogMessageA) + dialog.ShowCustom(dialogTitle, translate("Close"), dialogContent, window) + return + } + + nameIsDuplicate, err := myPeople.EditPerson(personIdentifier, newName, personSex) + if (err != nil){ + setErrorEncounteredPage(window, err, currentPage) + return + } + if (nameIsDuplicate == true){ + dialogTitle := translate("Name Already Exists.") + dialogMessageA := getLabelCentered("Another person already has this name.") + dialogMessageB := getLabelCentered("You must enter a unique name.") + dialogContent := container.NewVBox(dialogMessageA, dialogMessageB) + dialog.ShowCustom(dialogTitle, translate("Close"), dialogContent, window) + return + } + nextPage() + })) + + page := container.NewVBox(title, backButton, widget.NewSeparator(), subtitle, widget.NewSeparator(), currentNameRow, widget.NewSeparator(), enterNameEntryWithDescription, renameButton) + + setPageContent(page, window) +} + +func setDeleteGenomePersonPage(window fyne.Window, personIdentifier string, previousPage func(), nextPage func()){ + + currentPage := func(){setDeleteGenomePersonPage(window, personIdentifier, previousPage, nextPage)} + + title := getPageTitleCentered("Genetics - Delete Person") + + backButton := getBackButtonCentered(previousPage) + + subtitle := getPageSubtitleCentered("Delete Person") + + personFound, personName, personCreatedTime, _, err := myPeople.GetPersonInfo(personIdentifier) + if (err != nil){ + setErrorEncounteredPage(window, err, previousPage) + return + } + if (personFound == false){ + setErrorEncounteredPage(window, errors.New("setDeleteGenomePersonPage called with unknown person."), previousPage) + return + } + + description1 := getLabelCentered("Confirm to delete " + personName + "?") + + createdTimeAgoText, err := helpers.ConvertUnixTimeToTimeAgoTranslated(personCreatedTime, false) + if (err != nil){ + setErrorEncounteredPage(window, err, previousPage) + return + } + + description2 := getItalicLabelCentered("Person created " + createdTimeAgoText + ".") + + page := container.NewVBox(title, backButton, widget.NewSeparator(), subtitle, widget.NewSeparator(), description1, description2) + + //Outputs: + // -bool: Any couples/genomes exist for this person + // -*fyne.Container: label describing the couples and genomes that will be deleted + // -error + getPersonGenomesAndCouplesLabel := func()(bool, *fyne.Container, error){ + + allPersonGenomesList, err := myGenomes.GetAllPersonGenomesMapList(personIdentifier) + if (err != nil) { return false, nil, err } + + numberOfPersonGenomes := len(allPersonGenomesList) + + numberOfPersonCouples, err := myCouples.GetNumberOfCouplesForPerson(personIdentifier) + if (err != nil){ return false, nil, err } + + if (numberOfPersonGenomes == 0 && numberOfPersonCouples == 0){ + return false, nil, nil + } + + numberOfPersonGenomesString := helpers.ConvertIntToString(numberOfPersonGenomes) + numberOfPersonCouplesString := helpers.ConvertIntToString(numberOfPersonCouples) + + getGenomeOrGenomesText := func()string{ + if (numberOfPersonGenomes == 1){ + result := translate("genome") + return result + } + result := translate("genomes") + return result + } + + genomeOrGenomesText := getGenomeOrGenomesText() + + getCoupleOrCouplesText := func()string{ + + if (numberOfPersonCouples == 1){ + result := translate("couple") + return result + } + + result := translate("couples") + return result + } + + coupleOrCouplesText := getCoupleOrCouplesText() + + if (numberOfPersonGenomes != 0 && numberOfPersonCouples != 0){ + + genomesAndCouplesText := numberOfPersonGenomesString + " " + genomeOrGenomesText + " & " + numberOfPersonCouplesString + " " + coupleOrCouplesText + "." + genomesAndCouplesLabel := getBoldLabelCentered(genomesAndCouplesText) + return true, genomesAndCouplesLabel, nil + } + + if (numberOfPersonGenomes != 0){ + + genomesText := numberOfPersonGenomesString + " " + genomeOrGenomesText + "." + genomesLabel := getBoldLabelCentered(genomesText) + return true, genomesLabel, nil + } + + couplesText := numberOfPersonCouplesString + " " + coupleOrCouplesText + "." + couplesLabel := getBoldLabelCentered(couplesText) + return true, couplesLabel, nil + } + + anyGenomesOrCouplesExist, personGenomesAndCouplesLabel, err := getPersonGenomesAndCouplesLabel() + if (err != nil){ + setErrorEncounteredPage(window, err, previousPage) + return + } + + if (anyGenomesOrCouplesExist == true){ + + deletionDescription := getLabelCentered("Deleting this person will delete:") + + page.Add(widget.NewSeparator()) + page.Add(deletionDescription) + page.Add(personGenomesAndCouplesLabel) + page.Add(widget.NewSeparator()) + } + + deleteButton := getWidgetCentered(widget.NewButtonWithIcon("Delete", theme.DeleteIcon(), func(){ + + err := myPeople.DeletePerson(personIdentifier) + if (err != nil){ + setErrorEncounteredPage(window, err, currentPage) + return + } + + nextPage() + })) + + page.Add(deleteButton) + + setPageContent(page, window) +} + +// This function provides a page to manage a Person's genomes +func setManagePersonGenomesPage(window fyne.Window, personIdentifier string, previousPage func()){ + + currentPage := func(){setManagePersonGenomesPage(window, personIdentifier, previousPage)} + + title := getPageTitleCentered("Genetics - Manage Person Genomes") + + backButton := getBackButtonCentered(previousPage) + + description1 := getLabelCentered("Manage the person's genome files below.") + + importGenomeButton := getWidgetCentered(widget.NewButtonWithIcon("Import Genome", theme.ContentAddIcon(), func(){ + setImportRawGenomePage(window, personIdentifier, currentPage, currentPage) + })) + + getPersonGenomesContainer := func()(*fyne.Container, error){ + + allGenomesMapList, err := myGenomes.GetAllPersonGenomesMapList(personIdentifier) + if (err != nil){ return nil, err } + + if (len(allGenomesMapList) == 0){ + noGenomesExistLabel := getBoldLabelCentered("No genomes exist.") + return noGenomesExistLabel, nil + } + + indexColumn := container.NewVBox() + genomeNameColumn := container.NewVBox() + manageButtonColumn := container.NewVBox() + + for index, genomeMap := range allGenomesMapList{ + + indexString := helpers.ConvertIntToString(index+1) + indexLabel := getBoldLabel(indexString + ".") + + companyName, exists := genomeMap["CompanyName"] + if (exists == false){ + return nil, errors.New("Malformed myGenomesMapList: Item missing CompanyName") + } + + companyNameLabel := widget.NewLabel(companyName) + + genomeIdentifier, exists := genomeMap["GenomeIdentifier"] + if (exists == false){ + return nil, errors.New("Malformed myGenomesMapList: Item missing GenomeIdentifier") + } + + manageButton := widget.NewButtonWithIcon("Manage", theme.VisibilityIcon(), func(){ + setManageGenomePage(window, genomeIdentifier, currentPage) + }) + + indexColumn.Add(indexLabel) + genomeNameColumn.Add(companyNameLabel) + manageButtonColumn.Add(manageButton) + + if (index != len(allGenomesMapList)-1){ + indexColumn.Add(widget.NewSeparator()) + genomeNameColumn.Add(widget.NewSeparator()) + manageButtonColumn.Add(widget.NewSeparator()) + } + } + + genomesContainer := container.NewHBox(layout.NewSpacer(), indexColumn, genomeNameColumn, manageButtonColumn, layout.NewSpacer()) + + return genomesContainer, nil + } + + personGenomesContainer, err := getPersonGenomesContainer() + if (err != nil){ + setErrorEncounteredPage(window, err, previousPage) + return + } + + page := container.NewVBox(title, backButton, widget.NewSeparator(), description1, importGenomeButton, widget.NewSeparator(), personGenomesContainer) + + setPageContent(page, window) +} + +func setImportRawGenomePage(window fyne.Window, personIdentifier string, previousPage func(), nextPage func()){ + + currentPage := func(){setImportRawGenomePage(window, personIdentifier, previousPage, nextPage)} + + title := getPageTitleCentered("Import Raw Genome") + + backButton := getBackButtonCentered(previousPage) + + description1 := getLabelCentered("Import a raw genome file below.") + description2 := getLabelCentered("You must export your raw data from a sequencing company.") + description3 := getLabelCentered("Supported Companies: 23andMe, AncestryDNA") + + selectFileButton := getWidgetCentered(widget.NewButtonWithIcon("Select File", theme.FileIcon(), func(){ + + openFileCallbackFunction := func(file fyne.URIReadCloser, err error){ + if (err != nil){ + setErrorEncounteredPage(window, err, currentPage) + return + } + if (file == nil){ + return + } + + uriFilePath := file.URI().String() + + filePath := strings.TrimPrefix(uriFilePath, "file://") + + setLoadingScreen(window, "Importing Genome", "Importing genome file...") + + fileExists, fileBytes, err := localFilesystem.GetFileContents(filePath) + if (err != nil){ + setErrorEncounteredPage(window, err, currentPage) + return + } + if (fileExists == false){ + setErrorEncounteredPage(window, errors.New("Unable to read chosen file: File not found."), currentPage) + return + } + + fileString := string(fileBytes) + + fileIsValid, fileAlreadyExists, err := myGenomes.AddRawGenome(personIdentifier, fileString) + if (err != nil){ + setErrorEncounteredPage(window, err, currentPage) + return + } + if (fileIsValid == false){ + currentPage() + dialogTitle := translate("Unable To Read File.") + dialogMessageA := getLabelCentered("Seekia was unable to read the selected genome file.") + dialogMessageB := getLabelCentered("Only 23andMe and AncestryDNA files are supported.") + dialogMessageC := getLabelCentered("More companies will be supported in the future.") + dialogContent := container.NewVBox(dialogMessageA, dialogMessageB, dialogMessageC) + dialog.ShowCustom(dialogTitle, translate("Close"), dialogContent, window) + return + } + if (fileAlreadyExists == true){ + currentPage() + dialogTitle := translate("Genome Already Exists.") + dialogMessageA := getLabelCentered("The genome file you selected is already imported.") + dialogMessageB := getLabelCentered("You can only import the same file once for each Person.") + dialogContent := container.NewVBox(dialogMessageA, dialogMessageB) + dialog.ShowCustom(dialogTitle, translate("Close"), dialogContent, window) + return + } + nextPage() + } + dialog.ShowFileOpen(openFileCallbackFunction, window) + })) + + page := container.NewVBox(title, backButton, widget.NewSeparator(), description1, description2, description3, selectFileButton) + + setPageContent(page, window) +} + +// This provides a page to manage a person's Genome +func setManageGenomePage(window fyne.Window, genomeIdentifier string, previousPage func()){ + + currentPage := func(){setManageGenomePage(window, genomeIdentifier, previousPage)} + + setLoadingScreen(window, "Loading Genome", "Loading genome...") + + title := getPageTitleCentered("Genetics - Manage Genome") + + backButton := getBackButtonCentered(previousPage) + + genomeFound, personIdentifier, exportTime, importTime, isPhased, snpCount, companyName, importVersion, fileHash, err := myGenomes.GetMyRawGenomeMetadata(genomeIdentifier) + if (err != nil){ + setErrorEncounteredPage(window, err, previousPage) + return + } + if (genomeFound == false){ + setErrorEncounteredPage(window, errors.New("setManageGenomePage called with missing genomeIdentifier"), previousPage) + return + } + currentImportVersion, err := readRawGenomes.GetCurrentCompanyImportVersion(companyName) + if (err != nil){ + setErrorEncounteredPage(window, err, previousPage) + return + } + if (importVersion != currentImportVersion){ + // We will refresh the metadata for the genome + err := myGenomes.RefreshRawGenomeMetadata(genomeIdentifier) + if (err != nil){ + setErrorEncounteredPage(window, err, previousPage) + return + } + currentPage() + return + } + + isPhasedString := helpers.ConvertBoolToYesOrNoString(isPhased) + snpCountString := helpers.ConvertInt64ToString(snpCount) + + companyLabel := widget.NewLabel("Company:") + companyNameLabel := getBoldLabel(companyName) + + companyNameRow := container.NewHBox(layout.NewSpacer(), companyLabel, companyNameLabel, layout.NewSpacer()) + + snpCountLabel := widget.NewLabel("SNP Count:") + snpCountText := getBoldLabel(snpCountString) + snpCountHelpButton := widget.NewButtonWithIcon("", theme.QuestionIcon(), func(){ + setGenomeSNPCountExplainerPage(window, currentPage) + }) + + snpCountRow := container.NewHBox(layout.NewSpacer(), snpCountLabel, snpCountText, snpCountHelpButton, layout.NewSpacer()) + + isPhasedLabel := widget.NewLabel("Is Phased:") + isPhasedText := getBoldLabel(isPhasedString) + isPhasedHelpButton := widget.NewButtonWithIcon("", theme.QuestionIcon(), func(){ + setGenomePhasingExplainerPage(window, currentPage) + }) + + isPhasedRow := container.NewHBox(layout.NewSpacer(), isPhasedLabel, isPhasedText, isPhasedHelpButton, layout.NewSpacer()) + + fileHashTrimmed, _, err := helpers.TrimAndFlattenString(fileHash, 6) + if (err != nil) { + setErrorEncounteredPage(window, err, previousPage) + return + } + fileHashLabel := widget.NewLabel("File Hash: ") + fileHashText := getBoldLabel(fileHashTrimmed) + fileHashRow := container.NewHBox(layout.NewSpacer(), fileHashLabel, fileHashText, layout.NewSpacer()) + + exportTimeAgo, err := helpers.ConvertUnixTimeToTimeAgoTranslated(exportTime, false) + if (err != nil){ + setErrorEncounteredPage(window, err, previousPage) + return + } + + exportedTimeLabel := getItalicLabelCentered("Exported from " + companyName + " " + exportTimeAgo + ".") + + importedTimeAgo, err := helpers.ConvertUnixTimeToTimeAgoTranslated(importTime, false) + if (err != nil){ + setErrorEncounteredPage(window, err, previousPage) + return + } + importedTimeLabel := getItalicLabelCentered("Imported " + importedTimeAgo + ".") + + deleteGenomeButton := getWidgetCentered(widget.NewButtonWithIcon("Delete Genome", theme.DeleteIcon(), func(){ + setConfirmDeletePersonGenomePage(window, personIdentifier, genomeIdentifier, currentPage, previousPage) + })) + + page := container.NewVBox(title, backButton, widget.NewSeparator(), companyNameRow, snpCountRow, isPhasedRow, fileHashRow, widget.NewSeparator(), exportedTimeLabel, importedTimeLabel, widget.NewSeparator(), deleteGenomeButton) + + setPageContent(page, window) +} + +func setConfirmDeletePersonGenomePage(window fyne.Window, personIdentifier string, genomeIdentifier string, previousPage func(), nextPage func()){ + + currentPage := func(){setConfirmDeletePersonGenomePage(window, personIdentifier, genomeIdentifier, previousPage, nextPage)} + + title := getPageTitleCentered("Confirm Delete Genome") + + backButton := getBackButtonCentered(previousPage) + + description1 := getBoldLabelCentered("Delete Genome?") + + description2 := getLabelCentered("This will delete this person's raw genome.") + description3 := getLabelCentered("You must run a new genetic analysis afterwards.") + + genomeFound, genomePersonIdentifier, timeGenomeWasExported, timeGenomeWasImported, _, _, companyName, _, _, err := myGenomes.GetMyRawGenomeMetadata(genomeIdentifier) + if (err != nil){ + setErrorEncounteredPage(window, err, previousPage) + return + } + if (genomeFound == false){ + setErrorEncounteredPage(window, errors.New("setDeletePersonGenomePage called with missing genome."), previousPage) + return + } + if (genomePersonIdentifier != personIdentifier){ + setErrorEncounteredPage(window, errors.New("Cannot delete genome: Genome person identifier does not match."), previousPage) + return + } + + exportTimeAgo, err := helpers.ConvertUnixTimeToTimeAgoTranslated(timeGenomeWasExported, false) + if (err != nil){ + setErrorEncounteredPage(window, err, previousPage) + return + } + + exportedTimeLabel := getItalicLabelCentered("Exported from " + companyName + " " + exportTimeAgo + ".") + + importedTimeAgo, err := helpers.ConvertUnixTimeToTimeAgoTranslated(timeGenomeWasImported, false) + if (err != nil){ + setErrorEncounteredPage(window, err, previousPage) + return + } + importedTimeLabel := getItalicLabelCentered("Imported " + importedTimeAgo + ".") + + deleteButton := getWidgetCentered(widget.NewButtonWithIcon("Delete Genome", theme.DeleteIcon(), func(){ + + err := myGenomes.DeleteMyRawGenome(genomeIdentifier) + if (err != nil){ + setErrorEncounteredPage(window, err, currentPage) + return + } + nextPage() + })) + + page := container.NewVBox(title, backButton, widget.NewSeparator(), description1, description2, description3, widget.NewSeparator(), exportedTimeLabel, importedTimeLabel, widget.NewSeparator(), deleteButton) + + setPageContent(page, window) +} + +func setAnalyzePersonGeneticsPage(window fyne.Window, personIdentifier string, previousPage func()){ + + appMemory.SetMemoryEntry("CurrentViewedPage", "AnalyzePersonGeneticsPage") + + currentPage := func(){setAnalyzePersonGeneticsPage(window, personIdentifier, previousPage)} + + title := getPageTitleCentered(translate("Genetics - Analyze Person Genetics")) + + backButton := getBackButtonCentered(previousPage) + + description := getLabelCentered(translate("Analyze this person's genetics.")) + + personFound, personName, _, _, err := myPeople.GetPersonInfo(personIdentifier) + if (err != nil){ + setErrorEncounteredPage(window, err, previousPage) + return + } + if (personFound == false){ + setErrorEncounteredPage(window, errors.New("setAnalyzePersonGeneticsPage called with missing person"), previousPage) + return + } + + allPersonRawGenomeIdentifiersList, err := myGenomes.GetAllPersonRawGenomeIdentifiersList(personIdentifier) + if (err != nil){ + setErrorEncounteredPage(window, err, previousPage) + return + } + if (len(allPersonRawGenomeIdentifiersList) == 0){ + noGenomesExistLabel := getBoldLabelCentered("No genomes found.") + description2 := getLabelCentered("You must import a genome to perform a genetic analysis.") + description3 := getLabelCentered("Go to the previous page and select Manage Genomes.") + page := container.NewVBox(title, backButton, widget.NewSeparator(), noGenomesExistLabel, description2, description3) + setPageContent(page, window) + return + } + + anyAnalysisFound, newestAnalysisIdentifier, timeOfNewestAnalysis, newestAnalysisListOfGenomesAnalyzed, newerAnalysisVersionAvailable, err := myAnalyses.GetPersonNewestGeneticAnalysisInfo(personIdentifier) + if (err != nil){ + setErrorEncounteredPage(window, err, previousPage) + return + } + if (anyAnalysisFound == false){ + // We will skip to the ConfirmPerformAnalysis page + setConfirmPerformPersonAnalysisPage(window, personIdentifier, previousPage, currentPage) + return + } + if (newerAnalysisVersionAvailable == true){ + + description1 := getLabelCentered("A new analysis method is available!") + description2 := getLabelCentered("You must perform a new analysis to see your new results.") + + performNewAnalysisButton := getWidgetCentered(widget.NewButtonWithIcon("Perform New Analysis", theme.NavigateNextIcon(), func(){ + setConfirmPerformPersonAnalysisPage(window, personIdentifier, currentPage, currentPage) + })) + + page := container.NewVBox(title, backButton, widget.NewSeparator(), description, widget.NewSeparator(), description1, description2, performNewAnalysisButton) + + setPageContent(page, window) + return + } + + // This will return true if a newer analysis can be performed using newly imported genomes + getAnalysisIsMissingGenomesBool := func()(bool, error){ + + genomesAreIdentical := helpers.CheckIfTwoListsContainIdenticalItems(allPersonRawGenomeIdentifiersList, newestAnalysisListOfGenomesAnalyzed) + if (genomesAreIdentical == false){ + return true, nil + } + return false, nil + } + + analysisIsMissingGenomes, err := getAnalysisIsMissingGenomesBool() + if (err != nil) { + setErrorEncounteredPage(window, err, previousPage) + return + } + if (analysisIsMissingGenomes == true){ + + description1 := getLabelCentered("You have imported/deleted a genome.") + description2 := getLabelCentered("You must perform a new analysis.") + performAnalysisButton := getWidgetCentered(widget.NewButtonWithIcon("Perform Analysis", theme.NavigateNextIcon(), func(){ + setConfirmPerformPersonAnalysisPage(window, personIdentifier, currentPage, currentPage) + })) + + page := container.NewVBox(title, backButton, widget.NewSeparator(), description, widget.NewSeparator(), description1, description2, performAnalysisButton) + + setPageContent(page, window) + return + } + + analysisReadyLabel := getBoldLabelCentered("Genetic Analysis Complete!") + + personAnalyzedLabel := widget.NewLabel("Person Analyzed:") + personNameLabel := getBoldLabel(personName) + personAnalyzedRow := container.NewHBox(layout.NewSpacer(), personAnalyzedLabel, personNameLabel, layout.NewSpacer()) + + numberOfGenomesInAnalysis := len(newestAnalysisListOfGenomesAnalyzed) + + numberOfGenomesInAnalysisString := helpers.ConvertIntToString(numberOfGenomesInAnalysis) + + numberOfGenomesInAnalysisLabel := widget.NewLabel("Number of genomes analyzed:") + numberOfGenomesInAnalysisText := getBoldLabel(numberOfGenomesInAnalysisString) + numberOfGenomesInAnalysisRow := container.NewHBox(layout.NewSpacer(), numberOfGenomesInAnalysisLabel, numberOfGenomesInAnalysisText, layout.NewSpacer()) + + timeAgoPerformed, err := helpers.ConvertUnixTimeToTimeAgoTranslated(timeOfNewestAnalysis, false) + if (err != nil){ + setErrorEncounteredPage(window, err, previousPage) + return + } + + timeAgoPerformedLabel := getItalicLabelCentered("Analysis performed " + timeAgoPerformed + ".") + + viewAnalysisButton := getWidgetCentered(widget.NewButtonWithIcon("View Analysis", theme.VisibilityIcon(), func(){ + + analysisFound, analysisMapList, err := myAnalyses.GetGeneticAnalysis(newestAnalysisIdentifier) + if (err != nil){ + setErrorEncounteredPage(window, err, currentPage) + return + } + if (analysisFound == false){ + setErrorEncounteredPage(window, errors.New("Person analysis not found after being found already."), currentPage) + return + } + + setViewPersonGeneticAnalysisPage(window, personIdentifier, analysisMapList, numberOfGenomesInAnalysis, currentPage) + })) + + page := container.NewVBox(title, backButton, widget.NewSeparator(), description, widget.NewSeparator(), analysisReadyLabel, personAnalyzedRow, numberOfGenomesInAnalysisRow, timeAgoPerformedLabel, viewAnalysisButton) + + setPageContent(page, window) +} + + +func setConfirmPerformPersonAnalysisPage(window fyne.Window, personIdentifier string, previousPage func(), pageToVisitAfter func()){ + + appMemory.SetMemoryEntry("CurrentViewedPage", "ConfirmPerformGeneticAnalysisPage") + + currentPage := func(){setConfirmPerformPersonAnalysisPage(window, personIdentifier, previousPage, pageToVisitAfter)} + + title := getPageTitleCentered("Genetics - Perform Analysis") + + backButton := getBackButtonCentered(previousPage) + + allPersonGenomesMapList, err := myGenomes.GetAllPersonGenomesMapList(personIdentifier) + if (err != nil){ + setErrorEncounteredPage(window, err, previousPage) + return + } + + numberOfPersonGenomes := len(allPersonGenomesMapList) + + if (numberOfPersonGenomes == 0){ + setErrorEncounteredPage(window, errors.New("setConfirmPerformPersonAnalysisPage called with person who has no genomes."), previousPage) + return + } + + // Now we check if there is an analysis running + + anyProcessFound, processIdentifier, err := myAnalyses.GetPersonGeneticAnalysisProcessIdentifier(personIdentifier) + if (err != nil){ + setErrorEncounteredPage(window, err, previousPage) + return + } + if (anyProcessFound == true){ + + processFound, processIsComplete, _, _, _, err := myAnalyses.GetAnalysisProcessInfo(processIdentifier) + if (err != nil){ + setErrorEncounteredPage(window, err, previousPage) + return + } + if (processFound == false){ + setErrorEncounteredPage(window, errors.New("Genetic analysis process not found after being found already."), previousPage) + return + } + if (processIsComplete == false){ + + // An analysis is already running. + + description1 := getLabelCentered("A genetic analysis is already being generated.") + description2 := getLabelCentered("View the status of the analysis?") + + viewStatusButton := getWidgetCentered(widget.NewButtonWithIcon("View Status", theme.VisibilityIcon(), func(){ + setMonitorGeneticAnalysisGenerationPage(window, processIdentifier, currentPage, pageToVisitAfter) + })) + + page := container.NewVBox(title, backButton, widget.NewSeparator(), description1, description2, viewStatusButton) + setPageContent(page, window) + return + } + + // Existing analysis probably encountered error. + // We will just show a retry option. + } + + description1 := getBoldLabelCentered("Perform genetic analysis?") + description2 := getLabelCentered("This will create an analysis of all of this person's genomes.") + description3 := getLabelCentered("It is performed offline and is not shared without your approval.") + + startAnalysisFunction := func(){ + + newProcessIdentifier, err := myAnalyses.StartCreateNewPersonGeneticAnalysis(personIdentifier) + if (err != nil){ + setErrorEncounteredPage(window, err, previousPage) + return + } + + setMonitorGeneticAnalysisGenerationPage(window, newProcessIdentifier, currentPage, pageToVisitAfter) + } + + startAnalysisButton := getWidgetCentered(widget.NewButtonWithIcon("Start Analysis", theme.ConfirmIcon(), startAnalysisFunction)) + + page := container.NewVBox(title, backButton, widget.NewSeparator(), description1, description2, description3, startAnalysisButton) + + setPageContent(page, window) +} + +// This function provides a page to monitor an active analysis generation +func setMonitorGeneticAnalysisGenerationPage(window fyne.Window, processIdentifier string, previousPage func(), pageToVisitAfter func()){ + + pageIdentifier, err := helpers.GetNewRandomHexString(16) + if (err != nil) { + setErrorEncounteredPage(window, err, pageToVisitAfter) + return + } + + appMemory.SetMemoryEntry("CurrentViewedPage", pageIdentifier) + + checkIfPageHasChangedFunction := func()bool{ + exists, currentViewedPage := appMemory.GetMemoryEntry("CurrentViewedPage") + if (exists == true && currentViewedPage == pageIdentifier){ + return false + } + return true + } + + title := getPageTitleCentered("Monitor Genetic Analysis Generation") + + backButton := getBackButtonCentered(previousPage) + + processDetailsABinding := binding.NewString() + processDetailsBBinding := binding.NewString() + progressPercentageBinding := binding.NewFloat() + + processDetailsABinding.Set("Genetic analysis is being generated...") + processDetailsBBinding.Set("You can leave this page.") + + processDetailsALabel := widget.NewLabelWithData(processDetailsABinding) + processDetailsALabel.TextStyle = getFyneTextStyle_Bold() + processDetailsBLabel := widget.NewLabelWithData(processDetailsBBinding) + + processDetailsALabelCentered := getWidgetCentered(processDetailsALabel) + processDetailsBLabelCentered := getWidgetCentered(processDetailsBLabel) + + loadingBar := getWidgetCentered(widget.NewProgressBarWithData(progressPercentageBinding)) + + updateBindingsFunction := func(){ + + for{ + + pageHasChanged := checkIfPageHasChangedFunction() + if (pageHasChanged == true){ + return + } + + processFound, processIsComplete, processEncounteredError, errorEncounteredByProcess, processPercentageComplete, err := myAnalyses.GetAnalysisProcessInfo(processIdentifier) + if (err != nil){ + setErrorEncounteredPage(window, err, previousPage) + return + } + if (processFound == false){ + setErrorEncounteredPage(window, errors.New("setMonitorGeneticAnalysisGenerationPage called with missing process"), previousPage) + return + } + if (processIsComplete == false){ + percentageCompleteFloat := float64(processPercentageComplete)/100 + progressPercentageBinding.Set(percentageCompleteFloat) + time.Sleep(time.Millisecond * 50) + continue + } + + // Process is complete + + progressPercentageBinding.Set(1) + + if (processEncounteredError == true){ + processDetailsABinding.Set("Process Encountered Error!") + processDetailsBBinding.Set("ERROR: " + errorEncounteredByProcess.Error()) + return + } + + pageToVisitAfter() + return + } + } + + page := container.NewVBox(title, backButton, widget.NewSeparator(), processDetailsALabelCentered, processDetailsBLabelCentered, loadingBar) + + setPageContent(page, window) + + go updateBindingsFunction() +} + + +func setManageCouplesPage(window fyne.Window, previousPage func()){ + + currentPage := func(){setManageCouplesPage(window, previousPage)} + + title := getPageTitleCentered("Genetics - Manage Couples") + + backButton := getBackButtonCentered(previousPage) + + description1 := getLabelCentered("Perform a genetic analysis of a couple.") + + createACoupleButton := getWidgetCentered(widget.NewButtonWithIcon("Create Couple", theme.ContentAddIcon(), func(){ + setCreateCouplePage(window, currentPage) + })) + + getManageMyCouplesContainer := func()(*fyne.Container, error){ + + myCouplesMapList, err := myCouples.GetMyGenomeCouplesMapList() + if (err != nil){ return nil, err } + + if (len(myCouplesMapList) == 0){ + + noCouplesExistLabel := getBoldLabelCentered("No couples exist.") + + return noCouplesExistLabel, nil + } + + myCouplesText := getItalicLabelCentered("My Couples:") + + manageButtonsGrid := container.NewGridWithColumns(1) + + for _, coupleMap := range myCouplesMapList{ + + personAIdentifier, exists := coupleMap["PersonAIdentifier"] + if (exists == false) { + return nil, errors.New("Malformed myCoupleAnalysesMapList: Item missing personAIdentifier") + } + + personBIdentifier, exists := coupleMap["PersonBIdentifier"] + if (exists == false) { + return nil, errors.New("Malformed myCoupleAnalysesMapList: Item missing personBIdentifier") + } + + personFound, personAName, _, _, err := myPeople.GetPersonInfo(personAIdentifier) + if (err != nil) { return nil, err } + if (personFound == false){ + return nil, errors.New("Couple person not found.") + } + + personFound, personBName, _, _, err := myPeople.GetPersonInfo(personBIdentifier) + if (err != nil) { return nil, err } + if (personFound == false){ + return nil, errors.New("Couple person not found.") + } + + personANameTrimmed, _, err := helpers.TrimAndFlattenString(personAName, 15) + if (err != nil) { return nil, err } + + personBNameTrimmed, _, err := helpers.TrimAndFlattenString(personBName, 15) + if (err != nil) { return nil, err } + + coupleName := personANameTrimmed + " + " + personBNameTrimmed + + manageCoupleButton := widget.NewButton(coupleName, func(){ + setManageCouplePage(window, personAIdentifier, personBIdentifier, currentPage) + }) + + manageButtonsGrid.Add(manageCoupleButton) + } + + manageButtonsGridCentered := getContainerCentered(manageButtonsGrid) + + manageMyCouplesContainer := container.NewVBox(myCouplesText, manageButtonsGridCentered) + + return manageMyCouplesContainer, nil + } + + manageMyCouplesContainer, err := getManageMyCouplesContainer() + if (err != nil){ + setErrorEncounteredPage(window, err, previousPage) + return + } + + page := container.NewVBox(title, backButton, widget.NewSeparator(), description1, widget.NewSeparator(), createACoupleButton, widget.NewSeparator(), manageMyCouplesContainer) + + setPageContent(page, window) +} + + +func setCreateCouplePage(window fyne.Window, previousPage func()){ + + currentPage := func(){setCreateCouplePage(window, previousPage)} + + title := getPageTitleCentered("Genetics - Create Couple") + + backButton := getBackButtonCentered(previousPage) + + myGenomePeopleMapList, err := myPeople.GetMyGenomePeopleMapList() + if (err != nil) { + setErrorEncounteredPage(window, err, previousPage) + return + } + + if (len(myGenomePeopleMapList) < 2){ + + description1 := getLabelCentered("You have created fewer than 2 people.") + description2 := getLabelCentered("You must create at least 2 people to create a couple.") + description3 := getLabelCentered("Create a new person on the Genetics - Manage People page.") + + page := container.NewVBox(title, backButton, widget.NewSeparator(), description1, description2, description3) + + setPageContent(page, window) + return + } + + description1 := getLabelCentered("Choose 2 people to pair below.") + description2 := getLabelCentered("You can create new people on the Manage People page.") + + selectedPeopleListBinding := binding.NewStringList() + + getChoosePeopleGrid := func()(*fyne.Container, error){ + + getNumberOfGridColumns := func()int{ + if (len(myGenomePeopleMapList) == 2){ + return 2 + } + return 3 + } + + numberOfGridColumns := getNumberOfGridColumns() + + choosePeopleGrid := container.NewGridWithColumns(numberOfGridColumns) + + for _, personMap := range myGenomePeopleMapList{ + + personIdentifier, exists := personMap["PersonIdentifier"] + if (exists == false){ + return nil, errors.New("myGenomePeopleMapList contains item missing PersonIdentifier") + } + + personName, exists := personMap["PersonName"] + if (exists == false){ + return nil, errors.New("myGenomePeopleMapList contains item missing PersonName") + } + + personNameTrimmed, _, err := helpers.TrimAndFlattenString(personName, 15) + if (err != nil) { return nil, err } + + personCheck := widget.NewCheck(personNameTrimmed, func(response bool){ + if (response == true){ + err := selectedPeopleListBinding.Append(personIdentifier) + if (err != nil){ + setErrorEncounteredPage(window, err, currentPage) + } + return + } + + existingList, err := selectedPeopleListBinding.Get() + if (err != nil){ + setErrorEncounteredPage(window, err, currentPage) + return + } + + newList, deletedAny := helpers.DeleteAllMatchingItemsFromStringList(existingList, personIdentifier) + if (deletedAny == false){ + setErrorEncounteredPage(window, errors.New("Person not found when trying to delete person from chosen people list."), currentPage) + return + } + err = selectedPeopleListBinding.Set(newList) + if (err != nil){ + setErrorEncounteredPage(window, err, currentPage) + } + }) + + personCheckBoxed := getWidgetBoxed(personCheck) + + choosePeopleGrid.Add(personCheckBoxed) + } + + choosePeopleGridCentered := getContainerCentered(choosePeopleGrid) + + return choosePeopleGridCentered, nil + } + + choosePeopleGrid, err := getChoosePeopleGrid() + if (err != nil){ + setErrorEncounteredPage(window, err, previousPage) + return + } + + createCoupleButton := getWidgetCentered(widget.NewButtonWithIcon("Create Couple", theme.ConfirmIcon(), func(){ + + selectedPeopleList, err := selectedPeopleListBinding.Get() + if (err != nil){ + setErrorEncounteredPage(window, err, previousPage) + return + } + if (len(selectedPeopleList) < 2){ + + dialogTitle := translate("Not Enough People Selected.") + dialogMessageA := getLabelCentered("You must select two people to create a couple.") + dialogContent := container.NewVBox(dialogMessageA) + dialog.ShowCustom(dialogTitle, translate("Close"), dialogContent, window) + return + } + if (len(selectedPeopleList) > 2){ + + dialogTitle := translate("Too Many People Selected.") + dialogMessageA := getLabelCentered("You must select two people to create a couple.") + dialogContent := container.NewVBox(dialogMessageA) + dialog.ShowCustom(dialogTitle, translate("Close"), dialogContent, window) + return + } + + personAIdentifier := selectedPeopleList[0] + personBIdentifier := selectedPeopleList[1] + + _, err = myCouples.AddCouple(personAIdentifier, personBIdentifier) + if (err != nil) { + setErrorEncounteredPage(window, err, previousPage) + return + } + + setManageCouplePage(window, personAIdentifier, personBIdentifier, previousPage) + })) + + page := container.NewVBox(title, backButton, widget.NewSeparator(), description1, description2, widget.NewSeparator(), choosePeopleGrid, createCoupleButton) + + setPageContent(page, window) +} + + +func setManageCouplePage(window fyne.Window, personAIdentifier string, personBIdentifier string, previousPage func()){ + + currentPage := func(){setManageCouplePage(window, personAIdentifier, personBIdentifier, previousPage)} + + title := getPageTitleCentered("Genetics - Manage Couple") + + backButton := getBackButtonCentered(previousPage) + + personAFound, personAName, _, _, err := myPeople.GetPersonInfo(personAIdentifier) + if (err != nil){ + setErrorEncounteredPage(window, err, previousPage) + return + } + if (personAFound == false){ + setErrorEncounteredPage(window, errors.New("setManageCouplePage called with unknown personAIdentifier"), previousPage) + return + } + + personBFound, personBName, _, _, err := myPeople.GetPersonInfo(personBIdentifier) + if (err != nil){ + setErrorEncounteredPage(window, err, previousPage) + return + } + if (personBFound == false){ + setErrorEncounteredPage(window, errors.New("setManageCouplePage called with unknown personBIdentifier"), previousPage) + return + } + + coupleNameLabel := getLabelCentered("Couple Name:") + coupleNameText := getBoldLabelCentered(personAName + " + " + personBName) + + analyzeIcon, err := getFyneImageIcon("Stats") + if (err != nil){ + setErrorEncounteredPage(window, err, previousPage) + return + } + analyzeGeneticsButton := widget.NewButton("Analyze Genetics", func(){ + setAnalyzeCoupleGeneticsPage(window, personAIdentifier, personBIdentifier, currentPage) + }) + analyzeGeneticsButtonWithIcon := getContainerCentered(container.NewGridWithColumns(1, analyzeIcon, analyzeGeneticsButton)) + + deleteCoupleButton := getWidgetCentered(widget.NewButtonWithIcon("Delete Couple", theme.DeleteIcon(), func(){ + setDeleteCouplePage(window, personAIdentifier, personBIdentifier, currentPage, previousPage) + })) + + page := container.NewVBox(title, backButton, widget.NewSeparator(), coupleNameLabel, coupleNameText, widget.NewSeparator(), analyzeGeneticsButtonWithIcon, widget.NewSeparator(), deleteCoupleButton) + + setPageContent(page, window) +} + +func setDeleteCouplePage(window fyne.Window, personAIdentifier string, personBIdentifier string, previousPage func(), nextPage func()){ + + currentPage := func(){setDeleteCouplePage(window, personAIdentifier, personBIdentifier, previousPage, nextPage)} + + title := getPageTitleCentered("Genetics - Delete Couple") + + backButton := getBackButtonCentered(previousPage) + + subtitle := getPageSubtitleCentered("Delete Couple") + + personAFound, personAName, _, _, err := myPeople.GetPersonInfo(personAIdentifier) + if (err != nil){ + setErrorEncounteredPage(window, err, previousPage) + return + } + if (personAFound == false){ + setErrorEncounteredPage(window, errors.New("setDeleteGenomeCouplePage called with unknown personAIdentifier"), previousPage) + return + } + + personBFound, personBName, _, _, err := myPeople.GetPersonInfo(personBIdentifier) + if (err != nil){ + setErrorEncounteredPage(window, err, previousPage) + return + } + if (personBFound == false){ + setErrorEncounteredPage(window, errors.New("setDeleteGenomeCouplePage called with unknown personBIdentifier"), previousPage) + return + } + + description := getLabelCentered("Confirm to delete this couple?") + + coupleName := personAName + " + " + personBName + + coupleNameLabel := widget.NewLabel("Couple Name:") + coupleNameText := getBoldLabel(coupleName) + + coupleNameRow := container.NewHBox(layout.NewSpacer(), coupleNameLabel, coupleNameText, layout.NewSpacer()) + + deleteButton := getWidgetCentered(widget.NewButtonWithIcon("Delete", theme.DeleteIcon(), func(){ + + err := myCouples.DeleteCouple(personAIdentifier, personBIdentifier) + if (err != nil){ + setErrorEncounteredPage(window, err, currentPage) + return + } + + nextPage() + })) + + page := container.NewVBox(title, backButton, widget.NewSeparator(), subtitle, widget.NewSeparator(), description, coupleNameRow, deleteButton) + + setPageContent(page, window) +} + + +func setAnalyzeCoupleGeneticsPage(window fyne.Window, inputPersonAIdentifier string, inputPersonBIdentifier string, previousPage func()){ + + appMemory.SetMemoryEntry("CurrentViewedPage", "AnalyzeCoupleGeneticsPage") + + currentPage := func(){setAnalyzeCoupleGeneticsPage(window, inputPersonAIdentifier, inputPersonBIdentifier, previousPage)} + + // We have to sort the person identifiers so they are in the same order as they exist in the couple analysis + + personAIdentifier, personBIdentifier, err := myAnalyses.GetPeopleIdentifiersSortedForCouple(inputPersonAIdentifier, inputPersonBIdentifier) + if (err != nil){ + setErrorEncounteredPage(window, err, previousPage) + return + } + + title := getPageTitleCentered("Genetics - Analyze Couple Genetics") + + backButton := getBackButtonCentered(previousPage) + + description := getLabelCentered("Analyze this couple's genetics.") + + personAFound, personAName, _, _, err := myPeople.GetPersonInfo(personAIdentifier) + if (err != nil){ + setErrorEncounteredPage(window, err, previousPage) + return + } + if (personAFound == false){ + setErrorEncounteredPage(window, errors.New("setAnalyzeCoupleGeneticsPage called with missing person"), previousPage) + return + } + + personBFound, personBName, _, _, err := myPeople.GetPersonInfo(personBIdentifier) + if (err != nil){ + setErrorEncounteredPage(window, err, previousPage) + return + } + if (personBFound == false){ + setErrorEncounteredPage(window, errors.New("setAnalyzeCoupleGeneticsPage called with missing person"), previousPage) + return + } + + allPersonARawGenomeIdentifiersList, err := myGenomes.GetAllPersonRawGenomeIdentifiersList(personAIdentifier) + if (err != nil) { + setErrorEncounteredPage(window, err, previousPage) + return + } + + allPersonBRawGenomeIdentifiersList, err := myGenomes.GetAllPersonRawGenomeIdentifiersList(personBIdentifier) + if (err != nil) { + setErrorEncounteredPage(window, err, previousPage) + return + } + + if (len(allPersonARawGenomeIdentifiersList) == 0 || len(allPersonBRawGenomeIdentifiersList) == 0){ + // At least one of the people is missing genomes + + getGenomesMissingText := func()string{ + if (len(allPersonARawGenomeIdentifiersList) == 0 && len(allPersonBRawGenomeIdentifiersList) == 0){ + missingLabelText := personAName + " and " + personBName + " have no genomes." + return missingLabelText + } + if (len(allPersonARawGenomeIdentifiersList) == 0 && len(allPersonBRawGenomeIdentifiersList) != 0){ + + missingLabelText := personAName + " has no genomes." + return missingLabelText + } + // len(allPersonARawGenomeIdentifiersList) != 0 && len(allPersonBRawGenomeIdentifiersList) == 0){ + + missingLabelText := personBName + " has no genomes." + return missingLabelText + } + + genomesMissingText := getGenomesMissingText() + + noGenomesExistLabel := getBoldLabelCentered(genomesMissingText) + description2 := getLabelCentered("Each person must have at least 1 genome to perform a couple analysis.") + description3 := getLabelCentered("Go to the Manage Person page to import a genome for a person.") + page := container.NewVBox(title, backButton, widget.NewSeparator(), noGenomesExistLabel, description2, description3) + setPageContent(page, window) + return + } + + anyPersonAAnalysisFound, newestPersonAAnalysisIdentifier, _, newestPersonAAnalysisListOfGenomesAnalyzed, newerPersonAAnalysisVersionAvailable, err := myAnalyses.GetPersonNewestGeneticAnalysisInfo(personAIdentifier) + if (err != nil){ + setErrorEncounteredPage(window, err, previousPage) + return + } + + anyPersonBAnalysisFound, newestPersonBAnalysisIdentifier, _, newestPersonBAnalysisListOfGenomesAnalyzed, newerPersonBAnalysisVersionAvailable, err := myAnalyses.GetPersonNewestGeneticAnalysisInfo(personBIdentifier) + if (err != nil){ + setErrorEncounteredPage(window, err, previousPage) + return + } + + anyCoupleAnalysisFound, newestCoupleAnalysisIdentifier, timeOfNewestAnalysis, newestCoupleAnalysisListOfGenomesAnalyzed_PersonA, newestCoupleAnalysisListOfGenomesAnalyzed_PersonB, newerCoupleAnalysisVersionAvailable, err := myAnalyses.GetCoupleNewestGeneticAnalysisInfo(personAIdentifier, personBIdentifier) + if (err != nil){ + setErrorEncounteredPage(window, err, previousPage) + return + } + if (anyCoupleAnalysisFound == false || anyPersonAAnalysisFound == false || anyPersonBAnalysisFound == false){ + // We will skip to the ConfirmPerformAnalysis page + setConfirmPerformCoupleAnalysisPage(window, personAIdentifier, personBIdentifier, previousPage, currentPage) + return + } + if (newerPersonAAnalysisVersionAvailable == true || newerPersonBAnalysisVersionAvailable == true || newerCoupleAnalysisVersionAvailable == true){ + + description1 := getLabelCentered("A new analysis method is available!") + description2 := getLabelCentered("You must perform a new analysis to see the couple's new results.") + + performNewAnalysisButton := getWidgetCentered(widget.NewButtonWithIcon("Perform New Analysis", theme.NavigateNextIcon(), func(){ + setConfirmPerformCoupleAnalysisPage(window, personAIdentifier, personBIdentifier, currentPage, currentPage) + })) + + page := container.NewVBox(title, backButton, widget.NewSeparator(), description, widget.NewSeparator(), description1, description2, performNewAnalysisButton) + + setPageContent(page, window) + return + } + + // This will return true if a newer analysis can be performed using newly imported genomes + getAnalysisIsMissingGenomesBool := func()(bool, error){ + + genomesAreIdentical := helpers.CheckIfTwoListsContainIdenticalItems(allPersonARawGenomeIdentifiersList, newestPersonAAnalysisListOfGenomesAnalyzed) + if (genomesAreIdentical == false){ + return true, nil + } + + genomesAreIdentical = helpers.CheckIfTwoListsContainIdenticalItems(allPersonBRawGenomeIdentifiersList, newestPersonBAnalysisListOfGenomesAnalyzed) + if (genomesAreIdentical == false){ + return true, nil + } + + genomesAreIdentical = helpers.CheckIfTwoListsContainIdenticalItems(allPersonARawGenomeIdentifiersList, newestCoupleAnalysisListOfGenomesAnalyzed_PersonA) + if (genomesAreIdentical == false){ + return true, nil + } + + genomesAreIdentical = helpers.CheckIfTwoListsContainIdenticalItems(allPersonBRawGenomeIdentifiersList, newestCoupleAnalysisListOfGenomesAnalyzed_PersonB) + if (genomesAreIdentical == false){ + return true, nil + } + + return false, nil + } + + analysisIsMissingGenomes, err := getAnalysisIsMissingGenomesBool() + if (err != nil) { + setErrorEncounteredPage(window, err, previousPage) + return + } + if (analysisIsMissingGenomes == true){ + + description1 := getLabelCentered("At least 1 person in the couple has imported/deleted a genome.") + description2 := getLabelCentered("A new analysis must be performed.") + performAnalysisButton := getWidgetCentered(widget.NewButtonWithIcon("Perform Analysis", theme.NavigateNextIcon(), func(){ + setConfirmPerformCoupleAnalysisPage(window, personAIdentifier, personBIdentifier, currentPage, currentPage) + })) + + page := container.NewVBox(title, backButton, widget.NewSeparator(), description, widget.NewSeparator(), description1, description2, performAnalysisButton) + + setPageContent(page, window) + return + } + + analysisReadyLabel := getBoldLabelCentered("Genetic Analysis Complete!") + + coupleAnalyzedLabel := widget.NewLabel("Couple Analyzed:") + coupleName := personAName + " + " + personBName + coupleNameLabel := getBoldLabel(coupleName) + personAnalyzedRow := container.NewHBox(layout.NewSpacer(), coupleAnalyzedLabel, coupleNameLabel, layout.NewSpacer()) + + numberOfPersonAGenomesInAnalysis := len(newestPersonAAnalysisListOfGenomesAnalyzed) + numberOfPersonBGenomesInAnalysis := len(newestPersonBAnalysisListOfGenomesAnalyzed) + + timeAgoPerformed, err := helpers.ConvertUnixTimeToTimeAgoTranslated(timeOfNewestAnalysis, false) + if (err != nil){ + setErrorEncounteredPage(window, err, previousPage) + return + } + + timeAgoPerformedLabel := getItalicLabelCentered("Analysis performed " + timeAgoPerformed + ".") + + viewAnalysisButton := getWidgetCentered(widget.NewButtonWithIcon("View Analysis", theme.VisibilityIcon(), func(){ + + personAAnalysisFound, personAAnalysisMapList, err := myAnalyses.GetGeneticAnalysis(newestPersonAAnalysisIdentifier) + if (err != nil){ + setErrorEncounteredPage(window, err, currentPage) + return + } + if (personAAnalysisFound == false){ + setErrorEncounteredPage(window, errors.New("Person analysis not found after being found already."), currentPage) + return + } + + personBAnalysisFound, personBAnalysisMapList, err := myAnalyses.GetGeneticAnalysis(newestPersonBAnalysisIdentifier) + if (err != nil){ + setErrorEncounteredPage(window, err, currentPage) + return + } + if (personBAnalysisFound == false){ + setErrorEncounteredPage(window, errors.New("Person analysis not found after being found already."), currentPage) + return + } + + coupleAnalysisFound, coupleAnalysisMapList, err := myAnalyses.GetGeneticAnalysis(newestCoupleAnalysisIdentifier) + if (err != nil){ + setErrorEncounteredPage(window, err, currentPage) + return + } + if (coupleAnalysisFound == false){ + setErrorEncounteredPage(window, errors.New("Couple analysis not found after being found already."), currentPage) + return + } + + setViewCoupleGeneticAnalysisPage(window, personAIdentifier, personBIdentifier, personAAnalysisMapList, personBAnalysisMapList, coupleAnalysisMapList, numberOfPersonAGenomesInAnalysis, numberOfPersonBGenomesInAnalysis, currentPage) + + })) + + page := container.NewVBox(title, backButton, widget.NewSeparator(), description, widget.NewSeparator(), analysisReadyLabel, personAnalyzedRow, timeAgoPerformedLabel, viewAnalysisButton) + + setPageContent(page, window) +} + + +func setConfirmPerformCoupleAnalysisPage(window fyne.Window, personAIdentifier string, personBIdentifier string, previousPage func(), pageToVisitAfter func()){ + + appMemory.SetMemoryEntry("CurrentViewedPage", "ConfirmPerformCoupleGeneticAnalysisPage") + + currentPage := func(){setConfirmPerformCoupleAnalysisPage(window, personAIdentifier, personBIdentifier, previousPage, pageToVisitAfter)} + + title := getPageTitleCentered("Genetics - Perform Couple Analysis") + + backButton := getBackButtonCentered(previousPage) + + allPersonAGenomesMapList, err := myGenomes.GetAllPersonGenomesMapList(personAIdentifier) + if (err != nil){ + setErrorEncounteredPage(window, err, previousPage) + return + } + + numberOfPersonAGenomes := len(allPersonAGenomesMapList) + + if (numberOfPersonAGenomes == 0){ + setErrorEncounteredPage(window, errors.New("setConfirmPerformCoupleAnalysisPage called with person who has no genomes."), previousPage) + return + } + + allPersonBGenomesMapList, err := myGenomes.GetAllPersonGenomesMapList(personBIdentifier) + if (err != nil){ + setErrorEncounteredPage(window, err, previousPage) + return + } + + numberOfPersonBGenomes := len(allPersonBGenomesMapList) + + if (numberOfPersonBGenomes == 0){ + setErrorEncounteredPage(window, errors.New("setConfirmPerformCoupleAnalysisPage called with person who has no genomes."), previousPage) + return + } + + // Now we check if there is an analysis running + + anyProcessFound, processIdentifier, err := myAnalyses.GetCoupleGeneticAnalysisProcessIdentifier(personAIdentifier, personBIdentifier) + if (err != nil){ + setErrorEncounteredPage(window, err, previousPage) + return + } + if (anyProcessFound == true){ + + processFound, processIsComplete, _, _, _, err := myAnalyses.GetAnalysisProcessInfo(processIdentifier) + if (err != nil){ + setErrorEncounteredPage(window, err, previousPage) + return + } + if (processFound == false){ + setErrorEncounteredPage(window, errors.New("Couple genetic analysis process not found after being found already."), previousPage) + return + } + if (processIsComplete == false){ + + // An analysis is runnning. We will display that on the page + + description1 := getLabelCentered("A genetic analysis is already being generated.") + description2 := getLabelCentered("View the status of the analysis?") + + viewStatusButton := getWidgetCentered(widget.NewButtonWithIcon("View Status", theme.VisibilityIcon(), func(){ + setMonitorGeneticAnalysisGenerationPage(window, processIdentifier, currentPage, pageToVisitAfter) + })) + + page := container.NewVBox(title, backButton, widget.NewSeparator(), description1, description2, viewStatusButton) + setPageContent(page, window) + return + } + + // Process probably encountered error + // We will show retry page + } + + description1 := getBoldLabelCentered("Perform genetic analysis?") + description2 := getLabelCentered("This will create an analysis of the couple's genomes.") + + startAnalysisFunction := func(){ + + newProcessIdentifier, err := myAnalyses.StartCreateNewCoupleGeneticAnalysis(personAIdentifier, personBIdentifier) + if (err != nil){ + setErrorEncounteredPage(window, err, previousPage) + return + } + + setMonitorGeneticAnalysisGenerationPage(window, newProcessIdentifier, currentPage, pageToVisitAfter) + } + + startAnalysisButton := getWidgetCentered(widget.NewButtonWithIcon("Start Analysis", theme.ConfirmIcon(), startAnalysisFunction)) + + page := container.NewVBox(title, backButton, widget.NewSeparator(), description1, description2, startAnalysisButton) + + setPageContent(page, window) +} + + + + diff --git a/gui/matchesGui.go b/gui/matchesGui.go new file mode 100644 index 0000000..2852e67 --- /dev/null +++ b/gui/matchesGui.go @@ -0,0 +1,2118 @@ +package gui + +// matchesGui.go implements pages to view a user's mate matches + +import "fyne.io/fyne/v2" +import "fyne.io/fyne/v2/canvas" +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/appMemory" +import "seekia/internal/badgerDatabase" +import "seekia/internal/desires/mateDesires" +import "seekia/internal/desires/myLocalDesires" +import "seekia/internal/desires/myMateDesires" +import "seekia/internal/encoding" +import "seekia/internal/helpers" +import "seekia/internal/identity" +import "seekia/internal/imagery" +import "seekia/internal/myBlockedUsers" +import "seekia/internal/myIdentity" +import "seekia/internal/myIgnoredUsers" +import "seekia/internal/myLikedUsers" +import "seekia/internal/myMatches" +import "seekia/internal/myMatchScore" +import "seekia/internal/mySettings" +import "seekia/internal/network/appNetworkType/getAppNetworkType" +import "seekia/internal/profiles/attributeDisplay" +import "seekia/internal/profiles/viewableProfiles" + +import "strings" +import "time" +import "errors" +import "slices" +import "sync" + + +func setMatchesPage(window fyne.Window){ + + appMemory.SetMemoryEntry("CurrentViewedPage", "Matches") + + checkIfPageHasChangedFunction := func()bool{ + exists, currentViewedPage := appMemory.GetMemoryEntry("CurrentViewedPage") + if (exists == false || currentViewedPage != "Matches"){ + return true + } + return false + } + + currentPage := func(){setMatchesPage(window)} + + title := getPageTitleCentered("My Matches") + + desiresIcon, err := getFyneImageIcon("Desires") + if (err != nil){ + setErrorEncounteredPage(window, err, func(){setHomePage(window)}) + return + } + + statsIcon, err := getFyneImageIcon("Stats") + if (err != nil){ + setErrorEncounteredPage(window, err, func(){setHomePage(window)}) + return + } + + matchScoreIcon, err := getFyneImageIcon("MatchScore") + if (err != nil){ + setErrorEncounteredPage(window, err, func(){setHomePage(window)}) + return + } + + customizeIcon, err := getFyneImageIcon("Info") + if (err != nil){ + setErrorEncounteredPage(window, err, func(){setHomePage(window)}) + return + } + + desiresButton := widget.NewButton(translate("Desires"), func(){ + setViewAllMyDesiresPage(window, currentPage) + }) + desiresButtonWithIcon := container.NewGridWithColumns(1, desiresIcon, desiresButton) + + customizeButton := widget.NewButton("Customize", func(){ + setCustomizeMatchDisplayPage(window, currentPage) + }) + customizeButtonWithIcon := container.NewGridWithColumns(1, customizeIcon, customizeButton) + + matchScoreButton := widget.NewButton(translate("Score"), func(){ + setConfigureMatchScorePage(window, currentPage) + }) + matchScoreButtonWithIcon := container.NewGridWithColumns(1, matchScoreIcon, matchScoreButton) + + statsButton := widget.NewButton(translate("Stats"), func(){ + setMyMatchStatisticsPage(window, currentPage) + }) + statsButtonWithIcon := container.NewGridWithColumns(1, statsIcon, statsButton) + + buttonsRow := getContainerCentered(container.NewGridWithRows(1, matchScoreButtonWithIcon, desiresButtonWithIcon, statsButtonWithIcon, customizeButtonWithIcon)) + + currentSortByAttribute, err := myMatches.GetMatchesSortByAttribute() + if (err != nil) { + setErrorEncounteredPage(window, err, func(){setHomePage(window)}) + return + } + + sortingByLabel := getBoldLabel(translate("Sorting By:")) + + sortByAttributeTitle, _, _, _, _, err := attributeDisplay.GetProfileAttributeDisplayInfo(currentSortByAttribute) + if (err != nil){ + setErrorEncounteredPage(window, err, func(){setHomePage(window)}) + return + } + + sortByButton := widget.NewButton(translate(sortByAttributeTitle), func(){ + setSelectMatchesSortByAttributePage(window, currentPage) + }) + + getSortDirectionButtonWithIcon := func()(fyne.Widget, error){ + + currentSortDirection, err := myMatches.GetMatchesSortDirection() + if (err != nil) { return nil, err } + + if (currentSortDirection == "Ascending"){ + ascendingButton := widget.NewButtonWithIcon(translate("Ascending"), theme.MoveUpIcon(), func(){ + appMemory.SetMemoryEntry("StopBuildMatchesYesNo", "Yes") + _ = mySettings.SetSetting("MatchesSortDirection", "Descending") + _ = mySettings.SetSetting("MatchesSortedStatus", "No") + _ = mySettings.SetSetting("MatchesViewIndex", "0") + currentPage() + }) + + return ascendingButton, nil + } + + descendingButton := widget.NewButtonWithIcon(translate("Descending"), theme.MoveDownIcon(), func(){ + appMemory.SetMemoryEntry("StopBuildMatchesYesNo", "Yes") + _ = mySettings.SetSetting("MatchesSortDirection", "Ascending") + _ = mySettings.SetSetting("MatchesSortedStatus", "No") + _ = mySettings.SetSetting("MatchesViewIndex", "0") + + currentPage() + }) + + return descendingButton, nil + } + + sortDirectionButton, err := getSortDirectionButtonWithIcon() + if (err != nil) { + setErrorEncounteredPage(window, err, func(){setHomePage(window)}) + return + } + + sortByRow := container.NewHBox(layout.NewSpacer(), sortingByLabel, sortByButton, sortDirectionButton, layout.NewSpacer()) + + appNetworkType, err := getAppNetworkType.GetAppNetworkType() + if (err != nil) { + setErrorEncounteredPage(window, err, func(){setHomePage(window)}) + return + } + + matchesReady, err := myMatches.GetMatchesReadyStatus(appNetworkType) + if (err != nil) { + setErrorEncounteredPage(window, err, func(){setHomePage(window)}) + return + } + + if (matchesReady == false){ + + progressPercentageBinding := binding.NewFloat() + sortingProgressBinding := binding.NewString() + + startUpdateMatchesAndProgressBarFunction := func(){ + + err := myMatches.StartUpdatingMyMatches(appNetworkType) + if (err != nil) { + setErrorEncounteredPage(window, err, currentPage) + return + } + + sortingProgressBindingSet := false + + var encounteredError error + + for { + + pageHasChanged := checkIfPageHasChangedFunction() + if (pageHasChanged == true){ + appMemory.SetMemoryEntry("StopBuildMatchesYesNo", "Yes") + return + } + + buildEncounteredError, errorEncounteredString, buildIsStopped, matchesAreReady, currentPercentageProgress, err := myMatches.GetMyMatchesBuildStatus(appNetworkType) + if (err != nil){ + encounteredError = err + break + } + if (buildEncounteredError == true){ + encounteredError = errors.New(errorEncounteredString) + break + } + + if (buildIsStopped == true){ + return + } + + if (matchesAreReady == 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 && sortingProgressBindingSet == false){ + + numberOfMatches, err := myMatches.GetNumberOfGeneratedMatches() + if (err != nil) { + encounteredError = err + break + } + if (numberOfMatches != 0){ + numberOfMatchesString := helpers.ConvertIntToString(numberOfMatches) + sortingProgressBinding.Set("Sorting " + numberOfMatchesString + " Matches...") + } + sortingProgressBindingSet = true + } + + time.Sleep(100 * time.Millisecond) + } + // This will only be reached if an error occurred + errorToShow := errors.New("Error encountered while generating matches: " + encounteredError.Error()) + setErrorEncounteredPage(window, errorToShow, currentPage) + } + + loadingLabel := getBoldLabelCentered("Loading Matches...") + + loadingBar := getWidgetCentered(widget.NewProgressBarWithData(progressPercentageBinding)) + + loadingDetailsLabel := widget.NewLabelWithData(sortingProgressBinding) + loadingDetailsLabel.TextStyle = getFyneTextStyle_Italic() + loadingDetailsLabelCentered := getWidgetCentered(loadingDetailsLabel) + + page := container.NewVBox(title, widget.NewSeparator(), buttonsRow, widget.NewSeparator(), sortByRow, widget.NewSeparator(), loadingLabel, loadingBar, loadingDetailsLabelCentered) + + setPageContent(page, window) + + go startUpdateMatchesAndProgressBarFunction() + + return + } + + getSplashContainer := func()(*fyne.Container, error){ + + getRefreshResultsButtonText := func()(string, error){ + needsRefresh, err := myMatches.CheckIfMyMatchesNeedRefresh() + 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("MatchesGeneratedStatus", "No") + _ = mySettings.SetSetting("MatchesViewIndex", "0") + currentPage() + })) + + matchesReady, currentMatchesList, err := myMatches.GetMyMatchesList(appNetworkType) + if (err != nil) { return nil, err } + if (matchesReady == false){ + // This should not happen, as matches were found to be ready already. + return nil, errors.New("Matches not ready after being ready already.") + } + + if (len(currentMatchesList) == 0){ + + noMatchesFoundLabel := getBoldLabelCentered("No Matches Found") + + description1 := getLabelCentered("Edit your desires to find more matches.") + description2 := getLabelCentered("View your desire statistics to see which desires are reducing your matches.") + + viewDesireStatisticsButton := getWidgetCentered(widget.NewButtonWithIcon("View Desire Statistics", theme.VisibilityIcon(), func(){ + setMyMatchStatisticsPage(window, currentPage) + })) + + noMatchesFoundContainer := container.NewVBox(noMatchesFoundLabel, refreshResultsButton, widget.NewSeparator(), description1, description2, viewDesireStatisticsButton) + + return noMatchesFoundContainer, nil + } + + numberOfMatches := len(currentMatchesList) + + numberOfMatchesString := helpers.ConvertIntToString(numberOfMatches) + + getNumberOfMatchesFoundText := func()string{ + if (numberOfMatches == 1){ + result := translate("1 Match Found") + return result + } + + result := numberOfMatchesString + " " + translate("Matches Found") + + return result + } + + numberOfMatchesFoundText := getNumberOfMatchesFoundText() + + numberOfMatchesFoundLabel := getBoldLabelCentered(numberOfMatchesFoundText) + + getBeginRestartContinueButtons := func()(*fyne.Container, error){ + + getMatchesViewIndex := func()(int, error){ + exists, currentIndex, err := mySettings.GetSetting("MatchesViewIndex") + if (err != nil) { return 0, err } + if (exists == false){ + return 0, nil + } + currentIndexInt, err := helpers.ConvertStringToInt(currentIndex) + if (err != nil) { return 0, err } + + if (currentIndexInt > (numberOfMatches-1)){ + return 0, nil + } + + return currentIndexInt, nil + } + + currentViewIndex, err := getMatchesViewIndex() + if (err != nil) { return nil, err } + + if (currentViewIndex == 0){ + beginButton := getWidgetCentered(widget.NewButtonWithIcon("Begin Browsing", theme.NavigateNextIcon(), func(){ + setBrowseMatchesPage(window, currentPage) + })) + return beginButton, nil + } + + restartButton := getWidgetCentered(widget.NewButtonWithIcon("Restart From Beginning", theme.ContentUndoIcon(), func(){ + mySettings.SetSetting("MatchesViewIndex", "0") + setBrowseMatchesPage(window, currentPage) + })) + + currentViewIndexString := helpers.ConvertIntToString(currentViewIndex + 1) + + continueButtonText := "Continue from profile " + currentViewIndexString + " of " + numberOfMatchesString + + continueButton := getWidgetCentered(widget.NewButtonWithIcon(continueButtonText, theme.MailForwardIcon(), func(){ + setBrowseMatchesPage(window, currentPage) + })) + + buttons := container.NewVBox(restartButton, continueButton) + return buttons, nil + } + + beginRestartContinueButtons, err := getBeginRestartContinueButtons() + if (err != nil) { return nil, err } + + splashPageContent := container.NewVBox(numberOfMatchesFoundLabel, widget.NewSeparator(), beginRestartContinueButtons, refreshResultsButton) + + return splashPageContent, nil + } + + splashContainer, err := getSplashContainer() + if (err != nil){ + setErrorEncounteredPage(window, err, func(){setHomePage(window)}) + return + } + + page := container.NewVBox(title, widget.NewSeparator(), buttonsRow, widget.NewSeparator(), sortByRow, widget.NewSeparator(), splashContainer) + + setPageContent(page, window) +} + +// This page should only be called if matches are ready and at least 1 exists +func setBrowseMatchesPage(window fyne.Window, previousPage func()){ + + currentPage := func(){setBrowseMatchesPage(window, previousPage)} + + title := getPageTitleCentered("Browse Matches") + + backButton := getBackButtonCentered(previousPage) + + getPageContent := func()(*fyne.Container, error){ + + appNetworkType, err := getAppNetworkType.GetAppNetworkType() + if (err != nil) { return nil, err } + + matchesReady, currentMatchesList, err := myMatches.GetMyMatchesList(appNetworkType) + if (err != nil) { return nil, err } + if (matchesReady == false) { + // This should not happen, because this page should only be called if matches are already ready + return nil, errors.New("setBrowseMatchesPage called when matches are not ready.") + } + + if (len(currentMatchesList) == 0){ + return nil, errors.New("setBrowseMatchesPage called with no matches found.") + } + + numberOfMatches := len(currentMatchesList) + + getViewIndex := func()(int, error){ + + exists, currentIndexString, err := mySettings.GetSetting("MatchesViewIndex") + if (err != nil) { return 0, err } + if (exists == false){ + return 0, nil + } + currentIndex, err := helpers.ConvertStringToInt(currentIndexString) + if (err != nil){ + return 0, errors.New("MySettings malformed: Contains invalid MatchesViewIndex: " + currentIndexString) + } + if (currentIndex < 0){ + + return 0, nil + } + + maximumIndex := numberOfMatches-1 + if (currentIndex > maximumIndex){ + + return maximumIndex, nil + } + + return currentIndex, nil + } + + currentIndex, err := getViewIndex() + if (err != nil) { return nil, err } + + pageContent := container.NewVBox() + + if (numberOfMatches > 1){ + + getNavigationButtons := func() *fyne.Container{ + + getGoToBeginningButton := func()fyne.Widget{ + if (currentIndex == 0){ + emptyButton := widget.NewButton("", nil) + return emptyButton + } + + goToBeginningButton := widget.NewButtonWithIcon("", theme.MediaSkipPreviousIcon(), func(){ + _ = mySettings.SetSetting("MatchesViewIndex", "0") + currentPage() + }) + return goToBeginningButton + } + + getGoToEndButton := func()fyne.Widget{ + + finalIndex := numberOfMatches - 1 + + if (currentIndex >= finalIndex){ + emptyButton := widget.NewButton("", nil) + return emptyButton + } + + goToEndButton := widget.NewButtonWithIcon("", theme.MediaSkipNextIcon(), func(){ + finalIndexString := helpers.ConvertIntToString(finalIndex) + _ = mySettings.SetSetting("MatchesViewIndex", finalIndexString) + currentPage() + }) + + return goToEndButton + } + + getLeftButton := func()fyne.Widget{ + + if (currentIndex == 0){ + emptyButton := widget.NewButton("", nil) + return emptyButton + } + leftButton := widget.NewButtonWithIcon("", theme.NavigateBackIcon(), func(){ + newIndex := helpers.ConvertIntToString(currentIndex-1) + _ = mySettings.SetSetting("MatchesViewIndex", newIndex) + currentPage() + }) + return leftButton + } + + getRightButton := func()fyne.Widget{ + + finalIndex := numberOfMatches - 1 + + if (currentIndex >= finalIndex){ + + emptyButton := widget.NewButton("", nil) + return emptyButton + } + rightButton := widget.NewButtonWithIcon("", theme.NavigateNextIcon(), func(){ + newIndex := helpers.ConvertIntToString(currentIndex+1) + _ = mySettings.SetSetting("MatchesViewIndex", newIndex) + currentPage() + }) + + return rightButton + } + + goToBeginningButton := getGoToBeginningButton() + goToEndButton := getGoToEndButton() + + leftButton := getLeftButton() + rightButton := getRightButton() + + if (numberOfMatches == 1){ + buttons := getContainerCentered(container.NewGridWithRows(1, goToBeginningButton, leftButton, rightButton, goToEndButton)) + + return buttons + } + + matchRank := currentIndex + 1 + + currentRankString := helpers.ConvertIntToString(matchRank) + numberOfMatchesString := helpers.ConvertIntToString(numberOfMatches) + currentRankDescription := getBoldLabel(currentRankString + "/" + numberOfMatchesString) + + buttons := getContainerCentered(container.NewGridWithRows(1, goToBeginningButton, leftButton, currentRankDescription, rightButton, goToEndButton)) + + return buttons + } + + navButtons := getNavigationButtons() + + pageContent.Add(navButtons) + pageContent.Add(widget.NewSeparator()) + } + + getMatchDisplay := func()(*fyne.Container, error){ + + if (currentIndex >= len(currentMatchesList) || currentIndex < 0){ + return nil, errors.New("Match index out of range.") + } + + matchIdentityHash := currentMatchesList[currentIndex] + + userIsBlocked, _, _, _, err := myBlockedUsers.CheckIfUserIsBlocked(matchIdentityHash) + if (err != nil) { return nil, err } + if (userIsBlocked == true){ + + isBlockedDescription1 := getBoldLabelCentered("User is blocked.") + isBlockedDescription2 := getLabelCentered("You have blocked this match.") + isBlockedDescription3 := getLabelCentered("Refresh your matches to fix this.") + + refreshMatchesButton := getWidgetCentered(widget.NewButtonWithIcon("Refresh Matches", theme.ViewRefreshIcon(), func(){ + _ = mySettings.SetSetting("MatchesGeneratedStatus", "No") + _ = mySettings.SetSetting("MatchesViewIndex", "0") + setMatchesPage(window) + })) + + userIsBlockedSection := container.NewVBox(isBlockedDescription1, isBlockedDescription2, isBlockedDescription3, refreshMatchesButton) + return userIsBlockedSection, nil + } + + getShowIgnoredMatchesBool := func()(bool, error){ + + exists, isIgnoredIsEnabled, err := myLocalDesires.GetDesire("IsIgnored_FilterAll") + if (err != nil) { return false, err } + if (exists == false){ + // Desire is disabled. + return true, nil + } + if (isIgnoredIsEnabled == "No"){ + // Desire is disabled. + // We will show ignored matches + return true, nil + } + exists, isIgnoredDesire, err := myLocalDesires.GetDesire("IsIgnored") + if (err != nil) { return false, err } + if (exists == false){ + // Desire does not exist. + // We will show ignored matches + return true, nil + } + + // isIgnoredDesire is a list of base64 encoded values which the user wants + // We see if "Yes" is contained within the list + // If it is not, then the user desires only non-ignored users + myDesiredChoicesList := strings.Split(isIgnoredDesire, "+") + + yesBase64 := encoding.EncodeBytesToBase64String([]byte("Yes")) + + yesExists := slices.Contains(myDesiredChoicesList, yesBase64) + if (yesExists == true){ + // We want to see ignored users in our matches + return true, nil + } + + return false, nil + } + + showIgnoredMatchesBool, err := getShowIgnoredMatchesBool() + if (err != nil) { return nil, err } + + userIsIgnored, _, _, _, err := myIgnoredUsers.CheckIfUserIsIgnored(matchIdentityHash) + if (err != nil) { return nil, err } + if (userIsIgnored == true && showIgnoredMatchesBool == false){ + + isIgnoredDescription1 := getBoldLabelCentered("User is ignored.") + isIgnoredDescription2 := getLabelCentered("You have ignored this match.") + isIgnoredDescription3 := getLabelCentered("Refresh your matches to fix this.") + + refreshMatchesButton := getWidgetCentered(widget.NewButtonWithIcon("Refresh Matches", theme.ViewRefreshIcon(), func(){ + _ = mySettings.SetSetting("MatchesGeneratedStatus", "No") + _ = mySettings.SetSetting("MatchesViewIndex", "0") + setMatchesPage(window) + })) + + userIsIgnoredSection := container.NewVBox(isIgnoredDescription1, isIgnoredDescription2, isIgnoredDescription3, refreshMatchesButton) + return userIsIgnoredSection, nil + } + + profileExists, _, getAnyAttributeFunction, err := viewableProfiles.GetRetrieveAnyNewestViewableUserProfileAttributeFunction(matchIdentityHash, appNetworkType, true, false, true) + if (err != nil) { return nil, err } + if (profileExists == false) { + + matchNotFoundLabel := getBoldLabelCentered("Match Not Found") + + description1 := getLabelCentered("Their profile may have been deleted.") + description2 := getLabelCentered("Refresh your matches to fix this.") + + refreshMatchesButton := getWidgetCentered(widget.NewButtonWithIcon("Refresh Matches", theme.ViewRefreshIcon(), func(){ + _ = mySettings.SetSetting("MatchesGeneratedStatus", "No") + _ = mySettings.SetSetting("MatchesViewIndex", "0") + setMatchesPage(window) + })) + + matchNotFoundSection := container.NewVBox(matchNotFoundLabel, description1, description2, refreshMatchesButton) + return matchNotFoundSection, nil + } + + exists, _, userIsDisabled, err := getAnyAttributeFunction("Disabled") + if (err != nil){ return nil, err } + if (exists == true && userIsDisabled == "Yes"){ + + matchIsDisabledLabel := getBoldLabelCentered("User Is Disabled") + + description1 := getLabelCentered("This user has disabled their profile.") + description2 := getLabelCentered("Refresh your matches to fix this.") + + refreshMatchesButton := getWidgetCentered(widget.NewButtonWithIcon("Refresh Matches", theme.ViewRefreshIcon(), func(){ + _ = mySettings.SetSetting("MatchesGeneratedStatus", "No") + _ = mySettings.SetSetting("MatchesViewIndex", "0") + setMatchesPage(window) + })) + + matchIsDisabledSection := container.NewVBox(matchIsDisabledLabel, description1, description2, refreshMatchesButton) + return matchIsDisabledSection, nil + } + + // We see if user's newest viewable profile still fulfills our desires + + profilePassesDesires, err := myMateDesires.CheckIfMateProfilePassesAllMyDesires(false, "", getAnyAttributeFunction) + if (err != nil) { return nil, err } + if (profilePassesDesires == false){ + + userIsNotAMatchLabel := getBoldLabelCentered("User Is Not A Match Anymore") + + description1 := getLabelCentered("This user has changed their profile and they are no longer a match.") + description2 := getLabelCentered("Refresh your matches to fix this.") + + refreshMatchesButton := getWidgetCentered(widget.NewButtonWithIcon("Refresh Matches", theme.ViewRefreshIcon(), func(){ + _ = mySettings.SetSetting("MatchesGeneratedStatus", "No") + _ = mySettings.SetSetting("MatchesViewIndex", "0") + setMatchesPage(window) + })) + + userIsNotAMatchSection := container.NewVBox(userIsNotAMatchLabel, description1, description2, refreshMatchesButton) + + return userIsNotAMatchSection, nil + } + + getAvatarOrImage := func()(*canvas.Image, error){ + + photosExist, _, photosAttribute, err := getAnyAttributeFunction("Photos") + if (err != nil) { return nil, err } + + if (photosExist == true){ + + photosList := strings.Split(photosAttribute, "+") + + firstPhotoBase64String := photosList[0] + + goImageObject, err := imagery.ConvertWEBPBase64StringToCroppedDownsizedImageObject(firstPhotoBase64String) + if (err != nil) { + return nil, errors.New("Database corrupt: Contains profile with invalid Photos attribute") + } + + fyneImageObject := canvas.NewImageFromImage(goImageObject) + fyneImageObject.FillMode = canvas.ImageFillContain + + return fyneImageObject, nil + } + + getAvatarEmojiIdentifier := func()(int, error){ + + exists, _, emojiIdentifier, err := getAnyAttributeFunction("Avatar") + if (err != nil){ return 0, err } + if (exists == false){ + // This is the avatar for users who have not selected an avatar + return 2929, nil + } + + emojiIdentifierInt, err := helpers.ConvertStringToInt(emojiIdentifier) + if (err != nil) { + return 0, errors.New("Database corrupt: Contains user profile with invalid avatar: " + emojiIdentifier) + } + + return emojiIdentifierInt, nil + } + + avatarEmojiIdentifier, err := getAvatarEmojiIdentifier() + if (err != nil) { return nil, err } + + emojiImage, err := getEmojiImageObject(avatarEmojiIdentifier) + if (err != nil){ return nil, err } + + emojiFyneImage := canvas.NewImageFromImage(emojiImage) + emojiFyneImage.FillMode = canvas.ImageFillContain + + return emojiFyneImage, nil + } + + avatarOrImage, err := getAvatarOrImage() + if (err != nil) { return nil, err } + avatarOrImage.SetMinSize(getCustomFyneSize(10)) + + avatarOrImageHeightenerA := container.NewVBox(widget.NewLabel(""), widget.NewLabel(""), widget.NewLabel("")) + avatarOrImageHeightenerB := container.NewVBox(widget.NewLabel(""), widget.NewLabel(""), widget.NewLabel("")) + + avatarOrImageHeightened := getContainerCentered(container.NewGridWithRows(1, avatarOrImageHeightenerA, avatarOrImage, avatarOrImageHeightenerB)) + + viewProfileButton := getWidgetCentered(widget.NewButtonWithIcon("View Profile", theme.VisibilityIcon(), func(){ + setViewPeerProfilePageFromIdentityHash(window, matchIdentityHash, currentPage) + })) + + getAttributesSection := func()(*fyne.Container, error){ + + getDisplayAttributesList := func()([]string, error){ + + // First attribute is always the "Sort By" attribute + + currentSortByAttribute, err := myMatches.GetMatchesSortByAttribute() + if (err != nil) { return nil, err } + + exists, currentAttributesListString, err := mySettings.GetSetting("CustomMatchDisplayAttributesList") + if (err != nil) { return nil, err } + if (exists == false){ + newDisplayAttributesList := []string{currentSortByAttribute} + return newDisplayAttributesList, nil + } + + currentAttributesList := strings.Split(currentAttributesListString, ",") + + displayAttributesListPruned, _ := helpers.DeleteAllMatchingItemsFromStringList(currentAttributesList, currentSortByAttribute) + + newDisplayAttributesList := []string{currentSortByAttribute} + + newDisplayAttributesList = append(newDisplayAttributesList, displayAttributesListPruned...) + + return newDisplayAttributesList, nil + } + + displayAttributesList, err := getDisplayAttributesList() + if (err != nil){ return nil, err } + + attributesSection := container.NewVBox() + + for _, attributeName := range displayAttributesList{ + + attributeTitle, _, formatValueFunction, attributeUnits, missingValueText, err := attributeDisplay.GetProfileAttributeDisplayInfo(attributeName) + if (err != nil) { return nil, err } + + attributeTitleLabel := getBoldLabel(attributeTitle + ":") + + getAttributeValueText := func()(string, error){ + + attributeValueExists, _, attributeValue, err := getAnyAttributeFunction(attributeName) + if (err != nil) { return "", err } + if (attributeValueExists == false){ + return missingValueText, nil + } + + formattedValue, err := formatValueFunction(attributeValue) + if (err != nil) { return "", err } + + result := formattedValue + attributeUnits + + return result, nil + } + + attributeValueText, err := getAttributeValueText() + if (err != nil) { return nil, err } + + attributeValueLabel := widget.NewLabel(attributeValueText) + + attributeRow := container.NewHBox(layout.NewSpacer(), attributeTitleLabel, attributeValueLabel, layout.NewSpacer()) + + attributesSection.Add(attributeRow) + } + + return attributesSection, nil + } + + attributesSection, err := getAttributesSection() + if (err != nil) { return nil, err } + + getLikeOrUnlikeButtonWithIcon := func()(*fyne.Container, error){ + + userIsLiked, _, err := myLikedUsers.CheckIfUserIsLiked(matchIdentityHash) + if (err != nil) { return nil, err } + if (userIsLiked == true){ + unlikeIcon, err := getFyneImageIcon("Unlike") + if (err != nil) { return nil, err } + unlikeButton := widget.NewButton("Unlike", func(){ + setConfirmUnlikeUserPage(window, matchIdentityHash, currentPage, currentPage) + }) + + unlikeButtonWithIcon := container.NewGridWithColumns(1, unlikeIcon, unlikeButton) + return unlikeButtonWithIcon, nil + } + + likeIcon, err := getFyneImageIcon("Like") + if (err != nil) { return nil, err } + likeButton := widget.NewButton("Like", func(){ + setConfirmLikeUserPage(window, matchIdentityHash, currentPage, currentPage) + }) + + likeButtonWithIcon := container.NewGridWithColumns(1, likeIcon, likeButton) + return likeButtonWithIcon, nil + } + + likeOrUnlikeButtonWithIcon, err := getLikeOrUnlikeButtonWithIcon() + if (err != nil) { return nil, err } + + getIgnoreOrUnignoreButtonWithIcon := func()(*fyne.Container, error){ + + if (userIsIgnored == true){ + unignoreIcon := widget.NewIcon(theme.VisibilityIcon()) + + unignoreButton := widget.NewButton("Unignore", func(){ + setConfirmUnignoreUserPage(window, matchIdentityHash, currentPage, currentPage) + }) + + unignoreButtonWithIcon := container.NewGridWithColumns(1, unignoreIcon, unignoreButton) + return unignoreButtonWithIcon, nil + } + + ignoreIcon := widget.NewIcon(theme.VisibilityOffIcon()) + + ignoreButton := widget.NewButton("Ignore", func(){ + setConfirmIgnoreUserPage(window, matchIdentityHash, currentPage, currentPage) + }) + + ignoreButtonWithIcon := container.NewGridWithColumns(1, ignoreIcon, ignoreButton) + return ignoreButtonWithIcon, nil + } + + ignoreOrUnignoreButtonWithIcon, err := getIgnoreOrUnignoreButtonWithIcon() + if (err != nil) { return nil, err } + + greetIcon, err := getFyneImageIcon("Greet") + if (err != nil) { return nil, err } + greetButton := widget.NewButton("Greet", func(){ + setConfirmGreetOrRejectUserPage(window, "Greet", matchIdentityHash, currentPage, currentPage) + }) + greetButtonWithIcon := container.NewGridWithColumns(1, greetIcon, greetButton) + + moreIcon, err := getFyneImageIcon("Plus") + if (err != nil) { return nil, err } + moreButton := widget.NewButton("More", func(){ + setViewPeerActionsPage(window, matchIdentityHash, currentPage) + }) + moreButtonWithIcon := container.NewGridWithColumns(1, moreIcon, moreButton) + + actionsRow := getContainerCentered(container.NewGridWithRows(1, likeOrUnlikeButtonWithIcon, ignoreOrUnignoreButtonWithIcon, greetButtonWithIcon, moreButtonWithIcon)) + + matchDisplay := container.NewVBox(avatarOrImageHeightened, viewProfileButton, widget.NewSeparator(), attributesSection, widget.NewSeparator(), actionsRow) + + return matchDisplay, nil + } + + matchDisplay, err := getMatchDisplay() + if (err != nil) { return nil, err } + + pageContent.Add(matchDisplay) + + return pageContent, nil + } + + pageContent, err := getPageContent() + if (err != nil){ + setErrorEncounteredPage(window, err, previousPage) + return + } + + page := container.NewVBox(title, backButton, widget.NewSeparator(), pageContent) + + setPageContent(page, window) +} + +func setSelectMatchesSortByAttributePage(window fyne.Window, previousPage func()){ + + appMemory.SetMemoryEntry("CurrentViewedPage", "SortBySelectPage_Matches") + + title := getPageTitleCentered("Select Sort By Attribute") + + backButton := getBackButtonCentered(previousPage) + + description := getLabelCentered("Choose the attribute to sort your matches by.") + + getPageContent := func()(*fyne.Container, error){ + + generalAttributeButtonsGrid := container.NewGridWithColumns(1) + physicalAttributeButtonsGrid := container.NewGridWithColumns(1) + lifestyleAttributeButtonsGrid := container.NewGridWithColumns(1) + mentalAttributeButtonsGrid := container.NewGridWithColumns(1) + + addAttributeSelectButton := func(attributeType string, attributeName string, sortDirection string)error{ + + attributeTitle, _, _, _, _, err := attributeDisplay.GetProfileAttributeDisplayInfo(attributeName) + if (err != nil) { return err } + + attributeButton := widget.NewButton(attributeTitle, func(){ + _ = mySettings.SetSetting("MatchesSortedStatus", "No") + _ = mySettings.SetSetting("MatchesSortByAttribute", attributeName) + _ = mySettings.SetSetting("MatchesSortDirection", sortDirection) + _ = mySettings.SetSetting("MatchesViewIndex", "0") + + previousPage() + }) + + if (attributeType == "General"){ + + generalAttributeButtonsGrid.Add(attributeButton) + + } else if (attributeType == "Physical"){ + + physicalAttributeButtonsGrid.Add(attributeButton) + + } else if (attributeType == "Lifestyle"){ + + lifestyleAttributeButtonsGrid.Add(attributeButton) + + } else if (attributeType == "Mental"){ + + mentalAttributeButtonsGrid.Add(attributeButton) + + } else { + return errors.New("addSelectButton called with invalid attributeType: " + attributeType) + } + + return nil + } + + generalLabel := getBoldLabelCentered("General") + + err := addAttributeSelectButton("General", "MatchScore", "Descending") + if (err != nil) { return nil, err } + err = addAttributeSelectButton("General", "Distance", "Ascending") + if (err != nil) { return nil, err } + err = addAttributeSelectButton("General", "SearchTermsCount", "Descending") + if (err != nil) { return nil, err } + + physicalLabel := getBoldLabelCentered("Physical") + + err = addAttributeSelectButton("Physical", "Age", "Ascending") + if (err != nil) { return nil, err } + err = addAttributeSelectButton("Physical", "Height", "Descending") + if (err != nil) { return nil, err } + err = addAttributeSelectButton("Physical", "BodyFat", "Ascending") + if (err != nil) { return nil, err } + err = addAttributeSelectButton("Physical", "BodyMuscle", "Descending") + if (err != nil) { return nil, err } + err = addAttributeSelectButton("Physical", "SkinColor", "Ascending") + if (err != nil) { return nil, err } + err = addAttributeSelectButton("Physical", "HairTexture", "Ascending") + if (err != nil) { return nil, err } + + err = addAttributeSelectButton("Physical", "RacialSimilarity", "Descending") + if (err != nil) { return nil, err } + + err = addAttributeSelectButton("Physical", "EyeColorSimilarity", "Descending") + if (err != nil) { return nil, err } + err = addAttributeSelectButton("Physical", "EyeColorGenesSimilarity", "Descending") + if (err != nil) { return nil, err } + err = addAttributeSelectButton("Physical", "HairColorSimilarity", "Descending") + if (err != nil) { return nil, err } + err = addAttributeSelectButton("Physical", "HairColorGenesSimilarity", "Descending") + if (err != nil) { return nil, err } + err = addAttributeSelectButton("Physical", "SkinColorSimilarity", "Descending") + if (err != nil) { return nil, err } + err = addAttributeSelectButton("Physical", "SkinColorGenesSimilarity", "Descending") + if (err != nil) { return nil, err } + err = addAttributeSelectButton("Physical", "HairTextureSimilarity", "Descending") + if (err != nil) { return nil, err } + err = addAttributeSelectButton("Physical", "HairTextureGenesSimilarity", "Descending") + if (err != nil) { return nil, err } + err = addAttributeSelectButton("Physical", "FacialStructureGenesSimilarity", "Descending") + if (err != nil) { return nil, err } + err = addAttributeSelectButton("Physical", "23andMe_AncestralSimilarity", "Descending") + if (err != nil) { return nil, err } + err = addAttributeSelectButton("Physical", "23andMe_MaternalHaplogroupSimilarity", "Descending") + if (err != nil) { return nil, err } + err = addAttributeSelectButton("Physical", "23andMe_PaternalHaplogroupSimilarity", "Descending") + if (err != nil) { return nil, err } + + err = addAttributeSelectButton("Physical", "OffspringProbabilityOfAnyMonogenicDisease", "Ascending") + if (err != nil) { return nil, err } + + err = addAttributeSelectButton("Physical", "TotalPolygenicDiseaseRiskScore", "Ascending") + if (err != nil) { return nil, err } + + err = addAttributeSelectButton("Physical", "OffspringTotalPolygenicDiseaseRiskScore", "Ascending") + if (err != nil) { return nil, err } + + offspringLactoseToleranceProbabilityButton := widget.NewButton("Offspring Lactose Tolerance Probability", func(){ + //TODO + showUnderConstructionDialog(window) + }) + physicalAttributeButtonsGrid.Add(offspringLactoseToleranceProbabilityButton) + + offspringCurlyHairProbabilityButton := widget.NewButton("Offspring Curly Hair Probability", func(){ + //TODO + showUnderConstructionDialog(window) + }) + physicalAttributeButtonsGrid.Add(offspringCurlyHairProbabilityButton) + + offspringStraightHairProbabilityButton := widget.NewButton("Offspring Straight Hair Probability", func(){ + //TODO + showUnderConstructionDialog(window) + }) + physicalAttributeButtonsGrid.Add(offspringStraightHairProbabilityButton) + + err = addAttributeSelectButton("Physical", "23andMe_NeanderthalVariants", "Descending") + if (err != nil) { return nil, err } + + lifestyleLabel := getBoldLabelCentered("Lifestyle") + + err = addAttributeSelectButton("Lifestyle", "WealthInGold", "Descending") + if (err != nil) { return nil, err } + + err = addAttributeSelectButton("Lifestyle", "Fame", "Descending") + if (err != nil) { return nil, err } + + err = addAttributeSelectButton("Lifestyle", "FruitRating", "Descending") + if (err != nil) { return nil, err } + err = addAttributeSelectButton("Lifestyle", "VegetablesRating", "Descending") + if (err != nil) { return nil, err } + err = addAttributeSelectButton("Lifestyle", "NutsRating", "Descending") + if (err != nil) { return nil, err } + err = addAttributeSelectButton("Lifestyle", "GrainsRating", "Descending") + if (err != nil) { return nil, err } + err = addAttributeSelectButton("Lifestyle", "DairyRating", "Descending") + if (err != nil) { return nil, err } + err = addAttributeSelectButton("Lifestyle", "SeafoodRating", "Descending") + if (err != nil) { return nil, err } + err = addAttributeSelectButton("Lifestyle", "BeefRating", "Descending") + if (err != nil) { return nil, err } + err = addAttributeSelectButton("Lifestyle", "PorkRating", "Descending") + if (err != nil) { return nil, err } + err = addAttributeSelectButton("Lifestyle", "PoultryRating", "Descending") + if (err != nil) { return nil, err } + err = addAttributeSelectButton("Lifestyle", "EggsRating", "Descending") + if (err != nil) { return nil, err } + err = addAttributeSelectButton("Lifestyle", "BeansRating", "Descending") + if (err != nil) { return nil, err } + + err = addAttributeSelectButton("Lifestyle", "AlcoholFrequency", "Ascending") + if (err != nil) { return nil, err } + err = addAttributeSelectButton("Lifestyle", "TobaccoFrequency", "Ascending") + if (err != nil) { return nil, err } + err = addAttributeSelectButton("Lifestyle", "CannabisFrequency", "Ascending") + if (err != nil) { return nil, err } + + mentalLabel := getBoldLabelCentered("Mental") + + err = addAttributeSelectButton("Mental", "PetsRating", "Descending") + if (err != nil) { return nil, err } + err = addAttributeSelectButton("Mental", "CatsRating", "Descending") + if (err != nil) { return nil, err } + err = addAttributeSelectButton("Mental", "DogsRating", "Descending") + if (err != nil) { return nil, err } + + generalAttributeButtonsGridCentered := getContainerCentered(generalAttributeButtonsGrid) + physicalAttributeButtonsGridCentered := getContainerCentered(physicalAttributeButtonsGrid) + lifestyleAttributeButtonsGridCentered := getContainerCentered(lifestyleAttributeButtonsGrid) + mentalAttributeButtonsGridCentered := getContainerCentered(mentalAttributeButtonsGrid) + + pageContent := container.NewVBox(generalLabel, widget.NewSeparator(), generalAttributeButtonsGridCentered, widget.NewSeparator(), physicalLabel, widget.NewSeparator(), physicalAttributeButtonsGridCentered, widget.NewSeparator(), lifestyleLabel, widget.NewSeparator(), lifestyleAttributeButtonsGridCentered, widget.NewSeparator(), mentalLabel, widget.NewSeparator(), mentalAttributeButtonsGridCentered) + + return pageContent, nil + } + + pageContent, err := getPageContent() + if (err != nil){ + setErrorEncounteredPage(window, err, previousPage) + return + } + + page := container.NewVBox(title, backButton, widget.NewSeparator(), description, widget.NewSeparator(), pageContent) + + setPageContent(page, window) +} + + +func setMyMatchStatisticsPage(window fyne.Window, 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(){setMyMatchStatisticsPage(window, previousPage)} + + title := getPageTitleCentered("My Match Statistics") + + backButton := getBackButtonCentered(previousPage) + + description := getLabelCentered("Below are statistics about your matches.") + + accuracyWarningButton := getWidgetCentered(widget.NewButtonWithIcon("Statistics Warning", theme.WarningIcon(), func(){ + setMateDesireStatisticsWarningPage(window, currentPage) + })) + + totalMateUsersBinding := binding.NewString() + totalBlockedUsersBinding := binding.NewString() + numberOfMatchesBinding := binding.NewString() + matchPercentageBinding := binding.NewString() + + totalMateUsersDescription := widget.NewLabel("Total Mate Users:") + totalMateUsersLabel := widget.NewLabelWithData(totalMateUsersBinding) + totalMateUsersLabel.TextStyle = getFyneTextStyle_Bold() + totalMateUsersRow := container.NewHBox(layout.NewSpacer(), totalMateUsersDescription, totalMateUsersLabel, layout.NewSpacer()) + + totalBlockedUsersDescription := widget.NewLabel("Total Blocked Users:") + totalBlockedUsersLabel := widget.NewLabelWithData(totalBlockedUsersBinding) + totalBlockedUsersLabel.TextStyle = getFyneTextStyle_Bold() + totalBlockedUsersRow := container.NewHBox(layout.NewSpacer(), totalBlockedUsersDescription, totalBlockedUsersLabel, layout.NewSpacer()) + + numberOfMatchesDescription := widget.NewLabel("Number of Matches:") + numberOfMatchesLabel := widget.NewLabelWithData(numberOfMatchesBinding) + numberOfMatchesLabel.TextStyle = getFyneTextStyle_Bold() + numberOfMatchesRow := container.NewHBox(layout.NewSpacer(), numberOfMatchesDescription, numberOfMatchesLabel, layout.NewSpacer()) + + matchPercentageDescription := widget.NewLabel("Match Percentage:") + matchPercentageLabel := widget.NewLabelWithData(matchPercentageBinding) + matchPercentageLabel.TextStyle = getFyneTextStyle_Bold() + matchPercentageRow := container.NewHBox(layout.NewSpacer(), matchPercentageDescription, matchPercentageLabel, layout.NewSpacer()) + + appNetworkType, err := getAppNetworkType.GetAppNetworkType() + if (err != nil) { + setErrorEncounteredPage(window, err, previousPage) + return + } + + updateBindingsFunction := func(){ + + var resultsReadyBoolMutex sync.RWMutex + resultsReadyBool := false + + updateBindingsWithLoadingText := func(){ + + secondsElapsed := 0 + + for{ + + resultsReadyBoolMutex.RLock() + resultsReady := resultsReadyBool + resultsReadyBoolMutex.RUnlock() + + if (resultsReady == true){ + return + } + pageHasChanged := checkIfPageHasChangedFunction() + if (pageHasChanged == true){ + return + } + + if (secondsElapsed % 3 == 0){ + totalMateUsersBinding.Set("Loading.") + numberOfMatchesBinding.Set("Loading.") + matchPercentageBinding.Set("Loading.") + } else if (secondsElapsed %3 == 1){ + totalMateUsersBinding.Set("Loading..") + numberOfMatchesBinding.Set("Loading..") + matchPercentageBinding.Set("Loading..") + } else { + totalMateUsersBinding.Set("Loading...") + numberOfMatchesBinding.Set("Loading...") + matchPercentageBinding.Set("Loading...") + } + + time.Sleep(time.Second) + secondsElapsed += 1 + } + } + + go updateBindingsWithLoadingText() + + updateBindingsWithResultFunction := func()error{ + + // We use below to make sure we do not count ourselves as a peer profile + myIdentityExists, myIdentityHash, err := myIdentity.GetMyIdentityHash("Mate") + if (err != nil) { return err } + + mateIdentityHashesList, err := badgerDatabase.GetAllProfileIdentityHashes("Mate") + if (err != nil) { return err } + + numberOfUsersWithEnabledViewableProfile := 0 + numberOfBlockedUsers := 0 + numberOfMatches := 0 + + for _, userIdentityHash := range mateIdentityHashesList{ + + if (myIdentityExists == true && userIdentityHash == myIdentityHash){ + // We don't count ourselves as a potential match + continue + } + + profileExists, _, getAnyUserProfileAttributeFunction, err := viewableProfiles.GetRetrieveAnyNewestViewableUserProfileAttributeFunction(userIdentityHash, appNetworkType, true, false, false) + if (err != nil) { return err } + if (profileExists == false) { + continue + } + + exists, _, _, err := getAnyUserProfileAttributeFunction("Disabled") + if (err != nil) { return err } + if (exists == true){ + // Profile is disabled + continue + } + + numberOfUsersWithEnabledViewableProfile += 1 + + userIsBlocked, _, _, _, err := myBlockedUsers.CheckIfUserIsBlocked(userIdentityHash) + if (err != nil) { return err } + if (userIsBlocked == true){ + numberOfBlockedUsers += 1 + continue + } + + passesMyDesires, err := myMateDesires.CheckIfMateProfilePassesAllMyDesires(false, "", getAnyUserProfileAttributeFunction) + if (err != nil) { return err } + if (passesMyDesires == true){ + numberOfMatches += 1 + } + } + + resultsReadyBoolMutex.Lock() + resultsReadyBool = true + resultsReadyBoolMutex.Unlock() + + getMatchPercentage := func()float64{ + if (numberOfUsersWithEnabledViewableProfile == 0){ + return 0 + } + + matchPercentage := (float64(numberOfMatches)/float64(numberOfUsersWithEnabledViewableProfile)) * 100 + return matchPercentage + } + + matchPercentage := getMatchPercentage() + + totalUsersString := helpers.ConvertIntToString(numberOfUsersWithEnabledViewableProfile) + totalBlockedUsersString := helpers.ConvertIntToString(numberOfBlockedUsers) + numberOfMatchesString := helpers.ConvertIntToString(numberOfMatches) + matchPercentageString := helpers.ConvertFloat64ToStringRounded(matchPercentage, 1) + + totalMateUsersBinding.Set(totalUsersString) + totalBlockedUsersBinding.Set(totalBlockedUsersString) + numberOfMatchesBinding.Set(numberOfMatchesString) + matchPercentageBinding.Set(matchPercentageString + "%") + + return nil + } + + err := updateBindingsWithResultFunction() + if (err != nil){ + pageHasChanged := checkIfPageHasChangedFunction() + if (pageHasChanged == false){ + setErrorEncounteredPage(window, err, previousPage) + } + } + } + + viewAllDesireStatisticsButton := getWidgetCentered(widget.NewButtonWithIcon("View All My Desire Statistics", theme.VisibilityIcon(), func(){ + setViewAllMyDesireStatisticsPage(window, false, 0, 0, nil, currentPage) + })) + + page := container.NewVBox(title, backButton, widget.NewSeparator(), description, widget.NewSeparator(), accuracyWarningButton, widget.NewSeparator(), totalMateUsersRow, totalBlockedUsersRow, numberOfMatchesRow, matchPercentageRow, viewAllDesireStatisticsButton) + + setPageContent(page, window) + + go updateBindingsFunction() +} + + +func setCustomizeMatchDisplayPage(window fyne.Window, previousPage func()){ + + appMemory.SetMemoryEntry("CurrentViewedPage", "CustomizeMatchDisplay") + + currentPage := func(){setCustomizeMatchDisplayPage(window, previousPage)} + + title := getPageTitleCentered("Customize Match Display") + + backButton := getBackButtonCentered(previousPage) + + description1 := getLabelCentered("Select the attributes to show when browsing matches.") + description2 := getLabelCentered("A maximum of 5 attributes can be selected.") + + getCustomDisplayAttributesList := func()([]string, error){ + + exists, currentAttributesListString, err := mySettings.GetSetting("CustomMatchDisplayAttributesList") + if (err != nil) { return nil, err } + if (exists == false){ + emptyList := make([]string, 0) + return emptyList, nil + } + + currentAttributesList := strings.Split(currentAttributesListString, ",") + return currentAttributesList, nil + } + + customDisplayAttributesList, err := getCustomDisplayAttributesList() + if (err != nil){ + setErrorEncounteredPage(window, err, previousPage) + return + } + + addAttributeButton := getWidgetCentered(widget.NewButtonWithIcon("Add Attribute", theme.ContentAddIcon(), func(){ + if (len(customDisplayAttributesList) >= 5){ + dialogTitle := translate("Attribute Limit Reached.") + dialogMessageA := getLabelCentered("You can only select 5 attributes.") + dialogContent := container.NewVBox(dialogMessageA) + dialog.ShowCustom(dialogTitle, translate("Close"), dialogContent, window) + return + } + setAddAttributeToCustomMatchDisplayPage(window, currentPage, currentPage) + })) + + if (len(customDisplayAttributesList) == 0){ + + noAttributesSelectedLabel := getBoldLabelCentered("No Attributes Selected") + + page := container.NewVBox(title, backButton, widget.NewSeparator(), description1, description2, widget.NewSeparator(), noAttributesSelectedLabel, addAttributeButton) + + setPageContent(page, window) + return + } + + currentAttributesLabel := getItalicLabelCentered("Current Attributes:") + + getCurrentAttributesGrid := func()(*fyne.Container, error){ + + attributeIndexesColumn := container.NewVBox() + attributeTitlesColumn := container.NewVBox() + attributeRemoveButtonsColumn := container.NewVBox() + + for index, attributeName := range customDisplayAttributesList{ + + attributeIndexString := helpers.ConvertIntToString(index+1) + "." + + attributeTitle, _, _, _, _, err := attributeDisplay.GetProfileAttributeDisplayInfo(attributeName) + if (err != nil) { return nil, err } + + attributeIndexLabel := getBoldLabelCentered(attributeIndexString) + attributeTitleLabel := getBoldLabelCentered(attributeTitle) + + deleteAttributeButton := widget.NewButtonWithIcon("", theme.DeleteIcon(), func(){ + + newAttributesList, _ := helpers.DeleteAllMatchingItemsFromStringList(customDisplayAttributesList, attributeName) + + if (len(newAttributesList) == 0){ + + err := mySettings.DeleteSetting("CustomMatchDisplayAttributesList") + if (err != nil) { + setErrorEncounteredPage(window, err, currentPage) + return + } + + currentPage() + return + } + + newAttributesListString := strings.Join(newAttributesList, ",") + + err := mySettings.SetSetting("CustomMatchDisplayAttributesList", newAttributesListString) + if (err != nil) { + setErrorEncounteredPage(window, err, currentPage) + return + } + + currentPage() + }) + + attributeIndexesColumn.Add(attributeIndexLabel) + attributeTitlesColumn.Add(attributeTitleLabel) + attributeRemoveButtonsColumn.Add(deleteAttributeButton) + + attributeIndexesColumn.Add(widget.NewSeparator()) + attributeTitlesColumn.Add(widget.NewSeparator()) + attributeRemoveButtonsColumn.Add(widget.NewSeparator()) + } + + attributesGrid := container.NewHBox(layout.NewSpacer(), attributeIndexesColumn, attributeTitlesColumn, attributeRemoveButtonsColumn, layout.NewSpacer()) + + return attributesGrid, nil + } + + currentAttributesGrid, err := getCurrentAttributesGrid() + if (err != nil){ + setErrorEncounteredPage(window, err, previousPage) + return + } + + page := container.NewVBox(title, backButton, widget.NewSeparator(), description1, description2, widget.NewSeparator(), addAttributeButton, widget.NewSeparator(), currentAttributesLabel, currentAttributesGrid) + + setPageContent(page, window) +} + +func setAddAttributeToCustomMatchDisplayPage(window fyne.Window, previousPage func(), nextPage func()){ + + currentPage := func(){setAddAttributeToCustomMatchDisplayPage(window, previousPage, nextPage)} + + title := getPageTitleCentered("Add Attribute") + + backButton := getBackButtonCentered(previousPage) + + description := getLabelCentered("Choose the attribute to display.") + + getPageContent := func()(*fyne.Container, error){ + + getCustomDisplayAttributesList := func()([]string, error){ + + exists, currentAttributesListString, err := mySettings.GetSetting("CustomMatchDisplayAttributesList") + if (err != nil) { return nil, err } + if (exists == false){ + emptyList := make([]string, 0) + return emptyList, nil + } + currentAttributesList := strings.Split(currentAttributesListString, ",") + return currentAttributesList, nil + } + + customDisplayAttributesList, err := getCustomDisplayAttributesList() + if (err != nil){ return nil, err } + + if (len(customDisplayAttributesList) >= 5){ + return nil, errors.New("setAddAttributeToCustomMatchDisplayPage called when attributesList is full.") + } + + generalAttributeButtonsGrid := container.NewGridWithColumns(1) + physicalAttributeButtonsGrid := container.NewGridWithColumns(1) + lifestyleAttributeButtonsGrid := container.NewGridWithColumns(1) + mentalAttributeButtonsGrid := container.NewGridWithColumns(1) + + addAttributeButton := func(attributeName string, attributeType string)error{ + + isSelected := slices.Contains(customDisplayAttributesList, attributeName) + if (isSelected == true){ + // Attribute has already been selected + // We will not show it + return nil + } + + attributeTitle, _, _, _, _, err := attributeDisplay.GetProfileAttributeDisplayInfo(attributeName) + if (err != nil) { return err } + + attributeButton := widget.NewButton(attributeTitle, func(){ + + newAttributesList := append(customDisplayAttributesList, attributeName) + + newAttributesListString := strings.Join(newAttributesList, ",") + + err := mySettings.SetSetting("CustomMatchDisplayAttributesList", newAttributesListString) + if (err != nil) { + setErrorEncounteredPage(window, err, currentPage) + return + } + nextPage() + }) + + if (attributeType == "General"){ + + generalAttributeButtonsGrid.Add(attributeButton) + + } else if (attributeType == "Physical"){ + + physicalAttributeButtonsGrid.Add(attributeButton) + + } else if (attributeType == "Lifestyle"){ + + lifestyleAttributeButtonsGrid.Add(attributeButton) + + } else if (attributeType == "Mental"){ + + mentalAttributeButtonsGrid.Add(attributeButton) + + } else { + return errors.New("addAttributeButton called with invalid attributeType: " + attributeType) + } + + return nil + } + + generalAttributeNamesList := []string{ + "MatchScore", + "Username", + "Sexuality", + "Distance", + "HasMessagedMe", + "IHaveMessaged", + "HasRejectedMe", + "IsLiked", + "IsMyContact", + "SearchTermsCount", + "PrimaryLocationCountry", + } + + physicalAttributeNamesList := []string{ + "Age", + "Height", + "Sex", + "HairColor", + "HairTexture", + "EyeColor", + "SkinColor", + "RacialSimilarity", + "HasHIV", + "HasGenitalHerpes", + "BodyFat", + "BodyMuscle", + "23andMe_MaternalHaplogroup", + "23andMe_PaternalHaplogroup", + "23andMe_NeanderthalVariants", + "OffspringProbabilityOfAnyMonogenicDisease", + "EyeColorSimilarity", + "EyeColorGenesSimilarity", + "HairColorSimilarity", + "HairColorGenesSimilarity", + "SkinColorSimilarity", + "SkinColorGenesSimilarity", + "HairTextureSimilarity", + "HairTextureGenesSimilarity", + "FacialStructureGenesSimilarity", + "23andMe_AncestralSimilarity", + "23andMe_MaternalHaplogroupSimilarity", + "23andMe_PaternalHaplogroupSimilarity", + } + + lifestyleAttributeNamesList := []string{ + "Fame", + "WealthInGold", + "AlcoholFrequency", + "TobaccoFrequency", + "CannabisFrequency", + "FruitRating", + "VegetablesRating", + "NutsRating", + "GrainsRating", + "DairyRating", + "SeafoodRating", + "BeefRating", + "PorkRating", + "PoultryRating", + "EggsRating", + "BeansRating", + } + + mentalAttributeNamesList := []string{ + "GenderIdentity", + "PetsRating", + "DogsRating", + "CatsRating", + } + + for _, attributeName := range generalAttributeNamesList{ + + err = addAttributeButton(attributeName, "General") + if (err != nil) { return nil, err } + } + + for _, attributeName := range physicalAttributeNamesList{ + + err = addAttributeButton(attributeName, "Physical") + if (err != nil) { return nil, err } + } + + for _, attributeName := range lifestyleAttributeNamesList{ + + err = addAttributeButton(attributeName, "Lifestyle") + if (err != nil) { return nil, err } + } + + for _, attributeName := range mentalAttributeNamesList{ + + err = addAttributeButton(attributeName, "Mental") + if (err != nil) { return nil, err } + } + + + generalLabel := getBoldLabelCentered("General") + physicalLabel := getBoldLabelCentered("Physical") + lifestyleLabel := getBoldLabelCentered("Lifestyle") + mentalLabel := getBoldLabelCentered("Mental") + + generalAttributeButtonsGridCentered := getContainerCentered(generalAttributeButtonsGrid) + physicalAttributeButtonsGridCentered := getContainerCentered(physicalAttributeButtonsGrid) + lifestyleAttributeButtonsGridCentered := getContainerCentered(lifestyleAttributeButtonsGrid) + mentalAttributeButtonsGridCentered := getContainerCentered(mentalAttributeButtonsGrid) + + pageContent := container.NewVBox(generalLabel, widget.NewSeparator(), generalAttributeButtonsGridCentered, widget.NewSeparator(), physicalLabel, widget.NewSeparator(), physicalAttributeButtonsGridCentered, widget.NewSeparator(), lifestyleLabel, widget.NewSeparator(), lifestyleAttributeButtonsGridCentered, widget.NewSeparator(), mentalLabel, widget.NewSeparator(), mentalAttributeButtonsGridCentered) + + return pageContent, nil + } + + pageContent, err := getPageContent() + if (err != nil){ + setErrorEncounteredPage(window, err, previousPage) + return + } + + page := container.NewVBox(title, backButton, widget.NewSeparator(), description, widget.NewSeparator(), pageContent) + + setPageContent(page, window) +} + + +func setConfigureMatchScorePage(window fyne.Window, previousPage func()){ + + setLoadingScreen(window, "Configure Match Score", "Loading Configure Match Score page...") + + currentPage := func(){setConfigureMatchScorePage(window, previousPage)} + + title := getPageTitleCentered("Configure Match Score") + + backButton := getBackButtonCentered(previousPage) + + description1 := getLabelCentered("Customize how a user's match score is calculated.") + description2 := getLabelCentered("Each desire's points will be added to a user's match score if they fulfills the desire.") + description3 := getLabelCentered("Increase the Point value for desires that are more important to you.") + + viewMatchScoreDistributionButton := getWidgetCentered(widget.NewButtonWithIcon("View Match Score Distribution", theme.VisibilityIcon(), func(){ + setViewUserAttributeStatisticsPage_BarChart(window, "Mate", "MatchScore", "Number Of Users", " users", true, false, false, nil, false, nil, nil, currentPage) + })) + + //TODO: Split into multiple pages and add Navigation buttons + + getDesirePointsGrid := func()(*fyne.Container, error){ + + desireNameLabel := getItalicLabelCentered("Desire Name") + pointsLabel := getItalicLabelCentered("Points") + emptyLabel := widget.NewLabel("") + + desireTitleColumn := container.NewVBox(desireNameLabel, widget.NewSeparator()) + desirePointsColumn := container.NewVBox(pointsLabel, widget.NewSeparator()) + editPointsButtonsColumn := container.NewVBox(emptyLabel, widget.NewSeparator()) + + allMyDesiresList := myMateDesires.GetAllMyDesiresList(false) + + for _, desireName := range allMyDesiresList{ + + desireTitle, err := mateDesires.GetDesireTitleFromDesireName(desireName) + if (err != nil) { return nil, err } + + desireTitleTranslated := translate(desireTitle) + + desireTitleLabel := getBoldLabelCentered(desireTitleTranslated) + + desirePoints, err := myMatchScore.GetMyMatchScoreDesirePoints(desireName) + if (err != nil) { return nil, err } + + desirePointsString := helpers.ConvertIntToString(desirePoints) + desirePointsLabel := getBoldLabelCentered(desirePointsString) + + editPointsButton := widget.NewButtonWithIcon("", theme.DocumentCreateIcon(), func(){ + setEditMatchScoreDesirePointsPage(window, desireName, currentPage) + }) + + desireTitleColumn.Add(desireTitleLabel) + desirePointsColumn.Add(desirePointsLabel) + editPointsButtonsColumn.Add(editPointsButton) + + desireTitleColumn.Add(widget.NewSeparator()) + desirePointsColumn.Add(widget.NewSeparator()) + editPointsButtonsColumn.Add(widget.NewSeparator()) + } + + desirePointsGrid := container.NewHBox(layout.NewSpacer(), desireTitleColumn, desirePointsColumn, editPointsButtonsColumn, layout.NewSpacer()) + return desirePointsGrid, nil + } + + desirePointsGrid, err := getDesirePointsGrid() + if (err != nil) { + setErrorEncounteredPage(window, err, previousPage) + return + } + + page := container.NewVBox(title, backButton, widget.NewSeparator(), description1, description2, description3, widget.NewSeparator(), viewMatchScoreDistributionButton, widget.NewSeparator(), desirePointsGrid) + + setPageContent(page, window) +} + + +func setEditMatchScoreDesirePointsPage(window fyne.Window, desireName string, previousPage func()){ + + currentPage := func(){setEditMatchScoreDesirePointsPage(window, desireName, previousPage)} + + title := getPageTitleCentered("Edit Attribute Points") + + backButton := getBackButtonCentered(previousPage) + + description1 := getLabelCentered("Edit your desire's points.") + description2 := getLabelCentered("These will be added to a user's match score if they fulfill the desire.") + + desireTitle, err := mateDesires.GetDesireTitleFromDesireName(desireName) + if (err != nil) { + setErrorEncounteredPage(window, err, previousPage) + return + } + + desireNameLabel := widget.NewLabel("Desire Name:") + desireTitleLabel := getBoldLabel(desireTitle) + desireTitleRow := container.NewHBox(layout.NewSpacer(), desireNameLabel, desireTitleLabel, layout.NewSpacer()) + + desirePoints, err := myMatchScore.GetMyMatchScoreDesirePoints(desireName) + if (err != nil) { + setErrorEncounteredPage(window, err, previousPage) + return + } + + desirePointsString := helpers.ConvertIntToString(desirePoints) + + desirePointsTitle := getBoldLabelCentered("Desire Points:") + + desirePointsLabel := getBoldLabelCentered(desirePointsString) + + getDecreasePointsButton := func()fyne.Widget{ + + if (desirePoints > 1){ + decreaseButton := widget.NewButton("-", func(){ + err := myMatchScore.SetMyMatchScoreDesirePoints(desireName, desirePoints-1) + if (err != nil) { + setErrorEncounteredPage(window, err, currentPage) + return + } + currentPage() + }) + return decreaseButton + } + + emptyButton := widget.NewButton("", nil) + + return emptyButton + } + + getIncreasePointsButton := func()fyne.Widget{ + + if (desirePoints < 100){ + increaseButton := widget.NewButton("+", func(){ + err := myMatchScore.SetMyMatchScoreDesirePoints(desireName, desirePoints+1) + if (err != nil) { + setErrorEncounteredPage(window, err, currentPage) + return + } + currentPage() + }) + return increaseButton + } + emptyButton := widget.NewButton("", nil) + return emptyButton + } + + decreasePointsButton := getDecreasePointsButton() + increasePointsButton := getIncreasePointsButton() + + decreaseIncreaseButtons := getContainerCentered(container.NewGridWithColumns(2, decreasePointsButton, increasePointsButton)) + + page := container.NewVBox(title, backButton, widget.NewSeparator(), description1, description2, widget.NewSeparator(), desireTitleRow, widget.NewSeparator(), desirePointsTitle, desirePointsLabel, decreaseIncreaseButtons) + + setPageContent(page, window) +} + + +// This page will explain a user's match score by describing which desires the user passes +func setViewUserMatchScoreBreakdownPage(window fyne.Window, userIdentityHash [16]byte, previousPage func()){ + + userIdentityType, err := identity.GetIdentityTypeFromIdentityHash(userIdentityHash) + if (err != nil){ + userIdentityHashHex := encoding.EncodeBytesToHexString(userIdentityHash[:]) + setErrorEncounteredPage(window, errors.New("setViewUserMatchScoreBreakdownPage called with invalid userIdentityHash: " + userIdentityHashHex), previousPage) + return + } + if (userIdentityType != "Mate"){ + setErrorEncounteredPage(window, errors.New("setViewUserMatchScoreBreakdownPage called with non-mate identity hash."), previousPage) + return + } + + currentPage := func(){setViewUserMatchScoreBreakdownPage(window, userIdentityHash, previousPage)} + + title := getPageTitleCentered("User Match Score Breakdown") + + backButton := getBackButtonCentered(previousPage) + + appNetworkType, err := getAppNetworkType.GetAppNetworkType() + if (err != nil){ + setErrorEncounteredPage(window, err, previousPage) + return + } + + profileExists, _, getAnyUserProfileAttributeFunction, err := viewableProfiles.GetRetrieveAnyNewestViewableUserProfileAttributeFunction(userIdentityHash, appNetworkType, true, false, true) + if (err != nil) { + setErrorEncounteredPage(window, err, previousPage) + return + } + if (profileExists == false) { + // Profile must have been deleted or it is not viewable + description1 := getBoldLabelCentered("User's profile is not found.") + description2 := getLabelCentered("Thus, the user's match score breakdown cannot be shown.") + description3 := getLabelCentered("Their profile was either banned or deleted.") + description4 := getLabelCentered("Deletion should only happen if the user is no longer a match.") + + page := container.NewVBox(title, backButton, widget.NewSeparator(), description1, description2, description3, description4) + + setPageContent(page, window) + return + } + + exists, _, userIsDisabled, err := getAnyUserProfileAttributeFunction("Disabled") + if (err != nil) { + setErrorEncounteredPage(window, err, previousPage) + return + } + if (exists == true && userIsDisabled == "Yes"){ + // User's newest viewable profile is disabled. + description1 := getBoldLabelCentered("User's profile is disabled.") + description2 := getLabelCentered("Thus, the user's match score breakdown cannot be shown.") + + page := container.NewVBox(title, backButton, widget.NewSeparator(), description1, description2) + + setPageContent(page, window) + return + } + + // This bool will track if we have any desires at all + anyPreferenceExists := false + + userMatchScore := 0 + + // This map stores the number of points we added for each desire + // Map Structure: Desire name -> Points we added + desirePointsMap := make(map[string]int) + + // This is a list of desires which the user fulfills + fulfilledDesiresList := make([]string, 0) + + // This is a list of desires which the user does not fulfill + unfulfilledDesiresList := make([]string, 0) + + // This is a list of desires for which we do not know if the user fulfills our preference, not caused by them not responding + unknownStatusDesiresList := make([]string, 0) + + // This is a list of desires for which the user has no response + noResponseExistsDesiresList := make([]string, 0) + + // This is a list of desires which we have no preference for + noPreferenceExistsDesiresList := make([]string, 0) + + allMyDesiresList := myMateDesires.GetAllMyDesiresList(false) + + for _, desireName := range allMyDesiresList{ + + myDesireExists, statusIsKnown, userFulfillsDesire, err := myMateDesires.CheckIfMateProfileFulfillsMyDesire(desireName, getAnyUserProfileAttributeFunction) + if (err != nil){ + setErrorEncounteredPage(window, err, previousPage) + return + } + if (myDesireExists == false){ + + noPreferenceExistsDesiresList = append(noPreferenceExistsDesiresList, desireName) + continue + } + + anyPreferenceExists = true + + if (statusIsKnown == false){ + + noResponseIsPossible, _ := mateDesires.CheckIfDesireAllowsRequireResponse(desireName) + if (noResponseIsPossible == false){ + // Whether or not the user responded does not effect the desire's status is known status + // An example is OffspringProbabilityOfAnyMonogenicDisease, which requires information from us *and* the user + unknownStatusDesiresList = append(unknownStatusDesiresList, desireName) + continue + } + noResponseExistsDesiresList = append(noResponseExistsDesiresList, desireName) + continue + } + + if (userFulfillsDesire == false){ + + unfulfilledDesiresList = append(unfulfilledDesiresList, desireName) + continue + } + + fulfilledDesiresList = append(fulfilledDesiresList, desireName) + + pointsToAdd, err := myMatchScore.GetMyMatchScoreDesirePoints(desireName) + if (err != nil){ + setErrorEncounteredPage(window, err, previousPage) + return + } + + desirePointsMap[desireName] += pointsToAdd + + userMatchScore += pointsToAdd + } + + if (anyPreferenceExists == false){ + + description1 := getBoldLabelCentered("You have not added any desires.") + description2 := getLabelCentered("Thus, the match score for all users will be 0.") + description3 := getLabelCentered("Visit the Desires page to enter some desires.") + + page := container.NewVBox(title, backButton, widget.NewSeparator(), description1, description2, description3) + + setPageContent(page, window) + return + } + + matchScoreTitle := widget.NewLabel("User Match Score:") + + matchScoreString := helpers.ConvertIntToString(userMatchScore) + + matchScoreLabel := getBoldLabel(matchScoreString) + + matchScoreRow := container.NewHBox(layout.NewSpacer(), matchScoreTitle, matchScoreLabel, layout.NewSpacer()) + + getMatchScoreBreakdownGrid := func()(*fyne.Container, error){ + + desireNameHeader := getItalicLabelCentered("Desire Name") + userFulfillsDesireHeader := getItalicLabelCentered("User Fulfills Desire") + pointsAddedHeader := getItalicLabelCentered("Points Added") + + desireNameColumn := container.NewVBox(desireNameHeader, widget.NewSeparator()) + userFulfillsDesireColumn := container.NewVBox(userFulfillsDesireHeader, widget.NewSeparator()) + pointsAddedColumn := container.NewVBox(pointsAddedHeader, widget.NewSeparator()) + + for _, desireName := range fulfilledDesiresList{ + + desireTitle, err := mateDesires.GetDesireTitleFromDesireName(desireName) + if (err != nil) { return nil, err } + + desireTitleLabel := getBoldLabelCentered(desireTitle) + + userFulfillsDesireLabel := getBoldLabelCentered(translate("Yes")) + + pointsAdded, exists := desirePointsMap[desireName] + if (exists == false){ + return nil, errors.New("desirePointsMap missing desire: " + desireName) + } + + pointsAddedString := helpers.ConvertIntToString(pointsAdded) + pointsAddedLabel := getBoldLabelCentered("+" + pointsAddedString) + + desireNameColumn.Add(desireTitleLabel) + userFulfillsDesireColumn.Add(userFulfillsDesireLabel) + pointsAddedColumn.Add(pointsAddedLabel) + + desireNameColumn.Add(widget.NewSeparator()) + userFulfillsDesireColumn.Add(widget.NewSeparator()) + pointsAddedColumn.Add(widget.NewSeparator()) + } + + for _, desireName := range unfulfilledDesiresList{ + + desireTitle, err := mateDesires.GetDesireTitleFromDesireName(desireName) + if (err != nil) { return nil, err } + + desireTitleLabel := getBoldLabelCentered(desireTitle) + + userFulfillsDesireLabel := getBoldLabelCentered(translate("No")) + + pointsAddedLabel := getBoldLabelCentered("0") + + desireNameColumn.Add(desireTitleLabel) + userFulfillsDesireColumn.Add(userFulfillsDesireLabel) + pointsAddedColumn.Add(pointsAddedLabel) + + desireNameColumn.Add(widget.NewSeparator()) + userFulfillsDesireColumn.Add(widget.NewSeparator()) + pointsAddedColumn.Add(widget.NewSeparator()) + } + + for _, desireName := range unknownStatusDesiresList{ + + desireTitle, err := mateDesires.GetDesireTitleFromDesireName(desireName) + if (err != nil) { return nil, err } + + desireTitleLabel := getBoldLabelCentered(desireTitle) + + userFulfillsDesireLabel := getItalicLabelCentered(translate("Unknown")) + + pointsAddedLabel := getBoldLabelCentered("0") + + desireNameColumn.Add(desireTitleLabel) + userFulfillsDesireColumn.Add(userFulfillsDesireLabel) + pointsAddedColumn.Add(pointsAddedLabel) + + desireNameColumn.Add(widget.NewSeparator()) + userFulfillsDesireColumn.Add(widget.NewSeparator()) + pointsAddedColumn.Add(widget.NewSeparator()) + } + + for _, desireName := range noResponseExistsDesiresList{ + + desireTitle, err := mateDesires.GetDesireTitleFromDesireName(desireName) + if (err != nil) { return nil, err } + + desireTitleLabel := getBoldLabelCentered(desireTitle) + + userFulfillsDesireLabel := getItalicLabelCentered(translate("No Response")) + + pointsAddedLabel := getBoldLabelCentered("0") + + desireNameColumn.Add(desireTitleLabel) + userFulfillsDesireColumn.Add(userFulfillsDesireLabel) + pointsAddedColumn.Add(pointsAddedLabel) + + desireNameColumn.Add(widget.NewSeparator()) + userFulfillsDesireColumn.Add(widget.NewSeparator()) + pointsAddedColumn.Add(widget.NewSeparator()) + } + + matchScoreBreakdownGrid := container.NewHBox(layout.NewSpacer(), desireNameColumn, userFulfillsDesireColumn, pointsAddedColumn, layout.NewSpacer()) + + return matchScoreBreakdownGrid, nil + } + + matchScoreBreakdownGrid, err := getMatchScoreBreakdownGrid() + if (err != nil){ + setErrorEncounteredPage(window, err, previousPage) + return + } + + page := container.NewVBox(title, backButton, widget.NewSeparator(), matchScoreRow, widget.NewSeparator(), matchScoreBreakdownGrid) + + numberOfNoPreferenceDesires := len(noPreferenceExistsDesiresList) + + if (numberOfNoPreferenceDesires != 0){ + + numberOfNoPreferenceDesiresString := helpers.ConvertIntToString(numberOfNoPreferenceDesires) + + description1 := getLabelCentered("You have " + numberOfNoPreferenceDesiresString + " desires with no preference.") + description2 := getLabelCentered("These desires will have no impact on match scores.") + + viewMyNoPreferenceDesiresButton := getWidgetCentered(widget.NewButtonWithIcon("View My No Preference Desires", theme.VisibilityIcon(), func(){ + setViewMyNoPreferenceDesiresPage(window, noPreferenceExistsDesiresList, currentPage) + })) + + page.Add(widget.NewSeparator()) + page.Add(description1) + page.Add(description2) + page.Add(viewMyNoPreferenceDesiresButton) + } + + setPageContent(page, window) +} + + +func setViewMyNoPreferenceDesiresPage(window fyne.Window, myNoPreferenceDesiresList []string, previousPage func()){ + + title := getPageTitleCentered("My No Preference Desires") + + backButton := getBackButtonCentered(previousPage) + + description := getLabelCentered("Below is a list of your desires for which you have no provided no preference.") + + page := container.NewVBox(title, backButton, widget.NewSeparator(), description, widget.NewSeparator()) + + for _, desireName := range myNoPreferenceDesiresList{ + + desireTitle, err := mateDesires.GetDesireTitleFromDesireName(desireName) + if (err != nil) { + setErrorEncounteredPage(window, err, previousPage) + return + } + + desireTitleLabel := getBoldLabelCentered(desireTitle) + + page.Add(desireTitleLabel) + } + + setPageContent(page, window) +} + + diff --git a/gui/moderatorGui.go b/gui/moderatorGui.go new file mode 100644 index 0000000..232ad58 --- /dev/null +++ b/gui/moderatorGui.go @@ -0,0 +1,4855 @@ +package gui + +// moderatorGui.go implements pages for moderators to review and browse content and identities, and to manage their own reviews. + +//TODO: Add moderate full profiles page +//TODO: Add page to view my hidden content + +import "fyne.io/fyne/v2" +import "fyne.io/fyne/v2/canvas" +import "fyne.io/fyne/v2/container" +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/appMemory" +import "seekia/internal/badgerDatabase" +import "seekia/internal/contentMetadata" +import "seekia/internal/encoding" +import "seekia/internal/helpers" +import "seekia/internal/identity" +import "seekia/internal/imagery" +import "seekia/internal/messaging/chatMessageStorage" +import "seekia/internal/moderation/bannedModeratorConsensus" +import "seekia/internal/moderation/moderatorRanking" +import "seekia/internal/moderation/myHiddenContent" +import "seekia/internal/moderation/myIdentityScore" +import "seekia/internal/moderation/myReviews" +import "seekia/internal/moderation/mySkippedContent" +import "seekia/internal/moderation/myUnreviewed" +import "seekia/internal/moderation/readReviews" +import "seekia/internal/moderation/reportStorage" +import "seekia/internal/moderation/reviewStorage" +import "seekia/internal/moderation/verifiedVerdict" +import "seekia/internal/myIdentity" +import "seekia/internal/mySettings" +import "seekia/internal/network/appNetworkType/getAppNetworkType" +import "seekia/internal/network/backgroundDownloads" +import "seekia/internal/profiles/attributeDisplay" +import "seekia/internal/profiles/calculatedAttributes" +import "seekia/internal/profiles/myProfileStatus" +import "seekia/internal/profiles/profileFormat" +import "seekia/internal/profiles/profileStorage" +import "seekia/internal/profiles/readProfiles" + +import "slices" +import "bytes" +import "strings" +import "time" +import "errors" + +func setModeratePage(window fyne.Window, previousPageExists bool, previousPage func()){ + + appMemory.SetMemoryEntry("CurrentViewedPage", "Moderate") + + currentPage := func(){setModeratePage(window, previousPageExists, previousPage)} + homePage := func(){setHomePage(window)} + + title := getPageTitleCentered("Moderate") + + page := container.NewVBox(title) + + if (previousPageExists == true){ + + backButton := getBackButtonCentered(previousPage) + page.Add(backButton) + } + + page.Add(widget.NewSeparator()) + + description := getLabelCentered("Be a Seekia moderator.") + page.Add(description) + page.Add(widget.NewSeparator()) + + rulesIcon, err := getFyneImageIcon("Info") + if (err != nil){ + setErrorEncounteredPage(window, err, homePage) + return + } + + profileIcon, err := getFyneImageIcon("Profile") + if (err != nil){ + setErrorEncounteredPage(window, err, homePage) + return + } + + settingsIcon, err := getFyneImageIcon("Settings") + if (err != nil){ + setErrorEncounteredPage(window, err, homePage) + return + } + + moderatorsIcon, err := getFyneImageIcon("Users") + if (err != nil){ + setErrorEncounteredPage(window, err, homePage) + return + } + + statsIcon, err := getFyneImageIcon("Stats") + if (err != nil){ + setErrorEncounteredPage(window, err, homePage) + return + } + + contentIcon, err := getFyneImageIcon("Choice") + if (err != nil){ + setErrorEncounteredPage(window, err, homePage) + return + } + + manageProfileButton := widget.NewButton("Profile", func(){ + setProfilePage(window, true, "Moderator", true, currentPage) + }) + manageProfileButtonWithIcon := container.NewGridWithColumns(1, profileIcon, manageProfileButton) + + moderatorSettingsButton := widget.NewButton("Settings", func(){ + setModeratorSettingsPage(window, currentPage) + }) + moderatorSettingsButtonWithIcon := container.NewGridWithColumns(1, settingsIcon, moderatorSettingsButton) + + rulesButton := widget.NewButton("Rules", func(){ + setViewSeekiaRulesPage(window, currentPage) + //TODO: Create a rules page for moderators, explaining how to be a moderator + }) + rulesButtonWithIcon := container.NewGridWithColumns(1, rulesIcon, rulesButton) + + moderatorsButton := widget.NewButton("Mods", func(){ + setViewModeratorsPage(window, currentPage) + }) + moderatorsButtonWithIcon := container.NewGridWithColumns(1, moderatorsIcon, moderatorsButton) + + statsButton := widget.NewButton("Stats", func(){ + // TODO: Page to show statistics about the user's moderator reviews, and moderation statistics of the entire network + // Example: Unreviewed profiles, number of banned profiles, and more + showUnderConstructionDialog(window) + }) + statsButtonWithIcon := container.NewGridWithColumns(1, statsIcon, statsButton) + + contentButton := widget.NewButton("Content", func(){ + setBrowseContentPage(window, currentPage) + }) + contentButtonWithIcon := container.NewGridWithColumns(1, contentIcon, contentButton) + + buttonsRow := getContainerCentered(container.NewGridWithRows(1, rulesButtonWithIcon, manageProfileButtonWithIcon, moderatorSettingsButtonWithIcon, moderatorsButtonWithIcon, statsButtonWithIcon, contentButtonWithIcon)) + + page.Add(buttonsRow) + page.Add(widget.NewSeparator()) + + exists, moderatorModeStatus, err := mySettings.GetSetting("ModeratorModeOnOffStatus") + if (err != nil){ + setErrorEncounteredPage(window, err, homePage) + return + } + if (exists == false || moderatorModeStatus == "Off"){ + + description1 := getBoldLabelCentered("You must enable moderator mode to moderate.") + description2 := getLabelCentered("Enable it on the Settings page.") + + page.Add(description1) + page.Add(description2) + + setPageContent(page, window) + return + } + + identityExists, myIdentityHash, err := myIdentity.GetMyIdentityHash("Moderator") + if (err != nil){ + setErrorEncounteredPage(window, err, homePage) + return + } + if (identityExists == false){ + + description1 := getBoldLabelCentered("Your moderator identity does not exist.") + description2 := getLabelCentered("You must choose an identity.") + description3 := getLabelCentered("This is how users of Seekia will identify you.") + + chooseIdentityButton := getWidgetCentered(widget.NewButtonWithIcon("Choose Identity", theme.NavigateNextIcon(), func(){ + setChooseNewIdentityHashPage(window, "Moderator", currentPage, currentPage) + })) + + page.Add(description1) + page.Add(description2) + page.Add(description3) + page.Add(chooseIdentityButton) + + setPageContent(page, window) + return + } + + appNetworkType, err := getAppNetworkType.GetAppNetworkType() + if (err != nil) { + setErrorEncounteredPage(window, err, previousPage) + return + } + + identityExists, iAmOnlineStatus, err := myProfileStatus.GetMyProfileIsActiveStatus(myIdentityHash, appNetworkType) + if (err != nil){ + setErrorEncounteredPage(window, err, homePage) + return + } + if (identityExists == false) { + setErrorEncounteredPage(window, errors.New("My identity not found after being found already."), homePage) + return + } + if (iAmOnlineStatus == false){ + + description1 := getBoldLabelCentered("Your Moderator profile is offline.") + description2 := getLabelCentered("Broadcast your profile on the Profile - Broadcast page.") + + page.Add(description1) + page.Add(description2) + + setPageContent(page, window) + return + } + + //TODO: Check for moderation parameters + + moderateDescriptionA := getBoldLabelCentered("You are ready to moderate.") + moderateDescriptionB := getLabelCentered("Choose the type of content to moderate.") + page.Add(moderateDescriptionA) + page.Add(moderateDescriptionB) + page.Add(widget.NewSeparator()) + + mateIcon, err := getFyneImageIcon("Mate") + if (err != nil){ + setErrorEncounteredPage(window, err, homePage) + return + } + mateButton := widget.NewButton("Mate Profiles", func(){ + setChooseProfileContentToModeratePage(window, "Mate", currentPage) + }) + mateColumn := container.NewGridWithColumns(1, mateIcon, mateButton) + + hostIcon, err := getFyneImageIcon("Host") + if (err != nil){ + setErrorEncounteredPage(window, err, homePage) + return + } + hostButton := widget.NewButton("Host Profiles", func(){ + setChooseProfileContentToModeratePage(window, "Host", currentPage) + }) + hostColumn := container.NewGridWithColumns(1, hostIcon, hostButton) + + moderatorIcon, err := getFyneImageIcon("Moderate") + if (err != nil){ + setErrorEncounteredPage(window, err, homePage) + return + } + moderatorButton := widget.NewButton("Moderator Profiles", func(){ + setChooseProfileContentToModeratePage(window, "Moderator", currentPage) + }) + moderatorColumn := container.NewGridWithColumns(1, moderatorIcon, moderatorButton) + + profilesRow := getContainerCentered(container.NewGridWithRows(1, mateColumn, hostColumn, moderatorColumn)) + page.Add(profilesRow) + + textMessagesImage, err := getFyneImageIcon("InspectText") + if (err != nil){ + setErrorEncounteredPage(window, err, homePage) + return + } + textMessagesButton := widget.NewButton("Text Messages", func(){ + setModerateMessagesPage(window, "Text", false, [26]byte{}, currentPage) + }) + textMessagesColumn := container.NewGridWithColumns(1, textMessagesImage, textMessagesButton) + + imageMessagesIcon, err := getFyneImageIcon("Photo") + if (err != nil){ + setErrorEncounteredPage(window, err, homePage) + return + } + imageMessagesButton := widget.NewButton("Image Messages", func(){ + setModerateMessagesPage(window, "Image", false, [26]byte{}, currentPage) + }) + imageMessagesColumn := container.NewGridWithColumns(1, imageMessagesIcon, imageMessagesButton) + + messageButtonsRow := getContainerCentered(container.NewGridWithRows(1, textMessagesColumn, imageMessagesColumn)) + page.Add(messageButtonsRow) + + identitiesIcon, err := getFyneImageIcon("Profile") + if (err != nil){ + setErrorEncounteredPage(window, err, homePage) + return + } + + identitiesButton := widget.NewButton("Identities", func(){ + setModerateIdentitiesPage(window, currentPage) + }) + + identitiesColumn := getContainerCentered(container.NewGridWithColumns(1, identitiesIcon, identitiesButton)) + page.Add(identitiesColumn) + + setPageContent(page, window) +} + + +func setChooseProfileContentToModeratePage(window fyne.Window, profileType string, previousPage func()){ + + setLoadingScreen(window, "Moderate " + profileType + " Profiles", "Loading profiles...") + + currentPage := func(){setChooseProfileContentToModeratePage(window, profileType, previousPage)} + + title := getPageTitleCentered("Moderate " + profileType + " Profiles") + + backButton := getBackButtonCentered(previousPage) + + //TODO: Check if we are moderating profiles of this profileType + + description := getLabelCentered("Choose a profile attribute to moderate (under construction).") + + appNetworkType, err := getAppNetworkType.GetAppNetworkType() + if (err != nil) { + setErrorEncounteredPage(window, err, previousPage) + return + } + + moderateFullProfilesButton := getWidgetCentered(widget.NewButton("Moderate Full Profiles", func(){ + //TODO: A page to moderate full profiles, which have at least 1 full profile ban. + // This page will show profiles whose full profile has been banned. + // This is necessary, because profiles which have been fully banned must be fully approved to undo the effect of the ban. + // Approving all of the profile's attributes individually is cumbersome, and takes up more space in the form of reviews. + // Also, viewing a full profile is necessary when the full profile has been banned, because + // whatever is unruleful about the profile can't be isolated to a single attribute. + // If it could be isolated to a single attribute, the moderator would have banned that attribute, not the full profile + // The exception is with malicious moderators, who could abuse this feature to waste + // the time of moderators by banning many full profiles. + showUnderConstructionDialog(window) + })) + + nameLabel := getItalicLabelCentered("Name") + isCanonicalLabel := getItalicLabelCentered("Is Canonical") + anyUnreviewedLabel := getItalicLabelCentered("Any Unreviewed?") + emptyLabel := widget.NewLabel("") + + attributeNamesColumn := container.NewVBox(nameLabel, widget.NewSeparator()) + attributeIsCanonicalColumn := container.NewVBox(isCanonicalLabel, widget.NewSeparator()) + anyUnreviewedExistColumn := container.NewVBox(anyUnreviewedLabel, widget.NewSeparator()) + viewAttributeButtonsColumn := container.NewVBox(emptyLabel, widget.NewSeparator()) + + addAttributeRow := func(attributeName string, attributeIsCanonical bool, attributeIdentifier int)error{ + + attributeIsCanonicalString := helpers.ConvertBoolToYesOrNoString(attributeIsCanonical) + + anyExist, _, _, _, err := myUnreviewed.GetMyHighestPriorityUnreviewedProfileAttributeHash(profileType, attributeIdentifier, appNetworkType) + if (err != nil) { return err } + + getAnyUnreviewedExistLabel := func()*fyne.Container{ + + if (anyExist == false){ + + noLabel := getLabelCentered(translate("No")) + return noLabel + } + + yesLabel := getBoldLabelCentered(translate("Yes")) + + return yesLabel + } + + anyUnreviewedExistLabel := getAnyUnreviewedExistLabel() + + attributeNameLabel := getBoldLabelCentered(attributeName) + attributeIsCanonicalLabel := getBoldLabelCentered(attributeIsCanonicalString) + viewAttributeButton := widget.NewButtonWithIcon("", theme.NavigateNextIcon(), func(){ + setModerateProfileAttributesPage(window, profileType, attributeIdentifier, false, [27]byte{}, [28]byte{}, [16]byte{}, currentPage) + }) + + attributeNamesColumn.Add(attributeNameLabel) + attributeIsCanonicalColumn.Add(attributeIsCanonicalLabel) + anyUnreviewedExistColumn.Add(anyUnreviewedExistLabel) + viewAttributeButtonsColumn.Add(viewAttributeButton) + + attributeNamesColumn.Add(widget.NewSeparator()) + attributeIsCanonicalColumn.Add(widget.NewSeparator()) + anyUnreviewedExistColumn.Add(widget.NewSeparator()) + viewAttributeButtonsColumn.Add(widget.NewSeparator()) + + return nil + } + + addAttributeRows := func()error{ + + if (profileType == "Mate"){ + err := addAttributeRow("Photos", false, 18) + if (err != nil){ return err } + } + + err := addAttributeRow("Username", false, 10) + if (err != nil) { return err } + + err = addAttributeRow("Description", false, 9) + if (err != nil) { return err } + + if (profileType == "Mate"){ + err := addAttributeRow("Age", true, 8) + if (err != nil) { return err } + + err = addAttributeRow("Sex", true, 7) + if (err != nil) { return err } + + err = addAttributeRow("Height", true, 6) + if (err != nil) { return err } + } + + //TODO: Add more attributes + + return nil + } + + err = addAttributeRows() + if (err != nil){ + setErrorEncounteredPage(window, err, previousPage) + return + } + + isCanonicalHelpButton := widget.NewButtonWithIcon("", theme.QuestionIcon(), func(){ + setAttributeIsCanonicalExplainerPage(window, currentPage) + }) + anyUnreviewedExistHelpButton := widget.NewButtonWithIcon("", theme.QuestionIcon(), func(){ + setAnyUnreviewedAttributesExistExplainerPage(window, currentPage) + }) + + attributeIsCanonicalColumn.Add(isCanonicalHelpButton) + anyUnreviewedExistColumn.Add(anyUnreviewedExistHelpButton) + + attributeGrid := container.NewHBox(layout.NewSpacer(), attributeNamesColumn, attributeIsCanonicalColumn, anyUnreviewedExistColumn, viewAttributeButtonsColumn, layout.NewSpacer()) + + page := container.NewVBox(title, backButton, widget.NewSeparator(), description, widget.NewSeparator(), moderateFullProfilesButton, widget.NewSeparator(), attributeGrid) + + setPageContent(page, window) +} + +// This page is used by a moderator to browse their unreviewed profile attributes, review the content, and submit a verdict +// It provides skip and hide buttons, and serve new attributes to review to the moderator +func setModerateProfileAttributesPage(window fyne.Window, profileType string, attributeIdentifier int, attributeProvided bool, attributeHash [27]byte, attributeProfileHash [28]byte, attributeAuthorIdentityHash [16]byte, previousPage func()){ + + setLoadingScreen(window, "Moderating Attributes", "Loading attribute...") + + currentPage := func(){setModerateProfileAttributesPage(window, profileType, attributeIdentifier, attributeProvided, attributeHash, attributeProfileHash, attributeAuthorIdentityHash, previousPage)} + + refreshPageWithNewContent := func(){setModerateProfileAttributesPage(window, profileType, attributeIdentifier, false, [27]byte{}, [28]byte{}, [16]byte{}, previousPage)} + + title := getPageTitleCentered("Moderating Attributes") + + backButton := getBackButtonCentered(previousPage) + + appNetworkType, err := getAppNetworkType.GetAppNetworkType() + if (err != nil) { + setErrorEncounteredPage(window, err, previousPage) + return + } + + if (attributeProvided == false){ + + attributeExists, newAttributeHash, newAttributeProfileHash, newAttributeAuthorIdentityHash, err := myUnreviewed.GetMyHighestPriorityUnreviewedProfileAttributeHash(profileType, attributeIdentifier, appNetworkType) + if (err != nil){ + setErrorEncounteredPage(window, err, previousPage) + return + } + + if (attributeExists == false){ + + attributeName, err := profileFormat.GetAttributeNameFromAttributeIdentifier(attributeIdentifier) + if (err != nil){ + setErrorEncounteredPage(window, err, previousPage) + return + } + + description1 := getBoldLabelCentered("No " + attributeName + " attributes remaining to moderate.") + description2 := getLabelCentered("Check back later for new attributes to review.") + //TODO: Add check for if space is full, and add description to say to increase space allowed for moderation content + + page := container.NewVBox(title, backButton, widget.NewSeparator(), description1, description2) + + setPageContent(page, window) + return + } + + setModerateProfileAttributesPage(window, profileType, attributeIdentifier, true, newAttributeHash, newAttributeProfileHash, newAttributeAuthorIdentityHash, previousPage) + return + } + + viewAttributeDetailsButton := getWidgetCentered(widget.NewButtonWithIcon("View Attribute Details", theme.VisibilityIcon(), func(){ + setViewAttributeModerationDetailsPage(window, attributeHash, currentPage) + })) + + profileExists, profileBytes, err := profileStorage.GetStoredProfile(attributeProfileHash) + if (err != nil) { + setErrorEncounteredPage(window, err, previousPage) + return + } + if (profileExists == false){ + // Profile must have been deleted after myUnreviewed found it + // We will refresh page and show loading screen so we can tell if this is happening + setLoadingScreen(window, "Moderating Attributes", "Trying to find attribute...") + + time.Sleep(time.Second) + + refreshPageWithNewContent() + return + } + + attributeName, err := profileFormat.GetAttributeNameFromAttributeIdentifier(attributeIdentifier) + if (err != nil) { + setErrorEncounteredPage(window, err, previousPage) + return + } + + attributeTitle, _, _, _, _, err := attributeDisplay.GetProfileAttributeDisplayInfo(attributeName) + if (err != nil){ + setErrorEncounteredPage(window, err, previousPage) + return + } + + attributeNameLabel := widget.NewLabel("Attribute Name:") + attributeTitleLabel := getBoldLabel(attributeTitle) + attributeTitleRow := container.NewHBox(layout.NewSpacer(), attributeNameLabel, attributeTitleLabel, layout.NewSpacer()) + + attributeDisplayContainer, err := getProfileAttributeDisplayForModeration(window, attributeName, attributeIdentifier, profileBytes, currentPage) + if (err != nil) { + setErrorEncounteredPage(window, err, previousPage) + return + } + + appNetworkTypeString := helpers.ConvertByteToString(appNetworkType) + attributeHashHex := encoding.EncodeBytesToHexString(attributeHash[:]) + + submitAttributeReview := func(verdict string, reasonExists bool, reason string)error{ + + newReviewMap := map[string]string{ + "NetworkType": appNetworkTypeString, + "ReviewedHash": attributeHashHex, + "Verdict": verdict, + } + + if (reasonExists == true){ + newReviewMap["Reason"] = reason + } + + myIdentityExists, err := myReviews.CreateAndBroadcastMyReview(newReviewMap) + if (err != nil) { return err } + if (myIdentityExists == false) { + return errors.New("My moderator identity not found when trying to submit review.") + } + + return nil + } + + submitBanIdentityReview := func(reasonExists bool, reason string)error{ + + attributeAuthorString, _, err := identity.EncodeIdentityHashBytesToString(attributeAuthorIdentityHash) + if (err != nil) { return err } + + attributeProfileHashHex := encoding.EncodeBytesToHexString(attributeProfileHash[:]) + + identityReviewMap := map[string]string{ + "NetworkType": appNetworkTypeString, + "ReviewedHash": attributeAuthorString, + "Verdict": "Ban", + "ErrantAttributes": attributeHashHex, + "ErrantProfiles": attributeProfileHashHex, + } + + if (reasonExists == true){ + identityReviewMap["Reason"] = reason + } + + myIdentityExists, err := myReviews.CreateAndBroadcastMyReview(identityReviewMap) + if (err != nil) { return err } + if (myIdentityExists == false) { + return errors.New("My moderator identity not found when trying to submit review.") + } + + return nil + } + + approveAttributeWithReasonFunction := func(reasonExists bool, reason string)error{ + + err := submitAttributeReview("Approve", reasonExists, reason) + if (err != nil) { return err } + + return nil + } + banAttributeWithReasonFunction := func(reasonExists bool, reason string)error{ + err := submitAttributeReview("Ban", reasonExists, reason) + if (err != nil) { return err } + + return nil + } + banAttributeAndIdentityWithReasonFunction := func(reasonExists bool, reason string)error{ + err := submitAttributeReview("Ban", reasonExists, reason) + if (err != nil) { return err } + + err = submitBanIdentityReview(reasonExists, reason) + if (err != nil) { return err } + + return nil + } + + approveAttributeButton := widget.NewButtonWithIcon("Approve Attribute", theme.ConfirmIcon(), func(){ + + setEnterReasonAndSubmitReviewPage(window, "Submit Approve Review", "Approve Attribute", "Approve", approveAttributeWithReasonFunction, currentPage, refreshPageWithNewContent) + }) + banAttributeButton := widget.NewButtonWithIcon("Ban Attribute", theme.CancelIcon(), func(){ + + setEnterReasonAndSubmitReviewPage(window, "Submit Ban Review", "Ban Attribute", "Ban", banAttributeWithReasonFunction, currentPage, refreshPageWithNewContent) + }) + + banAttributeAndIdentityButton := widget.NewButtonWithIcon("Ban Attribute And Identity", theme.CancelIcon(), func(){ + + setEnterReasonAndSubmitReviewPage(window, "Submit Ban Reviews", "Ban Attribute And Identity", "Ban", banAttributeAndIdentityWithReasonFunction, currentPage, refreshPageWithNewContent) + }) + + reviewButtonsColumn := getContainerCentered(container.NewGridWithColumns(1, approveAttributeButton, banAttributeButton, banAttributeAndIdentityButton)) + + skipButton := widget.NewButtonWithIcon("Skip", theme.MediaFastForwardIcon(), func(){ + + // This will skip this attribute. It will be moved to the back of the myUnreviewed queue + err := mySkippedContent.AddAttributeToMySkippedAttributesMap(attributeHash, attributeAuthorIdentityHash) + if (err != nil){ + setErrorEncounteredPage(window, err, currentPage) + return + } + refreshPageWithNewContent() + }) + hideButton := widget.NewButtonWithIcon("Hide", theme.VisibilityOffIcon(), func(){ + setConfirmHideContentPage(window, attributeHash[:], attributeAuthorIdentityHash, currentPage, refreshPageWithNewContent) + }) + + skipAndHideButtonsColumn := getContainerCentered(container.NewGridWithColumns(1, skipButton, hideButton)) + + page := container.NewVBox(title, backButton, widget.NewSeparator(), viewAttributeDetailsButton, widget.NewSeparator(), attributeTitleRow, widget.NewSeparator(), attributeDisplayContainer, widget.NewSeparator(), reviewButtonsColumn, widget.NewSeparator(), skipAndHideButtonsColumn) + + setPageContent(page, window) +} + + +// This page is used by a moderator to browse their unreviewed messages, review their content, and submit a verdict +func setModerateMessagesPage(window fyne.Window, imageOrText string, messageProvided bool, messageHash [26]byte, previousPage func()){ + + currentPage := func(){setModerateMessagesPage(window, imageOrText, messageProvided, messageHash, previousPage)} + refreshPageWithNewContent := func(){setModerateMessagesPage(window, imageOrText, false, [26]byte{}, previousPage)} + + title := getPageTitleCentered("Moderating Messages") + + backButton := getBackButtonCentered(previousPage) + + appNetworkType, err := getAppNetworkType.GetAppNetworkType() + if (err != nil) { + setErrorEncounteredPage(window, err, previousPage) + return + } + + if (messageProvided == false){ + + exists, moderatingMessages, err := mySettings.GetSetting("ModerateMessagesOnOffStatus") + if (err != nil){ + setErrorEncounteredPage(window, err, previousPage) + return + } + if (exists == false || moderatingMessages != "On"){ + + description1 := getBoldLabelCentered("You are not moderating messages.") + description2 := getLabelCentered("You must enable message moderation mode.") + + enableButton := getWidgetCentered(widget.NewButtonWithIcon("Enable", theme.NavigateNextIcon(), func(){ + setManageModerateMessagesModePage(window, currentPage) + })) + + page := container.NewVBox(title, backButton, widget.NewSeparator(), description1, description2, enableButton) + + setPageContent(page, window) + return + } + + setLoadingScreen(window, "Moderate Messages", "Loading message...") + + messageExists, newMessageHash, err := myUnreviewed.GetMyHighestPriorityUnreviewedMessageHash(appNetworkType, imageOrText) + if (err != nil){ + setErrorEncounteredPage(window, err, previousPage) + return + } + + if (messageExists == false){ + + description1 := getBoldLabelCentered("No messages remain to moderate.") + description2 := getLabelCentered("Check back later for new messages.") + //TODO: Add check for if space is full, and add message to say to increase space allowed for moderation content + + page := container.NewVBox(title, backButton, widget.NewSeparator(), description1, description2) + + setPageContent(page, window) + return + } + + setModerateMessagesPage(window, imageOrText, true, newMessageHash, previousPage) + return + } + + viewMessageDetailsButton := getWidgetCentered(widget.NewButtonWithIcon("View Message Details", theme.VisibilityIcon(), func(){ + setViewMessageModerationDetailsPage(window, messageHash, currentPage) + })) + + //Outputs: + // -bool: Necessary information found to display message + // -[16]byte: Message author + // -[32]byte: Message cipher key + // -string: Message communication + // -error + getMessageMetadata := func()(bool, [16]byte, [32]byte, string, error){ + + messageExists, cipherKeyFound, ableToDecrypt, messageCipherKey, senderIdentityHash, messageCommunication, err := chatMessageStorage.GetDecryptedMessageForModeration(messageHash) + if (err != nil){ return false, [16]byte{}, [32]byte{}, "", err } + if (messageExists == false){ + // We cannot review messages which we do not have downloaded + // Message may have been deleted after earlier check + return false, [16]byte{}, [32]byte{}, "", nil + } + if (cipherKeyFound == false){ + // No valid reviews/reports exist for this message. + // myUnreviewed should have checked this before. + // Report could have been deleted. + return false, [16]byte{}, [32]byte{}, "", nil + } + if (ableToDecrypt == false){ + // myUnreviewed should have checked for this + return false, [16]byte{}, [32]byte{}, "", errors.New("myUnreviewed returning message that cannot be read.") + } + + return true, senderIdentityHash, messageCipherKey, messageCommunication, nil + } + + messageInfoFound, senderIdentityHash, messageCipherKey, messageCommunication, err := getMessageMetadata() + if (err != nil){ + setErrorEncounteredPage(window, err, previousPage) + return + } + if (messageInfoFound == false){ + // We will refresh page and show loading screen so we can tell if this is happening + setLoadingScreen(window, "Moderate Messages", "Trying to find message...") + + time.Sleep(time.Second) + + refreshPageWithNewContent() + return + } + + messageContentContainer, err := getMessageDisplayForModeration(window, messageCommunication, currentPage) + if (err != nil) { + setErrorEncounteredPage(window, err, previousPage) + return + } + + appNetworkTypeString := helpers.ConvertByteToString(appNetworkType) + messageHashHex := encoding.EncodeBytesToHexString(messageHash[:]) + + submitMessageReview := func(verdict string, reasonExists bool, reason string)error{ + + messageCipherKeyHex := encoding.EncodeBytesToHexString(messageCipherKey[:]) + + newReviewMap := map[string]string{ + "NetworkType": appNetworkTypeString, + "ReviewedHash": messageHashHex, + "MessageCipherKey": messageCipherKeyHex, + "Verdict": verdict, + } + + if (reasonExists == true){ + newReviewMap["Reason"] = reason + } + + myIdentityExists, err := myReviews.CreateAndBroadcastMyReview(newReviewMap) + if (err != nil) { return err } + if (myIdentityExists == false) { + return errors.New("My moderator identity not found when trying to submit review.") + } + + return nil + } + + submitBanIdentityReview := func(reasonExists bool, reason string)error{ + + senderIdentityHashString, _, err := identity.EncodeIdentityHashBytesToString(senderIdentityHash) + if (err != nil) { return err } + + identityReviewMap := map[string]string{ + "NetworkType": appNetworkTypeString, + "ReviewedHash": senderIdentityHashString, + "Verdict": "Ban", + "ErrantMessages": messageHashHex, + } + + if (reasonExists == true){ + identityReviewMap["Reason"] = reason + } + + myIdentityExists, err := myReviews.CreateAndBroadcastMyReview(identityReviewMap) + if (err != nil) { return err } + if (myIdentityExists == false) { + return errors.New("My moderator identity not found when trying to submit review.") + } + + return nil + } + + approveMessageWithReasonFunction := func(reasonExists bool, reason string)error{ + + err := submitMessageReview("Approve", reasonExists, reason) + if (err != nil) { return err } + + return nil + } + banMessageWithReasonFunction := func(reasonExists bool, reason string)error{ + err := submitMessageReview("Ban", reasonExists, reason) + if (err != nil) { return err } + + return nil + } + banMessageAndIdentityWithReasonFunction := func(reasonExists bool, reason string)error{ + err := submitMessageReview("Ban", reasonExists, reason) + if (err != nil) { return err } + + err = submitBanIdentityReview(reasonExists, reason) + if (err != nil) { return err } + + return nil + } + + approveMessageButton := widget.NewButtonWithIcon("Approve Message", theme.ConfirmIcon(), func(){ + + setEnterReasonAndSubmitReviewPage(window, "Submit Approve Review", "Approve Message", "Approve", approveMessageWithReasonFunction, currentPage, refreshPageWithNewContent) + }) + banMessageButton := widget.NewButtonWithIcon("Ban Message", theme.CancelIcon(), func(){ + + setEnterReasonAndSubmitReviewPage(window, "Submit Ban Review", "Ban Message", "Ban", banMessageWithReasonFunction, currentPage, refreshPageWithNewContent) + }) + + banMessageAndSenderButton := widget.NewButtonWithIcon("Ban Message And Sender", theme.CancelIcon(), func(){ + + setEnterReasonAndSubmitReviewPage(window, "Submit Ban Reviews", "Ban Message and Sender", "Ban", banMessageAndIdentityWithReasonFunction, currentPage, refreshPageWithNewContent) + }) + + reviewButtonsColumn := getContainerCentered(container.NewGridWithColumns(1, approveMessageButton, banMessageButton, banMessageAndSenderButton)) + + skipButton := widget.NewButtonWithIcon("Skip", theme.MediaFastForwardIcon(), func(){ + + // This will skip this attribute. It will be moved to the back of the myUnreviewed queue + err := mySkippedContent.AddMessageToMySkippedMessagesMap(messageHash, senderIdentityHash) + if (err != nil){ + setErrorEncounteredPage(window, err, currentPage) + return + } + refreshPageWithNewContent() + }) + hideButton := widget.NewButtonWithIcon("Hide", theme.VisibilityOffIcon(), func(){ + setConfirmHideContentPage(window, messageHash[:], senderIdentityHash, currentPage, refreshPageWithNewContent) + }) + + skipAndHideButtonsColumn := getContainerCentered(container.NewGridWithColumns(1, skipButton, hideButton)) + + page := container.NewVBox(title, backButton, widget.NewSeparator(), viewMessageDetailsButton, widget.NewSeparator(), messageContentContainer, widget.NewSeparator(), reviewButtonsColumn, widget.NewSeparator(), skipAndHideButtonsColumn) + + setPageContent(page, window) +} + +// This will hide the profile/message/attribute permanently so it does not show up anymore +// This is useful if the content is in a language the user does not understand +func setConfirmHideContentPage(window fyne.Window, contentHash []byte, authorIdentityHash [16]byte, previousPage func(), pageToVisitAfter func()){ + + currentPage := func(){setConfirmHideContentPage(window, contentHash, authorIdentityHash, previousPage, pageToVisitAfter)} + + contentType, err := helpers.GetContentTypeFromContentHash(contentHash) + if (err != nil){ + contentHashHex := encoding.EncodeBytesToHexString(contentHash) + setErrorEncounteredPage(window, errors.New("setConfirmHideContentPage called with invalid contentHash: " + contentHashHex), previousPage) + return + } + + if (contentType != "Message" && contentType != "Profile" && contentType != "Attribute"){ + contentHashHex := encoding.EncodeBytesToHexString(contentHash) + setErrorEncounteredPage(window, errors.New("setConfirmHideContentPage called with invalid contentHash: " + contentHashHex), previousPage) + return + } + + title := getPageTitleCentered("Confirm Hide " + contentType) + + backButton := getBackButtonCentered(previousPage) + + description1 := getBoldLabelCentered("Confirm Hide " + contentType + "?") + + description2 := getLabelCentered("This will prevent this " + contentType + " from showing up in your moderation queue.") + description3 := getLabelCentered("This is useful when you don't want to review the " + contentType + ".") + description4 := getLabelCentered("For example, the " + contentType + " may be written in a language you don't understand.") + + confirmButton := getWidgetCentered(widget.NewButtonWithIcon("Confirm", theme.ConfirmIcon(), func(){ + + if (contentType == "Message"){ + + if (len(contentHash) != 26){ + contentHashHex := encoding.EncodeBytesToHexString(contentHash) + setErrorEncounteredPage(window, errors.New("GetContentTypeFromContentHash returning Message for different length content hash: " + contentHashHex), currentPage) + return + } + + messageHash := [26]byte(contentHash) + + err := myHiddenContent.AddMessageToMyHiddenMessagesMap(messageHash, authorIdentityHash) + if (err != nil){ + setErrorEncounteredPage(window, err, currentPage) + return + } + } else if (contentType == "Profile"){ + + if (len(contentHash) != 28){ + contentHashHex := encoding.EncodeBytesToHexString(contentHash) + setErrorEncounteredPage(window, errors.New("GetContentTypeFromContentHash returning Profile for different length content hash: " + contentHashHex), currentPage) + return + } + + profileHash := [28]byte(contentHash) + + err := myHiddenContent.AddProfileToMyHiddenProfilesMap(profileHash, authorIdentityHash) + if (err != nil){ + setErrorEncounteredPage(window, err, currentPage) + return + } + + } else if (contentType == "Attribute"){ + + if (len(contentHash) != 27){ + contentHashHex := encoding.EncodeBytesToHexString(contentHash) + setErrorEncounteredPage(window, errors.New("GetContentTypeFromContentHash returning Attribute for different length content hash: " + contentHashHex), currentPage) + return + } + + attributeHash := [27]byte(contentHash) + + err := myHiddenContent.AddAttributeToMyHiddenAttributesMap(attributeHash, authorIdentityHash) + if (err != nil){ + setErrorEncounteredPage(window, err, currentPage) + return + } + } + + pageToVisitAfter() + })) + + page := container.NewVBox(title, backButton, widget.NewSeparator(), description1, description2, description3, description4, confirmButton) + + setPageContent(page, window) +} + + +func setEnterReasonAndSubmitReviewPage(window fyne.Window, pageTitle string, actionName string, verdict string, submitFunctionWithReason func(bool, string)error, previousPage func(), pageToVisitAfter func()){ + + title := getPageTitleCentered(pageTitle) + + backButton := getBackButtonCentered(previousPage) + + actionLabel := getItalicLabelCentered("Action:") + actionDescription := getBoldLabelCentered(actionName) + + enterReasonLabel := getLabelCentered("Enter " + verdict + " Reason (Optional):") + + reasonEntry := widget.NewMultiLineEntry() + reasonEntry.SetPlaceHolder("Enter reason...") + + reasonEntryBoxed := getWidgetBoxed(reasonEntry) + + submitButton := getWidgetCentered(widget.NewButtonWithIcon("Submit", theme.ConfirmIcon(), func(){ + + reason := reasonEntry.Text + + if (reason == ""){ + err := submitFunctionWithReason(false, "") + if (err != nil) { + setErrorEncounteredPage(window, err, previousPage) + return + } + } else { + err := submitFunctionWithReason(true, reason) + if (err != nil) { + setErrorEncounteredPage(window, err, previousPage) + return + } + } + + pageToVisitAfter() + })) + + widener := widget.NewLabel(" ") + + submitButtonWithWidener := container.NewGridWithColumns(1, submitButton, widener) + + entryWithSubmitButton := getContainerCentered(container.NewGridWithColumns(1, reasonEntryBoxed, submitButtonWithWidener)) + + page := container.NewVBox(title, backButton, widget.NewSeparator(), actionLabel, actionDescription, widget.NewSeparator(), enterReasonLabel, entryWithSubmitButton) + + setPageContent(page, window) +} + +func setViewProfileForModerationPage(window fyne.Window, profileHash [28]byte, previousPage func()){ + + currentPage := func(){setViewProfileForModerationPage(window, profileHash, previousPage)} + + title := getPageTitleCentered("Viewing Profile") + + backButton := getBackButtonCentered(previousPage) + + profileHashTitle := widget.NewLabel("Profile Hash:") + + profileHashHex := encoding.EncodeBytesToHexString(profileHash[:]) + + profileHashTrimmed, _, err := helpers.TrimAndFlattenString(profileHashHex, 10) + if (err != nil) { + setErrorEncounteredPage(window, err, previousPage) + return + } + profileHashLabel := getBoldLabel(profileHashTrimmed) + viewProfileHashButton := widget.NewButtonWithIcon("", theme.VisibilityIcon(), func(){ + setViewContentHashPage(window, "Profile", profileHash[:], currentPage) + }) + profileHashRow := container.NewHBox(layout.NewSpacer(), profileHashTitle, profileHashLabel, viewProfileHashButton, layout.NewSpacer()) + + getProfileDisplayContainer := func()(*fyne.Container, error){ + + profileExists, profileBytes, err := profileStorage.GetStoredProfile(profileHash) + if (err != nil) { return nil, err } + if (profileExists == false){ + description1 := getBoldLabelCentered("Profile not downloaded.") + description2 := getLabelCentered("Try to download?") + + downloadButton := getWidgetCentered(widget.NewButton("Download", func(){ + setDownloadContentFromHashPage(window, "Profile", profileHash[:], currentPage) + })) + + content := container.NewVBox(description1, description2, downloadButton) + return content, nil + } + + ableToRead, currentProfileHash, profileVersion, _, profileIdentityHash, _, _, rawProfileMap, err := readProfiles.ReadProfileAndHash(false, profileBytes) + if (err != nil) { return nil, err } + if (ableToRead == false){ + return nil, errors.New("Database corrupt: Contains invalid profile.") + } + if (profileHash != currentProfileHash) { + return nil, errors.New("Database corrupt: Profile hash does not match entry key") + } + + profileAuthorLabel := widget.NewLabel("Profile Author:") + + profileIdentityHashString, _, err := identity.EncodeIdentityHashBytesToString(profileIdentityHash) + if (err != nil){ return nil, err } + + profileIdentityHashLabel := getBoldLabel(profileIdentityHashString) + profileIdentityHashRow := container.NewHBox(layout.NewSpacer(), profileAuthorLabel, profileIdentityHashLabel, layout.NewSpacer()) + + getAnyProfileAttributeFunction, err := calculatedAttributes.GetRetrieveAnyProfileAttributeIncludingCalculatedFunction(profileVersion, rawProfileMap) + if (err != nil) { return nil, err } + + viewProfileButton := getWidgetCentered(widget.NewButtonWithIcon("View Profile", theme.VisibilityIcon(), func(){ + setViewUserProfilePage(window, false, profileHash, 0, getAnyProfileAttributeFunction, currentPage) + })) + + profileDisplayWithInfo := container.NewVBox(profileIdentityHashRow, viewProfileButton) + + return profileDisplayWithInfo, nil + } + + profileDisplayContainer, err := getProfileDisplayContainer() + if (err != nil){ + setErrorEncounteredPage(window, err, previousPage) + return + } + + page := container.NewVBox(title, backButton, widget.NewSeparator(), profileHashRow, profileDisplayContainer) + + setPageContent(page, window) +} + +// This page allows a moderator to view a profile attribute value +// It is different from setModerateProfileAttributesPage, +// which allows the moderator to review the attribute, provides skip/hide buttons, and serves +// the next attribute to review +func setViewProfileAttributeForModerationPage(window fyne.Window, attributeHash [27]byte, previousPage func()){ + + currentPage := func(){setViewProfileAttributeForModerationPage(window, attributeHash, previousPage)} + + title := getPageTitleCentered("Viewing Profile Attribute") + + backButton := getBackButtonCentered(previousPage) + + attributeHashTitle := widget.NewLabel("Attribute Hash:") + + attributeHashHex := encoding.EncodeBytesToHexString(attributeHash[:]) + + attributeHashTrimmed, _, err := helpers.TrimAndFlattenString(attributeHashHex, 10) + if (err != nil) { + setErrorEncounteredPage(window, err, previousPage) + return + } + attributeHashLabel := getBoldLabel(attributeHashTrimmed) + viewAttributeHashButton := widget.NewButtonWithIcon("", theme.VisibilityIcon(), func(){ + setViewContentHashPage(window, "Attribute", attributeHash[:], currentPage) + }) + attributeHashRow := container.NewHBox(layout.NewSpacer(), attributeHashTitle, attributeHashLabel, viewAttributeHashButton, layout.NewSpacer()) + + // We have to find the profile hash that the attribute belongs to + // If we cannot find any profiles with this attribute, we will offer the user to try to download profiles for this attribute hash + + attributeMetadataFound, attributeIdentifier, _, _, _, profileFound, _, profileBytes, err := profileStorage.GetProfileAttributeMetadataAndProfile(attributeHash) + if (err != nil){ + setErrorEncounteredPage(window, err, previousPage) + return + } + if (attributeMetadataFound == false || profileFound == false){ + + description1 := getBoldLabelCentered("You do not have this profile attribute downloaded.") + description2 := getLabelCentered("Try to download it?") + description3 := getLabelCentered("It may not exist on the network anymore.") + + downloadButton := getWidgetCentered(widget.NewButtonWithIcon("Download", theme.DownloadIcon(), func(){ + //TODO + showUnderConstructionDialog(window) + })) + + page := container.NewVBox(title, backButton, widget.NewSeparator(), attributeHashRow, widget.NewSeparator(), description1, description2, description3, downloadButton) + + setPageContent(page, window) + return + } + + attributeName, err := profileFormat.GetAttributeNameFromAttributeIdentifier(attributeIdentifier) + if (err != nil){ + setErrorEncounteredPage(window, err, previousPage) + return + } + + attributeTitle, _, _, _, _, err := attributeDisplay.GetProfileAttributeDisplayInfo(attributeName) + if (err != nil){ + setErrorEncounteredPage(window, err, previousPage) + return + } + + attributeNameLabel := widget.NewLabel("Attribute Name:") + attributeTitleLabel := getBoldLabel(attributeTitle) + attributeTitleRow := container.NewHBox(layout.NewSpacer(), attributeNameLabel, attributeTitleLabel, layout.NewSpacer()) + + attributeDisplayContainer, err := getProfileAttributeDisplayForModeration(window, attributeName, attributeIdentifier, profileBytes, currentPage) + if (err != nil){ + setErrorEncounteredPage(window, err, previousPage) + return + } + + page := container.NewVBox(title, backButton, widget.NewSeparator(), attributeHashRow, widget.NewSeparator(), attributeTitleRow, widget.NewSeparator(), attributeDisplayContainer) + + setPageContent(page, window) +} + +func setViewMessageForModerationPage(window fyne.Window, messageHash [26]byte, previousPage func()){ + + currentPage := func(){setViewMessageForModerationPage(window, messageHash, previousPage)} + + title := getPageTitleCentered("Viewing Message") + + backButton := getBackButtonCentered(previousPage) + + messageHashHex := encoding.EncodeBytesToHexString(messageHash[:]) + + messageHashTitle := widget.NewLabel("Message Hash:") + messageHashTrimmed, _, err := helpers.TrimAndFlattenString(messageHashHex, 10) + if (err != nil) { + setErrorEncounteredPage(window, err, previousPage) + return + } + messageHashLabel := getBoldLabel(messageHashTrimmed) + viewMessageHashButton := widget.NewButtonWithIcon("", theme.VisibilityIcon(), func(){ + setViewContentHashPage(window, "Message", messageHash[:], currentPage) + }) + messageHashRow := container.NewHBox(layout.NewSpacer(), messageHashTitle, messageHashLabel, viewMessageHashButton, layout.NewSpacer()) + + messageExists, cipherKeyFound, ableToDecrypt, _, senderIdentityHash, messageCommunication, err := chatMessageStorage.GetDecryptedMessageForModeration(messageHash) + if (err != nil){ + setErrorEncounteredPage(window, err, previousPage) + return + } + if (messageExists == false){ + description1 := getBoldLabelCentered("Message not downloaded.") + description2 := getLabelCentered("Try to download?") + + downloadButton := getWidgetCentered(widget.NewButton("Download", func(){ + setDownloadContentFromHashPage(window, "Message", messageHash[:], currentPage) + })) + + page := container.NewVBox(title, backButton, widget.NewSeparator(), messageHashRow, widget.NewSeparator(), description1, description2, downloadButton) + + setPageContent(page, window) + return + } + + if (cipherKeyFound == false){ + // No valid reviews/reports exist for message hash. User can try to download + description1 := getBoldLabelCentered("No reviews or reports for message found.") + description2 := getLabelCentered("Without a review/report, the contents are encrypted.") + description3 := getLabelCentered("Try to download reviews/reports?") + + downloadButton := getWidgetCentered(widget.NewButtonWithIcon("Download", theme.DownloadIcon(), func(){ + setDownloadReviewsAndReportsForReviewedHashPage(window, messageHash[:], currentPage, currentPage) + })) + + page := container.NewVBox(title, backButton, widget.NewSeparator(), messageHashRow, widget.NewSeparator(), description1, description2, description3, downloadButton) + + setPageContent(page, window) + return + } + if (ableToDecrypt == false){ + description1 := getBoldLabelCentered("Message is corrupt.") + description2 := getLabelCentered("It was created by a malicious user.") + description3 := getLabelCentered("It cannot be decrypted.") + description4 := getLabelCentered("No moderator should be able to approve it.") + description5 := getLabelCentered("Any moderators who have approved the message will be automatically banned.") + + page := container.NewVBox(title, backButton, widget.NewSeparator(), messageHashRow, widget.NewSeparator(), description1, description2, description3, description4, description5) + + setPageContent(page, window) + return + } + + senderIdentityHashTitle := widget.NewLabel("Sender Identity Hash:") + + senderIdentityHashString, _, err := identity.EncodeIdentityHashBytesToString(senderIdentityHash) + if (err != nil){ + setErrorEncounteredPage(window, err, previousPage) + return + } + + senderIdentityHashTrimmed, _, err := helpers.TrimAndFlattenString(senderIdentityHashString, 10) + if (err != nil){ + setErrorEncounteredPage(window, err, previousPage) + return + } + senderIdentityHashLabel := getBoldLabel(senderIdentityHashTrimmed) + viewSenderIdentityHashButton := widget.NewButtonWithIcon("", theme.VisibilityIcon(), func(){ + setViewIdentityHashPage(window, senderIdentityHash, currentPage) + }) + senderIdentityHashRow := container.NewHBox(layout.NewSpacer(), senderIdentityHashTitle, senderIdentityHashLabel, viewSenderIdentityHashButton, layout.NewSpacer()) + + messageContentLabel := getBoldItalicLabelCentered("Message Content:") + + messageContentContainer, err := getMessageDisplayForModeration(window, messageCommunication, currentPage) + if (err != nil){ + setErrorEncounteredPage(window, err, previousPage) + return + } + + page := container.NewVBox(title, backButton, widget.NewSeparator(), messageHashRow, widget.NewSeparator(), senderIdentityHashRow, widget.NewSeparator(), messageContentLabel, messageContentContainer) + + setPageContent(page, window) +} + +// This function will return a container to display a profile attribute +func getProfileAttributeDisplayForModeration(window fyne.Window, attributeName string, attributeIdentifier int, profileBytes []byte, currentPage func())(*fyne.Container, error){ + + ableToRead, _, _, _, _, profileIsDisabled, rawProfileMap, err := readProfiles.ReadProfile(false, profileBytes) + if (err != nil) { return nil, err } + if (ableToRead == false){ + return nil, errors.New("getProfileAttributeDisplayForModeration called with invalid profile.") + } + + if (profileIsDisabled == true){ + return nil, errors.New("getProfileAttributeDisplayForModeration called with disabled profile.") + } + + exists, attributeValue, err := readProfiles.GetFormattedProfileAttributeFromRawProfileMap(rawProfileMap, attributeName) + if (err != nil) { return nil, err } + if (exists == false){ + attributeIdentifierString := helpers.ConvertIntToString(attributeIdentifier) + return nil, errors.New("getProfileAttributeDisplayForModeration called with profile missing attribute: " + attributeIdentifierString) + } + + _, _, formatValueFunction, attributeUnits, _, err := attributeDisplay.GetProfileAttributeDisplayInfo(attributeName) + if (err != nil) { return nil, err } + + if (attributeName == "Photos"){ + + photosBase64List := strings.Split(attributeValue, "+") + if (len(photosBase64List) == 0) { + return nil, errors.New("Verified mate profile has an empty photos list") + } + + getNumberOfImagesGridColumns := func()int{ + if (len(photosBase64List) == 1){ + return 1 + } + if (len(photosBase64List) == 2){ + return 2 + } + return 3 + } + + numberOfImagesGridColumns := getNumberOfImagesGridColumns() + + imagesGrid := container.NewGridWithColumns(numberOfImagesGridColumns) + + for _, base64Photo := range photosBase64List{ + + goImage, err := imagery.ConvertWebpBase64StringToImageObject(base64Photo) + if (err != nil) { return nil, err } + + viewImageButton := widget.NewButtonWithIcon("", theme.ZoomInIcon(), func(){ + setViewFullpageImagePage(window, goImage, currentPage) + }) + + fyneImage := canvas.NewImageFromImage(goImage) + fyneImage.FillMode = canvas.ImageFillContain + imageSize := getCustomFyneSize(10) + fyneImage.SetMinSize(imageSize) + + viewImageButtonCentered := getWidgetCentered(viewImageButton) + + imageWithFullpageButton := container.NewVBox(fyneImage, viewImageButtonCentered) + + imagesGrid.Add(imageWithFullpageButton) + } + + imagesGridCentered := getContainerCentered(imagesGrid) + + return imagesGridCentered, nil + } + + //TODO: Add more attributes that need a custom display, such as 23andMe_AncestryComposition + + attributeValueFormatted, err := formatValueFunction(attributeValue) + if (err != nil) { return nil, err } + + attributeValueTrimmed, _, err := helpers.TrimAndFlattenString(attributeValueFormatted, 20) + if (err != nil) { return nil, err } + + viewTextButton := widget.NewButtonWithIcon("", theme.VisibilityIcon(), func(){ + setViewTextPage(window, "Viewing Attribute", attributeValueFormatted, false, currentPage) + }) + + valueLabel := widget.NewLabel("Value:") + + attributeValueLabel := getBoldLabel(attributeValueTrimmed + " " + attributeUnits) + + attributeValueRow := container.NewHBox(layout.NewSpacer(), valueLabel, attributeValueLabel, viewTextButton, layout.NewSpacer()) + + return attributeValueRow, nil +} + +func getMessageDisplayForModeration(window fyne.Window, messageCommunication string, currentPage func())(*fyne.Container, error){ + + isImageMessage := strings.HasPrefix(messageCommunication, ">!>Photo=") + if (isImageMessage == true){ + + imageBase64Webp := strings.TrimPrefix(messageCommunication, ">!>Photo=") + + goImage, err := imagery.ConvertWEBPBase64StringToCroppedDownsizedImageObject(imageBase64Webp) + if (err != nil) { return nil, err } + + viewImageButton := getWidgetCentered(widget.NewButtonWithIcon("", theme.ZoomInIcon(), func(){ + setViewFullpageImagePage(window, goImage, currentPage) + })) + + fyneImage := canvas.NewImageFromImage(goImage) + fyneImage.FillMode = canvas.ImageFillContain + imageSize := getCustomFyneSize(20) + fyneImage.SetMinSize(imageSize) + + imageCentered := getFyneImageCentered(fyneImage) + + contentDisplayContainer := container.NewVBox(imageCentered, viewImageButton) + + return contentDisplayContainer, nil + } + //imageOrText == "Text" + + communicationTrimmed, _, err := helpers.TrimAndFlattenString(messageCommunication, 25) + if (err != nil) { return nil, err } + + communicationLabel := getLabelCentered(communicationTrimmed) + + viewCommunicationButton := widget.NewButtonWithIcon("", theme.VisibilityIcon(), func(){ + setViewTextPage(window, "Viewing Message", messageCommunication, false, currentPage) + }) + + viewTextRow := container.NewHBox(layout.NewSpacer(), communicationLabel, viewCommunicationButton, layout.NewSpacer()) + + return viewTextRow, nil +} + +func setDownloadContentFromHashPage(window fyne.Window, contentType string, contentHash []byte, previousPage func()){ + + //TODO: Make sure the content has not expired from the network, and is not banned (in which case it will not exist on the network) +} + +func setModerateIdentitiesPage(window fyne.Window, previousPage func()){ + + currentPage := func(){setModerateIdentitiesPage(window, previousPage)} + + title := getPageTitleCentered("Moderate Identities") + + backButton := getBackButtonCentered(previousPage) + + description := getLabelCentered("Choose a method to moderate user identities.") + + viewControversialContentButton := widget.NewButton("View Controversial Content", func(){ + + _ = mySettings.SetSetting("ViewedContentSortByAttribute", "Controversy") + _ = mySettings.SetSetting("ViewedContentSortDirection", "Descending") + _ = mySettings.SetSetting("ViewedContentGeneratedStatus", "No") + _ = mySettings.SetSetting("ViewedContentViewIndex", "0") + + setBrowseContentPage(window, currentPage) + }) + + viewControversialModeratorsButton := widget.NewButton("View Controversial Moderators", func(){ + + _ = mySettings.SetSetting("ViewedModeratorsSortByAttribute", "Controversy") + _ = mySettings.SetSetting("ViewedModeratorsSortDirection", "Descending") + _ = mySettings.SetSetting("ViewedModeratorsGeneratedStatus", "No") + _ = mySettings.SetSetting("ViewedModeratorsViewIndex", "0") + + setViewModeratorsPage(window, currentPage) + }) + + viewMostBannedModeratorsButton := widget.NewButton("View Most Banned Moderators", func(){ + + _ = mySettings.SetSetting("ViewedModeratorsSortByAttribute", "BanAdvocates") + _ = mySettings.SetSetting("ViewedModeratorsSortDirection", "Descending") + _ = mySettings.SetSetting("ViewedModeratorsGeneratedStatus", "No") + _ = mySettings.SetSetting("ViewedModeratorsViewIndex", "0") + + setViewModeratorsPage(window, currentPage) + }) + + viewMostReportedModeratorsButton := widget.NewButton("View Most Reported Moderators", func(){ + //TODO + showUnderConstructionDialog(window) + }) + + viewMostBannedHostsButton := widget.NewButton("View Most Banned Hosts", func(){ + + _ = mySettings.SetSetting("ViewedHostsSortByAttribute", "BanAdvocates") + _ = mySettings.SetSetting("ViewedHostsSortDirection", "Descending") + _ = mySettings.SetSetting("ViewedHostsGeneratedStatus", "No") + _ = mySettings.SetSetting("ViewedHostsViewIndex", "0") + + setViewHostsPage(window, currentPage) + }) + + buttonsGrid := getContainerCentered(container.NewGridWithColumns(1, viewControversialContentButton, viewControversialModeratorsButton, viewMostBannedModeratorsButton, viewMostReportedModeratorsButton, viewMostBannedHostsButton)) + + page := container.NewVBox(title, backButton, widget.NewSeparator(), description, buttonsGrid) + + setPageContent(page, window) +} + + +// This page allows moderators to view the moderation status of an identity +func setViewIdentityModerationDetailsPage(window fyne.Window, identityHash [16]byte, previousPage func()){ + + setLoadingScreen(window, "View Moderation Details", "Loading details...") + + currentPage := func(){setViewIdentityModerationDetailsPage(window, identityHash, previousPage)} + + title := getPageTitleCentered("Viewing Moderation Details - Identity") + + backButton := getBackButtonCentered(previousPage) + + identityHashLabel := widget.NewLabel("Identity Hash:") + + identityHashString, _, err := identity.EncodeIdentityHashBytesToString(identityHash) + if (err != nil){ + identityHashHex := encoding.EncodeBytesToHexString(identityHash[:]) + setErrorEncounteredPage(window, errors.New("setViewIdentityModerationDetailsPage called with invalid identity hash: " + identityHashHex), previousPage) + return + } + + identityHashTrimmed, _, err := helpers.TrimAndFlattenString(identityHashString, 10) + if (err != nil){ + setErrorEncounteredPage(window, err, previousPage) + return + } + viewIdentityHashButton := widget.NewButton(identityHashTrimmed, func(){ + setViewIdentityHashPage(window, identityHash, currentPage) + }) + + identityHashRow := container.NewHBox(layout.NewSpacer(), identityHashLabel, viewIdentityHashButton, layout.NewSpacer()) + + appNetworkType, err := getAppNetworkType.GetAppNetworkType() + if (err != nil) { + setErrorEncounteredPage(window, err, previousPage) + return + } + + downloadingRequiredReviews, parametersExist, identityIsBannedConsensus, numberOfBanAdvocates, banScoreSum, _, err := verifiedVerdict.GetVerifiedIdentityVerdict(identityHash, appNetworkType) + if (err != nil){ + setErrorEncounteredPage(window, err, previousPage) + return + } + + getModeratorConsensusRow := func()*fyne.Container{ + + moderatorConsensusLabel := widget.NewLabel("Moderator Consensus:") + + if (downloadingRequiredReviews == false || parametersExist == false){ + + verdictConsensusLabel := getBoldLabel("Unknown") + moderatorConsensusRow := container.NewHBox(layout.NewSpacer(), moderatorConsensusLabel, verdictConsensusLabel, layout.NewSpacer()) + + return moderatorConsensusRow + } + + getConsensusVerdictString := func()string{ + if (identityIsBannedConsensus == false){ + return "Not Banned" + } + return "Banned" + } + consensusVerdictString := getConsensusVerdictString() + + verdictConsensusLabel := getBoldLabel(consensusVerdictString) + moderatorConsensusRow := container.NewHBox(layout.NewSpacer(), moderatorConsensusLabel, verdictConsensusLabel, layout.NewSpacer()) + + return moderatorConsensusRow + } + + moderatorConsensusRow := getModeratorConsensusRow() + + getMyVerdict := func()(string, error){ + + myIdentityExists, iHaveBanned, err := myReviews.GetMyNewestIdentityModerationVerdict(identityHash, appNetworkType) + if (err != nil) { return "", err } + if (myIdentityExists == false || iHaveBanned == false){ + return "None", nil + } + return "Ban", nil + } + + myVerdict, err := getMyVerdict() + if (err != nil){ + setErrorEncounteredPage(window, err, previousPage) + return + } + + myVerdictLabel := widget.NewLabel("My Verdict:") + myVerdictText := getBoldLabel(myVerdict) + + //TODO: Add button to change my verdict, and see when my verdict was authored. + + myVerdictRow := container.NewHBox(layout.NewSpacer(), myVerdictLabel, myVerdictText, layout.NewSpacer()) + + //TODO: Add button to view identity sticky status and verdict history + + if (downloadingRequiredReviews == false){ + + description1 := getBoldLabelCentered("This identity is outside of your moderation range.") + description2 := getLabelCentered("We cannot determine the moderation consensus.") + description3 := getLabelCentered("Do you want to download the reviews to view the consensus?") + + downloadButton := getWidgetCentered(widget.NewButtonWithIcon("Download", theme.DownloadIcon(), func(){ + setDownloadReviewsAndReportsForReviewedHashPage(window, identityHash[:], currentPage, currentPage) + })) + + page := container.NewVBox(title, backButton, widget.NewSeparator(), identityHashRow, widget.NewSeparator(), moderatorConsensusRow, widget.NewSeparator(), myVerdictRow, widget.NewSeparator(), description1, description2, description3, downloadButton) + + setPageContent(page, window) + return + } + if (parametersExist == false){ + + description1 := getLabelCentered("Your client is missing the moderation parameters.") + description2 := getLabelCentered("Please wait for the parameters to be downloaded.") + + viewStatusButton := getWidgetCentered(widget.NewButtonWithIcon("View Status", theme.VisibilityIcon(), func(){ + //TODO + showUnderConstructionDialog(window) + })) + + page := container.NewVBox(title, backButton, widget.NewSeparator(), identityHashRow, widget.NewSeparator(), moderatorConsensusRow, widget.NewSeparator(), myVerdictRow, widget.NewSeparator(), description1, description2, viewStatusButton) + + setPageContent(page, window) + return + } + + numberBanString := helpers.ConvertIntToString(numberOfBanAdvocates) + banAdvocatesLabel := widget.NewLabel("Ban Advocates:") + numberBanLabel := getBoldLabel(numberBanString) + numberBanRow := container.NewHBox(layout.NewSpacer(), banAdvocatesLabel, numberBanLabel, layout.NewSpacer()) + + banScoreString := helpers.ConvertFloat64ToStringRounded(banScoreSum, 2) + banScoreLabel := getLabelCentered("Ban Score:") + banScoreText := getBoldLabel(banScoreString) + banScoreRow := container.NewHBox(layout.NewSpacer(), banScoreLabel, banScoreText, layout.NewSpacer()) + + viewReviewerVerdictsButton := getWidgetCentered(widget.NewButton("View Reviewers", func(){ + setViewIdentityReviewerVerdictsPage(window, identityHash, 0, currentPage) + })) + + numberOfReports, err := reportStorage.GetNumberOfReportsForReportedHash(identityHash[:], appNetworkType) + if (err != nil){ + setErrorEncounteredPage(window, err, previousPage) + return + } + numberOfReportsString := helpers.ConvertInt64ToString(numberOfReports) + numberOfReportsLabel := widget.NewLabel("Number of reports:") + numberOfReportsText := getBoldLabel(numberOfReportsString) + numberOfReportsRow := container.NewHBox(layout.NewSpacer(), numberOfReportsLabel, numberOfReportsText, layout.NewSpacer()) + + page := container.NewVBox(title, backButton, widget.NewSeparator(), identityHashRow, widget.NewSeparator(), moderatorConsensusRow, widget.NewSeparator(), myVerdictRow, widget.NewSeparator(), numberBanRow, banScoreRow, viewReviewerVerdictsButton, widget.NewSeparator(), numberOfReportsRow) + + if (numberOfReports != 0){ + viewReportsButton := getWidgetCentered(widget.NewButton("View Reports", func(){ + //TODO + showUnderConstructionDialog(window) + })) + page.Add(viewReportsButton) + } + page.Add(widget.NewSeparator()) + + updateButton := getWidgetCentered(widget.NewButtonWithIcon("Update", theme.ViewRefreshIcon(), currentPage)) + + page.Add(updateButton) + + setPageContent(page, window) +} + + +// This page allows moderators to view the moderation status of a profile +func setViewProfileModerationDetailsPage(window fyne.Window, profileHash [28]byte, previousPage func()){ + + setLoadingScreen(window, "View Moderation Details", "Loading details...") + + currentPage := func(){setViewProfileModerationDetailsPage(window, profileHash, previousPage)} + + title := getPageTitleCentered("Viewing Moderation Details - Profile") + + backButton := getBackButtonCentered(previousPage) + + profileHashLabel := widget.NewLabel("Profile Hash:") + + profileHashHex := encoding.EncodeBytesToHexString(profileHash[:]) + + profileHashTrimmed, _, err := helpers.TrimAndFlattenString(profileHashHex, 6) + if (err != nil){ + setErrorEncounteredPage(window, err, previousPage) + return + } + viewProfileHashButton := widget.NewButton(profileHashTrimmed, func(){ + setViewContentHashPage(window, "Profile", profileHash[:], currentPage) + }) + + profileHashRow := container.NewHBox(layout.NewSpacer(), profileHashLabel, viewProfileHashButton, layout.NewSpacer()) + + appNetworkType, err := getAppNetworkType.GetAppNetworkType() + if (err != nil) { + setErrorEncounteredPage(window, err, previousPage) + return + } + + profileIsDisabled, profileMetadataIsKnown, _, profileNetworkType, profileIdentityHash, _, downloadingRequiredReviews, parametersExist, profileVerdict, numberOfApproveAdvocates, numberOfBanAdvocates, approveScoreSum, banScoreSum, _, _, _, _, _, _, _, _, _, err := verifiedVerdict.GetVerifiedProfileVerdict(profileHash) + if (err != nil){ + setErrorEncounteredPage(window, err, previousPage) + return + } + + if (profileMetadataIsKnown == true){ + + if (profileNetworkType != appNetworkType){ + setErrorEncounteredPage(window, errors.New("setViewProfileModerationDetailsPage called with profile for different network type."), previousPage) + return + } + } + + getProfileAuthorRow := func()(*fyne.Container, error){ + + profileAuthorLabel := widget.NewLabel("Profile Author:") + + if (profileMetadataIsKnown == false){ + + unknownLabel := getBoldLabel("Unknown") + + profileAuthorRow := container.NewHBox(layout.NewSpacer(), profileAuthorLabel, unknownLabel, layout.NewSpacer()) + + return profileAuthorRow, nil + } + + profileIdentityHashString, _, err := identity.EncodeIdentityHashBytesToString(profileIdentityHash) + if (err != nil){ return nil, err } + + profileIdentityHashTrimmed, _, err := helpers.TrimAndFlattenString(profileIdentityHashString, 10) + if (err != nil){ return nil, err } + + viewProfileIdentityHashButton := widget.NewButton(profileIdentityHashTrimmed, func(){ + setViewIdentityModerationDetailsPage(window, profileIdentityHash, currentPage) + }) + + profileAuthorRow := container.NewHBox(layout.NewSpacer(), profileAuthorLabel, viewProfileIdentityHashButton, layout.NewSpacer()) + + return profileAuthorRow, nil + } + + profileAuthorRow, err := getProfileAuthorRow() + if (err != nil){ + setErrorEncounteredPage(window, err, previousPage) + return + } + + if (profileIsDisabled == true){ + + description1 := getBoldLabelCentered("This profile is disabled.") + description2 := getLabelCentered("It cannot be banned.") + + page := container.NewVBox(title, backButton, widget.NewSeparator(), profileHashRow, widget.NewSeparator(), profileAuthorRow, widget.NewSeparator(), description1, description2) + + setPageContent(page, window) + return + } + + getModeratorConsensusRow := func()*fyne.Container{ + + moderatorConsensusLabel := widget.NewLabel("Moderator Consensus:") + + if (profileMetadataIsKnown == false || downloadingRequiredReviews == false || parametersExist == false){ + + verdictConsensusLabel := getBoldLabel("Unknown") + + moderatorConsensusRow := container.NewHBox(layout.NewSpacer(), moderatorConsensusLabel, verdictConsensusLabel, layout.NewSpacer()) + + return moderatorConsensusRow + } + + verdictConsensusLabel := getBoldLabel(profileVerdict) + + moderatorConsensusRow := container.NewHBox(layout.NewSpacer(), moderatorConsensusLabel, verdictConsensusLabel, layout.NewSpacer()) + + return moderatorConsensusRow + } + + moderatorConsensusRow := getModeratorConsensusRow() + + getMyVerdict := func()(string, error){ + + if (profileIsDisabled == true){ + return "None", nil + } + + myIdentityExists, profileIsDisabledB, profileMetadataExists, myReviewExists, myReviewVerdict, err := myReviews.GetMyNewestProfileModerationVerdict(profileHash, true) + if (err != nil) { return "", err } + if (myIdentityExists == false){ + return "None", nil + } + if (profileIsDisabled != profileIsDisabledB){ + return "", errors.New("GetMyNewestProfileModerationVerdict returning different profileIsDisabled status than GetVerifiedProfileVerdict.") + } + if (profileMetadataExists == false){ + return "Unknown", nil + } + if (myReviewExists == false){ + return "None", nil + } + + return myReviewVerdict, nil + } + + myVerdict, err := getMyVerdict() + if (err != nil){ + setErrorEncounteredPage(window, err, previousPage) + return + } + + myVerdictLabel := widget.NewLabel("My Verdict:") + myVerdictText := getBoldLabel(myVerdict) + + myVerdictRow := container.NewHBox(layout.NewSpacer(), myVerdictLabel, myVerdictText, layout.NewSpacer()) + + //TODO: Add button to view profile sticky status and verdict history + + if (profileMetadataIsKnown == false){ + // We do not have the required profile + // We can attempt to download it. + + description1 := getLabelCentered("This profile is not downloaded.") + description2 := getLabelCentered("It must be downloaded to know its moderation status.") + description3 := getLabelCentered("You can try to download it, but it may have been deleted from the network.") + + downloadButton := getWidgetCentered(widget.NewButtonWithIcon("Download", theme.DownloadIcon(), func(){ + //TODO + showUnderConstructionDialog(window) + })) + + //TODO: Add a way to also download profile's reviews/reports at the same time so we can quickly determine verdict + + page := container.NewVBox(title, backButton, widget.NewSeparator(), profileHashRow, widget.NewSeparator(), moderatorConsensusRow, widget.NewSeparator(), myVerdictRow, widget.NewSeparator(), description1, description2, description3, downloadButton) + + setPageContent(page, window) + return + } + + viewProfileButton := getWidgetCentered(widget.NewButtonWithIcon("View Profile", theme.VisibilityIcon(), func(){ + setViewProfileForModerationPage(window, profileHash, currentPage) + })) + + viewAttributesButton := getWidgetCentered(widget.NewButtonWithIcon("View Attributes", theme.VisibilityIcon(), func(){ + //TODO + showUnderConstructionDialog(window) + })) + + if (downloadingRequiredReviews == false){ + + description1 := getLabelCentered("This profile is outside of your moderation range.") + description2 := getLabelCentered("We cannot determine the moderation consensus.") + description3 := getLabelCentered("Do you want to download the reviews to view the consensus?") + + downloadButton := getWidgetCentered(widget.NewButtonWithIcon("Download", theme.DownloadIcon(), func(){ + setDownloadReviewsAndReportsForReviewedHashPage(window, profileHash[:], currentPage, currentPage) + })) + + page := container.NewVBox(title, backButton, widget.NewSeparator(), profileHashRow, widget.NewSeparator(), profileAuthorRow, viewProfileButton, widget.NewSeparator(), viewAttributesButton, widget.NewSeparator(), moderatorConsensusRow, widget.NewSeparator(), myVerdictRow, widget.NewSeparator(), description1, description2, description3, downloadButton) + + setPageContent(page, window) + return + } + if (parametersExist == false){ + + description1 := getLabelCentered("Your client is missing the moderation parameters.") + description2 := getLabelCentered("Please wait for the parameters to be downloaded.") + + viewStatusButton := getWidgetCentered(widget.NewButtonWithIcon("View Status", theme.VisibilityIcon(), func(){ + //TODO + showUnderConstructionDialog(window) + })) + + page := container.NewVBox(title, backButton, widget.NewSeparator(), profileHashRow, widget.NewSeparator(), profileAuthorRow, widget.NewSeparator(), viewProfileButton, widget.NewSeparator(), viewAttributesButton, widget.NewSeparator(), moderatorConsensusRow, widget.NewSeparator(), myVerdictRow, widget.NewSeparator(), description1, description2, viewStatusButton) + + setPageContent(page, window) + return + } + + numberApproveString := helpers.ConvertIntToString(numberOfApproveAdvocates) + approveAdvocatesLabel := widget.NewLabel("Approve Advocates:") + numberApproveLabel := getBoldLabel(numberApproveString) + numberApproveRow := container.NewHBox(layout.NewSpacer(), approveAdvocatesLabel, numberApproveLabel, layout.NewSpacer()) + + numberBanString := helpers.ConvertIntToString(numberOfBanAdvocates) + banAdvocatesLabel := widget.NewLabel("Ban Advocates:") + numberBanLabel := getBoldLabel(numberBanString) + numberBanRow := container.NewHBox(layout.NewSpacer(), banAdvocatesLabel, numberBanLabel, layout.NewSpacer()) + + approveScoreString := helpers.ConvertFloat64ToStringRounded(approveScoreSum, 2) + approveScoreLabel := getLabelCentered("Approve Score:") + approveScoreText := getBoldLabel(approveScoreString) + approveScoreRow := container.NewHBox(layout.NewSpacer(), approveScoreLabel, approveScoreText, layout.NewSpacer()) + + banScoreString := helpers.ConvertFloat64ToStringRounded(banScoreSum, 2) + banScoreLabel := getLabelCentered("Ban Score:") + banScoreText := getBoldLabel(banScoreString) + banScoreRow := container.NewHBox(layout.NewSpacer(), banScoreLabel, banScoreText, layout.NewSpacer()) + + viewReviewerVerdictsButton := getWidgetCentered(widget.NewButton("View Reviewers", func(){ + setViewProfileReviewerVerdictsPage(window, profileHash, 0, currentPage) + })) + + numberOfReports, err := reportStorage.GetNumberOfReportsForReportedHash(profileHash[:], appNetworkType) + if (err != nil){ + setErrorEncounteredPage(window, err, previousPage) + return + } + numberOfReportsString := helpers.ConvertInt64ToString(numberOfReports) + numberOfReportsLabel := widget.NewLabel("Number of reports:") + numberOfReportsText := getBoldLabel(numberOfReportsString) + numberOfReportsRow := container.NewHBox(layout.NewSpacer(), numberOfReportsLabel, numberOfReportsText, layout.NewSpacer()) + + page := container.NewVBox(title, backButton, widget.NewSeparator(), profileHashRow, widget.NewSeparator(), profileAuthorRow, widget.NewSeparator(), viewProfileButton, widget.NewSeparator(), viewAttributesButton, widget.NewSeparator(), moderatorConsensusRow, widget.NewSeparator(), myVerdictRow, widget.NewSeparator(), numberApproveRow, numberBanRow, approveScoreRow, banScoreRow, viewReviewerVerdictsButton, widget.NewSeparator(), numberOfReportsRow) + + if (numberOfReports != 0){ + viewReportsButton := getWidgetCentered(widget.NewButton("View Reports", func(){ + //TODO + })) + page.Add(viewReportsButton) + } + page.Add(widget.NewSeparator()) + + updateButton := getWidgetCentered(widget.NewButtonWithIcon("Update", theme.ViewRefreshIcon(), func(){ + currentPage() + })) + + page.Add(updateButton) + + setPageContent(page, window) +} + + +// This page allows moderators to view the moderation status of a profile attribute +func setViewAttributeModerationDetailsPage(window fyne.Window, attributeHash [27]byte, previousPage func()){ + + setLoadingScreen(window, "View Moderation Details", "Loading details...") + + currentPage := func(){setViewAttributeModerationDetailsPage(window, attributeHash, previousPage)} + + title := getPageTitleCentered("Viewing Moderation Details - Attribute") + + backButton := getBackButtonCentered(previousPage) + + attributeHashLabel := widget.NewLabel("Attribute Hash:") + + attributeHashHex := encoding.EncodeBytesToHexString(attributeHash[:]) + + attributeHashTrimmed, _, err := helpers.TrimAndFlattenString(attributeHashHex, 6) + if (err != nil){ + setErrorEncounteredPage(window, err, previousPage) + return + } + viewAttributeHashButton := widget.NewButton(attributeHashTrimmed, func(){ + setViewContentHashPage(window, "Attribute", attributeHash[:], currentPage) + }) + + attributeHashRow := container.NewHBox(layout.NewSpacer(), attributeHashLabel, viewAttributeHashButton, layout.NewSpacer()) + + appNetworkType, err := getAppNetworkType.GetAppNetworkType() + if (err != nil) { + setErrorEncounteredPage(window, err, previousPage) + return + } + + attributeMetadataIsKnown, _, attributeAuthor, attributeNetworkType, attributeProfileHashesList, err := profileStorage.GetProfileAttributeMetadata(attributeHash) + if (err != nil){ + setErrorEncounteredPage(window, err, previousPage) + return + } + + if (attributeMetadataIsKnown == true){ + + if (appNetworkType != attributeNetworkType){ + setErrorEncounteredPage(window, errors.New("setViewAttributeModerationDetailsPage called with attribute belonging to different appNetworkType."), previousPage) + return + } + } + + getAttributeAuthorRow := func()(*fyne.Container, error){ + + attributeAuthorLabel := widget.NewLabel("Attribute Author:") + + if (attributeMetadataIsKnown == false){ + + unknownLabel := getBoldLabel("Unknown") + + attributeAuthorRow := container.NewHBox(layout.NewSpacer(), attributeAuthorLabel, unknownLabel, layout.NewSpacer()) + + return attributeAuthorRow, nil + } + + attributeAuthorString, _, err := identity.EncodeIdentityHashBytesToString(attributeAuthor) + if (err != nil){ return nil, err } + + authorIdentityHashTrimmed, _, err := helpers.TrimAndFlattenString(attributeAuthorString, 10) + if (err != nil){ return nil, err } + + viewAttributeAuthorButton := widget.NewButton(authorIdentityHashTrimmed, func(){ + setViewIdentityModerationDetailsPage(window, attributeAuthor, currentPage) + }) + + attributeAuthorRow := container.NewHBox(layout.NewSpacer(), attributeAuthorLabel, viewAttributeAuthorButton, layout.NewSpacer()) + + return attributeAuthorRow, nil + } + + attributeAuthorRow, err := getAttributeAuthorRow() + if (err != nil){ + setErrorEncounteredPage(window, err, previousPage) + return + } + + getMyVerdict := func()(string, error){ + + myIdentityExists, attributeMetadataKnown, myReviewExists, myReviewVerdict, _, err := myReviews.GetMyNewestProfileAttributeModerationVerdict(attributeHash, true) + if (err != nil) { return "", err } + if (myIdentityExists == false){ + return "None", nil + } + if (attributeMetadataKnown == false){ + // Metadata must have been recently deleted + return "Unknown", nil + } + if (myReviewExists == false){ + return "None", nil + } + return myReviewVerdict, nil + } + + myVerdict, err := getMyVerdict() + if (err != nil){ + setErrorEncounteredPage(window, err, previousPage) + return + } + + myVerdictLabel := widget.NewLabel("My Verdict:") + myVerdictText := getBoldLabel(myVerdict) + + myVerdictRow := container.NewHBox(layout.NewSpacer(), myVerdictLabel, myVerdictText, layout.NewSpacer()) + + if (attributeMetadataIsKnown == false){ + // We do not have a profile containing this attribute + // We can attempt to download it. + + description1 := getLabelCentered("This attribute is not downloaded.") + description2 := getLabelCentered("It must be downloaded to know its moderation status.") + description3 := getLabelCentered("You can try to download it, but it may have been deleted from the network.") + + downloadButton := getWidgetCentered(widget.NewButtonWithIcon("Download", theme.DownloadIcon(), func(){ + //TODO + showUnderConstructionDialog(window) + })) + + //TODO: Add a way to also download reviews/reports at the same time so we can determine attribute verdict quickly + + page := container.NewVBox(title, backButton, widget.NewSeparator(), attributeHashRow, widget.NewSeparator(), attributeAuthorRow, widget.NewSeparator(), myVerdictRow, widget.NewSeparator(), description1, description2, description3, downloadButton) + + setPageContent(page, window) + return + } + + viewAttributeButton := getWidgetCentered(widget.NewButtonWithIcon("View Attribute", theme.VisibilityIcon(), func(){ + setViewProfileAttributeForModerationPage(window, attributeHash, currentPage) + })) + + viewAttributeProfilesButton := getWidgetCentered(widget.NewButtonWithIcon("View Attribute Profiles", theme.VisibilityIcon(), func(){ + //TODO: A list of profiles the attribute belongs to. + showUnderConstructionDialog(window) + })) + + downloadingRequiredReviews, err := backgroundDownloads.CheckIfAppCanDetermineIdentityVerdicts(attributeAuthor) + if (err != nil){ + setErrorEncounteredPage(window, err, previousPage) + return + } + if (downloadingRequiredReviews == false){ + + description1 := getLabelCentered("This attribute's author is outside of your moderation range.") + description2 := getLabelCentered("We cannot determine the moderation consensus.") + description3 := getLabelCentered("Do you want to download the reviews to view the consensus?") + + downloadButton := getWidgetCentered(widget.NewButtonWithIcon("Download", theme.DownloadIcon(), func(){ + setDownloadReviewsAndReportsForReviewedHashPage(window, attributeAuthor[:], currentPage, currentPage) + })) + + page := container.NewVBox(title, backButton, widget.NewSeparator(), attributeHashRow, widget.NewSeparator(), attributeAuthorRow, widget.NewSeparator(), myVerdictRow, widget.NewSeparator(), viewAttributeButton, viewAttributeProfilesButton, widget.NewSeparator(), description1, description2, description3, downloadButton) + + setPageContent(page, window) + return + } + + //TODO: Check for parameters + parametersExist := true + + if (parametersExist == false){ + + description1 := getLabelCentered("Your client is missing the moderation parameters.") + description2 := getLabelCentered("Please wait for the parameters to download.") + description3 := getLabelCentered("Once they download, you will be able to see who has reviewed the attribute.") + + viewStatusButton := getWidgetCentered(widget.NewButtonWithIcon("View Status", theme.VisibilityIcon(), func(){ + //TODO + showUnderConstructionDialog(window) + })) + + refreshButton := getWidgetCentered(widget.NewButtonWithIcon("Refresh", theme.ViewRefreshIcon(), currentPage)) + + page := container.NewVBox(title, backButton, widget.NewSeparator(), attributeHashRow, widget.NewSeparator(), attributeAuthorRow, widget.NewSeparator(), myVerdictRow, widget.NewSeparator(), viewAttributeButton, viewAttributeProfilesButton, widget.NewSeparator(), description1, description2, description3, viewStatusButton, refreshButton) + + setPageContent(page, window) + return + } + + attributeApproveAdvocatesMap, attributeBanAdvocatesMap, err := reviewStorage.GetProfileAttributeVerdictMaps(attributeHash, attributeNetworkType, true, attributeProfileHashesList) + if (err != nil){ + setErrorEncounteredPage(window, err, previousPage) + return + } + + numberOfApproveAdvocates := len(attributeApproveAdvocatesMap) + numberOfBanAdvocates := len(attributeBanAdvocatesMap) + + numberApproveString := helpers.ConvertIntToString(numberOfApproveAdvocates) + approveAdvocatesLabel := widget.NewLabel("Approve Advocates:") + numberApproveLabel := getBoldLabel(numberApproveString) + numberApproveRow := container.NewHBox(layout.NewSpacer(), approveAdvocatesLabel, numberApproveLabel, layout.NewSpacer()) + + numberBanString := helpers.ConvertIntToString(numberOfBanAdvocates) + banAdvocatesLabel := widget.NewLabel("Ban Advocates:") + numberBanLabel := getBoldLabel(numberBanString) + numberBanRow := container.NewHBox(layout.NewSpacer(), banAdvocatesLabel, numberBanLabel, layout.NewSpacer()) + + viewReviewerVerdictsButton := getWidgetCentered(widget.NewButton("View Reviewers", func(){ + setViewAttributeReviewerVerdictsPage(window, attributeHash, 0, currentPage) + })) + + numberOfReports, err := reportStorage.GetNumberOfReportsForReportedHash(attributeHash[:], appNetworkType) + if (err != nil){ + setErrorEncounteredPage(window, err, previousPage) + return + } + + numberOfReportsString := helpers.ConvertInt64ToString(numberOfReports) + numberOfReportsLabel := widget.NewLabel("Number of reports:") + numberOfReportsText := getBoldLabel(numberOfReportsString) + numberOfReportsRow := container.NewHBox(layout.NewSpacer(), numberOfReportsLabel, numberOfReportsText, layout.NewSpacer()) + + page := container.NewVBox(title, backButton, widget.NewSeparator(), attributeHashRow, widget.NewSeparator(), attributeAuthorRow, widget.NewSeparator(), viewAttributeButton, viewAttributeProfilesButton, widget.NewSeparator(), myVerdictRow, widget.NewSeparator(), numberApproveRow, numberBanRow, viewReviewerVerdictsButton, widget.NewSeparator(), numberOfReportsRow) + + if (numberOfReports != 0){ + viewReportsButton := getWidgetCentered(widget.NewButton("View Reports", func(){ + //TODO + showUnderConstructionDialog(window) + })) + page.Add(viewReportsButton) + } + page.Add(widget.NewSeparator()) + + updateButton := getWidgetCentered(widget.NewButtonWithIcon("Update", theme.ViewRefreshIcon(), currentPage)) + + page.Add(updateButton) + + setPageContent(page, window) +} + + +// This page allows moderators to view the moderation status of a message +func setViewMessageModerationDetailsPage(window fyne.Window, messageHash [26]byte, previousPage func()){ + + setLoadingScreen(window, "View Moderation Details", "Loading details...") + + currentPage := func(){setViewMessageModerationDetailsPage(window, messageHash, previousPage)} + + title := getPageTitleCentered("Viewing Moderation Details - Message") + + backButton := getBackButtonCentered(previousPage) + + messageHashLabel := widget.NewLabel("Message Hash:") + + messageHashHex := encoding.EncodeBytesToHexString(messageHash[:]) + + messageHashTrimmed, _, err := helpers.TrimAndFlattenString(messageHashHex, 6) + if (err != nil){ + setErrorEncounteredPage(window, err, previousPage) + return + } + viewMessageHashButton := widget.NewButton(messageHashTrimmed, func(){ + setViewContentHashPage(window, "Message", messageHash[:], currentPage) + }) + + messageHashRow := container.NewHBox(layout.NewSpacer(), messageHashLabel, viewMessageHashButton, layout.NewSpacer()) + + //TODO: Add button to view message sticky status and message verdict history + + getMessageAuthorRow := func()(*fyne.Container, error){ + + messageAuthorLabel := widget.NewLabel("Message Author:") + + messageExists, messageCipherKeyFound, messageIsDecryptable, _, messageAuthorIdentityHash, _, err := chatMessageStorage.GetDecryptedMessageForModeration(messageHash) + if (err != nil){ return nil, err } + if (messageExists == false || messageCipherKeyFound == false || messageIsDecryptable == false){ + + //TODO: Show user if message is not decryptable, in which case, author must be malicious + + unknownLabel := getBoldLabel("Unknown") + + messageAuthorRow := container.NewHBox(layout.NewSpacer(), messageAuthorLabel, unknownLabel, layout.NewSpacer()) + + return messageAuthorRow, nil + } + + messageAuthorIdentityHashString, _, err := identity.EncodeIdentityHashBytesToString(messageAuthorIdentityHash) + if (err != nil){ return nil, err } + + messageAuthorIdentityHashTrimmed, _, err := helpers.TrimAndFlattenString(messageAuthorIdentityHashString, 10) + if (err != nil){ return nil, err } + + viewMessageAuthorIdentityHashButton := widget.NewButton(messageAuthorIdentityHashTrimmed, func(){ + setViewIdentityModerationDetailsPage(window, messageAuthorIdentityHash, currentPage) + }) + + messageAuthorRow := container.NewHBox(layout.NewSpacer(), messageAuthorLabel, viewMessageAuthorIdentityHashButton, layout.NewSpacer()) + + return messageAuthorRow, nil + } + + messageAuthorRow, err := getMessageAuthorRow() + if (err != nil){ + setErrorEncounteredPage(window, err, previousPage) + return + } + + appNetworkType, err := getAppNetworkType.GetAppNetworkType() + if (err != nil) { + setErrorEncounteredPage(window, err, previousPage) + return + } + + messageMetadataIsKnown, _, messageNetworkType, _, _, downloadingRequiredReviews, parametersExist, messageVerdict, numberOfApproveAdvocates, numberOfBanAdvocates, approveScoreSum, banScoreSum, _, _, err := verifiedVerdict.GetVerifiedMessageVerdict(messageHash) + if (err != nil) { + setErrorEncounteredPage(window, err, previousPage) + return + } + + if (messageMetadataIsKnown == true){ + + if (appNetworkType != messageNetworkType){ + setErrorEncounteredPage(window, errors.New("setViewMessageModerationDetailsPage called with message belonging to different appNetworkType."), previousPage) + return + } + } + + getModeratorConsensusRow := func()*fyne.Container{ + + moderatorConsensusLabel := widget.NewLabel("Moderator Consensus:") + + if (messageMetadataIsKnown == false || downloadingRequiredReviews == false || parametersExist == false){ + verdictConsensusLabel := getBoldItalicLabel("Unknown") + + moderatorConsensusRow := container.NewHBox(layout.NewSpacer(), moderatorConsensusLabel, verdictConsensusLabel, layout.NewSpacer()) + + return moderatorConsensusRow + } + + verdictConsensusLabel := getBoldLabel(messageVerdict) + moderatorConsensusRow := container.NewHBox(layout.NewSpacer(), moderatorConsensusLabel, verdictConsensusLabel, layout.NewSpacer()) + + return moderatorConsensusRow + } + + moderatorConsensusRow := getModeratorConsensusRow() + + getMyVerdict := func()(string, error){ + + myIdentityExists, messageMetadataExists, myReviewExists, myReviewVerdict, err := myReviews.GetMyNewestMessageModerationVerdict(messageHash) + if (err != nil) { return "", err } + if (myIdentityExists == false){ + return "None", nil + } + if (messageMetadataExists == false){ + return "Unknown", nil + } + if (myReviewExists == false){ + return "None", nil + } + return myReviewVerdict, nil + } + + myVerdict, err := getMyVerdict() + if (err != nil){ + setErrorEncounteredPage(window, err, previousPage) + return + } + + myVerdictLabel := widget.NewLabel("My Verdict:") + myVerdictText := getBoldLabel(myVerdict) + + myVerdictRow := container.NewHBox(layout.NewSpacer(), myVerdictLabel, myVerdictText, layout.NewSpacer()) + + if (messageMetadataIsKnown == false){ + // We do not have the required message + // We can attempt to download it. + + description1 := getBoldLabelCentered("This message is not downloaded.") + description2 := getLabelCentered("It must be downloaded to know its moderation status.") + description3 := getLabelCentered("You can try to download it, but it may have been deleted from the network.") + + downloadButton := getWidgetCentered(widget.NewButtonWithIcon("Download", theme.DownloadIcon(), func(){ + //TODO + showUnderConstructionDialog(window) + })) + + //TODO: Add a way to also download reviews/reports at the same time, so we can determine message verdict quickly + + page := container.NewVBox(title, backButton, widget.NewSeparator(), messageHashRow, widget.NewSeparator(), messageAuthorRow, widget.NewSeparator(), moderatorConsensusRow, widget.NewSeparator(), myVerdictRow, widget.NewSeparator(), description1, description2, description3, downloadButton) + + setPageContent(page, window) + return + } + + viewMessageButton := getWidgetCentered(widget.NewButtonWithIcon("View Message", theme.VisibilityIcon(), func(){ + setViewMessageForModerationPage(window, messageHash, currentPage) + })) + + if (downloadingRequiredReviews == false){ + + description1 := getLabelCentered("This message is outside of your moderation range.") + description2 := getLabelCentered("We cannot determine the moderation consensus.") + description3 := getLabelCentered("Do you want to download the reviews to view the consensus?") + + downloadButton := getWidgetCentered(widget.NewButtonWithIcon("Download", theme.DownloadIcon(), func(){ + setDownloadReviewsAndReportsForReviewedHashPage(window, messageHash[:], currentPage, currentPage) + })) + + page := container.NewVBox(title, backButton, widget.NewSeparator(), messageHashRow, widget.NewSeparator(), messageAuthorRow, widget.NewSeparator(), viewMessageButton, widget.NewSeparator(), moderatorConsensusRow, widget.NewSeparator(), myVerdictRow, widget.NewSeparator(), description1, description2, description3, downloadButton) + + setPageContent(page, window) + return + } + if (parametersExist == false){ + + description1 := getLabelCentered("Your client is missing the moderation parameters.") + description2 := getLabelCentered("Please wait for the parameters to be downloaded.") + + viewStatusButton := getWidgetCentered(widget.NewButtonWithIcon("View Status", theme.VisibilityIcon(), func(){ + //TODO + showUnderConstructionDialog(window) + })) + + page := container.NewVBox(title, backButton, widget.NewSeparator(), messageHashRow, widget.NewSeparator(), messageAuthorRow, widget.NewSeparator(), viewMessageButton, widget.NewSeparator(), moderatorConsensusRow, widget.NewSeparator(), myVerdictRow, widget.NewSeparator(), description1, description2, viewStatusButton) + + setPageContent(page, window) + return + } + + numberApproveString := helpers.ConvertIntToString(numberOfApproveAdvocates) + approveAdvocatesLabel := widget.NewLabel("Approve Advocates:") + numberApproveLabel := getBoldLabel(numberApproveString) + numberApproveRow := container.NewHBox(layout.NewSpacer(), approveAdvocatesLabel, numberApproveLabel, layout.NewSpacer()) + + numberBanString := helpers.ConvertIntToString(numberOfBanAdvocates) + banAdvocatesLabel := widget.NewLabel("Ban Advocates:") + numberBanLabel := getBoldLabel(numberBanString) + numberBanRow := container.NewHBox(layout.NewSpacer(), banAdvocatesLabel, numberBanLabel, layout.NewSpacer()) + + approveScoreString := helpers.ConvertFloat64ToStringRounded(approveScoreSum, 2) + approveScoreLabel := getLabelCentered("Approve Score:") + approveScoreText := getBoldLabel(approveScoreString) + approveScoreRow := container.NewHBox(layout.NewSpacer(), approveScoreLabel, approveScoreText, layout.NewSpacer()) + + banScoreString := helpers.ConvertFloat64ToStringRounded(banScoreSum, 2) + banScoreLabel := getLabelCentered("Ban Score:") + banScoreText := getBoldLabel(banScoreString) + banScoreRow := container.NewHBox(layout.NewSpacer(), banScoreLabel, banScoreText, layout.NewSpacer()) + + viewReviewerVerdictsButton := getWidgetCentered(widget.NewButton("View Reviewers", func(){ + setViewMessageReviewerVerdictsPage(window, messageHash, 0, currentPage) + })) + + numberOfReports, err := reportStorage.GetNumberOfReportsForReportedHash(messageHash[:], messageNetworkType) + if (err != nil){ + setErrorEncounteredPage(window, err, previousPage) + return + } + + numberOfReportsString := helpers.ConvertInt64ToString(numberOfReports) + numberOfReportsLabel := widget.NewLabel("Number of reports:") + numberOfReportsText := getBoldLabel(numberOfReportsString) + numberOfReportsRow := container.NewHBox(layout.NewSpacer(), numberOfReportsLabel, numberOfReportsText, layout.NewSpacer()) + + page := container.NewVBox(title, backButton, widget.NewSeparator(), messageHashRow, widget.NewSeparator(), messageAuthorRow, widget.NewSeparator(), viewMessageButton, widget.NewSeparator(), moderatorConsensusRow, widget.NewSeparator(), myVerdictRow, widget.NewSeparator(), numberApproveRow, numberBanRow, approveScoreRow, banScoreRow, viewReviewerVerdictsButton, widget.NewSeparator(), numberOfReportsRow) + + if (numberOfReports != 0){ + viewReportsButton := getWidgetCentered(widget.NewButton("View Reports", func(){ + //TODO + showUnderConstructionDialog(window) + })) + page.Add(viewReportsButton) + } + page.Add(widget.NewSeparator()) + + updateButton := getWidgetCentered(widget.NewButtonWithIcon("Update", theme.ViewRefreshIcon(), currentPage)) + + page.Add(updateButton) + + setPageContent(page, window) +} + +func setDownloadReviewsAndReportsForReviewedHashPage(window fyne.Window, reviewedHash []byte, previousPage func(), pageToVisitAfter func()){ + + + //TODO: This page must also wait for moderator identity hashes from newly downloaded reviews to be downloaded + // This page must also make sure that the content is downloaded + + showUnderConstructionDialog(window) +} + + +// This page will show all moderators who have banned the identity +func setViewIdentityReviewerVerdictsPage(window fyne.Window, identityHash [16]byte, viewIndex int, previousPage func()){ + + setLoadingScreen(window, "View Identity Ban Advocates", "Loading verdicts...") + + currentPage := func(){setViewIdentityReviewerVerdictsPage(window, identityHash, viewIndex, previousPage)} + + title := getPageTitleCentered("View Identity Ban Advocates") + + backButton := getBackButtonCentered(previousPage) + + downloadingRequiredReviews, err := backgroundDownloads.CheckIfAppCanDetermineIdentityVerdicts(identityHash) + if (err != nil){ + setErrorEncounteredPage(window, err, previousPage) + return + } + if (downloadingRequiredReviews == false){ + description1 := getBoldLabelCentered("This identity is outside of your moderator range.") + description2 := getLabelCentered("Download the reviews for this identity?") + + downloadButton := getWidgetCentered(widget.NewButton("Download", func(){ + setDownloadReviewsAndReportsForReviewedHashPage(window, identityHash[:], currentPage, currentPage) + })) + + page := container.NewVBox(title, backButton, widget.NewSeparator(), description1, description2, downloadButton) + setPageContent(page, window) + return + } + + appNetworkType, err := getAppNetworkType.GetAppNetworkType() + if (err != nil) { + setErrorEncounteredPage(window, err, previousPage) + return + } + + banAdvocatesMap, err := reviewStorage.GetIdentityBanAdvocatesMap(identityHash, appNetworkType) + if (err != nil){ + setErrorEncounteredPage(window, err, previousPage) + return + } + + if (len(banAdvocatesMap) == 0){ + + noReviewersDescription := getBoldLabelCentered("No moderators have banned this identity.") + + updateButton := getWidgetCentered(widget.NewButtonWithIcon("Update", theme.ViewRefreshIcon(), currentPage)) + + page := container.NewVBox(title, backButton, widget.NewSeparator(), noReviewersDescription, updateButton) + + setPageContent(page, window) + return + } + + description := getLabelCentered("Below are the moderators who have banned the identity.") + + //TODO: Navigation buttons and pages + + //Outputs: + // -bool: Downloading required reviews/profiles (moderator mode is enabled) + // -bool: Parameters exist + // -*fyne.Container: Reviewer verdicts grid + // -error + getReviewerVerdictsGrid := func()(bool, bool, *fyne.Container, error){ + + notBannedReviewersList := make([][16]byte, 0) + bannedReviewersList := make([][16]byte, 0) + + for moderatorIdentityHash, _ := range banAdvocatesMap{ + + requiredDataIsBeingDownloaded, parametersExist, isBanned, err := bannedModeratorConsensus.GetModeratorIsBannedStatus(false, moderatorIdentityHash, appNetworkType) + if (err != nil) { return false, false, nil, err } + if (requiredDataIsBeingDownloaded == false){ + return false, parametersExist, nil, nil + } + if (parametersExist == false){ + return true, false, nil, nil + } + + if (isBanned == true){ + bannedReviewersList = append(bannedReviewersList, moderatorIdentityHash) + } else{ + notBannedReviewersList = append(notBannedReviewersList, moderatorIdentityHash) + } + } + + err := helpers.SortIdentityHashListToUnicodeOrder(notBannedReviewersList) + if (err != nil) { return false, false, nil, err } + err = helpers.SortIdentityHashListToUnicodeOrder(bannedReviewersList) + if (err != nil) { return false, false, nil, err } + + allReviewersListSorted := slices.Concat(notBannedReviewersList, bannedReviewersList) + + //TODO: Add pages and navigation + + emptyLabelA := widget.NewLabel("") + moderatorLabel := getItalicLabelCentered("Moderator") + isBannedLabel := getItalicLabelCentered("Is Banned") + reasonLabel := getItalicLabelCentered("Reason") + emptyLabelC := widget.NewLabel("") + + viewModeratorButtonsColumn := container.NewVBox(emptyLabelA, widget.NewSeparator()) + moderatorIdentityHashColumn := container.NewVBox(moderatorLabel, widget.NewSeparator()) + moderatorIsBannedColumn := container.NewVBox(isBannedLabel, widget.NewSeparator()) + reasonColumn := container.NewVBox(reasonLabel, widget.NewSeparator()) + viewReviewButtonsColumn := container.NewVBox(emptyLabelC, widget.NewSeparator()) + + for _, moderatorIdentityHash := range allReviewersListSorted{ + + viewModeratorButton := widget.NewButtonWithIcon("", theme.VisibilityIcon(), func(){ + setViewModeratorDetailsPage(window, moderatorIdentityHash, currentPage) + }) + + moderatorIdentityHashString, _, err := identity.EncodeIdentityHashBytesToString(moderatorIdentityHash) + if (err != nil) { return false, false, nil, err } + + identityHashTrimmed, _, err := helpers.TrimAndFlattenString(moderatorIdentityHashString, 10) + if (err != nil) { return false, false, nil, err } + identityHashLabel := getBoldLabelCentered(identityHashTrimmed) + + moderatorIsBannedBool := slices.Contains(bannedReviewersList, moderatorIdentityHash) + moderatorIsBanned := helpers.ConvertBoolToYesOrNoString(moderatorIsBannedBool) + moderatorIsBannedTranslated := translate(moderatorIsBanned) + + moderatorIsBannedLabel := getBoldLabelCentered(moderatorIsBannedTranslated) + + reviewFound, newestReviewBytes, _, err := reviewStorage.GetModeratorNewestIdentityReview(moderatorIdentityHash, identityHash, appNetworkType) + if (err != nil) { return false, false, nil, err } + if (reviewFound == false){ + // Review was deleted or changed after reviewers were retrieved + continue + } + + ableToRead, reviewHash, _, reviewNetworkType, retrievedModeratorIdentityHash, _, _, currentReviewedHash, reviewVerdict, reviewMap, err := readReviews.ReadReviewAndHash(false, newestReviewBytes) + if (err != nil) { return false, false, nil, err } + if (ableToRead == false){ + return false, false, nil, errors.New("GetModeratorNewestIdentityReview returning invalid review.") + } + if (reviewNetworkType != appNetworkType){ + return false, false, nil, errors.New("GetModeratorNewestIdentityReview returning review of different networkType.") + } + if (moderatorIdentityHash != retrievedModeratorIdentityHash) { + return false, false, nil, errors.New("GetModeratorNewestIdentityReview returning review by different moderator.") + } + areEqual := bytes.Equal(currentReviewedHash, identityHash[:]) + if (areEqual == false){ + return false, false, nil, errors.New("GetModeratorNewestIdentityReview returning review for different reviewedHash.") + } + if (reviewVerdict != "Ban"){ + return false, false, nil, errors.New("GetModeratorNewestIdentityReview returning non-Ban review: " + reviewVerdict) + } + + getViewReasonButtonOrText := func()(*fyne.Container, error){ + + reasonString, exists := reviewMap["Reason"] + if (exists == false) { + noneLabel := getBoldLabelCentered(translate("None")) + return noneLabel, nil + } + + viewReasonButton := getWidgetCentered(widget.NewButtonWithIcon("", theme.VisibilityIcon(), func(){ + setViewTextPage(window, "Viewing Verdict Reason", reasonString, false, currentPage) + })) + + return viewReasonButton, nil + } + viewReasonButtonOrText, err := getViewReasonButtonOrText() + if (err != nil) { return false, false, nil, err } + + viewReviewButton := widget.NewButtonWithIcon("View Review", theme.VisibilityIcon(), func(){ + setViewReviewDetailsPage(window, reviewHash, currentPage) + }) + + viewModeratorButtonsColumn.Add(viewModeratorButton) + moderatorIdentityHashColumn.Add(identityHashLabel) + moderatorIsBannedColumn.Add(moderatorIsBannedLabel) + reasonColumn.Add(viewReasonButtonOrText) + viewReviewButtonsColumn.Add(viewReviewButton) + + viewModeratorButtonsColumn.Add(widget.NewSeparator()) + moderatorIdentityHashColumn.Add(widget.NewSeparator()) + moderatorIsBannedColumn.Add(widget.NewSeparator()) + reasonColumn.Add(widget.NewSeparator()) + viewReviewButtonsColumn.Add(widget.NewSeparator()) + } + + reviewsGrid := container.NewHBox(layout.NewSpacer(), viewModeratorButtonsColumn, moderatorIdentityHashColumn, moderatorIsBannedColumn, reasonColumn, viewReviewButtonsColumn, layout.NewSpacer()) + + return true, true, reviewsGrid, nil + } + + moderatorModeEnabled, parametersExist, verdictsGrid, err := getReviewerVerdictsGrid() + if (err != nil){ + setErrorEncounteredPage(window, err, previousPage) + return + } + if (moderatorModeEnabled == false){ + + description1 := getBoldLabelCentered("Moderator mode is disabled.") + description2 := getLabelCentered("You must enable moderator mode to view the reviewer verdicts.") + + enableModeratorModeButton := getWidgetCentered(widget.NewButtonWithIcon("Enable Moderator Mode", theme.NavigateNextIcon(), func(){ + setManageModeratorModePage(window, currentPage) + })) + + page := container.NewVBox(title, backButton, widget.NewSeparator(), description1, description2, enableModeratorModeButton) + setPageContent(page, window) + return + } + if (parametersExist == false){ + + description1 := getBoldLabelCentered("Your client is missing the moderation parameters.") + description2 := getLabelCentered("Please wait for them to download.") + + refreshButton := getWidgetCentered(widget.NewButtonWithIcon("Refresh", theme.ViewRefreshIcon(), currentPage)) + + //TODO: Add button to view progress + + page := container.NewVBox(title, backButton, widget.NewSeparator(), description1, description2, refreshButton) + setPageContent(page, window) + return + } + + page := container.NewVBox(title, backButton, widget.NewSeparator(), description, widget.NewSeparator(), verdictsGrid) + + setPageContent(page, window) +} + + +// This page will show all moderators who have banned/approved the profile +func setViewProfileReviewerVerdictsPage(window fyne.Window, profileHash [28]byte, viewIndex int, previousPage func()){ + + setLoadingScreen(window, "View Profile Verdicts", "Loading verdicts...") + + currentPage := func(){setViewProfileReviewerVerdictsPage(window, profileHash, viewIndex, previousPage)} + + title := getPageTitleCentered("View Profile Verdicts") + + backButton := getBackButtonCentered(previousPage) + + _, profileIsDisabled, err := readProfiles.ReadProfileHashMetadata(profileHash) + if (err != nil){ + profileHashHex := encoding.EncodeBytesToHexString(profileHash[:]) + setErrorEncounteredPage(window, errors.New("setViewProfileReviewerVerdictsPage called with invalid profileHash: " + profileHashHex), previousPage) + return + } + if (profileIsDisabled == true){ + description1 := getBoldLabelCentered("This profile is disabled.") + description2 := getLabelCentered("It cannot be reviewed.") + + page := container.NewVBox(title, backButton, widget.NewSeparator(), description1, description2) + + setPageContent(page, window) + return + } + + profileMetadataIsKnown, _, profileNetworkType, profileIdentityHash, _, _, _, profileAttributeHashesMap, err := contentMetadata.GetProfileMetadata(profileHash) + if (err != nil){ + setErrorEncounteredPage(window, err, previousPage) + return + } + if (profileMetadataIsKnown == false){ + + description1 := getLabelCentered("The profile is not downloaded.") + description2 := getLabelCentered("You must download it to view its reviews.") + description3 := getLabelCentered("It may not be possible if the profile has been deleted from the network.") + description4 := getLabelCentered("Try to download the profile?") + + downloadButton := getWidgetCentered(widget.NewButtonWithIcon("Download", theme.DownloadIcon(), func(){ + //TODO + showUnderConstructionDialog(window) + })) + + page := container.NewVBox(title, backButton, widget.NewSeparator(), description1, description2, description3, description4, downloadButton) + + setPageContent(page, window) + return + } + + appNetworkType, err := getAppNetworkType.GetAppNetworkType() + if (err != nil){ + setErrorEncounteredPage(window, err, previousPage) + return + } + + if (profileNetworkType != appNetworkType){ + setErrorEncounteredPage(window, errors.New("setViewProfileReviewerVerdictsPage called with profile belonging to different networkType."), previousPage) + return + } + + downloadingRequiredReviews, err := backgroundDownloads.CheckIfAppCanDetermineIdentityVerdicts(profileIdentityHash) + if (err != nil) { + setErrorEncounteredPage(window, err, previousPage) + return + } + if (downloadingRequiredReviews == false){ + + description1 := getBoldLabelCentered("This profile's author is outside of your moderator range.") + description2 := getLabelCentered("Download the reviews for this profile?") + + downloadButton := getWidgetCentered(widget.NewButton("Download", func(){ + setDownloadReviewsAndReportsForReviewedHashPage(window, profileHash[:], currentPage, currentPage) + })) + + page := container.NewVBox(title, backButton, widget.NewSeparator(), description1, description2, downloadButton) + setPageContent(page, window) + return + } + + profileAttributeHashesList := helpers.GetListOfMapValues(profileAttributeHashesMap) + + approveAdvocatesMap, banAdvocatesMap, err := reviewStorage.GetProfileVerdictMaps(profileHash, profileNetworkType, true, profileAttributeHashesList) + if (err != nil){ + setErrorEncounteredPage(window, err, previousPage) + return + } + + if (len(approveAdvocatesMap) == 0 && len(banAdvocatesMap) == 0){ + + noReviewersDescription := getLabelCentered("No moderators have reviewed the profile.") + + updateButton := getWidgetCentered(widget.NewButtonWithIcon("Update", theme.ViewRefreshIcon(), currentPage)) + + page := container.NewVBox(title, backButton, widget.NewSeparator(), noReviewersDescription, updateButton) + + setPageContent(page, window) + return + } + + description := getLabelCentered("Below are the moderators who have reviewed the profile.") + + //TODO: Navigation pages and buttons + + //Outputs: + // -bool: Downloading required reviews/profiles (moderator mode is enabled) + // -bool: Parameters exist + // -*fyne.Container: Reviewer verdicts grid + // -error + getReviewerVerdictsGrid := func()(bool, bool, *fyne.Container, error){ + + approveAdvocatesList := helpers.GetListOfMapKeys(approveAdvocatesMap) + banAdvocatesList := helpers.GetListOfMapKeys(banAdvocatesMap) + + allReviewersList := slices.Concat(approveAdvocatesList, banAdvocatesList) + + notBannedReviewersList := make([][16]byte, 0) + bannedReviewersList := make([][16]byte, 0) + + for _, moderatorIdentityHash := range allReviewersList{ + + requiredDataIsBeingDownloaded, parametersExist, isBanned, err := bannedModeratorConsensus.GetModeratorIsBannedStatus(false, moderatorIdentityHash, profileNetworkType) + if (err != nil) { return false, false, nil, err } + if (requiredDataIsBeingDownloaded == false){ + return false, parametersExist, nil, nil + } + if (parametersExist == false){ + return true, false, nil, nil + } + + if (isBanned == true){ + bannedReviewersList = append(bannedReviewersList, moderatorIdentityHash) + } else{ + notBannedReviewersList = append(notBannedReviewersList, moderatorIdentityHash) + } + } + + err := helpers.SortIdentityHashListToUnicodeOrder(notBannedReviewersList) + if (err != nil) { return false, false, nil, err } + err = helpers.SortIdentityHashListToUnicodeOrder(bannedReviewersList) + if (err != nil) { return false, false, nil, err } + + allReviewersListSorted := slices.Concat(notBannedReviewersList, bannedReviewersList) + + //TODO: Add pages and navigation + + emptyLabel1 := widget.NewLabel("") + emptyLabel2 := widget.NewLabel("") + emptyLabel3 := widget.NewLabel("") + emptyLabel4 := widget.NewLabel("") + emptyLabel5 := widget.NewLabel("") + approvedLabel := getItalicLabelCentered("Approved") + bannedLabel := getItalicLabelCentered("Banned") + + emptyLabel6 := widget.NewLabel("") + authorLabel := getItalicLabelCentered("Author") + isBannedLabel := getItalicLabelCentered("Is Banned") + verdictLabel := getItalicLabelCentered("Verdict") + fullProfileLabel := getItalicLabelCentered("Full Profile") + attributesLabelA := getItalicLabelCentered("Attributes") + attributesLabelB := getItalicLabelCentered("Attributes") + + viewModeratorButtonsColumn := container.NewVBox(emptyLabel1, emptyLabel6, widget.NewSeparator()) + moderatorIdentityHashColumn := container.NewVBox(emptyLabel2, authorLabel, widget.NewSeparator()) + moderatorIsBannedColumn := container.NewVBox(emptyLabel3, isBannedLabel, widget.NewSeparator()) + verdictColumn := container.NewVBox(emptyLabel4, verdictLabel, widget.NewSeparator()) + fullProfileReviewColumn := container.NewVBox(emptyLabel5, fullProfileLabel, widget.NewSeparator()) + approvedAttributesColumn := container.NewVBox(approvedLabel, attributesLabelA, widget.NewSeparator()) + bannedAttributesColumn := container.NewVBox(bannedLabel, attributesLabelB, widget.NewSeparator()) + + for _, moderatorIdentityHash := range allReviewersListSorted{ + + viewModeratorButton := widget.NewButtonWithIcon("", theme.VisibilityIcon(), func(){ + setViewModeratorDetailsPage(window, moderatorIdentityHash, currentPage) + }) + + moderatorIdentityHashString, _, err := identity.EncodeIdentityHashBytesToString(moderatorIdentityHash) + if (err != nil) { return false, false, nil, err } + + identityHashTrimmed, _, err := helpers.TrimAndFlattenString(moderatorIdentityHashString, 10) + if (err != nil) { return false, false, nil, err } + identityHashLabel := getBoldLabelCentered(identityHashTrimmed) + + moderatorIsBannedBool := slices.Contains(bannedReviewersList, moderatorIdentityHash) + moderatorIsBanned := helpers.ConvertBoolToYesOrNoString(moderatorIsBannedBool) + moderatorIsBannedTranslated := translate(moderatorIsBanned) + + moderatorIsBannedLabel := getBoldLabelCentered(moderatorIsBannedTranslated) + + profileVerdictExists, profileVerdict, fullProfileReviewExists, fullProfileReviewBytes, _, attributeApproveReviewsMap, attributeBanReviewsMap, err := reviewStorage.GetModeratorNewestProfileReviews(moderatorIdentityHash, profileHash, profileNetworkType, profileAttributeHashesList) + if (err != nil) { return false, false, nil, err } + if (profileVerdictExists == false){ + // Review was deleted or changed after reviewers were retrieved + continue + } + + profileVerdictLabel := getBoldLabelCentered(profileVerdict) + + viewModeratorButtonsColumn.Add(viewModeratorButton) + moderatorIdentityHashColumn.Add(identityHashLabel) + moderatorIsBannedColumn.Add(moderatorIsBannedLabel) + verdictColumn.Add(profileVerdictLabel) + + if (fullProfileReviewExists == false){ + + noneLabel := getLabelCentered("None") + + fullProfileReviewColumn.Add(noneLabel) + } else { + + ableToRead, reviewHash, _, reviewNetworkType, retrievedModeratorIdentityHash, _, _, currentReviewedHash, _, _, err := readReviews.ReadReviewAndHash(false, fullProfileReviewBytes) + if (err != nil) { return false, false, nil, err } + if (ableToRead == false){ + return false, false, nil, errors.New("GetModeratorNewestProfileReviews returning invalid full profile review.") + } + if (reviewNetworkType != profileNetworkType){ + return false, false, nil, errors.New("GetModeratorNewestProfileReviews returning full profile review belonging to different network type.") + } + if (moderatorIdentityHash != retrievedModeratorIdentityHash) { + return false, false, nil, errors.New("GetModeratorNewestProfileReviews returning full profile review by different moderator.") + } + areEqual := bytes.Equal(currentReviewedHash, profileHash[:]) + if (areEqual == false){ + return false, false, nil, errors.New("GetModeratorNewestProfileReviews returning full profile review for different reviewedHash.") + } + + fullProfileReviewButton := widget.NewButtonWithIcon("", theme.VisibilityIcon(), func(){ + setViewReviewDetailsPage(window, reviewHash, currentPage) + }) + + fullProfileReviewColumn.Add(fullProfileReviewButton) + } + + if (len(attributeApproveReviewsMap) == 0){ + + noneLabel := getLabelCentered("None") + approvedAttributesColumn.Add(noneLabel) + } else { + + numberOfApprovedAttributes := len(attributeApproveReviewsMap) + numberOfApprovedAttributesString := helpers.ConvertIntToString(numberOfApprovedAttributes) + + viewApprovedAttributesButton := widget.NewButtonWithIcon(numberOfApprovedAttributesString, theme.VisibilityIcon(), func(){ + //TODO: A page to view the attribute approvals + showUnderConstructionDialog(window) + }) + + approvedAttributesColumn.Add(viewApprovedAttributesButton) + } + + if (len(attributeBanReviewsMap) == 0){ + + noneLabel := getLabelCentered("None") + bannedAttributesColumn.Add(noneLabel) + } else { + + numberOfBannedAttributes := len(attributeBanReviewsMap) + numberOfBannedAttributesString := helpers.ConvertIntToString(numberOfBannedAttributes) + + viewBannedAttributesButton := widget.NewButtonWithIcon(numberOfBannedAttributesString, theme.VisibilityIcon(), func(){ + //TODO: A page to view the attribute bans + showUnderConstructionDialog(window) + }) + + bannedAttributesColumn.Add(viewBannedAttributesButton) + } + + viewModeratorButtonsColumn.Add(widget.NewSeparator()) + moderatorIdentityHashColumn.Add(widget.NewSeparator()) + moderatorIsBannedColumn.Add(widget.NewSeparator()) + fullProfileReviewColumn.Add(widget.NewSeparator()) + approvedAttributesColumn.Add(widget.NewSeparator()) + bannedAttributesColumn.Add(widget.NewSeparator()) + } + + reviewsGrid := container.NewHBox(layout.NewSpacer(), viewModeratorButtonsColumn, moderatorIdentityHashColumn, moderatorIsBannedColumn, fullProfileReviewColumn, approvedAttributesColumn, bannedAttributesColumn, layout.NewSpacer()) + + return true, true, reviewsGrid, nil + } + + moderatorModeEnabled, parametersExist, verdictsGrid, err := getReviewerVerdictsGrid() + if (err != nil){ + setErrorEncounteredPage(window, err, previousPage) + return + } + if (moderatorModeEnabled == false){ + + description1 := getBoldLabelCentered("Moderator mode is disabled.") + description2 := getLabelCentered("You must enable moderator mode to view the reviewer verdicts.") + + enableModeratorModeButton := getWidgetCentered(widget.NewButtonWithIcon("Enable Moderator Mode", theme.NavigateNextIcon(), func(){ + setManageModeratorModePage(window, currentPage) + })) + + page := container.NewVBox(title, backButton, widget.NewSeparator(), description1, description2, enableModeratorModeButton) + setPageContent(page, window) + return + } + if (parametersExist == false){ + description1 := getBoldLabelCentered("Your client is missing the moderation parameters.") + description2 := getLabelCentered("Please wait for them to download.") + + refreshButton := getWidgetCentered(widget.NewButtonWithIcon("Refresh", theme.ViewRefreshIcon(), currentPage)) + + //TODO: Add button to view progress + + page := container.NewVBox(title, backButton, widget.NewSeparator(), description1, description2, refreshButton) + setPageContent(page, window) + return + } + + page := container.NewVBox(title, backButton, widget.NewSeparator(), description, widget.NewSeparator(), verdictsGrid) + + setPageContent(page, window) +} + +// This page will show all moderators who have banned/approved the attribute +func setViewAttributeReviewerVerdictsPage(window fyne.Window, attributeHash [27]byte, viewIndex int, previousPage func()){ + + setLoadingScreen(window, "View Attribute Verdicts", "Loading verdicts...") + + currentPage := func(){setViewAttributeReviewerVerdictsPage(window, attributeHash, viewIndex, previousPage)} + + title := getPageTitleCentered("View Attribute Verdicts") + + backButton := getBackButtonCentered(previousPage) + + attributeMetadataIsKnown, _, attributeAuthor, attributeNetworkType, attributeProfileHashesList, err := profileStorage.GetProfileAttributeMetadata(attributeHash) + if (err != nil){ + setErrorEncounteredPage(window, err, previousPage) + return + } + if (attributeMetadataIsKnown == false){ + + description1 := getBoldLabelCentered("The attribute is not downloaded.") + description2 := getLabelCentered("You must download it to view its reviews.") + description3 := getLabelCentered("It may not be possible if the attribute has been deleted from the network.") + description4 := getLabelCentered("Try to download the attribute?") + + downloadButton := getWidgetCentered(widget.NewButtonWithIcon("Download", theme.DownloadIcon(), func(){ + //TODO: Create a way to download profiles which contain an attribute + showUnderConstructionDialog(window) + })) + + page := container.NewVBox(title, backButton, widget.NewSeparator(), description1, description2, description3, description4, downloadButton) + + setPageContent(page, window) + return + } + + appNetworkType, err := getAppNetworkType.GetAppNetworkType() + if (err != nil) { + setErrorEncounteredPage(window, err, previousPage) + return + } + if (appNetworkType != attributeNetworkType){ + setErrorEncounteredPage(window, errors.New("setViewAttributeReviewerVerdictsPage called with attribute belonging to different network type than app."), previousPage) + return + } + + downloadingRequiredReviews, err := backgroundDownloads.CheckIfAppCanDetermineIdentityVerdicts(attributeAuthor) + if (err != nil) { + setErrorEncounteredPage(window, err, previousPage) + return + } + if (downloadingRequiredReviews == false){ + + description1 := getBoldLabelCentered("This attribute's author is outside of your moderator range.") + description2 := getLabelCentered("Download the reviews for this attribute?") + + downloadButton := getWidgetCentered(widget.NewButton("Download", func(){ + setDownloadReviewsAndReportsForReviewedHashPage(window, attributeHash[:], currentPage, currentPage) + })) + + page := container.NewVBox(title, backButton, widget.NewSeparator(), description1, description2, downloadButton) + setPageContent(page, window) + return + } + + attributeApproveAdvocatesMap, attributeBanAdvocatesMap, err := reviewStorage.GetProfileAttributeVerdictMaps(attributeHash, attributeNetworkType, true, attributeProfileHashesList) + if (err != nil){ + setErrorEncounteredPage(window, err, previousPage) + return + } + if (len(attributeApproveAdvocatesMap) == 0 && len(attributeBanAdvocatesMap) == 0){ + + noReviewersDescription := getBoldLabelCentered("No moderators have reviewed the attribute.") + + updateButton := getWidgetCentered(widget.NewButtonWithIcon("Update", theme.ViewRefreshIcon(), currentPage)) + + page := container.NewVBox(title, backButton, widget.NewSeparator(), noReviewersDescription, updateButton) + + setPageContent(page, window) + return + } + + description1 := getLabelCentered("These are the moderators who have reviewed the attribute.") + description2 := getLabelCentered("This includes moderators who have approved a profile that contains this attribute.") + + //TODO: Navigation pages and buttons + + //Outputs: + // -bool: Downloading required reviews/profiles (moderator mode is enabled) + // -bool: Parameters exist + // -*fyne.Container: Reviewer verdicts grid + // -error + getReviewerVerdictsGrid := func()(bool, bool, *fyne.Container, error){ + + approveAdvocatesList := helpers.GetListOfMapKeys(attributeApproveAdvocatesMap) + banAdvocatesList := helpers.GetListOfMapKeys(attributeApproveAdvocatesMap) + + allReviewersList := slices.Concat(approveAdvocatesList, banAdvocatesList) + + notBannedReviewersList := make([][16]byte, 0) + bannedReviewersList := make([][16]byte, 0) + + for _, moderatorIdentityHash := range allReviewersList{ + + requiredDataIsBeingDownloaded, parametersExist, isBanned, err := bannedModeratorConsensus.GetModeratorIsBannedStatus(false, moderatorIdentityHash, attributeNetworkType) + if (err != nil) { return false, false, nil, err } + if (requiredDataIsBeingDownloaded == false){ + return false, parametersExist, nil, nil + } + if (parametersExist == false){ + return true, false, nil, nil + } + + if (isBanned == true){ + bannedReviewersList = append(bannedReviewersList, moderatorIdentityHash) + } else{ + notBannedReviewersList = append(notBannedReviewersList, moderatorIdentityHash) + } + } + + err := helpers.SortIdentityHashListToUnicodeOrder(notBannedReviewersList) + if (err != nil) { return false, false, nil, err } + err = helpers.SortIdentityHashListToUnicodeOrder(bannedReviewersList) + if (err != nil) { return false, false, nil, err } + + allReviewersListSorted := slices.Concat(notBannedReviewersList, bannedReviewersList) + + //TODO: Add pages and navigation + + emptyLabelA := widget.NewLabel("") + authorLabel := getItalicLabelCentered("Author") + isBannedLabel := getItalicLabelCentered("Is Banned") + reviewTypeTitle := getItalicLabelCentered("Review Type") + verdictLabel := getItalicLabelCentered("Verdict") + reasonLabel := getItalicLabelCentered("Reason") + emptyLabelC := widget.NewLabel("") + + viewModeratorButtonsColumn := container.NewVBox(emptyLabelA, widget.NewSeparator()) + moderatorIdentityHashColumn := container.NewVBox(authorLabel, widget.NewSeparator()) + moderatorIsBannedColumn := container.NewVBox(isBannedLabel, widget.NewSeparator()) + reviewTypeColumn := container.NewVBox(reviewTypeTitle, widget.NewSeparator()) + verdictColumn := container.NewVBox(verdictLabel, widget.NewSeparator()) + reasonColumn := container.NewVBox(reasonLabel, widget.NewSeparator()) + viewReviewButtonsColumn := container.NewVBox(emptyLabelC, widget.NewSeparator()) + + for _, moderatorIdentityHash := range allReviewersListSorted{ + + viewModeratorButton := widget.NewButtonWithIcon("", theme.VisibilityIcon(), func(){ + setViewModeratorDetailsPage(window, moderatorIdentityHash, currentPage) + }) + + moderatorIdentityHashString, _, err := identity.EncodeIdentityHashBytesToString(moderatorIdentityHash) + if (err != nil) { return false, false, nil, err } + + identityHashTrimmed, _, err := helpers.TrimAndFlattenString(moderatorIdentityHashString, 10) + if (err != nil) { return false, false, nil, err } + identityHashLabel := getBoldLabelCentered(identityHashTrimmed) + + moderatorIsBannedBool := slices.Contains(bannedReviewersList, moderatorIdentityHash) + moderatorIsBanned := helpers.ConvertBoolToYesOrNoString(moderatorIsBannedBool) + moderatorIsBannedTranslated := translate(moderatorIsBanned) + + moderatorIsBannedLabel := getBoldLabelCentered(moderatorIsBannedTranslated) + + reviewFound, reviewType, newestReviewBytes, newestReviewMap, reviewVerdict, _, err := reviewStorage.GetModeratorNewestProfileAttributeReview(moderatorIdentityHash, attributeHash, attributeNetworkType, true) + if (err != nil) { return false, false, nil, err } + if (reviewFound == false){ + // Review was deleted or changed after reviewers were retrieved + continue + } + + reviewTypeLabel := getBoldLabelCentered(reviewType) + + ableToRead, reviewHash, _, reviewNetworkType, retrievedModeratorIdentityHash, _, _, _, _, _, err := readReviews.ReadReviewAndHash(false, newestReviewBytes) + if (err != nil) { return false, false, nil, err } + if (ableToRead == false){ + return false, false, nil, errors.New("GetModeratorNewestProfileAttributeReview returning invalid review.") + } + if (reviewNetworkType != attributeNetworkType){ + return false, false, nil, errors.New("GetModeratorNewestProfileAttributeReview returning review belonging to different networkType.") + } + if (moderatorIdentityHash != retrievedModeratorIdentityHash) { + return false, false, nil, errors.New("GetModeratorNewestProfileAttributeReview returning review by different moderator.") + } + + reviewVerdictTranslated := translate(reviewVerdict) + reviewVerdictLabel := getBoldLabelCentered(reviewVerdictTranslated) + + getViewReasonButtonOrText := func()(*fyne.Container, error){ + + reasonString, exists := newestReviewMap["Reason"] + if (exists == false) { + noneLabel := getBoldLabelCentered(translate("None")) + return noneLabel, nil + } + + viewReasonButton := getWidgetCentered(widget.NewButtonWithIcon("", theme.VisibilityIcon(), func(){ + setViewTextPage(window, "Viewing Verdict Reason", reasonString, false, currentPage) + })) + + return viewReasonButton, nil + } + viewReasonButtonOrText, err := getViewReasonButtonOrText() + if (err != nil) { return false, false, nil, err } + + viewReviewButton := widget.NewButtonWithIcon("View Review", theme.VisibilityIcon(), func(){ + setViewReviewDetailsPage(window, reviewHash, currentPage) + }) + + viewModeratorButtonsColumn.Add(viewModeratorButton) + moderatorIdentityHashColumn.Add(identityHashLabel) + moderatorIsBannedColumn.Add(moderatorIsBannedLabel) + reviewTypeColumn.Add(reviewTypeLabel) + verdictColumn.Add(reviewVerdictLabel) + reasonColumn.Add(viewReasonButtonOrText) + viewReviewButtonsColumn.Add(viewReviewButton) + + viewModeratorButtonsColumn.Add(widget.NewSeparator()) + moderatorIdentityHashColumn.Add(widget.NewSeparator()) + moderatorIsBannedColumn.Add(widget.NewSeparator()) + reviewTypeColumn.Add(widget.NewSeparator()) + verdictColumn.Add(widget.NewSeparator()) + reasonColumn.Add(widget.NewSeparator()) + viewReviewButtonsColumn.Add(widget.NewSeparator()) + } + + reviewsGrid := container.NewHBox(layout.NewSpacer(), viewModeratorButtonsColumn, moderatorIdentityHashColumn, moderatorIsBannedColumn, reviewTypeColumn, verdictColumn, reasonColumn, viewReviewButtonsColumn, layout.NewSpacer()) + + return true, true, reviewsGrid, nil + } + + moderatorModeEnabled, parametersExist, verdictsGrid, err := getReviewerVerdictsGrid() + if (err != nil){ + setErrorEncounteredPage(window, err, previousPage) + return + } + if (moderatorModeEnabled == false){ + + description1 := getBoldLabelCentered("Moderator mode is disabled.") + description2 := getLabelCentered("You must enable moderator mode to view the reviewer verdicts.") + + enableModeratorModeButton := getWidgetCentered(widget.NewButtonWithIcon("Enable Moderator Mode", theme.NavigateNextIcon(), func(){ + setManageModeratorModePage(window, currentPage) + })) + + page := container.NewVBox(title, backButton, widget.NewSeparator(), description1, description2, enableModeratorModeButton) + setPageContent(page, window) + return + } + if (parametersExist == false){ + description1 := getBoldLabelCentered("Your client is missing the moderation parameters.") + description2 := getLabelCentered("Please wait for them to download.") + + refreshButton := getWidgetCentered(widget.NewButtonWithIcon("Refresh", theme.ViewRefreshIcon(), currentPage)) + + //TODO: Add button to view progress + + page := container.NewVBox(title, backButton, widget.NewSeparator(), description1, description2, refreshButton) + setPageContent(page, window) + return + } + + page := container.NewVBox(title, backButton, widget.NewSeparator(), description1, description2, verdictsGrid) + + setPageContent(page, window) +} + + +// This page will show all moderators who have banned/approved the message +func setViewMessageReviewerVerdictsPage(window fyne.Window, messageHash [26]byte, viewIndex int, previousPage func()){ + + setLoadingScreen(window, "View Message Verdicts", "Loading verdicts...") + + currentPage := func(){setViewMessageReviewerVerdictsPage(window, messageHash, viewIndex, previousPage)} + + title := getPageTitleCentered("View Message Verdicts") + + backButton := getBackButtonCentered(previousPage) + + messageMetadataIsKnown, _, messageNetworkType, _, messageInbox, messageCipherKeyHash, err := contentMetadata.GetMessageMetadata(messageHash) + if (err != nil) { + setErrorEncounteredPage(window, err, previousPage) + return + } + if (messageMetadataIsKnown == false){ + + description1 := getBoldLabelCentered("The message is not downloaded.") + description2 := getLabelCentered("You must download the message to view its reviews.") + description3 := getLabelCentered("It may not be possible if the message has been deleted from the network.") + description4 := getLabelCentered("Try to download the message?") + + downloadButton := getWidgetCentered(widget.NewButtonWithIcon("Download", theme.DownloadIcon(), func(){ + //TODO + showUnderConstructionDialog(window) + })) + + page := container.NewVBox(title, backButton, widget.NewSeparator(), description1, description2, description3, description4, downloadButton) + + setPageContent(page, window) + return + } + + appNetworkType, err := getAppNetworkType.GetAppNetworkType() + if (err != nil) { + setErrorEncounteredPage(window, err, previousPage) + return + } + if (appNetworkType != messageNetworkType){ + // TODO: Show an explanatory page instead of an error. Users may want to view messages by pasting a hash into a text entry lookup, without knowing which network type the message belongs to. + // Add this explanatory page for all of the relevant pages within the moderatorGui file. + setErrorEncounteredPage(window, errors.New("setViewMessageReviewerVerdictsPage called with message belonging to different network type than app."), previousPage) + return + } + + downloadingRequiredReviews, err := backgroundDownloads.CheckIfAppCanDetermineMessageVerdict(messageNetworkType, messageInbox, true, messageHash) + if (err != nil) { + setErrorEncounteredPage(window, err, previousPage) + return + } + if (downloadingRequiredReviews == false){ + + description1 := getBoldLabelCentered("This message's inbox is outside of your moderator range.") + description2 := getLabelCentered("Download the reviews for this message?") + + downloadButton := getWidgetCentered(widget.NewButton("Download", func(){ + setDownloadReviewsAndReportsForReviewedHashPage(window, messageHash[:], currentPage, currentPage) + })) + + page := container.NewVBox(title, backButton, widget.NewSeparator(), description1, description2, downloadButton) + setPageContent(page, window) + return + } + + approveAdvocatesMap, banAdvocatesMap, err := reviewStorage.GetMessageVerdictMaps(messageHash, messageNetworkType, messageCipherKeyHash) + if (err != nil){ + setErrorEncounteredPage(window, err, previousPage) + return + } + + if (len(approveAdvocatesMap) == 0 && len(banAdvocatesMap) == 0){ + + noReviewersDescription := getBoldLabelCentered("No moderators have reviewed the message.") + + updateButton := getWidgetCentered(widget.NewButtonWithIcon("Update", theme.ViewRefreshIcon(), currentPage)) + + page := container.NewVBox(title, backButton, widget.NewSeparator(), noReviewersDescription, updateButton) + + setPageContent(page, window) + return + } + + description := getLabelCentered("Below are the moderators who have reviewed the message.") + + //TODO: Navigation button and pages + + //Outputs: + // -bool: Downloading required reviews/profiles (moderator mode is enabled) + // -bool: Parameters exist + // -*fyne.Container: Reviewer verdicts grid + // -error + getReviewerVerdictsGrid := func()(bool, bool, *fyne.Container, error){ + + approveAdvocatesList := helpers.GetListOfMapKeys(approveAdvocatesMap) + banAdvocatesList := helpers.GetListOfMapKeys(banAdvocatesMap) + + allReviewersList := slices.Concat(approveAdvocatesList, banAdvocatesList) + + notBannedReviewersList := make([][16]byte, 0) + bannedReviewersList := make([][16]byte, 0) + + for _, moderatorIdentityHash := range allReviewersList{ + + requiredDataIsBeingDownloaded, parametersExist, isBanned, err := bannedModeratorConsensus.GetModeratorIsBannedStatus(false, moderatorIdentityHash, messageNetworkType) + if (err != nil) { return false, false, nil, err } + if (requiredDataIsBeingDownloaded == false){ + return false, parametersExist, nil, nil + } + if (parametersExist == false){ + return true, false, nil, nil + } + + if (isBanned == true){ + bannedReviewersList = append(bannedReviewersList, moderatorIdentityHash) + } else{ + notBannedReviewersList = append(notBannedReviewersList, moderatorIdentityHash) + } + } + + err := helpers.SortIdentityHashListToUnicodeOrder(notBannedReviewersList) + if (err != nil) { return false, false, nil, err } + err = helpers.SortIdentityHashListToUnicodeOrder(bannedReviewersList) + if (err != nil) { return false, false, nil, err } + + allReviewersListSorted := slices.Concat(notBannedReviewersList, bannedReviewersList) + + //TODO: Add pages and navigation + + emptyLabelA := widget.NewLabel("") + authorLabel := getItalicLabelCentered("Author") + isBannedLabel := getItalicLabelCentered("Is Banned") + verdictLabel := getItalicLabelCentered("Verdict") + reasonLabel := getItalicLabelCentered("Reason") + emptyLabelC := widget.NewLabel("") + + viewModeratorButtonsColumn := container.NewVBox(emptyLabelA, widget.NewSeparator()) + moderatorIdentityHashColumn := container.NewVBox(authorLabel, widget.NewSeparator()) + moderatorIsBannedColumn := container.NewVBox(isBannedLabel, widget.NewSeparator()) + verdictColumn := container.NewVBox(verdictLabel, widget.NewSeparator()) + reasonColumn := container.NewVBox(reasonLabel, widget.NewSeparator()) + viewReviewButtonsColumn := container.NewVBox(emptyLabelC, widget.NewSeparator()) + + for _, moderatorIdentityHash := range allReviewersListSorted{ + + viewModeratorButton := widget.NewButtonWithIcon("", theme.VisibilityIcon(), func(){ + setViewModeratorDetailsPage(window, moderatorIdentityHash, currentPage) + }) + + moderatorIdentityHashString, _, err := identity.EncodeIdentityHashBytesToString(moderatorIdentityHash) + if (err != nil) { return false, false, nil, err } + + identityHashTrimmed, _, err := helpers.TrimAndFlattenString(moderatorIdentityHashString, 10) + if (err != nil) { return false, false, nil, err } + identityHashLabel := getBoldLabelCentered(identityHashTrimmed) + + moderatorIsBannedBool := slices.Contains(bannedReviewersList, moderatorIdentityHash) + moderatorIsBanned := helpers.ConvertBoolToYesOrNoString(moderatorIsBannedBool) + moderatorIsBannedTranslated := translate(moderatorIsBanned) + + moderatorIsBannedLabel := getBoldLabelCentered(moderatorIsBannedTranslated) + + reviewFound, newestReviewBytes, _, err := reviewStorage.GetModeratorNewestMessageReview(moderatorIdentityHash, messageHash, messageNetworkType, messageCipherKeyHash) + if (err != nil) { return false, false, nil, err } + if (reviewFound == false){ + // Review was deleted or changed after reviewers were retrieved + continue + } + + ableToRead, reviewHash, _, reviewNetworkType, retrievedModeratorIdentityHash, _, _, currentReviewedHash, reviewVerdict, reviewMap, err := readReviews.ReadReviewAndHash(false, newestReviewBytes) + if (err != nil) { return false, false, nil, err } + if (ableToRead == false){ + return false, false, nil, errors.New("GetModeratorNewestMessageReview returning invalid review.") + } + if (reviewNetworkType != messageNetworkType){ + return false, false, nil, errors.New("GetModeratorNewestMessageReview returning review belonging to different networkType.") + } + if (moderatorIdentityHash != retrievedModeratorIdentityHash) { + return false, false, nil, errors.New("GetModeratorNewestMessageReview returning review by different moderator.") + } + areEqual := bytes.Equal(currentReviewedHash, messageHash[:]) + if (areEqual == false){ + return false, false, nil, errors.New("GetModeratorNewestMessageReview returning review for different reviewedHash.") + } + + reviewVerdictTranslated := translate(reviewVerdict) + reviewVerdictLabel := getBoldLabelCentered(reviewVerdictTranslated) + + getViewReasonButtonOrText := func()(*fyne.Container, error){ + + reasonString, exists := reviewMap["Reason"] + if (exists == false) { + noneLabel := getBoldLabelCentered(translate("None")) + return noneLabel, nil + } + + viewReasonButton := getWidgetCentered(widget.NewButtonWithIcon("", theme.VisibilityIcon(), func(){ + setViewTextPage(window, "Viewing Verdict Reason", reasonString, false, currentPage) + })) + + return viewReasonButton, nil + } + viewReasonButtonOrText, err := getViewReasonButtonOrText() + if (err != nil) { return false, false, nil, err } + + viewReviewButton := widget.NewButtonWithIcon("View Review", theme.VisibilityIcon(), func(){ + setViewReviewDetailsPage(window, reviewHash, currentPage) + }) + + viewModeratorButtonsColumn.Add(viewModeratorButton) + moderatorIdentityHashColumn.Add(identityHashLabel) + moderatorIsBannedColumn.Add(moderatorIsBannedLabel) + verdictColumn.Add(reviewVerdictLabel) + reasonColumn.Add(viewReasonButtonOrText) + viewReviewButtonsColumn.Add(viewReviewButton) + + viewModeratorButtonsColumn.Add(widget.NewSeparator()) + moderatorIdentityHashColumn.Add(widget.NewSeparator()) + moderatorIsBannedColumn.Add(widget.NewSeparator()) + verdictColumn.Add(widget.NewSeparator()) + reasonColumn.Add(widget.NewSeparator()) + viewReviewButtonsColumn.Add(widget.NewSeparator()) + } + + reviewsGrid := container.NewHBox(layout.NewSpacer(), viewModeratorButtonsColumn, moderatorIdentityHashColumn, moderatorIsBannedColumn, verdictColumn, reasonColumn, viewReviewButtonsColumn, layout.NewSpacer()) + + return true, true, reviewsGrid, nil + } + + moderatorModeEnabled, parametersExist, verdictsGrid, err := getReviewerVerdictsGrid() + if (err != nil){ + setErrorEncounteredPage(window, err, previousPage) + return + } + if (moderatorModeEnabled == false){ + + description1 := getBoldLabelCentered("Moderator mode is disabled.") + description2 := getLabelCentered("You must enable moderator mode to view the reviewer verdicts.") + + enableModeratorModeButton := getWidgetCentered(widget.NewButtonWithIcon("Enable Moderator Mode", theme.NavigateNextIcon(), func(){ + setManageModeratorModePage(window, currentPage) + })) + + page := container.NewVBox(title, backButton, widget.NewSeparator(), description1, description2, enableModeratorModeButton) + setPageContent(page, window) + return + } + if (parametersExist == false){ + + description1 := getBoldLabelCentered("Your app is missing the moderation parameters.") + description2 := getLabelCentered("Please wait for them to download.") + + refreshButton := getWidgetCentered(widget.NewButtonWithIcon("Refresh", theme.ViewRefreshIcon(), currentPage)) + + //TODO: Add button to view progress + + page := container.NewVBox(title, backButton, widget.NewSeparator(), description1, description2, refreshButton) + setPageContent(page, window) + return + } + + page := container.NewVBox(title, backButton, widget.NewSeparator(), description, widget.NewSeparator(), verdictsGrid) + + setPageContent(page, window) +} + + +func setViewModeratorDetailsPage(window fyne.Window, moderatorIdentityHash [16]byte, previousPage func()){ + + appMemory.SetMemoryEntry("CurrentViewedPage", "ModeratorDetails") + + currentPage := func(){setViewModeratorDetailsPage(window, moderatorIdentityHash, previousPage)} + + moderatorIdentityHashString, userIdentityType, err := identity.EncodeIdentityHashBytesToString(moderatorIdentityHash) + if (err != nil) { + setErrorEncounteredPage(window, err, previousPage) + return + } + if (userIdentityType != "Moderator"){ + setErrorEncounteredPage(window, errors.New("ViewModeratorDetailsPage called with non-moderator identity: " + moderatorIdentityHashString), previousPage) + return + } + + title := getPageTitleCentered("Moderator Details") + + backButton := getBackButtonCentered(previousPage) + + identityHashLabel := widget.NewLabel("Identity Hash:") + identityHashText := getBoldLabel(moderatorIdentityHashString) + identityHashRow := container.NewHBox(layout.NewSpacer(), identityHashLabel, identityHashText, layout.NewSpacer()) + + appNetworkType, err := getAppNetworkType.GetAppNetworkType() + if (err != nil) { + setErrorEncounteredPage(window, err, previousPage) + return + } + + rankingKnown, moderatorRank, totalNumberOfModerators, err := moderatorRanking.GetModeratorRanking(moderatorIdentityHash, appNetworkType) + if (err != nil){ + setErrorEncounteredPage(window, err, previousPage) + return + } + + getModeratorRankText := func()string{ + if (rankingKnown == false){ + result := translate("Unknown") + return result + } + + moderatorRankString := helpers.ConvertIntToString(moderatorRank) + totalNumberOfModeratorsString := helpers.ConvertIntToString(totalNumberOfModerators) + + moderatorRankText := moderatorRankString + "/" + totalNumberOfModeratorsString + + return moderatorRankText + } + + moderatorRankText := getModeratorRankText() + + moderatorRankTitle := widget.NewLabel("Moderator Rank:") + moderatorRankLabel := getBoldLabel(moderatorRankText) + + moderatorRankRow := container.NewHBox(layout.NewSpacer(), moderatorRankTitle, moderatorRankLabel, layout.NewSpacer()) + + showModeratorModeIsDisabledPage := func(){ + + description1 := getBoldLabelCentered("Moderator mode is disabled.") + description2 := getLabelCentered("You must enable moderator mode to view the moderator details.") + + enableModeratorModeButton := getWidgetCentered(widget.NewButtonWithIcon("Enable Moderator Mode", theme.NavigateNextIcon(), func(){ + setManageModeratorModePage(window, currentPage) + })) + + page := container.NewVBox(title, backButton, widget.NewSeparator(), description1, description2, enableModeratorModeButton) + setPageContent(page, window) + } + + moderatorModeIsEnabled, parametersExist, isBannedStatus, err := bannedModeratorConsensus.GetModeratorIsBannedStatus(false, moderatorIdentityHash, appNetworkType) + if (err != nil) { + setErrorEncounteredPage(window, err, previousPage) + return + } + if (moderatorModeIsEnabled == false){ + + showModeratorModeIsDisabledPage() + return + } + if (parametersExist == false){ + + description1 := getBoldLabelCentered("Your client is missing the moderation parameters.") + description2 := getLabelCentered("Please wait for them to download.") + + refreshButton := getWidgetCentered(widget.NewButtonWithIcon("Refresh", theme.ViewRefreshIcon(), currentPage)) + + //TODO: Add button to view progress + + page := container.NewVBox(title, backButton, widget.NewSeparator(), description1, description2, refreshButton) + setPageContent(page, window) + return + } + + getBannedStatusString := func()string{ + if (isBannedStatus == true){ + return "Banned" + } + return "Not Banned" + } + bannedStatusString := getBannedStatusString() + + isBannedLabel := widget.NewLabel("Moderation Status:") + bannedStatusLabel := getBoldLabel(bannedStatusString) + bannedStatusRow := container.NewHBox(layout.NewSpacer(), isBannedLabel, bannedStatusLabel, layout.NewSpacer()) + + downloadingRequiredReviews, numberOfBanAdvocates, err := reviewStorage.GetNumberOfBanAdvocatesForIdentity(moderatorIdentityHash, appNetworkType) + if (err != nil){ + setErrorEncounteredPage(window, err, previousPage) + return + } + if (downloadingRequiredReviews == false){ + showModeratorModeIsDisabledPage() + return + } + + numberOfBanAdvocatesString := helpers.ConvertIntToString(numberOfBanAdvocates) + + numberOfBanAdvocatesLabel := widget.NewLabel("Number Of Ban Advocates:") + numberOfBanAdvocatesText := getBoldLabel(numberOfBanAdvocatesString) + numberOfBanAdvocatesRow := container.NewHBox(layout.NewSpacer(), numberOfBanAdvocatesLabel, numberOfBanAdvocatesText, layout.NewSpacer()) + + viewProfileButton := widget.NewButtonWithIcon("View Profile", theme.AccountIcon(), func(){ + setViewPeerProfilePageFromIdentityHash(window, moderatorIdentityHash, currentPage) + }) + + viewReviewsButton := widget.NewButtonWithIcon("View Reviews", theme.ListIcon(), func(){ + setViewAllReviewsCreatedByModeratorPage(window, moderatorIdentityHash, "Profile", 0, currentPage) + }) + + viewBanAdvocatesButton := widget.NewButtonWithIcon("View Ban Advocates", theme.ErrorIcon(), func(){ + setViewIdentityReviewerVerdictsPage(window, moderatorIdentityHash, 0, currentPage) + }) + + viewStatisticsButton := widget.NewButtonWithIcon("View Statistics", theme.InfoIcon(), func(){ + //TODO: A page to view a user's moderation statistics + // An example is the percentage of profiles/attributes/messages they have banned/approved + showUnderConstructionDialog(window) + }) + + buttonsGrid := getContainerCentered(container.NewGridWithColumns(1, viewProfileButton, viewReviewsButton, viewBanAdvocatesButton, viewStatisticsButton)) + + page := container.NewVBox(title, backButton, widget.NewSeparator(), identityHashRow, widget.NewSeparator(), moderatorRankRow, widget.NewSeparator(), bannedStatusRow, widget.NewSeparator(), numberOfBanAdvocatesRow, widget.NewSeparator(), buttonsGrid) + + setPageContent(page, window) +} + +func setViewAllReviewsCreatedByModeratorPage(window fyne.Window, moderatorIdentityHash [16]byte, reviewType string, viewIndex int64, previousPage func()){ + + if (reviewType != "Identity" && reviewType != "Profile" && reviewType != "Attribute" && reviewType != "Message"){ + setErrorEncounteredPage(window, errors.New("setViewAllReviewsCreatedByModeratorPage called with invalid reviewType: " + reviewType), previousPage) + return + } + + userIdentityType, err := identity.GetIdentityTypeFromIdentityHash(moderatorIdentityHash) + if (err != nil) { + moderatorIdentityHashHex := encoding.EncodeBytesToHexString(moderatorIdentityHash[:]) + setErrorEncounteredPage(window, errors.New("setViewAllReviewsCreatedByModeratorPage called with invalid identity hash: " + moderatorIdentityHashHex), previousPage) + return + } + if (userIdentityType != "Moderator"){ + setErrorEncounteredPage(window, errors.New("setViewAllReviewsCreatedByModeratorPage called with non-moderator identity."), previousPage) + return + } + + currentPage := func(){setViewAllReviewsCreatedByModeratorPage(window, moderatorIdentityHash, reviewType, viewIndex, previousPage)} + + title := getPageTitleCentered("Viewing Moderator Reviews") + + backButton := getBackButtonCentered(previousPage) + + reviewTypeTitle := getBoldLabelCentered("Review Type:") + + reviewTypesList := []string{"Identity", "Profile", "Attribute", "Message"} + + handleSelectFunction := func(newReviewType string){ + + setViewAllReviewsCreatedByModeratorPage(window, moderatorIdentityHash, newReviewType, 0, previousPage) + } + + reviewTypeSelector := widget.NewSelect(reviewTypesList, handleSelectFunction) + reviewTypeSelector.Selected = reviewType + + reviewTypeSelectorCentered := getWidgetCentered(reviewTypeSelector) + + description := getLabelCentered("These are the " + reviewType + " reviews created by the moderator.") + + getReviewsGrid := func()(*fyne.Container, error){ + + appNetworkType, err := getAppNetworkType.GetAppNetworkType() + if (err != nil) { return nil, err } + + reviewedHashLabel := getItalicLabelCentered("Reviewed Hash") + verdictLabel := getItalicLabelCentered("Verdict") + reasonLabel := getItalicLabelCentered("Reason") + emptyLabelA := widget.NewLabel("") + + reviewedHashColumn := container.NewVBox(reviewedHashLabel, widget.NewSeparator()) + verdictColumn := container.NewVBox(verdictLabel, widget.NewSeparator()) + reasonColumn := container.NewVBox(reasonLabel, widget.NewSeparator()) + viewReviewButtonsColumn := container.NewVBox(emptyLabelA, widget.NewSeparator()) + + reviewsList, err := reviewStorage.GetAllNewestReviewsCreatedByModerator(moderatorIdentityHash, reviewType, appNetworkType) + if (err != nil) { return nil, err } + + if (len(reviewsList) == 0){ + noReviewsExistLabel := getBoldLabelCentered("No " + reviewType + " reviews exist.") + return noReviewsExistLabel, nil + } + + //TODO: Add sort reviews alphabetically and navigation buttons + + for _, reviewBytes := range reviewsList{ + + ableToRead, reviewHash, _, reviewNetworkType, reviewerIdentityHash, _, currentReviewType, reviewedHash, reviewVerdict, reviewMap, err := readReviews.ReadReviewAndHash(false, reviewBytes) + if (err != nil) { return nil, err } + if (ableToRead == false){ + return nil, errors.New("GetAllNewestReviewsCreatedByModerator returning invalid review.") + } + if (reviewNetworkType != appNetworkType){ + return nil, errors.New("GetAllNewestReviewsCreatedByModerator returning review belonging to different networkType.") + } + if (reviewerIdentityHash != moderatorIdentityHash) { + return nil, errors.New("GetAllNewestReviewsCreatedByModerator returning review by different moderator.") + } + if (currentReviewType != reviewType){ + return nil, errors.New("GetAllNewestReviewsCreatedByModerator returning review of different reviewType") + } + + reviewedHashString, err := helpers.EncodeReviewedHashBytesToString(reviewedHash) + if (err != nil) { return nil, err } + + reviewedHashTrimmed, _, err := helpers.TrimAndFlattenString(reviewedHashString, 10) + if (err != nil) { return nil, err } + + reviewedHashLabel := getBoldLabelCentered(reviewedHashTrimmed) + + verdictLabel := getBoldLabelCentered(reviewVerdict) + + getViewReasonButtonOrLabel := func()*fyne.Container{ + reasonString, exists := reviewMap["Reason"] + if (exists == false) { + noneLabel := getBoldLabelCentered("None") + return noneLabel + } + viewReasonButton := getWidgetCentered(widget.NewButtonWithIcon("", theme.InfoIcon(), func(){ + setViewTextPage(window, "Viewing Verdict Reason", reasonString, false, currentPage) + })) + + return viewReasonButton + } + viewReasonButtonOrLabel := getViewReasonButtonOrLabel() + + viewReviewButton := widget.NewButtonWithIcon("", theme.VisibilityIcon(), func(){ + setViewReviewDetailsPage(window, reviewHash, currentPage) + }) + + reviewedHashColumn.Add(reviewedHashLabel) + verdictColumn.Add(verdictLabel) + reasonColumn.Add(viewReasonButtonOrLabel) + viewReviewButtonsColumn.Add(viewReviewButton) + + reviewedHashColumn.Add(widget.NewSeparator()) + verdictColumn.Add(widget.NewSeparator()) + reasonColumn.Add(widget.NewSeparator()) + viewReviewButtonsColumn.Add(widget.NewSeparator()) + } + + reviewsGrid := container.NewHBox(layout.NewSpacer(), reviewedHashColumn, verdictColumn, reasonColumn, viewReviewButtonsColumn, layout.NewSpacer()) + + return reviewsGrid, nil + } + + reviewsGrid, err := getReviewsGrid() + if (err != nil) { + setErrorEncounteredPage(window, err, previousPage) + return + } + + page := container.NewVBox(title, backButton, widget.NewSeparator(), reviewTypeTitle, reviewTypeSelectorCentered, widget.NewSeparator(), description, widget.NewSeparator(), reviewsGrid) + + setPageContent(page, window) +} + + +func setViewReviewDetailsPage(window fyne.Window, reviewHash [29]byte, previousPage func()){ + + currentPage := func(){setViewReviewDetailsPage(window, reviewHash, previousPage)} + + title := getPageTitleCentered("Viewing Review Details") + + backButton := getBackButtonCentered(previousPage) + + exists, reviewBytes, err := badgerDatabase.GetReview(reviewHash) + if (err != nil) { + setErrorEncounteredPage(window, err, previousPage) + return + } + if (exists == false){ + + description := getBoldLabelCentered("Review not found.") + + //TODO: Download review + + page := container.NewVBox(title, backButton, widget.NewSeparator(), description) + setPageContent(page, window) + return + } + + getPageContent := func()(*fyne.Container, error){ + + ableToRead, currentReviewHash, _, reviewNetworkType, reviewerIdentityHash, reviewBroadcastTime, reviewType, reviewedHash, reviewVerdict, reviewMap, err := readReviews.ReadReviewAndHash(false, reviewBytes) + if (err != nil) { return nil, err } + if (ableToRead == false){ + return nil, errors.New("Database corrupt: Contains invalid review.") + } + if (reviewHash != currentReviewHash) { + return nil, errors.New("Database corrupt: Review entry key does not match review hash") + } + + reviewNetworkTypeLabel := widget.NewLabel("Review Network Type:") + + reviewNetworkTypeString := helpers.ConvertByteToString(reviewNetworkType) + + reviewNetworkTypeText := getBoldLabel(reviewNetworkTypeString) + + reviewNetworkTypeRow := container.NewHBox(layout.NewSpacer(), reviewNetworkTypeLabel, reviewNetworkTypeText, layout.NewSpacer()) + + reviewAuthorLabel := widget.NewLabel("Review Author:") + + reviewerIdentityHashString, _, err := identity.EncodeIdentityHashBytesToString(reviewerIdentityHash) + if (err != nil) { return nil, err } + + reviewerIdentityHashTrimmed, _, err := helpers.TrimAndFlattenString(reviewerIdentityHashString, 15) + if (err != nil) { return nil, err } + + viewReviewAuthorButton := widget.NewButtonWithIcon(reviewerIdentityHashTrimmed, theme.VisibilityIcon(), func(){ + setViewModeratorDetailsPage(window, reviewerIdentityHash, currentPage) + }) + + reviewerIdentityRow := container.NewHBox(layout.NewSpacer(), reviewAuthorLabel, viewReviewAuthorButton, layout.NewSpacer()) + + reviewHashTitle := widget.NewLabel("Review Hash:") + + reviewHashHex := encoding.EncodeBytesToHexString(reviewHash[:]) + + reviewHashTrimmed, _, err := helpers.TrimAndFlattenString(reviewHashHex, 10) + if (err != nil) { return nil, err } + reviewHashLabel := getBoldLabel(reviewHashTrimmed) + viewReviewHashButton := widget.NewButtonWithIcon("", theme.VisibilityIcon(), func(){ + setViewContentHashPage(window, "Review", reviewHash[:], currentPage) + }) + reviewHashRow := container.NewHBox(layout.NewSpacer(), reviewHashTitle, reviewHashLabel, viewReviewHashButton, layout.NewSpacer()) + + reviewTypeTitle := widget.NewLabel("Review Type:") + reviewTypeLabel := getBoldLabel(reviewType) + reviewTypeRow := container.NewHBox(layout.NewSpacer(), reviewTypeTitle, reviewTypeLabel, layout.NewSpacer()) + + reviewedHashTitle := widget.NewLabel("Reviewed Hash:") + + reviewedHashString, err := helpers.EncodeReviewedHashBytesToString(reviewedHash) + if (err != nil) { return nil, err } + + reviewedHashTrimmed, _, err := helpers.TrimAndFlattenString(reviewedHashString, 10) + if (err != nil) { return nil, err } + reviewedHashLabel := getBoldLabel(reviewedHashTrimmed) + viewReviewedHashButton := widget.NewButtonWithIcon("", theme.VisibilityIcon(), func(){ + + if (reviewType == "Identity"){ + + if (len(reviewedHash) != 16){ + setErrorEncounteredPage(window, errors.New("ReadReview returning invalid reviewedHash length."), previousPage) + return + } + + reviewedIdentityHash := [16]byte(reviewedHash) + + setViewIdentityHashPage(window, reviewedIdentityHash, currentPage) + } else { + setViewContentHashPage(window, reviewType, reviewedHash, currentPage) + } + }) + reviewedHashRow := container.NewHBox(layout.NewSpacer(), reviewedHashTitle, reviewedHashLabel, viewReviewedHashButton, layout.NewSpacer()) + + broadcastTimeTranslated, err := helpers.ConvertUnixTimeToTimeFromNowTranslated(reviewBroadcastTime, true) + if (err != nil ){ return nil, err } + + broadcastTimeTitle := widget.NewLabel("Broadcast Time:") + broadcastTimeLabel := getBoldLabel(broadcastTimeTranslated) + broadcastTimeWarningButton := widget.NewButtonWithIcon("", theme.WarningIcon(), func(){ + title := translate("Broadcast Time Warning") + dialogMessage := "Broadcast times are not verified. They can be faked by the reviewer." + dialogContent := container.NewVBox(widget.NewLabel(dialogMessage)) + dialog.ShowCustom(title, translate("Close"), dialogContent, window) + }) + broadcastTimeRow := container.NewHBox(layout.NewSpacer(), broadcastTimeTitle, broadcastTimeLabel, broadcastTimeWarningButton, layout.NewSpacer()) + + verdictTitle := widget.NewLabel("Verdict:") + verdictLabel := getBoldLabel(reviewVerdict) + verdictRow := container.NewHBox(layout.NewSpacer(), verdictTitle, verdictLabel, layout.NewSpacer()) + + getReviewReasonRow := func()(*fyne.Container, error){ + + reasonTitle := widget.NewLabel("Reason:") + + reasonString, exists := reviewMap["Reason"] + if (exists == false){ + noneLabel := getBoldItalicLabel(translate("None")) + reasonRow := container.NewHBox(layout.NewSpacer(), reasonTitle, noneLabel, layout.NewSpacer()) + + return reasonRow, nil + } + + reasonTrimmed, _, err := helpers.TrimAndFlattenString(reasonString, 20) + if (err != nil) { return nil, err } + reasonLabel := getBoldLabel(reasonTrimmed) + viewReasonButton := widget.NewButtonWithIcon("", theme.VisibilityIcon(), func(){ + setViewTextPage(window, "Viewing Review Reason", reasonString, false, currentPage) + }) + + reasonRow := container.NewHBox(layout.NewSpacer(), reasonTitle, reasonLabel, viewReasonButton, layout.NewSpacer()) + + return reasonRow, nil + } + + reasonRow, err := getReviewReasonRow() + if (err != nil) { return nil, err } + + pageContent := container.NewVBox(reviewNetworkTypeRow, widget.NewSeparator(), reviewerIdentityRow, widget.NewSeparator(), reviewHashRow, widget.NewSeparator(), reviewTypeRow, widget.NewSeparator(), reviewedHashRow, widget.NewSeparator(), broadcastTimeRow, widget.NewSeparator(), verdictRow, widget.NewSeparator(), reasonRow, widget.NewSeparator()) + + if (reviewType == "Identity"){ + _, exists := reviewMap["ErrantMessages"] + if (exists == true){ + + errantMessagesTitle := widget.NewLabel("Errant Messages:") + errantMessagesRow := container.NewHBox(layout.NewSpacer(), errantMessagesTitle, layout.NewSpacer()) + + pageContent.Add(errantMessagesRow) + //TODO: Add errant Profiles and reviews, and add buttons to view each. + } + } + + if (reviewType != "Identity"){ + + viewReviewedContentOrIdentityButton := getWidgetCentered(widget.NewButtonWithIcon("View Reviewed " + reviewType, theme.VisibilityIcon(), func(){ + if (reviewType == "Message"){ + + if (len(reviewedHash) != 26){ + reviewedHashHex := encoding.EncodeBytesToHexString(reviewedHash) + setErrorEncounteredPage(window, errors.New("ReadReview returning invalid length reviewedHash for Message review: " + reviewedHashHex), currentPage) + return + } + + reviewedMessageHash := [26]byte(reviewedHash) + + setViewMessageForModerationPage(window, reviewedMessageHash, currentPage) + } else if (reviewType == "Profile"){ + + if (len(reviewedHash) != 28){ + reviewedHashHex := encoding.EncodeBytesToHexString(reviewedHash) + setErrorEncounteredPage(window, errors.New("ReadReview returning invalid length reviewedHash for Profile review: " + reviewedHashHex), currentPage) + return + } + + reviewedProfileHash := [28]byte(reviewedHash) + + setViewProfileForModerationPage(window, reviewedProfileHash, currentPage) + } else if (reviewType == "Attribute"){ + + if (len(reviewedHash) != 27){ + reviewedHashHex := encoding.EncodeBytesToHexString(reviewedHash) + setErrorEncounteredPage(window, errors.New("ReadReview returning invalid length reviewedHash for Attribute review: " + reviewedHashHex), currentPage) + return + } + + reviewedAttributeHash := [27]byte(reviewedHash) + + setViewProfileAttributeForModerationPage(window, reviewedAttributeHash, currentPage) + } + })) + + pageContent.Add(viewReviewedContentOrIdentityButton) + } + + viewReviewedHashModerationDetailsButton := getWidgetCentered(widget.NewButtonWithIcon("View Reviewed " + reviewType + " Details", theme.VisibilityIcon(), func(){ + + if (reviewType == "Identity"){ + + if (len(reviewedHash) != 16){ + reviewedHashHex := encoding.EncodeBytesToHexString(reviewedHash) + setErrorEncounteredPage(window, errors.New("ReadReview returning invalid length reviewedHash for Identity review: " + reviewedHashHex), currentPage) + return + } + + reviewedIdentityHash := [16]byte(reviewedHash) + + setViewIdentityModerationDetailsPage(window, reviewedIdentityHash, currentPage) + + } else if (reviewType == "Profile"){ + + if (len(reviewedHash) != 28){ + reviewedHashHex := encoding.EncodeBytesToHexString(reviewedHash) + setErrorEncounteredPage(window, errors.New("ReadReview returning invalid length reviewedHash for Profile review: " + reviewedHashHex), currentPage) + return + } + + reviewedProfileHash := [28]byte(reviewedHash) + + setViewProfileModerationDetailsPage(window, reviewedProfileHash, currentPage) + + } else if (reviewType == "Attribute"){ + + if (len(reviewedHash) != 27){ + reviewedHashHex := encoding.EncodeBytesToHexString(reviewedHash) + setErrorEncounteredPage(window, errors.New("ReadReview returning invalid length reviewedHash for Attribute review: " + reviewedHashHex), currentPage) + return + } + + reviewedAttributeHash := [27]byte(reviewedHash) + + setViewAttributeModerationDetailsPage(window, reviewedAttributeHash, currentPage) + + } else if (reviewType == "Message"){ + + if (len(reviewedHash) != 26){ + reviewedHashHex := encoding.EncodeBytesToHexString(reviewedHash) + setErrorEncounteredPage(window, errors.New("ReadReview returning invalid length reviewedHash for Message review: " + reviewedHashHex), currentPage) + return + } + + reviewedMessageHash := [26]byte(reviewedHash) + + setViewMessageModerationDetailsPage(window, reviewedMessageHash, currentPage) + } + })) + + pageContent.Add(viewReviewedHashModerationDetailsButton) + + //TODO: Add anything else? + + return pageContent, nil + } + + pageContent, err := getPageContent() + if (err != nil){ + setErrorEncounteredPage(window, err, previousPage) + return + } + + page := container.NewVBox(title, backButton, widget.NewSeparator(), pageContent) + + setPageContent(page, window) +} + + +func setModeratorSettingsPage(window fyne.Window, previousPage func()){ + + currentPage := func(){setModeratorSettingsPage(window, previousPage)} + + title := getPageTitleCentered("Moderator Settings") + + backButton := getBackButtonCentered(previousPage) + + description := getLabelCentered("Manage your moderation settings.") + + getSettingsGrid := func()(*fyne.Container, error){ + + settingTitleColumn := container.NewVBox() + settingStatusColumn := container.NewVBox() + manageSettingButtonsColumn := container.NewVBox() + + addSettingRow := func(addSeparator bool, settingTitle string, settingName string, defaultIsOn bool, manageSettingPage func())error{ + + settingTitleLabel := getBoldLabelCentered(settingTitle) + + getSettingOnOffStatus := func()(string, error){ + + exists, settingOnOffStatus, err := mySettings.GetSetting(settingName) + if (err != nil){ return "", err } + if (exists == false){ + if (defaultIsOn == true){ + return "On", nil + } + return "Off", nil + } + return settingOnOffStatus, 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, "Moderator Mode", "ModeratorModeOnOffStatus", false, func(){ + setManageModeratorModePage(window, currentPage) + }) + if (err != nil) { return nil, err } + + err = addSettingRow(true, "Moderate Messages", "ModerateMessagesOnOffStatus", false, func(){ + setManageModerateMessagesModePage(window, currentPage) + }) + if (err != nil) { return nil, err } + + err = addSettingRow(true, "Moderate Mate Identities", "ModerateMateContentOnOffStatus", true, func(){ + //TODO + showUnderConstructionDialog(window) + }) + if (err != nil) { return nil, err } + + err = addSettingRow(true, "Moderate Host Identities", "ModerateHostContentOnOffStatus", true, func(){ + //TODO + showUnderConstructionDialog(window) + }) + if (err != nil) { return nil, err } + + err = addSettingRow(true, "Moderate Moderator Identities", "ModerateModeratorContentOnOffStatus", true, func(){ + //TODO + showUnderConstructionDialog(window) + }) + 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 + } + + page := container.NewVBox(title, backButton, widget.NewSeparator(), description, widget.NewSeparator(), settingsGrid) + + setPageContent(page, window) +} + + +func setManageModeratorModePage(window fyne.Window, previousPage func()){ + + currentPage := func(){setManageModeratorModePage(window, previousPage)} + + title := getPageTitleCentered("Manage Moderator Mode") + + backButton := getBackButtonCentered(previousPage) + + subtitle := getPageSubtitleCentered("Moderator Mode") + + description1 := getLabelCentered("Activate this mode if you want to be a moderator.") + description2 := getLabelCentered("Seekia will start downloading content for you to moderate.") + + getModeratorModeStatus := func()(string, error){ + exists, moderatorModeStatus, err := mySettings.GetSetting("ModeratorModeOnOffStatus") + if (err != nil){ return "", err } + if (exists == false){ + return "Off", nil + } + if (moderatorModeStatus != "On" && moderatorModeStatus != "Off"){ + return "", errors.New("MySettings malformed: Invalid moderator mode status: " + moderatorModeStatus) + } + return moderatorModeStatus, nil + } + + moderatorModeStatus, err := getModeratorModeStatus() + if (err != nil){ + setErrorEncounteredPage(window, err, previousPage) + return + } + + moderatorModeStatusLabel := getLabelCentered("Moderator Mode Status:") + moderatorModeStatusText := getBoldLabelCentered(moderatorModeStatus) + + getEnableDisableText := func()string{ + if (moderatorModeStatus == "On"){ + return "Disable" + } + return "Enable" + } + + enableDisableText := getEnableDisableText() + + enableDisableButton := getWidgetCentered(widget.NewButton(enableDisableText, func(){ + + if (moderatorModeStatus == "On"){ + err := mySettings.SetSetting("ModeratorModeOnOffStatus", "Off") + if (err != nil){ + setErrorEncounteredPage(window, err, currentPage) + return + } + currentPage() + return + } + setConfirmEnableModeratorModePage(window, currentPage, currentPage) + })) + + page := container.NewVBox(title, backButton, widget.NewSeparator(), subtitle, widget.NewSeparator(), description1, description2, widget.NewSeparator(), moderatorModeStatusLabel, moderatorModeStatusText, enableDisableButton) + + setPageContent(page, window) +} + +func setConfirmEnableModeratorModePage(window fyne.Window, previousPage func(), nextPage func()){ + + title := getPageTitleCentered("Enable Moderator Mode") + + backButton := getBackButtonCentered(previousPage) + + description1 := getBoldLabelCentered("Enable Moderator Mode?") + + description2 := getLabelCentered("This mode will download unapproved content for you to moderate.") + description3 := getLabelCentered("This content may contain illegal and unruleful content.") + description4 := getLabelCentered("You must accept the legal risks of downloading this content.") + + enableButton := getWidgetCentered(widget.NewButtonWithIcon("Enable", theme.ConfirmIcon(), func(){ + err := mySettings.SetSetting("ModeratorModeOnOffStatus", "On") + if (err != nil){ + setErrorEncounteredPage(window, err, previousPage) + return + } + nextPage() + })) + + page := container.NewVBox(title, backButton, widget.NewSeparator(), description1, description2, description3, description4, enableButton) + + setPageContent(page, window) +} + + + +func setManageModerateMessagesModePage(window fyne.Window, previousPage func()){ + + currentPage := func(){setManageModeratorModePage(window, previousPage)} + + title := getPageTitleCentered("Manage Moderate Messages Mode") + + backButton := getBackButtonCentered(previousPage) + + subtitle := getPageSubtitleCentered("Moderate Messages Mode") + + description1 := getLabelCentered("Activate this mode if you want to moderate messages.") + description2 := getLabelCentered("Seekia will start downloading messages for you to moderate.") + description3 := getLabelCentered("You will be reviewing messages which have been reported by users.") + description4 := getLabelCentered("Be aware that these messages are more likely to contain unruleful content.") + + getModerateMessagesModeStatus := func()(string, error){ + exists, moderateMessagesModeStatus, err := mySettings.GetSetting("ModerateMessagesOnOffStatus") + if (err != nil){ return "", err } + if (exists == false){ + return "Off", nil + } + if (moderateMessagesModeStatus != "On" && moderateMessagesModeStatus != "Off"){ + return "", errors.New("MySettings malformed: Invalid moderate messages mode status: " + moderateMessagesModeStatus) + } + return moderateMessagesModeStatus, nil + } + + moderateMessagesModeStatus, err := getModerateMessagesModeStatus() + if (err != nil){ + setErrorEncounteredPage(window, err, previousPage) + return + } + + moderateMessagesModeStatusLabel := getLabelCentered("Moderate Messages Mode Status:") + moderateMessagesModeStatusText := getBoldLabelCentered(moderateMessagesModeStatus) + + getEnableDisableText := func()string{ + if (moderateMessagesModeStatus == "On"){ + return "Disable" + } + return "Enable" + } + + enableDisableText := getEnableDisableText() + + enableDisableButton := getWidgetCentered(widget.NewButton(enableDisableText, func(){ + + if (moderateMessagesModeStatus == "On"){ + err := mySettings.SetSetting("ModerateMessagesOnOffStatus", "Off") + if (err != nil){ + setErrorEncounteredPage(window, err, currentPage) + return + } + currentPage() + return + } + setConfirmEnableModerateMessagesModePage(window, currentPage, currentPage) + })) + + page := container.NewVBox(title, backButton, widget.NewSeparator(), subtitle, widget.NewSeparator(), description1, description2, description3, description4, widget.NewSeparator(), moderateMessagesModeStatusLabel, moderateMessagesModeStatusText, enableDisableButton) + + setPageContent(page, window) +} + +func setConfirmEnableModerateMessagesModePage(window fyne.Window, previousPage func(), nextPage func()){ + + title := getPageTitleCentered("Enable Moderate Messages Mode") + + backButton := getBackButtonCentered(previousPage) + + description1 := getBoldLabelCentered("Enable Moderate Messages Mode?") + + description2 := getLabelCentered("This mode will download reported messages for you to moderate.") + description3 := getLabelCentered("These are likely to contain illegal and unruleful content.") + description4 := getLabelCentered("You must accept the legal risks of downloading this content.") + + enableButton := getWidgetCentered(widget.NewButtonWithIcon("Enable", theme.ConfirmIcon(), func(){ + err := mySettings.SetSetting("ModerateMessagesOnOffStatus", "On") + if (err != nil){ + setErrorEncounteredPage(window, err, previousPage) + return + } + nextPage() + })) + + page := container.NewVBox(title, backButton, widget.NewSeparator(), description1, description2, description3, description4, enableButton) + + setPageContent(page, window) +} + + +func setBuildMyModeratorProfilePage(window fyne.Window, previousPage func()){ + + currentPage := func(){setBuildMyModeratorProfilePage(window, previousPage)} + + title := getPageTitleCentered("Build Moderator Profile") + + backButton := getBackButtonCentered(previousPage) + + description1 := getLabelCentered("Build your moderator profile.") + description2 := getLabelCentered("All information is optional.") + + usernameButton := widget.NewButton("Username", func(){ + setBuildProfilePage_Username(window, "Moderator", currentPage) + }) + + avatarButton := widget.NewButton("Avatar", func(){ + setBuildProfilePage_Avatar(window, "Moderator", currentPage) + }) + + descriptionButton := widget.NewButton("Description", func(){ + setBuildProfilePage_Description(window, "Moderator", currentPage) + }) + + profileLanguageButton := widget.NewButton(translate("Profile Language"), func(){ + setBuildProfilePage_ProfileLanguage(window, "Moderator", currentPage) + }) + + languageButton := widget.NewButton("Language", func(){ + //TODO + // The format of this attribute will be different than the one for mates + // It will not include a fluency field. It will simply contain a list of understood languages + showUnderConstructionDialog(window) + }) + + buttonsColumn := getContainerCentered(container.NewGridWithColumns(1, usernameButton, avatarButton, descriptionButton, profileLanguageButton, languageButton)) + + page := container.NewVBox(title, backButton, widget.NewSeparator(), description1, description2, widget.NewSeparator(), buttonsColumn) + + setPageContent(page, window) +} + + +func setViewMyReviewsPage(window fyne.Window, reviewType string, previousPage func()){ + + if (reviewType != "Identity" && reviewType != "Profile" && reviewType != "Attribute" && reviewType != "Message"){ + setErrorEncounteredPage(window, errors.New("setViewMyReviewsPage called with invalid reviewType: " + reviewType), previousPage) + return + } + + setLoadingScreen(window, "View My Reviews", "Loading my reviews...") + + currentPage := func(){setViewMyReviewsPage(window, reviewType, previousPage)} + + title := getPageTitleCentered("My Reviews") + + backButton := getBackButtonCentered(previousPage) + + myIdentityExists, myIdentityHash, err := myIdentity.GetMyIdentityHash("Moderator") + if (err != nil) { + setErrorEncounteredPage(window, err, previousPage) + return + } + if (myIdentityExists == false){ + description1 := getBoldLabelCentered("Your moderator identity does not exist.") + description2 := getLabelCentered("Thus, you have no moderator reviews.") + description3 := getLabelCentered("Create your identity?") + + createIdentityButton := getWidgetCentered(widget.NewButtonWithIcon("Create Identity", theme.NavigateNextIcon(), func(){ + setChooseNewIdentityHashPage(window, "Moderator", currentPage, currentPage) + })) + + page := container.NewVBox(title, backButton, widget.NewSeparator(), description1, description2, description3, createIdentityButton) + + setPageContent(page, window) + return + } + + //TODO: Add info about moderator's number of used reviews out of total that have been funded + + reviewTypeLabel := getBoldLabelCentered("Review Type:") + + allReviewTypesList := []string{"Identity", "Profile", "Attribute", "Message"} + + handleSelectFunction := func(newReviewType string){ + setViewMyReviewsPage(window, newReviewType, previousPage) + } + + reviewTypeSelector := widget.NewSelect(allReviewTypesList, handleSelectFunction) + reviewTypeSelector.Selected = reviewType + + reviewTypeSelectorCentered := getWidgetCentered(reviewTypeSelector) + + description := getLabelCentered("Below are your " + reviewType + " reviews.") + + getResultsContainer := func()(*fyne.Container, error){ + + appNetworkType, err := getAppNetworkType.GetAppNetworkType() + if (err != nil) { return nil, err } + + myIdentityExists, myReviewsList, err := myReviews.GetMyNewestReviewsListSorted(reviewType, appNetworkType) + if (err != nil) { return nil, err } + if (myIdentityExists == false){ + return nil, errors.New("My identity not found after being found already.") + } + + totalNumberOfReviews := len(myReviewsList) + + if (totalNumberOfReviews == 0){ + + reviewTypeLowercase := strings.ToLower(reviewType) + noReviewsExistText := getBoldLabelCentered("No " + reviewTypeLowercase + " reviews exist.") + + return noReviewsExistText, nil + } + + getFinalPageIndex := func()int{ + if (totalNumberOfReviews <= 5){ + return 0 + } + + maximumIndex := totalNumberOfReviews -5 + + return maximumIndex + } + + finalPageIndex := getFinalPageIndex() + + getViewIndex := func()(int, error){ + + if (totalNumberOfReviews <= 5){ + return 0, nil + } + + exists, currentViewIndex := appMemory.GetMemoryEntry("MyReviewsPageViewIndex_" + reviewType) + if (exists == false){ + return 0, nil + } + + currentViewIndexInt, err := helpers.ConvertStringToInt(currentViewIndex) + if (err != nil) { return 0, err } + + maximumIndex := totalNumberOfReviews - 1 + + if (currentViewIndexInt < 0){ + // Index is out of range, restart at 0 + appMemory.SetMemoryEntry("MyReviewsPageViewIndex_" + reviewType, "0") + + return 0, nil + } + if (currentViewIndexInt > maximumIndex){ + finalPageIndexString := helpers.ConvertIntToString(finalPageIndex) + appMemory.SetMemoryEntry("MyReviewsPageViewIndex_" + reviewType, finalPageIndexString) + + return finalPageIndex, nil + } + + return currentViewIndexInt, nil + } + + viewIndex, err := getViewIndex() + if (err != nil) { return nil, err } + + getNavigateToBeginningButton := func()fyne.Widget{ + + if (totalNumberOfReviews <= 5 || viewIndex == 0){ + + emptyButton := widget.NewButton(" ", nil) + return emptyButton + } + + goToBeginningButton := widget.NewButtonWithIcon("", theme.MediaSkipPreviousIcon(), func(){ + appMemory.SetMemoryEntry("MyReviewsPageViewIndex_" + reviewType, "0") + currentPage() + }) + + return goToBeginningButton + } + + getNavigateToEndButton := func()fyne.Widget{ + + emptyButton := widget.NewButton(" ", nil) + + if (totalNumberOfReviews <= 5 || viewIndex >= finalPageIndex){ + return emptyButton + } + + goToEndButton := widget.NewButtonWithIcon("", theme.MediaSkipNextIcon(), func(){ + + finalPageIndexString := helpers.ConvertIntToString(finalPageIndex) + + appMemory.SetMemoryEntry("MyReviewsPageViewIndex_" + reviewType, finalPageIndexString) + + currentPage() + }) + + return goToEndButton + } + + getNavigateLeftButton := func()fyne.Widget{ + + if (totalNumberOfReviews <= 5 || viewIndex == 0){ + + emptyButton := widget.NewButton(" ", nil) + return emptyButton + } + + button := widget.NewButtonWithIcon("", theme.NavigateBackIcon(), func(){ + + newIndex := helpers.ConvertIntToString(viewIndex-5) + + appMemory.SetMemoryEntry("MyReviewsPageViewIndex_" + reviewType, newIndex) + + currentPage() + }) + + return button + } + + getNavigateRightButton := func()fyne.Widget{ + + emptyButton := widget.NewButton(" ", nil) + + if (totalNumberOfReviews <= 5 || viewIndex >= finalPageIndex){ + + return emptyButton + } + + button := widget.NewButtonWithIcon("", theme.NavigateNextIcon(), func(){ + + newIndex := helpers.ConvertIntToString(viewIndex+5) + + appMemory.SetMemoryEntry("MyReviewsPageViewIndex_" + reviewType, newIndex) + + currentPage() + }) + + return button + } + + getViewingReviewsLabelWithNavRow := func()*fyne.Container{ + + if (totalNumberOfReviews == 1){ + viewingReviewText := getBoldLabelCentered("Viewing 1 Review") + + return viewingReviewText + } + + totalNumberOfReviewsString := helpers.ConvertIntToString(totalNumberOfReviews) + + if (totalNumberOfReviews <= 5){ + + viewingReviewsText := getBoldLabelCentered("Viewing " + totalNumberOfReviewsString + " Reviews") + + return viewingReviewsText + } + + // We need to show navigation buttons + + viewingReviewsText := getBoldLabel("Viewing " + totalNumberOfReviewsString + " Reviews") + + navigateToBeginningButton := getNavigateToBeginningButton() + navigateToEndButton := getNavigateToEndButton() + + navigateLeftButton := getNavigateLeftButton() + navigateRightButton := getNavigateRightButton() + + navigationButtonsWithNumShowingRow := container.NewHBox(layout.NewSpacer(), navigateToBeginningButton, navigateLeftButton, viewingReviewsText, navigateRightButton, navigateToEndButton, layout.NewSpacer()) + + return navigationButtonsWithNumShowingRow + } + + viewingReviewsLabelWithNavRow := getViewingReviewsLabelWithNavRow() + + reviewedHashLabel := getItalicLabelCentered("Reviewed Hash") + broadcastTimeLabel := getItalicLabelCentered("Broadcast Time") + verdictLabel := getItalicLabelCentered("Verdict") + reasonLabel := getItalicLabelCentered("Reason") + emptyLabel := widget.NewLabel("") + + reviewedHashColumn := container.NewVBox(reviewedHashLabel, widget.NewSeparator()) + broadcastTimeColumn := container.NewVBox(broadcastTimeLabel, widget.NewSeparator()) + verdictColumn := container.NewVBox(verdictLabel, widget.NewSeparator()) + reasonColumn := container.NewVBox(reasonLabel, widget.NewSeparator()) + viewReviewButtonsColumn := container.NewVBox(emptyLabel, widget.NewSeparator()) + + viewIndexOnwardsReviewsList := myReviewsList[viewIndex:] + + for index, reviewBytes := range viewIndexOnwardsReviewsList{ + + ableToRead, reviewHash, _, reviewNetworkType, reviewerIdentityHash, reviewBroadcastTime, currentReviewType, reviewedHash, reviewVerdict, reviewMap, err := readReviews.ReadReviewAndHash(false, reviewBytes) + if (err != nil) { return nil, err } + if (ableToRead == false){ + return nil, errors.New("GetMyNewestReviewsListSorted returning invalid review") + } + if (reviewNetworkType != appNetworkType){ + return nil, errors.New("GetMyNewestReviewsListSorted returning review belonging to different networkType.") + } + if (reviewerIdentityHash != myIdentityHash) { + return nil, errors.New("GetMyNewestReviewsListSorted returning review for different identity hash.") + } + if (currentReviewType != reviewType){ + return nil, errors.New("GetMyNewestReviewsListSorted returning review of a different reviewType") + } + + reviewedHashString, err := helpers.EncodeReviewedHashBytesToString(reviewedHash) + if (err != nil) { return nil, err } + + reviewedHashTrimmed, _, err := helpers.TrimAndFlattenString(reviewedHashString, 10) + if (err != nil) { return nil, err } + reviewedHashLabel := getBoldLabel(reviewedHashTrimmed) + + broadcastTimeString, err := helpers.ConvertUnixTimeToTimeFromNowTranslated(reviewBroadcastTime, false) + if (err != nil ){ return nil, err } + + broadcastTimeLabel := getBoldLabelCentered(broadcastTimeString) + + verdictLabel := getBoldLabelCentered(reviewVerdict) + + getReviewReasonCell := func()(*fyne.Container, error){ + + reasonString, exists := reviewMap["Reason"] + if (exists == false){ + noneLabel := getBoldItalicLabelCentered(translate("None")) + + return noneLabel, nil + } + + reasonTrimmed, _, err := helpers.TrimAndFlattenString(reasonString, 20) + if (err != nil) { return nil, err } + + reasonLabel := getBoldLabel(reasonTrimmed) + + viewReasonButton := widget.NewButtonWithIcon("", theme.VisibilityIcon(), func(){ + setViewTextPage(window, "Viewing Review Reason", reasonString, false, currentPage) + }) + + reasonCell := container.NewHBox(layout.NewSpacer(), reasonLabel, viewReasonButton, layout.NewSpacer()) + + return reasonCell, nil + } + + reasonCell, err := getReviewReasonCell() + if (err != nil) { return nil, err } + + viewReviewButton := widget.NewButtonWithIcon("View", theme.VisibilityIcon(), func(){ + setViewReviewDetailsPage(window, reviewHash, currentPage) + }) + + reviewedHashColumn.Add(reviewedHashLabel) + broadcastTimeColumn.Add(broadcastTimeLabel) + verdictColumn.Add(verdictLabel) + reasonColumn.Add(reasonCell) + viewReviewButtonsColumn.Add(viewReviewButton) + + reviewedHashColumn.Add(widget.NewSeparator()) + broadcastTimeColumn.Add(widget.NewSeparator()) + verdictColumn.Add(widget.NewSeparator()) + reasonColumn.Add(widget.NewSeparator()) + viewReviewButtonsColumn.Add(widget.NewSeparator()) + + if (index >= 4) { + break + } + } + + reviewsGrid := container.NewHBox(layout.NewSpacer(), reviewedHashColumn, broadcastTimeColumn, verdictColumn, reasonColumn, viewReviewButtonsColumn, layout.NewSpacer()) + + resultsContainer := container.NewVBox(viewingReviewsLabelWithNavRow, widget.NewSeparator(), reviewsGrid) + + return resultsContainer, nil + } + + resultsContainer, err := getResultsContainer() + if (err != nil) { + setErrorEncounteredPage(window, err, previousPage) + return + } + + page := container.NewVBox(title, backButton, widget.NewSeparator(), reviewTypeLabel, reviewTypeSelectorCentered, widget.NewSeparator(), description, widget.NewSeparator(), resultsContainer) + + setPageContent(page, window) +} + +func setViewMyModeratorScorePage(window fyne.Window, previousPage func()){ + + currentPage := func(){setViewMyModeratorScorePage(window, previousPage)} + + title := getPageTitleCentered("My Moderator Score") + + backButton := getBackButtonCentered(previousPage) + + description1 := getLabelCentered("Moderator scores are used to resolve conflicts when moderators disagree.") + description2 := getLabelCentered("You must have a minimum moderator score to be able to ban other moderators.") + description3 := getLabelCentered("If you have a higher moderator score than another, you can ban them.") + description4 := getLabelCentered("Increase your score by sending cryptocurrency.") + + identityExists, myIdentityScore, _, _, _, err := myIdentityScore.GetMyIdentityScore() + if (err != nil){ + setErrorEncounteredPage(window, err, previousPage) + return + } + if (identityExists == false){ + + description5 := getBoldLabelCentered("Your Moderator identity does not exist.") + description6 := getLabelCentered("You must create it to view your Moderator Identity score.") + + createIdentityButton := getWidgetCentered(widget.NewButtonWithIcon("Create Identity", theme.NavigateNextIcon(), func(){ + setChooseNewIdentityHashPage(window, "Moderator", currentPage, currentPage) + })) + page := container.NewVBox(title, backButton, widget.NewSeparator(), description1, description2, description3, description4, widget.NewSeparator(), description5, description6, createIdentityButton) + + setPageContent(page, window) + return + } + + myModeratorScoreString := helpers.ConvertFloat64ToStringRounded(myIdentityScore, 3) + moderatorScoreTitle := getItalicLabelCentered("My Moderator Identity Score:") + moderatorScoreLabel := getBoldLabelCentered(myModeratorScoreString) + + refreshButton := getWidgetCentered(widget.NewButtonWithIcon("Refresh", theme.ViewRefreshIcon(), func(){ + //TODO: Add page to download status and show progress + showUnderConstructionDialog(window) + })) + + getMyModeratorRankText := func()(string, error){ + + myIdentityExists, myIdentityHash, err := myIdentity.GetMyIdentityHash("Moderator") + if (err != nil) { return "", err } + if (myIdentityExists == false){ + return "", errors.New("My moderator identity not found after being found already.") + } + + appNetworkType, err := getAppNetworkType.GetAppNetworkType() + if (err != nil) { return "", err } + + rankingKnown, moderatorRank, totalNumberOfModerators, err := moderatorRanking.GetModeratorRanking(myIdentityHash, appNetworkType) + if (err != nil) { return "", err } + + if (rankingKnown == false){ + result := translate("Unknown") + return result, nil + } + + moderatorRankString := helpers.ConvertIntToString(moderatorRank) + totalNumberOfModeratorsString := helpers.ConvertIntToString(totalNumberOfModerators) + + moderatorRankText := moderatorRankString + "/" + totalNumberOfModeratorsString + + return moderatorRankText, nil + } + + myModeratorRankText, err := getMyModeratorRankText() + if (err != nil){ + setErrorEncounteredPage(window, err, previousPage) + return + } + + myModeratorRankTitle := widget.NewLabel("My Moderator Rank:") + myModeratorRankLabel := getBoldLabel(myModeratorRankText) + + myModeratorRankRow := container.NewHBox(layout.NewSpacer(), myModeratorRankTitle, myModeratorRankLabel, layout.NewSpacer()) + + getSendFundsButtonWithIcon := func(cryptocurrencyName string)(*fyne.Container, error){ + + cryptocurrencyIcon, err := getFyneImageIcon(cryptocurrencyName) + if (err != nil){ return nil, err } + iconSize := getCustomFyneSize(0) + cryptocurrencyIcon.SetMinSize(iconSize) + + sendFundsButton := widget.NewButton("Send " + cryptocurrencyName, func(){ + nextPage := func(){setViewMyAddressToIncreaseIdentityScorePage(window, cryptocurrencyName, 50, currentPage)} + + setCryptoIdentityScorePrivacyWarningPage(window, currentPage, nextPage) + }) + + sendFundsButtonWithIcon := container.NewGridWithColumns(1, cryptocurrencyIcon, sendFundsButton) + + return sendFundsButtonWithIcon, nil + } + + sendFundsButtonWithIcon_Ethereum, err := getSendFundsButtonWithIcon("Ethereum") + if (err != nil){ + setErrorEncounteredPage(window, err, previousPage) + return + } + + sendFundsButtonWithIcon_Cardano, err := getSendFundsButtonWithIcon("Cardano") + if (err != nil){ + setErrorEncounteredPage(window, err, previousPage) + return + } + + sendFundsButtonsRow := getContainerCentered(container.NewGridWithColumns(2, sendFundsButtonWithIcon_Ethereum, sendFundsButtonWithIcon_Cardano)) + + page := container.NewVBox(title, backButton, widget.NewSeparator(), description1, description2, description3, widget.NewSeparator(), moderatorScoreTitle, moderatorScoreLabel, refreshButton, widget.NewSeparator(), myModeratorRankRow, widget.NewSeparator(), sendFundsButtonsRow) + + setPageContent(page, window) +} + + +func setCryptoIdentityScorePrivacyWarningPage(window fyne.Window, previousPage func(), nextPage func()){ + + title := getPageTitleCentered("Privacy Warning") + + backButton := getBackButtonCentered(previousPage) + + description1 := getBoldLabelCentered("Be aware that your privacy is at risk when using cryptocurrencies.") + description2 := getLabelCentered("When you send money from your wallet, all users of Seekia will see your wallet and its balance.") + description3 := getLabelCentered("If you have a large amount of cryptocurrency, you should not send your funds from that wallet.") + description4 := getLabelCentered("For privacy, you should send funds directly from an exchange.") + description5 := getLabelCentered("You can also use a tool such as a zero knowledge accumulator for stronger privacy.") + + description6 := getBoldLabelCentered("Do you understand the privacy risks?") + + iUnderstandButton := getWidgetCentered(widget.NewButtonWithIcon("I Understand", theme.ConfirmIcon(), nextPage)) + + page := container.NewVBox(title, backButton, widget.NewSeparator(), description1, description2, description3, description4, description5, description6, iUnderstandButton) + + setPageContent(page, window) +} + +func setViewMyAddressToIncreaseIdentityScorePage(window fyne.Window, cryptocurrencyName string, reviewsToFund int64, previousPage func()){ + + if (cryptocurrencyName != "Ethereum" && cryptocurrencyName != "Cardano"){ + setErrorEncounteredPage(window, errors.New("setViewMyAddressToIncreaseIdentityScorePage called with invalid cryptocurrencyName: " + cryptocurrencyName), previousPage) + return + } + + currentPage := func(){setViewMyAddressToIncreaseIdentityScorePage(window, cryptocurrencyName, reviewsToFund, previousPage)} + + title := getPageTitleCentered("Send Moderator Funds") + + backButton := getBackButtonCentered(previousPage) + + //identityExists, identityScoreAddress, err := myIdentityScore.GetMyIdentityScoreReceivingAddress(cryptocurrency) + //if (err != nil) { + // setErrorEncounteredPage(window, err, previousPage) + // return + //} + //if (identityExists == false) { + // This should not occur, this page should not be reached unless identity exists. + // setErrorEncounteredPage(window, errors.New("Identity not found."), previousPage) + // return + //} + + warning1 := getBoldLabelCentered("Privacy Warning: This address is linked to your moderator identity.") + warning2 := getLabelCentered("All users of Seekia will be able to see all address transactions.") + warning3 := getBoldLabelCentered("All funds will be destroyed forever.") + + cryptoAddressRow, err := getCryptocurrencyAddressLabelWithCopyAndQRButtons(window, cryptocurrencyName, "SeekiaIsNotCompleteYet", currentPage) + if (err != nil){ + setErrorEncounteredPage(window, err, previousPage) + return + } + + content := container.NewVBox(title, backButton, widget.NewSeparator(), warning1, warning2, warning3, widget.NewSeparator(), cryptoAddressRow) + + setPageContent(content, window) +} + + diff --git a/gui/peerActionsGui.go b/gui/peerActionsGui.go new file mode 100644 index 0000000..c115a7a --- /dev/null +++ b/gui/peerActionsGui.go @@ -0,0 +1,1161 @@ +package gui + +// peerActionsGui.go implements pages to manage peer actions, such as blocking, liking, and hiding + +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/internal/desires/myLocalDesires" +import "seekia/internal/desires/myMateDesires" +import "seekia/internal/encoding" +import "seekia/internal/helpers" +import "seekia/internal/identity" +import "seekia/internal/messaging/myChatFilters" +import "seekia/internal/messaging/myChatMessages" +import "seekia/internal/myBlockedUsers" +import "seekia/internal/myContacts" +import "seekia/internal/myIdentity" +import "seekia/internal/myIgnoredUsers" +import "seekia/internal/myLikedUsers" +import "seekia/internal/myMatches" +import "seekia/internal/mySettings" +import "seekia/internal/network/appNetworkType/getAppNetworkType" +import "seekia/internal/profiles/myLocalProfiles" +import "seekia/internal/profiles/myProfileStatus" +import "seekia/internal/profiles/viewableProfiles" + +import "slices" +import "strings" +import "errors" + + +func setViewPeerActionsPage(window fyne.Window, userIdentityHash [16]byte, previousPage func()){ + + currentPage := func(){setViewPeerActionsPage(window, userIdentityHash, previousPage)} + + title := getPageTitleCentered("User Actions") + + backButton := getBackButtonCentered(previousPage) + + page := container.NewVBox(title, backButton, widget.NewSeparator()) + + userIdentityType, err := identity.GetIdentityTypeFromIdentityHash(userIdentityHash) + if (err != nil) { + userIdentityHashHex := encoding.EncodeBytesToHexString(userIdentityHash[:]) + setErrorEncounteredPage(window, errors.New("setViewPeerActionsPage called with invalid userIdentityHash: " + userIdentityHashHex), previousPage) + return + } + + userIsBlocked, timeOfBlockingUnix, reasonExists, blockReason, err := myBlockedUsers.CheckIfUserIsBlocked(userIdentityHash) + if (err != nil) { + setErrorEncounteredPage(window, err, previousPage) + return + } + + if (userIsBlocked == true){ + + isBlockedText := getBoldLabelCentered("User Is Blocked") + blockedAgoString, err := helpers.ConvertUnixTimeToTimeAgoTranslated(timeOfBlockingUnix, false) + if (err != nil) { + setErrorEncounteredPage(window, err, previousPage) + return + } + + blockedAgoLabel := getItalicLabelCentered("Blocked " + blockedAgoString + ".") + + page.Add(isBlockedText) + page.Add(blockedAgoLabel) + + if (reasonExists == true){ + + blockedReasonLabel := getBoldLabelCentered("Reason:") + page.Add(blockedReasonLabel) + + blockReasonTrimmed, changesOccurred, err := helpers.TrimAndFlattenString(blockReason, 20) + if (err != nil) { + setErrorEncounteredPage(window, err, previousPage) + return + } + if (changesOccurred == false){ + + blockedReasonText := getLabelCentered(blockReason) + + page.Add(blockedReasonText) + } else { + + blockedReasonText := widget.NewLabel(blockReasonTrimmed) + + viewReasonButton := widget.NewButtonWithIcon("", theme.VisibilityIcon(), func(){ + setViewTextPage(window, "Viewing Block Reason", blockReason, false, currentPage) + }) + + blockReasonRow := container.NewHBox(layout.NewSpacer(), blockedReasonText, viewReasonButton, layout.NewSpacer()) + + page.Add(blockReasonRow) + } + } + + unblockButton := getWidgetCentered(widget.NewButton("Unblock User", func(){ + setConfirmUnblockUserPage(window, userIdentityHash, currentPage, currentPage) + })) + + page.Add(unblockButton) + page.Add(widget.NewSeparator()) + } + + if (userIsBlocked == false && userIdentityType == "Mate"){ + + getButtonsWithIconsRow := func()(*fyne.Container, error){ + + getLikeOrUnlikeButtonWithIcon := func()(*fyne.Container, error){ + + userIsLiked, _, err := myLikedUsers.CheckIfUserIsLiked(userIdentityHash) + if (err != nil) { return nil, err } + if (userIsLiked == true){ + unlikeIcon, err := getFyneImageIcon("Unlike") + if (err != nil) { return nil, err } + unlikeButton := widget.NewButton("Unlike", func(){ + setConfirmUnlikeUserPage(window, userIdentityHash, currentPage, currentPage) + }) + + unlikeButtonWithIcon := container.NewGridWithColumns(1, unlikeIcon, unlikeButton) + return unlikeButtonWithIcon, nil + } + + likeIcon, err := getFyneImageIcon("Like") + if (err != nil) { return nil, err } + likeButton := widget.NewButton("Like", func(){ + setConfirmLikeUserPage(window, userIdentityHash, currentPage, currentPage) + }) + + likeButtonWithIcon := container.NewGridWithColumns(1, likeIcon, likeButton) + return likeButtonWithIcon, nil + } + + likeOrUnlikeButtonWithIcon, err := getLikeOrUnlikeButtonWithIcon() + if (err != nil) { return nil, err } + + getIgnoreOrUnignoreButtonWithIcon := func()(*fyne.Container, error){ + + userIsIgnored, _, _, _, err := myIgnoredUsers.CheckIfUserIsIgnored(userIdentityHash) + if (err != nil) { return nil, err } + if (userIsIgnored == true){ + + unignoreIcon := widget.NewIcon(theme.VisibilityIcon()) + + unignoreButton := widget.NewButton("Unignore", func(){ + setConfirmUnignoreUserPage(window, userIdentityHash, currentPage, currentPage) + }) + + unignoreButtonWithIcon := container.NewGridWithColumns(1, unignoreIcon, unignoreButton) + return unignoreButtonWithIcon, nil + } + + ignoreIcon := widget.NewIcon(theme.VisibilityOffIcon()) + + ignoreButton := widget.NewButton("Ignore", func(){ + setConfirmIgnoreUserPage(window, userIdentityHash, currentPage, currentPage) + }) + + ignoreButtonWithIcon := container.NewGridWithColumns(1, ignoreIcon, ignoreButton) + return ignoreButtonWithIcon, nil + } + + ignoreOrUnignoreButtonWithIcon, err := getIgnoreOrUnignoreButtonWithIcon() + if (err != nil) { return nil, err } + + greetIcon, err := getFyneImageIcon("Greet") + if (err != nil) { return nil, err } + greetButton := widget.NewButton("Greet", func(){ + setConfirmGreetOrRejectUserPage(window, "Greet", userIdentityHash, currentPage, currentPage) + }) + greetButtonWithIcon := container.NewGridWithColumns(1, greetIcon, greetButton) + + rejectIcon, err := getFyneImageIcon("Reject") + if (err != nil) { return nil, err } + rejectButton := widget.NewButton("Reject", func(){ + setConfirmGreetOrRejectUserPage(window, "Reject", userIdentityHash, currentPage, currentPage) + }) + rejectButtonWithIcon := container.NewGridWithColumns(1, rejectIcon, rejectButton) + + buttonsWithIconsRow := getContainerCentered(container.NewGridWithRows(1, likeOrUnlikeButtonWithIcon, greetButtonWithIcon, ignoreOrUnignoreButtonWithIcon, rejectButtonWithIcon)) + + return buttonsWithIconsRow, nil + } + + buttonsWithIconsRow, err := getButtonsWithIconsRow() + if (err != nil){ + setErrorEncounteredPage(window, err, previousPage) + return + } + + page.Add(buttonsWithIconsRow) + page.Add(widget.NewSeparator()) + } + + buttonsGrid := container.NewGridWithColumns(1) + + if (userIsBlocked == false){ + + contactExists, err := myContacts.CheckIfUserIsMyContact(userIdentityHash) + if (err != nil) { + setErrorEncounteredPage(window, err, previousPage) + return + } + + if (contactExists == true){ + + manageContactButton := widget.NewButton("Manage Contact", func(){ + setManageContactPage(window, userIdentityHash, currentPage) + }) + buttonsGrid.Add(manageContactButton) + + } else { + + addContactButton := widget.NewButtonWithIcon("Add Contact", theme.ContentAddIcon(), func(){ + setAddContactFromIdentityHashPage( window, userIdentityHash, currentPage, currentPage) + }) + buttonsGrid.Add(addContactButton) + } + + blockButton := widget.NewButtonWithIcon("Block", theme.CancelIcon(), func(){ + setConfirmBlockUserButton(window, userIdentityHash, currentPage, currentPage) + }) + buttonsGrid.Add(blockButton) + + if (userIdentityType != "Host"){ + chatButton := widget.NewButtonWithIcon("Chat", theme.MailComposeIcon(), func(){ + setViewAConversationPage(window, userIdentityHash, true, currentPage) + }) + buttonsGrid.Add(chatButton) + } + } + + viewProfileButton := widget.NewButtonWithIcon("View Profile", theme.VisibilityIcon(), func(){ + setViewPeerProfilePageFromIdentityHash(window, userIdentityHash, currentPage) + }) + buttonsGrid.Add(viewProfileButton) + + viewIdentityHashButton := widget.NewButtonWithIcon("View Identity Hash", theme.VisibilityIcon(), func(){ + setViewIdentityHashPage(window, userIdentityHash, currentPage) + }) + buttonsGrid.Add(viewIdentityHashButton) + + buttonsGridCentered := getContainerCentered(buttonsGrid) + + page.Add(buttonsGridCentered) + + setPageContent(page, window) +} + + +func setConfirmBlockUserButton(window fyne.Window, userIdentityHash [16]byte, previousPage func(), nextPage func()){ + + setLoadingScreen(window, "Block User", "Loading Block User page...") + + title := getPageTitleCentered("Confirm Block User") + + backButton := getBackButtonCentered(previousPage) + + description1 := getBoldLabelCentered("Block this user?") + description2 := getLabelCentered("The user will not be notified.") + description3 := getLabelCentered("To notify the user, you must Reject them first.") + + page := container.NewVBox(title, backButton, widget.NewSeparator(), description1, description2, description3, widget.NewSeparator()) + + getTotalNumberOfConversationMessages := func()(int, error){ + + // We see how many messages exist on both network types. + + numberOfConversationMessages_Network1, err := myChatMessages.GetNumberOfMyConversationMessages(userIdentityHash, 1) + if (err != nil) { return 0, err } + + numberOfConversationMessages_Network2, err := myChatMessages.GetNumberOfMyConversationMessages(userIdentityHash, 2) + if (err != nil) { return 0, err } + + result := numberOfConversationMessages_Network1 + numberOfConversationMessages_Network2 + + return result, nil + } + + numberOfConversationMessages, err := getTotalNumberOfConversationMessages() + if (err != nil){ + setErrorEncounteredPage(window, err, previousPage) + return + } + if (numberOfConversationMessages != 0){ + + numberOfConversationMessagesString := helpers.ConvertIntToString(numberOfConversationMessages) + + description4 := getLabelCentered("This will delete your conversation history.") + description5 := getBoldLabelCentered("You have " + numberOfConversationMessagesString + " conversation messages.") + + page.Add(description4) + page.Add(description5) + page.Add(widget.NewSeparator()) + } + + enterReasonText := getLabelCentered("Enter reason (optional):") + page.Add(enterReasonText) + + enterReasonDescriptionA := getLabelCentered("This reason is stored privately and is not shared.") + enterReasonDescriptionB := getLabelCentered("It can help you remember why you blocked a user.") + + enterReasonDescriptions := container.NewVBox(enterReasonDescriptionA, enterReasonDescriptionB) + + enterReasonTextEntry := widget.NewEntry() + enterReasonTextEntry.SetPlaceHolder("Enter reason.") + + enterReasonEntryBoxed := getWidgetBoxed(enterReasonTextEntry) + + enterReasonEntryWithDescriptions := getContainerCentered(container.NewGridWithColumns(1, enterReasonDescriptions, enterReasonEntryBoxed)) + + page.Add(enterReasonEntryWithDescriptions) + page.Add(widget.NewSeparator()) + + blockUserFunction := func()error{ + + reason := enterReasonTextEntry.Text + if (reason == ""){ + err := myBlockedUsers.BlockUser(userIdentityHash, false, "") + if (err != nil) { return err } + } else { + err := myBlockedUsers.BlockUser(userIdentityHash, true, reason) + if (err != nil) { return err } + } + + userIdentityType, err := identity.GetIdentityTypeFromIdentityHash(userIdentityHash) + if (err != nil) { return err } + + if (userIdentityType != "Host"){ + + //TODO: Add delete peer chat keys, secret inboxes, and device identifier + + err = myChatMessages.DeleteAllPeerConversationMessages(userIdentityHash) + if (err != nil) { return err } + + err := mySettings.SetSetting(userIdentityType + "ChatConversationsGeneratedStatus", "No") + if (err != nil) { return err } + } + + if (userIdentityType == "Mate"){ + + appNetworkType, err := getAppNetworkType.GetAppNetworkType() + if (err != nil) { return err } + + myMatchesReady, myMatchesList, err := myMatches.GetMyMatchesList(appNetworkType) + if (err != nil) { return err } + if (myMatchesReady == false){ + err = mySettings.SetSetting("MatchesNeedRefreshYesNo", "Yes") + if (err != nil) { return err } + } else { + userIsAMatch := slices.Contains(myMatchesList, userIdentityHash) + if (userIsAMatch == true){ + err = mySettings.SetSetting("MatchesNeedRefreshYesNo", "Yes") + if (err != nil) { return err } + } + } + } + + return nil + } + + confirmButton := getWidgetCentered(widget.NewButtonWithIcon("Block User", theme.ConfirmIcon(), func(){ + + setLoadingScreen(window, "Block User", "Blocking User...") + err := blockUserFunction() + if (err != nil){ + setErrorEncounteredPage(window, err, previousPage) + return + } + + nextPage() + })) + + page.Add(confirmButton) + + setPageContent(page, window) +} + + +func setConfirmUnblockUserPage(window fyne.Window, userIdentityHash [16]byte, previousPage func(), nextPage func()){ + + setLoadingScreen(window, "Unblock User", "Loading Unblock User page...") + + currentPage := func(){setConfirmUnblockUserPage(window, userIdentityHash, previousPage, nextPage)} + + title := getPageTitleCentered("Confirm Unblock User") + + backButton := getBackButtonCentered(previousPage) + + description1 := getBoldLabelCentered("Unblock this user?") + description2 := getLabelCentered("They will not be notified.") + + userIsBlocked, timeOfBlockingUnix, reasonExists, blockReason, err := myBlockedUsers.CheckIfUserIsBlocked(userIdentityHash) + if (err != nil) { + setErrorEncounteredPage(window, err, previousPage) + return + } + if (userIsBlocked == false){ + setErrorEncounteredPage(window, errors.New("setConfirmUnblockUserPage visited when user is not blocked."), previousPage) + return + } + + blockedAgoString, err := helpers.ConvertUnixTimeToTimeAgoTranslated(timeOfBlockingUnix, false) + if (err != nil) { + setErrorEncounteredPage(window, err, previousPage) + return + } + + blockedAgoTimeLabel := getItalicLabelCentered("Blocked " + blockedAgoString + ".") + + page := container.NewVBox(title, backButton, widget.NewSeparator(), description1, description2, blockedAgoTimeLabel) + + if (reasonExists == true){ + + page.Add(widget.NewSeparator()) + + blockReasonText := getLabelCentered("Reason for blocking:") + page.Add(blockReasonText) + + blockReasonTrimmed, changesOccurred, err := helpers.TrimAndFlattenString(blockReason, 20) + if (err != nil) { + setErrorEncounteredPage(window, err, previousPage) + return + } + if (changesOccurred == false){ + + blockedReasonText := getBoldLabelCentered(blockReason) + + page.Add(blockedReasonText) + } else { + + blockedReasonText := getBoldLabel(blockReasonTrimmed) + + viewReasonButton := widget.NewButtonWithIcon("", theme.VisibilityIcon(), func(){ + setViewTextPage(window, "Viewing Block Reason", blockReason, false, currentPage) + }) + + blockReasonRow := container.NewHBox(layout.NewSpacer(), blockedReasonText, viewReasonButton, layout.NewSpacer()) + + page.Add(blockReasonRow) + } + + page.Add(widget.NewSeparator()) + } + + unblockUserFunction := func()error{ + + err := myBlockedUsers.UnblockUser(userIdentityHash) + if (err != nil) { return err } + + userIdentityType, err := identity.GetIdentityTypeFromIdentityHash(userIdentityHash) + if (err != nil) { return err } + + if (userIdentityType != "Host"){ + + // If the blocked user sent us any messages, then there may be new messages available + // These are messages that we would have skipped when the sender was blocked + + err := mySettings.SetSetting(userIdentityType + "ChatConversationsNeedRefreshYesNo", "Yes") + if (err != nil) { return err } + } + + if (userIdentityType == "Mate"){ + + // Now we check if the user would have been a match if they were not blocked + // If yes, we know that matches need to be refreshed + + appNetworkType, err := getAppNetworkType.GetAppNetworkType() + if (err != nil) { return err } + + profileExists, _, getAnyUserAttributeFunction, err := viewableProfiles.GetRetrieveAnyNewestViewableUserProfileAttributeFunction(userIdentityHash, appNetworkType, true, false, false) + if (err != nil) { return err } + if (profileExists == true) { + + passesMyDesires, err := myMateDesires.CheckIfMateProfilePassesAllMyDesires(false, "", getAnyUserAttributeFunction) + if (err != nil) { return err } + if (passesMyDesires == true){ + err = mySettings.SetSetting("MatchesNeedRefreshYesNo", "Yes") + if (err != nil) { return err } + } + } + } + + return nil + } + + confirmButton := getWidgetCentered(widget.NewButtonWithIcon("Unblock User", theme.ConfirmIcon(), func(){ + + setLoadingScreen(window, "Unblock User", "Unblocking User...") + err := unblockUserFunction() + if (err != nil){ + setErrorEncounteredPage(window, err, previousPage) + return + } + + nextPage() + })) + + page.Add(confirmButton) + + setPageContent(page, window) +} + + +func setConfirmIgnoreUserPage(window fyne.Window, userIdentityHash [16]byte, previousPage func(), nextPage func()){ + + userIdentityType, err := identity.GetIdentityTypeFromIdentityHash(userIdentityHash) + if (err != nil) { + setErrorEncounteredPage(window, err, previousPage) + return + } + + if (userIdentityType != "Mate"){ + setErrorEncounteredPage(window, errors.New("Trying to ignore non-mate user."), previousPage) + return + } + + title := getPageTitleCentered("Confirm Ignore User") + + backButton := getBackButtonCentered(previousPage) + + description1 := getBoldLabelCentered("Ignore user?") + description2 := getLabelCentered("The user will not show up in your matches/chat conversations.") + description3 := getLabelCentered("You can change this by enabling Show Ignored Users in your desires/chat filters.") + description4 := getLabelCentered("The user will not be notified.") + description5 := getLabelCentered("To notify them, Reject them first.") + + enterReasonText := getBoldLabelCentered("Enter reason (optional):") + + enterReasonDescriptionA := getLabelCentered("This reason is stored privately and is not shared.") + enterReasonDescriptionB := getLabelCentered("It can help you remember why you hid a user.") + + enterReasonDescriptions := container.NewVBox(enterReasonDescriptionA, enterReasonDescriptionB) + + enterReasonTextEntry := widget.NewEntry() + enterReasonTextEntry.SetPlaceHolder("Enter reason.") + + enterReasonEntryBoxed := getWidgetBoxed(enterReasonTextEntry) + + enterReasonEntryWithDescriptions := getContainerCentered(container.NewGridWithColumns(1, enterReasonDescriptions, enterReasonEntryBoxed)) + + ignoreUserFunction := func()error{ + + reason := enterReasonTextEntry.Text + if (reason == ""){ + err := myIgnoredUsers.IgnoreUser(userIdentityHash, false, "") + if (err != nil) { return err } + } else { + err := myIgnoredUsers.IgnoreUser(userIdentityHash, true, reason) + if (err != nil) { return err } + } + + + hideIgnoredUsersBool, err := myChatFilters.GetChatFilterOnOffStatus("Mate", "HideIgnoredUsers") + if (err != nil) { return err } + if (hideIgnoredUsersBool == true){ + + err := mySettings.SetSetting("MateChatConversationsNeedRefreshYesNo", "Yes") + if (err != nil) { return err } + } + + getShowIgnoredMatchesBool := func()(bool, error){ + + //TODO + return false, nil + } + + showIgnoredMatchesBool, err := getShowIgnoredMatchesBool() + if (err != nil) { return err } + + if (showIgnoredMatchesBool == false){ + + appNetworkType, err := getAppNetworkType.GetAppNetworkType() + if (err != nil) { return err } + + myMatchesReady, myMatchesList, err := myMatches.GetMyMatchesList(appNetworkType) + if (err != nil) { return err } + if (myMatchesReady == false){ + err = mySettings.SetSetting("MatchesNeedRefreshYesNo", "Yes") + if (err != nil) { return err } + } else { + userIsAMatch := slices.Contains(myMatchesList, userIdentityHash) + if (userIsAMatch == true){ + err = mySettings.SetSetting("MatchesNeedRefreshYesNo", "Yes") + if (err != nil) { return err } + } + } + } + + return nil + } + + confirmButton := getWidgetCentered(widget.NewButtonWithIcon("Ignore User", theme.ConfirmIcon(), func(){ + + setLoadingScreen(window, "Ignore User", "Hiding User...") + err := ignoreUserFunction() + if (err != nil){ + setErrorEncounteredPage(window, err, previousPage) + return + } + + nextPage() + })) + + page := container.NewVBox(title, backButton, widget.NewSeparator(), description1, description2, description3, description4, description5, widget.NewSeparator(), enterReasonText, enterReasonEntryWithDescriptions, widget.NewSeparator(), confirmButton) + + setPageContent(page, window) +} + + +func setConfirmUnignoreUserPage(window fyne.Window, userIdentityHash [16]byte, previousPage func(), nextPage func()){ + + userIdentityType, err := identity.GetIdentityTypeFromIdentityHash(userIdentityHash) + if (err != nil) { + setErrorEncounteredPage(window, err, previousPage) + return + } + if (userIdentityType != "Mate"){ + setErrorEncounteredPage(window, errors.New("setConfirmUnignoreUserPage called with non-mate user."), previousPage) + return + } + + setLoadingScreen(window, "Unignore User", "Loading Unignore User page...") + + currentPage := func(){setConfirmUnignoreUserPage(window, userIdentityHash, previousPage, nextPage)} + + title := getPageTitleCentered("Confirm Unignore User") + + backButton := getBackButtonCentered(previousPage) + + description1 := getBoldLabelCentered("Unignore this user?") + description2 := getLabelCentered("They will not be notified.") + + userIsIgnored, timeOfHidingUnix, reasonExists, ignoreReason, err := myIgnoredUsers.CheckIfUserIsIgnored(userIdentityHash) + if (err != nil) { + setErrorEncounteredPage(window, err, previousPage) + return + } + if (userIsIgnored == false){ + setErrorEncounteredPage(window, errors.New("setConfirmUnignoreUserPage visited when user is not ignored."), previousPage) + return + } + + ignoredAgoString, err := helpers.ConvertUnixTimeToTimeAgoTranslated(timeOfHidingUnix, false) + if (err != nil) { + setErrorEncounteredPage(window, err, previousPage) + return + } + + ignoredAgoTimeLabel := getItalicLabelCentered("Ignored " + ignoredAgoString + ".") + + page := container.NewVBox(title, backButton, widget.NewSeparator(), description1, description2, ignoredAgoTimeLabel) + + if (reasonExists == true){ + + page.Add(widget.NewSeparator()) + + ignoreReasonText := getLabelCentered("Reason for hiding:") + page.Add(ignoreReasonText) + + ignoreReasonTrimmed, changesOccurred, err := helpers.TrimAndFlattenString(ignoreReason, 20) + if (err != nil) { + setErrorEncounteredPage(window, err, previousPage) + return + } + if (changesOccurred == false){ + + ignoreReasonText := getBoldLabelCentered(ignoreReason) + + page.Add(ignoreReasonText) + } else { + + ignoreReasonText := getBoldLabel(ignoreReasonTrimmed) + + viewReasonButton := widget.NewButtonWithIcon("", theme.VisibilityIcon(), func(){ + setViewTextPage(window, "Viewing Ignore Reason", ignoreReason, false, currentPage) + }) + + ignoreReasonRow := container.NewHBox(layout.NewSpacer(), ignoreReasonText, viewReasonButton, layout.NewSpacer()) + + page.Add(ignoreReasonRow) + } + + page.Add(widget.NewSeparator()) + } + + unignoreUserFunction := func()error{ + + err := myIgnoredUsers.UnignoreUser(userIdentityHash) + if (err != nil) { return err } + + hideIgnoredUsersBool, err := myChatFilters.GetChatFilterOnOffStatus("Mate", "HideIgnoredUsers") + if (err != nil) { return err } + if (hideIgnoredUsersBool == true){ + + err := mySettings.SetSetting("MateChatConversationsNeedRefreshYesNo", "Yes") + if (err != nil) { return err } + } + + getShowIgnoredMatchesBool := func()(bool, error){ + + //TODO + + return false, nil + } + + showIgnoredMatchesBool, err := getShowIgnoredMatchesBool() + if (err != nil) { return err } + if (showIgnoredMatchesBool == false){ + + // Now we check if user would have been a match if they were not ignored + // If yes, matches need to be refreshed + + appNetworkType, err := getAppNetworkType.GetAppNetworkType() + if (err != nil) { return err } + + profileExists, _, getAnyUserAttributeFunction, err := viewableProfiles.GetRetrieveAnyNewestViewableUserProfileAttributeFunction(userIdentityHash, appNetworkType, true, false, false) + if (err != nil) { return err } + if (profileExists == true) { + + passesMyDesires, err := myMateDesires.CheckIfMateProfilePassesAllMyDesires(false, "", getAnyUserAttributeFunction) + if (err != nil) { return err } + + if (passesMyDesires == true){ + err = mySettings.SetSetting("MatchesNeedRefreshYesNo", "Yes") + if (err != nil) { return err } + } + } + } + + return nil + } + + confirmButton := getWidgetCentered(widget.NewButtonWithIcon("Unignore User", theme.ConfirmIcon(), func(){ + + setLoadingScreen(window, "Unignore User", "Unhiding User...") + err := unignoreUserFunction() + if (err != nil){ + setErrorEncounteredPage(window, err, previousPage) + return + } + + nextPage() + })) + + page.Add(confirmButton) + + setPageContent(page, window) +} + + +func setConfirmLikeUserPage(window fyne.Window, userIdentityHash [16]byte, previousPage func(), nextPage func()){ + + userIdentityType, err := identity.GetIdentityTypeFromIdentityHash(userIdentityHash) + if (err != nil) { + userIdentityHashHex := encoding.EncodeBytesToHexString(userIdentityHash[:]) + setErrorEncounteredPage(window, errors.New("setConfirmLikeUserPage called with invalid identityHash: " + userIdentityHashHex), previousPage) + return + } + if (userIdentityType != "Mate"){ + setErrorEncounteredPage(window, errors.New("setConfirmLikeUserPage called with non-mate identity."), previousPage) + return + } + + title := getPageTitleCentered("Confirm Like User") + + backButton := getBackButtonCentered(previousPage) + + description1 := getBoldLabelCentered("Like user?") + description2 := getLabelCentered("The user will be saved in your liked users.") + description3 := getLabelCentered("They will not be notified.") + description4 := getLabelCentered("To notify them, Greet or message them.") + + likeUserFunction := func()error{ + + err := myLikedUsers.LikeUser(userIdentityHash) + if (err != nil) { return err } + + onlyShowLikedUsersBool, err := myChatFilters.GetChatFilterOnOffStatus("Mate", "OnlyShowLikedUsers") + if (err != nil) { return err } + if (onlyShowLikedUsersBool == true){ + + err := mySettings.SetSetting("MateChatConversationsNeedRefreshYesNo", "Yes") + if (err != nil) { return err } + } + + getOnlyShowLikedMatchesBool := func()(bool, error){ + + exists, onlyShowLikedMatches, err := myLocalDesires.GetDesire("OnlyShowLikedMatches") + if (err != nil) { return false, err } + if (exists == true && onlyShowLikedMatches == "Yes"){ + return true, nil + } + return false, nil + } + + onlyShowLikedMatchesBool, err := getOnlyShowLikedMatchesBool() + if (err != nil) { return err } + + if (onlyShowLikedMatchesBool == true){ + + // Now we check if user is now a match, now that they are liked + // If yes, matches need to be refreshed + + appNetworkType, err := getAppNetworkType.GetAppNetworkType() + if (err != nil) { return err } + + profileExists, _, getAnyUserAttributeFunction, err := viewableProfiles.GetRetrieveAnyNewestViewableUserProfileAttributeFunction(userIdentityHash, appNetworkType, true, false, false) + if (err != nil) { return err } + if (profileExists == true) { + + passesMyDesires, err := myMateDesires.CheckIfMateProfilePassesAllMyDesires(false, "", getAnyUserAttributeFunction) + if (err != nil) { return err } + + if (passesMyDesires == true){ + err = mySettings.SetSetting("MatchesNeedRefreshYesNo", "Yes") + if (err != nil) { return err } + } + } + } + + return nil + } + + confirmButton := getWidgetCentered(widget.NewButtonWithIcon("Like User", theme.ConfirmIcon(), func(){ + + setLoadingScreen(window, "Like User", "Like User...") + err := likeUserFunction() + if (err != nil){ + setErrorEncounteredPage(window, err, previousPage) + return + } + + nextPage() + })) + + page := container.NewVBox(title, backButton, widget.NewSeparator(), description1, description2, description3, description4, widget.NewSeparator(), confirmButton) + + setPageContent(page, window) +} + +func setConfirmUnlikeUserPage(window fyne.Window, userIdentityHash [16]byte, previousPage func(), nextPage func()){ + + userIdentityType, err := identity.GetIdentityTypeFromIdentityHash(userIdentityHash) + if (err != nil) { + setErrorEncounteredPage(window, err, previousPage) + return + } + if (userIdentityType != "Mate"){ + setErrorEncounteredPage(window, errors.New("setConfirmUnlikeUserPage called with non-mate identity."), previousPage) + return + } + + setLoadingScreen(window, "Unlike User", "Loading Unlike User page...") + + title := getPageTitleCentered("Confirm Unlike User") + + backButton := getBackButtonCentered(previousPage) + + description1 := getBoldLabelCentered("Unlike user?") + description2 := getLabelCentered("The user will be removed from your likes.") + description3 := getLabelCentered("They will not be notified.") + + userIsLiked, timeOfLikingUnix, err := myLikedUsers.CheckIfUserIsLiked(userIdentityHash) + if (err != nil) { + setErrorEncounteredPage(window, err, previousPage) + return + } + if (userIsLiked == false){ + setErrorEncounteredPage(window, errors.New("setConfirmUnlikeUserPage visited when user is not liked."), previousPage) + return + } + + likedAgoString, err := helpers.ConvertUnixTimeToTimeAgoTranslated(timeOfLikingUnix, false) + if (err != nil) { + setErrorEncounteredPage(window, err, previousPage) + return + } + + likedAgoLabel := getItalicLabelCentered("Liked " + likedAgoString + ".") + + unlikeUserFunction := func()error{ + + err := myLikedUsers.UnlikeUser(userIdentityHash) + if (err != nil) { return err } + + onlyShowLikedUsersBool, err := myChatFilters.GetChatFilterOnOffStatus("Mate", "OnlyShowLikedUsers") + if (err != nil) { return err } + if (onlyShowLikedUsersBool == true){ + + err := mySettings.SetSetting("MateChatConversationsNeedRefreshYesNo", "Yes") + if (err != nil) { return err } + } + + getOnlyShowLikedMatchesBool := func()(bool, error){ + + exists, onlyShowLikedMatches, err := myLocalDesires.GetDesire("OnlyShowLikedMatches") + if (err != nil) { return false, err } + if (exists == true && onlyShowLikedMatches == "Yes"){ + return true, nil + } + return false, nil + } + + onlyShowLikedMatchesBool, err := getOnlyShowLikedMatchesBool() + if (err != nil) { return err } + if (onlyShowLikedMatchesBool == true){ + + // We see if the user will no longer be a match. + // If so, we make sure that matchesNeedRefresh status is updated + + appNetworkType, err := getAppNetworkType.GetAppNetworkType() + if (err != nil) { return err } + + myMatchesReady, myMatchesList, err := myMatches.GetMyMatchesList(appNetworkType) + if (err != nil) { return err } + if (myMatchesReady == false){ + err = mySettings.SetSetting("MatchesNeedRefreshYesNo", "Yes") + if (err != nil) { return err } + } else { + userIsAMatch := slices.Contains(myMatchesList, userIdentityHash) + if (userIsAMatch == true){ + err = mySettings.SetSetting("MatchesNeedRefreshYesNo", "Yes") + if (err != nil) { return err } + } + } + } + + return nil + } + + confirmButton := getWidgetCentered(widget.NewButtonWithIcon("Unlike User", theme.ConfirmIcon(), func(){ + + setLoadingScreen(window, "Unlike User", "Unliking User...") + err := unlikeUserFunction() + if (err != nil){ + setErrorEncounteredPage(window, err, previousPage) + return + } + + nextPage() + })) + + page := container.NewVBox(title, backButton, widget.NewSeparator(), description1, description2, description3, likedAgoLabel, confirmButton) + + setPageContent(page, window) +} + +func setConfirmGreetOrRejectUserPage(window fyne.Window, greetOrReject string, userIdentityHash [16]byte, previousPage func(), afterSendPage func()){ + + currentPage := func(){setConfirmGreetOrRejectUserPage(window, greetOrReject, userIdentityHash, previousPage, afterSendPage)} + + if (greetOrReject != "Greet" && greetOrReject != "Reject"){ + setErrorEncounteredPage(window, errors.New("setConfirmGreetOrRejectUserPage called with invalid greetOrReject: " + greetOrReject), previousPage) + return + } + + userIdentityType, err := identity.GetIdentityTypeFromIdentityHash(userIdentityHash) + if (err != nil){ + setErrorEncounteredPage(window, err, previousPage) + return + } + if (userIdentityType != "Mate"){ + setErrorEncounteredPage(window, errors.New("Trying to greet/reject a non-mate user."), previousPage) + return + } + + title := getPageTitleCentered("Confirm " + greetOrReject + " User") + + backButton := getBackButtonCentered(previousPage) + + greetOrRejectLowercase := strings.ToLower(greetOrReject) + + myIdentityExists, myIdentityHash, err := myIdentity.GetMyIdentityHash("Mate") + if (err != nil) { + setErrorEncounteredPage(window, err, previousPage) + return + } + if (myIdentityExists == false){ + + description1 := getBoldLabelCentered("Your Mate identity does not exist.") + description2 := getLabelCentered("A " + greetOrRejectLowercase + " is a message sent to another user.") + description3 := getLabelCentered("You cannot " + greetOrRejectLowercase + " a user without a mate identity.") + description4 := getLabelCentered("Create your Mate identity?") + + createIdentityButton := getWidgetCentered(widget.NewButtonWithIcon("Create Mate Identity", theme.NavigateNextIcon(), func(){ + setChooseNewIdentityHashPage(window, "Mate", currentPage, currentPage) + })) + + page := container.NewVBox(title, backButton, widget.NewSeparator(), description1, description2, description3, description4, createIdentityButton) + + setPageContent(page, window) + return + } + + exists, iAmDisabled, err := myLocalProfiles.GetProfileData("Mate", "Disabled") + if (err != nil) { + setErrorEncounteredPage(window, err, previousPage) + return + } + if (exists == true && iAmDisabled == "Yes"){ + description1 := getBoldLabelCentered("Your Mate profile is disabled.") + description2 := getLabelCentered("A " + greetOrRejectLowercase + " is a message sent to another user.") + description3 := getLabelCentered("You cannot " + greetOrRejectLowercase + " users with a disabled profile.") + description4 := getLabelCentered("Enable your profile on the Broadcast page to continue.") + + visitBroadcastPageButton := getWidgetCentered(widget.NewButtonWithIcon("Visit Broadcast Page", theme.NavigateNextIcon(), func(){ + setBroadcastPage(window, "Mate", currentPage) + })) + + page := container.NewVBox(title, backButton, widget.NewSeparator(), description1, description2, description3, description4, visitBroadcastPageButton) + + setPageContent(page, window) + return + } + + appNetworkType, err := getAppNetworkType.GetAppNetworkType() + if (err != nil) { + setErrorEncounteredPage(window, err, previousPage) + return + } + + myIdentityFound, myProfileIsActiveStatus, err := myProfileStatus.GetMyProfileIsActiveStatus(myIdentityHash, appNetworkType) + if (err != nil) { + setErrorEncounteredPage(window, err, previousPage) + return + } + if (myIdentityFound == false) { + setErrorEncounteredPage(window, errors.New("My identity not found after being found already."), previousPage) + return + } + if (myProfileIsActiveStatus == false){ + + description1 := getBoldLabelCentered("Your Mate profile is not active.") + description2 := getLabelCentered("A " + greetOrRejectLowercase + " is a message sent to another user.") + description3 := getLabelCentered("You must broadcast your Mate profile to " + greetOrRejectLowercase + " users.") + description4 := getLabelCentered("Broadcast your Mate profile on the Broadcast page.") + + broadcastPageButton := getWidgetCentered(widget.NewButtonWithIcon("Visit Broadcast Page", theme.NavigateNextIcon(), func(){ + setBroadcastPage(window, "Mate", currentPage) + })) + + page := container.NewVBox(title, backButton, widget.NewSeparator(), description1, description2, description3, description4, broadcastPageButton) + setPageContent(page, window) + return + } + + description1 := getBoldLabelCentered(greetOrReject + " User?") + + description2 := getLabelCentered("This will send the user a " + greetOrReject + " message.") + + getDescription3Text := func()string{ + + if (greetOrReject == "Greet"){ + result := translate("This is a way of signaling interest in the user.") + + return result + } + // greetOrReject = "Reject" + + result := translate("This is a way of telling users you are not interested in them.") + + return result + } + + description3Text := getDescription3Text() + + description3Label := widget.NewLabel(description3Text) + + helpButton := widget.NewButtonWithIcon("", theme.QuestionIcon(), func(){ + setGreetAndRejectExplainerPage(window, currentPage) + }) + description3Row := container.NewHBox(layout.NewSpacer(), description3Label, helpButton, layout.NewSpacer()) + + description4 := getLabelCentered("You will pay for the message on the next page.") + + page := container.NewVBox(title, backButton, widget.NewSeparator(), description1, description2, description3Row, description4, widget.NewSeparator()) + + //Outputs: + // -bool: Section is needed + // -*fyne.Container: Greet or reject status section + // -error + getCurrentGreetOrRejectStatusSection := func()(bool, *fyne.Container, error){ + + myIdentityFound, conversationFound, _, _, iHaveContactedThem, iHaveRejectedThem, timeOfMyMostRecentGreetOrReject, theyHaveContactedMe, theyHaveRejectedMe, timeOfTheirMostRecentGreetOrReject, err := myChatMessages.GetMyConversationInfoAndSortedMessagesList(myIdentityHash, userIdentityHash, appNetworkType) + if (err != nil) { return false, nil, err } + if (myIdentityFound == false){ + return false, nil, errors.New("My identity not found after being found already.") + } + if (conversationFound == false){ + return false, nil, nil + } + if (iHaveContactedThem == false && theyHaveContactedMe == false){ + return false, nil, nil + } + + statusSection := container.NewVBox() + if (iHaveContactedThem == true){ + + timeAgoText, err := helpers.ConvertUnixTimeToTimeAgoTranslated(timeOfMyMostRecentGreetOrReject, false) + if (err != nil) { return false, nil, err } + + if (iHaveRejectedThem == true){ + greetOrRejectStatus := getBoldLabelCentered("You rejected this user " + timeAgoText + ".") + statusSection.Add(greetOrRejectStatus) + } else { + greetOrRejectStatus := getBoldLabelCentered("You greeted this user " + timeAgoText + ".") + statusSection.Add(greetOrRejectStatus) + } + } + if (theyHaveContactedMe == true){ + timeAgoText, err := helpers.ConvertUnixTimeToTimeAgoTranslated(timeOfTheirMostRecentGreetOrReject, false) + if (err != nil) { return false, nil, err } + + if (theyHaveRejectedMe == true){ + greetOrRejectStatus := getBoldLabelCentered("This user rejected you " + timeAgoText + ".") + statusSection.Add(greetOrRejectStatus) + } else { + greetOrRejectStatus := getBoldLabelCentered("This user greeted you " + timeAgoText + ".") + statusSection.Add(greetOrRejectStatus) + } + } + + return true, statusSection, nil + } + + greetOrRejectStatusSectionNeeded, greetOrRejectStatusSection, err := getCurrentGreetOrRejectStatusSection() + if (err != nil){ + setErrorEncounteredPage(window, err, previousPage) + return + } + + if (greetOrRejectStatusSectionNeeded == true){ + page.Add(greetOrRejectStatusSection) + } + + greetOrRejectButton := getWidgetCentered(widget.NewButtonWithIcon(greetOrReject, theme.ConfirmIcon(), func(){ + + getMessageCommunication := func()string{ + + if (greetOrReject == "Greet"){ + return ">!>Greet" + } + return ">!>Reject" + } + + messageCommunication := getMessageCommunication() + + setFundAndSendChatMessagePage(window, myIdentityHash, userIdentityHash, messageCommunication, false, currentPage, afterSendPage) + })) + + page.Add(greetOrRejectButton) + + setPageContent(page, window) +} + + diff --git a/gui/questionnaireGui.go b/gui/questionnaireGui.go new file mode 100644 index 0000000..f76e301 --- /dev/null +++ b/gui/questionnaireGui.go @@ -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 +} + + diff --git a/gui/resourcesGui.go b/gui/resourcesGui.go new file mode 100644 index 0000000..90d9f34 --- /dev/null +++ b/gui/resourcesGui.go @@ -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) +} + + diff --git a/gui/settingsGui.go b/gui/settingsGui.go new file mode 100644 index 0000000..a487da7 --- /dev/null +++ b/gui/settingsGui.go @@ -0,0 +1,1882 @@ +package gui + +// settingsGui.go implements pages to manage a user's settings and user data + +import "fyne.io/fyne/v2" +import "fyne.io/fyne/v2/container" +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/resources/currencies" + +import "seekia/internal/appMemory" +import "seekia/internal/encoding" +import "seekia/internal/globalSettings" +import "seekia/internal/helpers" +import "seekia/internal/identity" +import "seekia/internal/localFilesystem" +import "seekia/internal/logger" +import "seekia/internal/messaging/chatMessageStorage" +import "seekia/internal/messaging/myChatMessages" +import "seekia/internal/moderation/reportStorage" +import "seekia/internal/moderation/reviewStorage" +import "seekia/internal/myIdentity" +import "seekia/internal/mySeedPhrases" +import "seekia/internal/mySettings" +import "seekia/internal/network/appNetworkType/getAppNetworkType" +import "seekia/internal/network/appNetworkType/setAppNetworkType" +import "seekia/internal/network/myBroadcasts" +import "seekia/internal/profiles/profileStorage" +import "seekia/internal/seedPhrase" + +import "errors" + +func setSettingsPage(window fyne.Window){ + + appMemory.SetMemoryEntry("CurrentViewedPage", "Settings") + + currentPage := func(){setSettingsPage(window)} + + title := getPageTitleCentered("Settings") + + description := getLabelCentered("Manage your Seekia settings.") + + myDataButton := widget.NewButton("My Data", func(){ + setSettingsPage_MyData(window, currentPage) + }) + + themesButton := widget.NewButton("Themes", func(){ + setChooseAppThemePage(window, currentPage) + }) + + navigationButton := widget.NewButton("Navigation", func(){ + setNavigationSettingsPage(window, currentPage) + }) + + contentFilteringButton := widget.NewButton("Content Filtering", func(){ + setContentFilteringSettingsPage(window, currentPage) + }) + + networkSettingsButton := widget.NewButton("Network", func(){ + setManageNetworkSettingsPage(window, currentPage) + }) + + storageSettingsButton := widget.NewButton("Storage", func(){ + setManageStoragePage(window, currentPage) + }) + + toolsButton := widget.NewButton("Tools", func(){ + setToolsPage(window, currentPage) + }) + + languageButton := widget.NewButton("Language", func(){ + setSelectLanguagePage(window, true, currentPage) + }) + + logsButton := widget.NewButton("Logs", func(){ + setViewLogsPage(window, "General", currentPage) + }) + + developerButton := widget.NewButton("Developer", func(){ + setViewDeveloperSettingsPage(window, currentPage) + }) + + buttonGrid := getContainerCentered(container.NewGridWithColumns(1, myDataButton, themesButton, navigationButton, contentFilteringButton, networkSettingsButton, storageSettingsButton, toolsButton, languageButton, logsButton, developerButton)) + + pageContent := container.NewVBox(title, widget.NewSeparator(), description, widget.NewSeparator(), buttonGrid) + + setPageContent(pageContent, window) +} + +func getMetricImperialSwitchButton(window fyne.Window, currentPage func())(fyne.Widget, error){ + + currentUnitsExist, currentUnits, err := globalSettings.GetSetting("MetricOrImperial") + if (err != nil){ return nil, err } + + getSwitchButton := func()fyne.Widget{ + + if (currentUnitsExist == false || currentUnits == "Metric"){ + + button := widget.NewButton(translate("Metric"), func(){ + err := globalSettings.SetSetting("MetricOrImperial", "Imperial") + if (err != nil){ + setErrorEncounteredPage(window, err, currentPage) + return + } + currentPage() + }) + return button + } + + button := widget.NewButton(translate("Imperial"), func(){ + + err := globalSettings.SetSetting("MetricOrImperial", "Metric") + if (err != nil){ + setErrorEncounteredPage(window, err, currentPage) + return + } + currentPage() + }) + + return button + } + + switchButton := getSwitchButton() + + return switchButton, nil +} + + +func setSettingsPage_MyData(window fyne.Window, previousPage func()){ + + currentPage := func(){setSettingsPage_MyData(window, previousPage)} + + title := getPageTitleCentered("My Data") + + backButton := getBackButtonCentered(previousPage) + + myIdentityHashesButton := widget.NewButton("My Identity Hashes", func(){ + setViewMyIdentityHashesPage(window, "Mate", currentPage) + }) + + mySeedPhrasesButton := widget.NewButton("My Seed Phrases", func(){ + setViewMySeedPhrasesPage(window, "Mate", currentPage) + }) + + deleteIdentityButton := widget.NewButton("Delete Identity", func(){ + setDeleteMyIdentityPage(window, "Mate", currentPage) + }) + + importIdentityButton := widget.NewButton("Import Identity", func(){ + setImportMyIdentityPage(window, "Mate", currentPage) + }) + + exportDataButton := widget.NewButton("Export Data", func(){ + //TODO + // Page to export user data + // This should export the UserData folder(s) to a folder, and that data should be able to be imported to a new device + // The GUI should give options on which data to export + showUnderConstructionDialog(window) + }) + + importDataButton := widget.NewButton("Import Data", func(){ + //TODO + // On import, user should be able to select which elements to import, and any conflicts with existing data should be shown + // When importing chat keys, we must make sure to prune the user's undecryptable message hashes list + // The new keys should be used to attempt to decrypt those messages again (if they still exist on the network) + showUnderConstructionDialog(window) + }) + + buttonsGrid := getContainerCentered(container.NewGridWithColumns(1, myIdentityHashesButton, mySeedPhrasesButton, deleteIdentityButton, importIdentityButton, exportDataButton, importDataButton)) + + page := container.NewVBox(title, backButton, widget.NewSeparator(), buttonsGrid) + + setPageContent(page, window) +} + +func setViewMySeedPhrasesPage(window fyne.Window, myIdentityType string, previousPage func()){ + + title := getPageTitleCentered(translate("My Seed Phrase")) + + currentPage := func(){setViewMySeedPhrasesPage(window, myIdentityType, previousPage)} + + backButton := getBackButtonCentered(previousPage) + + description1 := getLabelCentered("Your seed phrase can recover your " + myIdentityType + " identity on any device.") + description2 := getLabelCentered("Write it down to prevent the loss of your identity.") + description3 := getBoldLabelCentered("Do not share your seed phrases!") + + identityTypeIcon, err := getIdentityTypeIcon(myIdentityType, -10) + if (err != nil){ + setErrorEncounteredPage(window, err, previousPage) + return + } + + getIdentityTypeLabelOrChangeButton := func()(fyne.Widget, error){ + + getHostModeEnabledBool := func()(bool, error){ + exists, hostModeOnOffStatus, err := mySettings.GetSetting("HostModeOnOffStatus") + if (err != nil) { return false, err } + if (exists == true && hostModeOnOffStatus == "On"){ + return true, nil + } + return false, nil + } + + getModeratorModeEnabledBool := func()(bool, error){ + exists, moderatorModeOnOffStatus, err := mySettings.GetSetting("ModeratorModeOnOffStatus") + if (err != nil) { return false, err } + if (exists == true && moderatorModeOnOffStatus == "On"){ + return true, nil + } + return false, nil + } + + hostModeEnabled, err := getHostModeEnabledBool() + if (err != nil) { return nil, err } + + moderatorModeEnabled, err := getModeratorModeEnabledBool() + if (err != nil) { return nil, err } + + hostIdentityExists, _, err := mySeedPhrases.GetMySeedPhrase("Host") + if (err != nil) { return nil, err } + + moderatorIdentityExists, _, err := mySeedPhrases.GetMySeedPhrase("Moderator") + if (err != nil) { return nil, err } + + if (hostModeEnabled == false && hostIdentityExists == false && moderatorModeEnabled == false && moderatorIdentityExists == false){ + + // Mate identity is the only identity to show + + mateLabel := getBoldLabel("Mate") + return mateLabel, nil + } + + getNextIdentityType := func()string{ + + if (myIdentityType == "Mate"){ + + if (hostModeEnabled == true || hostIdentityExists == true){ + return "Host" + } + + return "Moderator" + } + if (myIdentityType == "Host"){ + + if (moderatorModeEnabled == true || moderatorIdentityExists == true){ + return "Moderator" + } + + return "Mate" + } + + return "Mate" + } + + nextIdentityType := getNextIdentityType() + + changeIdentityTypeButton := widget.NewButton(myIdentityType, func(){ + setViewMySeedPhrasesPage(window, nextIdentityType, previousPage) + }) + + return changeIdentityTypeButton, nil + } + + identityTypeLabelOrChangeButton, err := getIdentityTypeLabelOrChangeButton() + if (err != nil){ + setErrorEncounteredPage(window, err, previousPage) + return + } + + identityTypeLabelOrChangeButtonWithIcon := getContainerCentered(container.NewGridWithColumns(1, identityTypeIcon, identityTypeLabelOrChangeButton)) + + currentSeedPhraseExists, currentSeedPhrase, err := mySeedPhrases.GetMySeedPhrase(myIdentityType) + if (err != nil) { + setErrorEncounteredPage(window, err, previousPage) + return + } + if (currentSeedPhraseExists == false) { + + identityDoesNotExistLabel := getBoldLabelCentered("Your " + myIdentityType + " identity does not exist.") + + createIdentityButton := getWidgetCentered(widget.NewButtonWithIcon("Create Identity", theme.NavigateNextIcon(), func(){ + setChooseNewIdentityHashPage(window, myIdentityType, currentPage, currentPage) + })) + + page := container.NewVBox(title, backButton, widget.NewSeparator(), description1, description2, description3, widget.NewSeparator(), identityTypeLabelOrChangeButtonWithIcon, identityDoesNotExistLabel, createIdentityButton, widget.NewSeparator()) + + setPageContent(page, window) + return + } + + seedPhraseLabel := widget.NewMultiLineEntry() + seedPhraseLabel.Wrapping = 3 + seedPhraseLabel.SetText(currentSeedPhrase) + seedPhraseLabel.OnChanged = func(_ string){ + seedPhraseLabel.SetText(currentSeedPhrase) + } + seedPhraseLabelBoxed := getWidgetBoxed(seedPhraseLabel) + + widener := widget.NewLabel(" ") + seedPhraseBoxWidened := getContainerCentered(container.NewGridWithColumns(1, seedPhraseLabelBoxed, widener)) + + page := container.NewVBox(title, backButton, widget.NewSeparator(), description1, description2, description3, widget.NewSeparator(), identityTypeLabelOrChangeButtonWithIcon, seedPhraseBoxWidened) + + setPageContent(page, window) +} + +func setViewMyIdentityHashesPage(window fyne.Window, myIdentityType string, previousPage func()){ + + currentPage := func(){setViewMyIdentityHashesPage(window, myIdentityType, previousPage)} + + title := getPageTitleCentered(translate("My Identity Hashes")) + + backButton := getBackButtonCentered(previousPage) + + description1 := getLabelCentered("All users of Seekia have a unique identity hash.") + description2 := getLabelCentered("Share your identity hash to advertise your identity.") + + identityTypeIcon, err := getIdentityTypeIcon(myIdentityType, -10) + if (err != nil){ + setErrorEncounteredPage(window, err, previousPage) + return + } + + getIdentityTypeLabelOrChangeButton := func()(fyne.Widget, error){ + + getHostModeEnabledBool := func()(bool, error){ + exists, hostModeOnOffStatus, err := mySettings.GetSetting("HostModeOnOffStatus") + if (err != nil) { return false, err } + if (exists == true && hostModeOnOffStatus == "On"){ + return true, nil + } + return false, nil + } + + getModeratorModeEnabledBool := func()(bool, error){ + exists, moderatorModeOnOffStatus, err := mySettings.GetSetting("ModeratorModeOnOffStatus") + if (err != nil) { return false, err } + if (exists == true && moderatorModeOnOffStatus == "On"){ + return true, nil + } + return false, nil + } + + hostModeEnabled, err := getHostModeEnabledBool() + if (err != nil) { return nil, err } + + moderatorModeEnabled, err := getModeratorModeEnabledBool() + if (err != nil) { return nil, err } + + hostIdentityExists, _, err := mySeedPhrases.GetMySeedPhrase("Host") + if (err != nil) { return nil, err } + + moderatorIdentityExists, _, err := mySeedPhrases.GetMySeedPhrase("Moderator") + if (err != nil) { return nil, err } + + if (hostModeEnabled == false && hostIdentityExists == false && moderatorModeEnabled == false && moderatorIdentityExists == false){ + + // Mate identity is the only identity to show + mateLabel := getBoldLabel("Mate") + return mateLabel, nil + } + + getNextIdentityType := func()string{ + + if (myIdentityType == "Mate"){ + + if (hostModeEnabled == true || hostIdentityExists == true){ + return "Host" + } + return "Moderator" + } + + if (myIdentityType == "Host"){ + + if (moderatorModeEnabled == true || moderatorIdentityExists == true){ + return "Moderator" + } + return "Mate" + } + return "Mate" + } + + nextIdentityType := getNextIdentityType() + + changeIdentityTypeButton := widget.NewButton(myIdentityType, func(){ + setViewMyIdentityHashesPage(window, nextIdentityType, previousPage) + }) + + return changeIdentityTypeButton, nil + } + + identityTypeLabelOrChangeButton, err := getIdentityTypeLabelOrChangeButton() + if (err != nil){ + setErrorEncounteredPage(window, err, previousPage) + return + } + + identityTypeLabelOrChangeButtonWithIcon := getContainerCentered(container.NewGridWithColumns(1, identityTypeIcon, identityTypeLabelOrChangeButton)) + + currentIdentityHashExists, currentIdentityHash, err := myIdentity.GetMyIdentityHash(myIdentityType) + if (err != nil) { + setErrorEncounteredPage(window, err, previousPage) + return + } + + if (currentIdentityHashExists == false) { + + identityDoesNotExistLabel := getBoldLabelCentered("Your " + myIdentityType + " identity does not exist.") + + createIdentityButton := getWidgetCentered(widget.NewButtonWithIcon("Create Identity", theme.NavigateNextIcon(), func(){ + setChooseNewIdentityHashPage(window, myIdentityType, currentPage, currentPage) + })) + + page := container.NewVBox(title, backButton, widget.NewSeparator(), description1, description2, widget.NewSeparator(), identityTypeLabelOrChangeButtonWithIcon, identityDoesNotExistLabel, createIdentityButton, widget.NewSeparator()) + + setPageContent(page, window) + return + } + + currentIdentityHashString, _, err := identity.EncodeIdentityHashBytesToString(currentIdentityHash) + if (err != nil){ + currentIdentityHashHex := encoding.EncodeBytesToHexString(currentIdentityHash[:]) + setErrorEncounteredPage(window, errors.New("GetMyIdentityHash returning invalid myIdentityHash: " + currentIdentityHashHex), previousPage) + return + } + + identityHashLabel := widget.NewMultiLineEntry() + identityHashLabel.SetText(currentIdentityHashString) + identityHashLabel.OnChanged = func(_ string){ + identityHashLabel.SetText(currentIdentityHashString) + } + identityHashLabelBoxed := getWidgetBoxed(identityHashLabel) + + boxWidener := widget.NewLabel(" ") + identityHashBoxWidened := getContainerCentered(container.NewGridWithColumns(1, identityHashLabelBoxed, boxWidener)) + + page := container.NewVBox(title, backButton, widget.NewSeparator(), description1, description2, widget.NewSeparator(), identityTypeLabelOrChangeButtonWithIcon, identityHashBoxWidened) + + setPageContent(page, window) +} + + +func setDeleteMyIdentityPage(window fyne.Window, myIdentityType string, previousPage func()){ + + currentPage := func(){setDeleteMyIdentityPage(window, myIdentityType, previousPage)} + + if (myIdentityType != "Mate" && myIdentityType != "Host" && myIdentityType != "Moderator"){ + setErrorEncounteredPage(window, errors.New("setDeleteMyIdentityPage called with invalid identity type: " + myIdentityType), previousPage) + return + } + + title := getPageTitleCentered(translate("Delete Identity")) + + backButton := getBackButtonCentered(previousPage) + + getDescriptionSection := func()*fyne.Container{ + + description1 := getLabelCentered("This page allows you to delete your " + myIdentityType + " identity.") + + if (myIdentityType == "Host"){ + description2 := getLabelCentered("You will lose your host identity balance and account credit.") + description3 := getLabelCentered("Write down your seed phrase to keep access to your identity and credit.") + description4 := getLabelCentered("Disable your profile before doing this.") + descriptionSection := container.NewVBox(description1, description2, description3, description4) + return descriptionSection + } + if (myIdentityType == "Moderator"){ + description2 := getLabelCentered("You will lose your identity score and account credit.") + description3 := getLabelCentered("This will also delete your message history.") + description4 := getLabelCentered("Write down your seed phrase to keep access to your identity and credit.") + descriptionSection := container.NewVBox(description1, description2, description3, description4) + return descriptionSection + } + + description2 := getLabelCentered("This will delete your messages, identity balance, and account credit.") + description3 := getLabelCentered("Write down your seed phrase to keep access to your identity and credit.") + description4 := getLabelCentered("Disable your profile before doing this.") + descriptionSection := container.NewVBox(description1, description2, description3, description4) + + return descriptionSection + } + + descriptionSection := getDescriptionSection() + + getIdentityTypeLabelOrChangeButton := func()(fyne.Widget, error){ + + hostIdentityExists, _, err := mySeedPhrases.GetMySeedPhrase("Host") + if (err != nil) { return nil, err } + + moderatorIdentityExists, _, err := mySeedPhrases.GetMySeedPhrase("Moderator") + if (err != nil) { return nil, err } + + if (myIdentityType == "Mate" && hostIdentityExists == false && moderatorIdentityExists == false){ + mateLabel := getBoldLabel("Mate") + return mateLabel, nil + } + + getNextIdentityType := func()string{ + + if (myIdentityType == "Mate"){ + if (hostIdentityExists == true){ + return "Host" + } + return "Moderator" + } + if (myIdentityType == "Host"){ + if (moderatorIdentityExists == true){ + return "Moderator" + } + } + return "Mate" + } + + nextIdentityType := getNextIdentityType() + + changeIdentityTypeButton := widget.NewButton(myIdentityType, func(){ + setDeleteMyIdentityPage(window, nextIdentityType, previousPage) + }) + + return changeIdentityTypeButton, nil + } + + identityTypeLabelOrChangeButton, err := getIdentityTypeLabelOrChangeButton() + if (err != nil){ + setErrorEncounteredPage(window, err, previousPage) + return + } + + identityTypeIcon, err := getIdentityTypeIcon(myIdentityType, -10) + if (err != nil){ + setErrorEncounteredPage(window, err, previousPage) + return + } + + identityTypeLabelOrChangeButtonWithIcon := getContainerCentered(container.NewGridWithColumns(1, identityTypeIcon, identityTypeLabelOrChangeButton)) + + identityExists, myIdentityHash, err := myIdentity.GetMyIdentityHash(myIdentityType) + if (err != nil){ + setErrorEncounteredPage(window, err, previousPage) + return + } + if (identityExists == false){ + + // This is only reached if the user has just deleted this identity + + description4 := getBoldLabelCentered("Your " + myIdentityType + " identity does not exist.") + description5 := getLabelCentered("There is no identity to delete.") + + page := container.NewVBox(title, backButton, widget.NewSeparator(), descriptionSection, widget.NewSeparator(), identityTypeLabelOrChangeButtonWithIcon, description4, description5, widget.NewSeparator()) + + setPageContent(page, window) + return + } + + myIdentityHashString, _, err := identity.EncodeIdentityHashBytesToString(myIdentityHash) + if (err != nil){ + setErrorEncounteredPage(window, err, previousPage) + return + } + + identityHashTrimmed, _, err := helpers.TrimAndFlattenString(myIdentityHashString, 10) + if (err != nil){ + setErrorEncounteredPage(window, err, previousPage) + return + } + + currentIdentityHashText := widget.NewLabel("Current Identity:") + identityHashLabel := getBoldLabel(identityHashTrimmed) + myIdentityHashRow := getContainerCentered(container.NewHBox(currentIdentityHashText, identityHashLabel)) + + deleteIdentityButton := getWidgetCentered(widget.NewButtonWithIcon("Delete Identity", theme.DeleteIcon(), func(){ + setConfirmDeleteMyIdentityPage(window, myIdentityType, currentPage, currentPage) + })) + + page := container.NewVBox(title, backButton, widget.NewSeparator(), descriptionSection, widget.NewSeparator(), identityTypeLabelOrChangeButtonWithIcon, myIdentityHashRow, deleteIdentityButton, widget.NewSeparator()) + + setPageContent(page, window) +} + + +func setConfirmDeleteMyIdentityPage(window fyne.Window, myIdentityType string, previousPage func(), nextPage func()){ + + title := getPageTitleCentered("Confirm Delete " + myIdentityType + " Identity") + + backButton := getBackButtonCentered(previousPage) + + identityExists, myIdentityHash, err := myIdentity.GetMyIdentityHash(myIdentityType) + if (err != nil){ + setErrorEncounteredPage(window, err, previousPage) + return + } + if (identityExists == false){ + setErrorEncounteredPage(window, errors.New("setConfirmDeleteMyIdentityPage called with missing identity."), previousPage) + return + } + + subtitle := getPageSubtitleCentered("Delete your " + myIdentityType + " identity?") + + page := container.NewVBox(title, backButton, widget.NewSeparator(), subtitle, widget.NewSeparator()) + + if (myIdentityType == "Mate"){ + + description2 := getBoldLabelCentered(translate("You will lose access to your identity balance and account credit.")) + description3 := getBoldLabelCentered(translate("This will delete your chat messages and decryption keys.")) + description4 := getLabelCentered(translate("Write down your seed phrase to retain your identity and account credit.")) + description5 := getLabelCentered(translate("Export your data to save your chat messages and decryption keys.")) + + page.Add(description2) + page.Add(description3) + page.Add(description4) + page.Add(description5) + + } else if (myIdentityType == "Host"){ + + description2 := getBoldLabelCentered(translate("You will lose access to your identity balance and account credit.")) + description3 := getLabelCentered(translate("Write down your seed phrase to retain your identity and account credit.")) + + page.Add(description2) + page.Add(description3) + + } else if (myIdentityType == "Moderator"){ + + description2 := getBoldLabelCentered(translate("You will lose access to your identity score and account credit.")) + description3 := getBoldLabelCentered(translate("This will delete your chat messages and decryption keys.")) + description4 := getLabelCentered(translate("Write down your seed phrase to retain your identity and account credit.")) + description5 := getLabelCentered(translate("Export your data to save your chat messages and decryption keys.")) + + page.Add(description2) + page.Add(description3) + page.Add(description4) + page.Add(description5) + } + + description5 := getLabelCentered("This will not delete your local profile.") + page.Add(description5) + + page.Add(widget.NewSeparator()) + + if (myIdentityType != "Host"){ + + //Outputs: + // -int: Number of messages to delete + // -int: Number of conversations to delete + getMessagesToDeleteInfo := func(networkType byte)(int, int, error){ + + updateProgressFunction := func(_ int)error{ + return nil + } + + myChatMessagesMapList, err := myChatMessages.GetUpdatedMyChatMessagesMapList(myIdentityType, networkType, updateProgressFunction) + if (err != nil) { return 0, 0, err } + + numberOfMessagesToDelete := len(myChatMessagesMapList) + + //Map Structure: Recipient Identity Hash -> Nothing + conversationRecipientsMap := make(map[string]struct{}) + + for _, messageMap := range myChatMessagesMapList{ + theirIdentityHash, exists := messageMap["TheirIdentityHash"] + if (exists == false) { + return 0, 0, errors.New("Malformed myChatMessages map list: Item missing TheirIdentityHash") + } + + conversationRecipientsMap[theirIdentityHash] = struct{}{} + } + + numberOfConversationsToDelete := len(conversationRecipientsMap) + + return numberOfMessagesToDelete, numberOfConversationsToDelete, nil + } + + numberOfMessagesToDelete_Network1, numberOfConversationsToDelete_Network1, err := getMessagesToDeleteInfo(1) + if (err != nil){ + setErrorEncounteredPage(window, err, previousPage) + return + } + + numberOfMessagesToDelete_Network2, numberOfConversationsToDelete_Network2, err := getMessagesToDeleteInfo(2) + if (err != nil){ + setErrorEncounteredPage(window, err, previousPage) + return + } + + numberOfMessagesToDelete := numberOfMessagesToDelete_Network1 + numberOfMessagesToDelete_Network2 + numberOfConversationsToDelete := numberOfConversationsToDelete_Network1 + numberOfConversationsToDelete_Network2 + + if (numberOfMessagesToDelete != 0){ + + thisWillDeleteLabel := widget.NewLabel("This will delete") + + numberOfMessagesString := helpers.ConvertIntToString(numberOfMessagesToDelete) + numberOfMessagesLabel := getBoldLabel(numberOfMessagesString) + + getMessageOrMessagesText := func()string{ + if (numberOfMessagesToDelete == 1){ + return "message" + } + return "messages" + } + messageOrMessagesText := getMessageOrMessagesText() + messagesAndLabel := widget.NewLabel(messageOrMessagesText + " and") + + numberOfConversationsString := helpers.ConvertIntToString(numberOfConversationsToDelete) + numberOfConversationsLabel := getBoldLabel(numberOfConversationsString) + + getConversationOrConversationsText := func()string{ + if (numberOfConversationsToDelete == 1){ + return "conversation" + } + return "conversations" + } + conversationOrConversationsText := getConversationOrConversationsText() + conversationsLabel := widget.NewLabel(conversationOrConversationsText) + + chatDeleteWarningRow := container.NewHBox(layout.NewSpacer(), thisWillDeleteLabel, numberOfMessagesLabel, messagesAndLabel, numberOfConversationsLabel, conversationsLabel, layout.NewSpacer()) + + page.Add(chatDeleteWarningRow) + page.Add(widget.NewSeparator()) + } + } + + //TODO: Show account credit balance + + if (myIdentityType == "Mate"){ + description6 := getLabelCentered("Disable your profile before doing this on the Profile - Broadcast page.") + description7 := getLabelCentered("Otherwise, it will automatically expire from the network in 3 months.") + + page.Add(description6) + page.Add(description7) + page.Add(widget.NewSeparator()) + + } else if (myIdentityType == "Host"){ + + description6 := getLabelCentered("Disable your profile before doing this on the Profile - Broadcast page.") + description7 := getLabelCentered("Otherwise, users will keep trying to connect to your inactive host address.") + //TODO: Retrieve true expiration time from parameters + description8 := getLabelCentered("This should stop after 6 hours, when your profile expires.") + + page.Add(description6) + page.Add(description7) + page.Add(description8) + page.Add(widget.NewSeparator()) + } + + deleteIdentityFunction := func()error{ + + err := myBroadcasts.DeleteMyBroadcastProfiles(myIdentityHash) + if (err != nil) { return err } + + if (myIdentityType != "Host"){ + + err := myChatMessages.DeleteMyChatMessagesMapList(myIdentityType) + if (err != nil) { return err } + + err = mySettings.SetSetting(myIdentityType + "ChatConversationsGeneratedStatus", "No") + if (err != nil) { return err } + + //TODO: Delete secret inboxes + //TODO: Delete saved message cipher keys + //TODO: Delete everything else relevant + } + + err = mySeedPhrases.DeleteMySeedPhrase(myIdentityType) + if (err != nil) { return err } + + return nil + } + + confirmDeleteIdentityButton := getWidgetCentered(widget.NewButtonWithIcon("Delete Identity", theme.DeleteIcon(), func(){ + err := deleteIdentityFunction() + if (err != nil){ + setErrorEncounteredPage(window, err, previousPage) + } + nextPage() + })) + + page.Add(confirmDeleteIdentityButton) + + setPageContent(page, window) +} + +func setImportMyIdentityPage(window fyne.Window, myIdentityType string, previousPage func()){ + + if (myIdentityType != "Mate" && myIdentityType != "Host" && myIdentityType != "Moderator"){ + setErrorEncounteredPage(window, errors.New("setImportMyIdentityPage called with invalid identity type: " + myIdentityType), previousPage) + return + } + + currentPage := func(){setImportMyIdentityPage(window, myIdentityType, previousPage)} + + title := getPageTitleCentered(translate("Import Identity")) + + backButton := getBackButtonCentered(previousPage) + + description := getLabelCentered("Import your " + myIdentityType + " identity from a seed phrase.") + + getNextIdentityType := func()string{ + if (myIdentityType == "Mate"){ + return "Host" + } + if (myIdentityType == "Host"){ + return "Moderator" + } + return "Mate" + } + + nextIdentityType := getNextIdentityType() + + identityTypeIcon, err := getIdentityTypeIcon(myIdentityType, -10) + if (err != nil){ + setErrorEncounteredPage(window, err, previousPage) + return + } + + changeIdentityTypeButton := widget.NewButton(myIdentityType, func(){ + setImportMyIdentityPage(window, nextIdentityType, previousPage) + }) + + changeIdentityTypeButtonWithIcon := getContainerCentered(container.NewGridWithColumns(1, identityTypeIcon, changeIdentityTypeButton)) + + identityExists, _, err := mySeedPhrases.GetMySeedPhrase(myIdentityType) + if (err != nil){ + setErrorEncounteredPage(window, err, previousPage) + return + } + + if (identityExists == true){ + + description2 := getBoldLabelCentered("Your " + myIdentityType + " identity exists.") + description3 := getLabelCentered("You must delete it before restoring a new identity.") + description4 := getLabelCentered("Delete your identity on the Settings - My Data - Delete Identity page") + + page := container.NewVBox(title, backButton, widget.NewSeparator(), description, widget.NewSeparator(), changeIdentityTypeButtonWithIcon, description2, description3, description4) + + setPageContent(page, window) + return + } + + seedPhraseEntry := widget.NewMultiLineEntry() + seedPhraseEntry.Wrapping = 3 + seedPhraseEntry.SetPlaceHolder(translate("Enter seed phrase to import...")) + + seedPhraseEntryBoxed := getWidgetBoxed(seedPhraseEntry) + + submitSeedButton := getWidgetCentered(widget.NewButtonWithIcon(translate("Import " + myIdentityType + " Identity"), theme.UploadIcon(), func(){ + + newSeedPhrase := seedPhraseEntry.Text + + seedPhraseIsValid := seedPhrase.VerifySeedPhrase(newSeedPhrase) + if (seedPhraseIsValid == false){ + dialogTitle := translate("Invalid Seed Phrase") + dialogMessageA := getLabelCentered("Your seed phrase is invalid.") + dialogMessageB := getLabelCentered("A Seekia seed phrase is 15 words long.") + dialogContent := container.NewVBox(dialogMessageA, dialogMessageB) + dialog.ShowCustom(dialogTitle, translate("Close"), dialogContent, window) + return + } + + err = mySeedPhrases.SetMySeedPhrase(myIdentityType, newSeedPhrase) + if (err != nil) { + setErrorEncounteredPage(window, err, currentPage) + return + } + setViewMySeedPhrasesPage(window, myIdentityType, previousPage) + })) + + widener := widget.NewLabel(" ") + heightener := widget.NewLabel("") + submitSeedButtonWithWidener := container.NewVBox(submitSeedButton, widener, heightener) + + entryWithSubmitSeedButton := getContainerCentered(container.NewGridWithColumns(1, seedPhraseEntryBoxed, submitSeedButtonWithWidener)) + + page := container.NewVBox(title, backButton, widget.NewSeparator(), description, widget.NewSeparator(), changeIdentityTypeButtonWithIcon, entryWithSubmitSeedButton) + + setPageContent(page, window) +} + + +func setChooseAppThemePage(window fyne.Window, previousPage func()){ + + currentPage := func(){setChooseAppThemePage(window, previousPage)} + + title := getPageTitleCentered(translate("App Themes")) + + backButton := getBackButtonCentered(previousPage) + + description := getLabelCentered("Choose your Seekia app theme.") + + getThemeButtonsGrid := func()(*fyne.Container, error){ + + getCurrentAppTheme := func()(string, error){ + + exists, currentTheme, err := globalSettings.GetSetting("AppTheme") + if (err != nil){ return "", err } + if (exists == false){ + return "Light", nil + } + return currentTheme, nil + } + + currentAppTheme, err := getCurrentAppTheme() + if (err != nil){ return nil, err } + + getThemeButtonWithIcon := func(themeName string, themeIconName string)(*fyne.Container, error){ + + themeIcon, err := getFyneImageIcon(themeIconName) + if (err != nil) { return nil, err } + + chooseThemeButton := widget.NewButton(translate(themeName), func(){ + + _ = globalSettings.SetSetting("AppTheme", themeName) + + customTheme, _ := getCustomFyneTheme(themeName) + + app := fyne.CurrentApp() + + app.Settings().SetTheme(customTheme) + + currentPage() + }) + + if (themeName == currentAppTheme){ + chooseThemeButton.Importance = widget.HighImportance + } + + buttonWithIcon := getContainerCentered(container.NewGridWithColumns(1, themeIcon, chooseThemeButton)) + + return buttonWithIcon, nil + } + + lightThemeButton, err := getThemeButtonWithIcon("Light", "Sun") + if (err != nil) { return nil, err } + + darkThemeButton, err := getThemeButtonWithIcon("Dark", "Moon") + if (err != nil) { return nil, err } + + loveThemeButton, err := getThemeButtonWithIcon("Love", "Mate") + if (err != nil) { return nil, err } + + oceanThemeButton, err := getThemeButtonWithIcon("Ocean", "Ocean") + if (err != nil) { return nil, err } + + buttonsGrid := container.NewGridWithColumns(2, lightThemeButton, darkThemeButton, loveThemeButton, oceanThemeButton) + + return buttonsGrid, nil + } + + themeButtonsGrid, err := getThemeButtonsGrid() + if (err != nil){ + setErrorEncounteredPage(window, err, previousPage) + return + } + + themeButtonsGridCentered := getContainerCentered(themeButtonsGrid) + + //TODO: Add a way to create a custom theme within the GUI + + page := container.NewVBox(title, backButton, widget.NewSeparator(), description, widget.NewSeparator(), themeButtonsGridCentered) + + setPageContent(page, window) +} + +func setNavigationSettingsPage(window fyne.Window, previousPage func()){ + + currentPage := func(){setNavigationSettingsPage(window, previousPage)} + + title := getPageTitleCentered(translate("Navigation Settings")) + + backButton := getBackButtonCentered(previousPage) + + description := getLabelCentered("Manage your navigation settings.") + + showButtonTextColumn := container.NewVBox() + showButtonChecksColumn := container.NewVBox() + + addShowButtonCheckRow := func(showButtonText string, settingName string)error{ + + showButtonTextLabel := getBoldLabelCentered(showButtonText) + + showButtonCheck := widget.NewCheck("", func(selected bool){ + + yesOrNoString := helpers.ConvertBoolToYesOrNoString(selected) + + err := mySettings.SetSetting(settingName, yesOrNoString) + if (err != nil){ + setErrorEncounteredPage(window, err, currentPage) + return + } + + currentPage() + }) + + exists, showButtonCurrent, err := mySettings.GetSetting(settingName) + if (err != nil) { return err } + if (exists == true && showButtonCurrent == "Yes"){ + showButtonCheck.Checked = true + } + + showButtonTextColumn.Add(showButtonTextLabel) + showButtonChecksColumn.Add(showButtonCheck) + + return nil + } + + err := addShowButtonCheckRow("Show Host Button", "ShowHostButtonNavigation") + if (err != nil){ + setErrorEncounteredPage(window, err, previousPage) + return + } + + showButtonTextColumn.Add(widget.NewSeparator()) + showButtonChecksColumn.Add(widget.NewSeparator()) + + err = addShowButtonCheckRow("Show Moderate Button", "ShowModerateButtonNavigation") + if (err != nil){ + setErrorEncounteredPage(window, err, previousPage) + return + } + + showButtonChecksGrid := container.NewHBox(layout.NewSpacer(), showButtonTextColumn, showButtonChecksColumn, layout.NewSpacer()) + + navigationLocationLabel := getLabelCentered("Navigation Bar Location:") + + getMyCurrentNavigationBarLocation := func()(string, error){ + + exists, currentNavigationLocation, err := mySettings.GetSetting("NavigationBarLocation") + if (err != nil){ return "", err } + if (exists == false){ + return "Top", nil + } + + if (currentNavigationLocation != "Top" && currentNavigationLocation != "Bottom" && currentNavigationLocation != "Left" && currentNavigationLocation != "Right"){ + return "", errors.New("Invalid navigation bar location: " + currentNavigationLocation) + } + + return currentNavigationLocation, nil + } + + currentNavigationLocation, err := getMyCurrentNavigationBarLocation() + if (err != nil){ + setErrorEncounteredPage(window, err, previousPage) + return + } + + option1Translated := translate("Top") + option2Translated := translate("Bottom") + option3Translated := translate("Left") + option4Translated := translate("Right") + + untranslatedOptionsMap := map[string]string{ + option1Translated: "Top", + option2Translated: "Bottom", + option3Translated: "Left", + option4Translated: "Right", + } + + locationOptionsList := []string{option1Translated, option2Translated, option3Translated, option4Translated} + + locationSelector := widget.NewSelect(locationOptionsList, func(response string){ + + responseUntranslated, exists := untranslatedOptionsMap[response] + if (exists == false){ + setErrorEncounteredPage(window, errors.New("untranslatedOptionsMap missing response: " + response), currentPage) + return + } + + if (responseUntranslated == currentNavigationLocation){ + return + } + + err = mySettings.SetSetting("NavigationBarLocation", responseUntranslated) + if (err != nil){ + setErrorEncounteredPage(window, err, currentPage) + return + } + + currentPage() + }) + + locationSelector.Selected = currentNavigationLocation + + locationSelectorCentered := getWidgetCentered(locationSelector) + + page := container.NewVBox(title, backButton, widget.NewSeparator(), description, widget.NewSeparator(), showButtonChecksGrid, widget.NewSeparator(), navigationLocationLabel, locationSelectorCentered) + + setPageContent(page, window) +} + + +func setContentFilteringSettingsPage(window fyne.Window, previousPage func()){ + + currentPage := func(){setContentFilteringSettingsPage(window, previousPage)} + + title := getPageTitleCentered(translate("Content Filtering Settings")) + + backButton := getBackButtonCentered(previousPage) + + description := getLabelCentered("Manage your content filtering settings.") + + pixelateImagesButton := getWidgetCentered(widget.NewButton("Pixelate Images", func(){ + setManagePixelateImagesSettingPage(window, currentPage) + })) + + // TODO: Add ability to mute/block/censor words? + + page := container.NewVBox(title, backButton, widget.NewSeparator(), description, widget.NewSeparator(), pixelateImagesButton) + + setPageContent(page, window) +} + +func setManagePixelateImagesSettingPage(window fyne.Window, previousPage func()){ + + currentPage := func(){setManagePixelateImagesSettingPage(window, previousPage)} + + title := getPageTitleCentered("Pixelate Images Setting") + + backButton := getBackButtonCentered(previousPage) + + description1 := getLabelCentered("Seekia can pixelate the images you receive in messages.") + description2 := getLabelCentered("You must slowly reveal the image by incrementally depixelating the image.") + description3 := getLabelCentered("If you are not afraid of seeing unreviewed images, you can disable this feature.") + + getCurrentStatus := func()(string, error){ + + exists, currentSetting, err := mySettings.GetSetting("PixelateImagesOnOffStatus") + if (err != nil) { return "", err } + if (exists == false){ + return "On", nil + } + return currentSetting, nil + } + + currentStatus, err := getCurrentStatus() + if (err != nil){ + setErrorEncounteredPage(window, err, previousPage) + return + } + + currentStatusTitle := widget.NewLabel("Current Status: ") + currentStatusLabel := getBoldLabel(currentStatus) + + currentStatusRow := container.NewHBox(layout.NewSpacer(), currentStatusTitle, currentStatusLabel, layout.NewSpacer()) + + getEnableOrDisableButton := func()fyne.Widget{ + + if (currentStatus == "On"){ + + disableButton := widget.NewButton("Disable", func(){ + err := mySettings.SetSetting("PixelateImagesOnOffStatus", "Off") + if (err != nil){ + setErrorEncounteredPage(window, err, currentPage) + return + } + currentPage() + }) + + return disableButton + } + + enableButton := widget.NewButton("Enable", func(){ + err := mySettings.SetSetting("PixelateImagesOnOffStatus", "On") + if (err != nil){ + setErrorEncounteredPage(window, err, currentPage) + return + } + currentPage() + }) + + return enableButton + } + + enableOrDisableButton := getEnableOrDisableButton() + + enableOrDisableButtonCentered := getWidgetCentered(enableOrDisableButton) + + page := container.NewVBox(title, backButton, widget.NewSeparator(), description1, description2, description3, widget.NewSeparator(), currentStatusRow, enableOrDisableButtonCentered) + + setPageContent(page, window) +} + +func setManageNetworkSettingsPage(window fyne.Window, previousPage func()){ + + appMemory.SetMemoryEntry("CurrentViewedPage", "ManageNetworkConnection") + + title := getPageTitleCentered("Manage Network Connection") + + //TODO + // Enable download profiles over clearnet mode + // This should not be possible for messages, only profiles + // Disable Tor mode (if using system-wide tor proxy already, such as Whonix. We can autodetect if user is using Whonix) + // Add HiddenServiceOnly mode so users can only download messages over .onion rather than using exit nodes to access clearnet hosts? + + backButton := getBackButtonCentered(previousPage) + + description := getBoldLabelCentered("Under Construction") + + page := container.NewVBox(title, backButton, widget.NewSeparator(), description) + + setPageContent(page, window) +} + +func setManageStoragePage(window fyne.Window, previousPage func()){ + + setLoadingScreen(window, "Manage Storage", "Loading Manage Storage Page...") + + currentPage := func(){setManageStoragePage(window, previousPage)} + + title := getPageTitleCentered(translate("Manage Storage")) + + backButton := getBackButtonCentered(previousPage) + + getAllowedStorageGigabytes := func()(float64, error){ + + exists, allowedStorageSpaceString, err := globalSettings.GetSetting("AllowedStorageSpace") + if (err != nil){ return 0, err } + if (exists == false){ + + //We default to 10 gigabytes + + return 10, nil + } + allowedStorageSpaceFloat, err := helpers.ConvertStringToFloat64(allowedStorageSpaceString) + if (err != nil){ + return 0, errors.New("MySettings contains invalid AllowedStorageSpace: " + allowedStorageSpaceString) + } + + return allowedStorageSpaceFloat, nil + } + + allowedStorageSpace, err := getAllowedStorageGigabytes() + if (err != nil){ + setErrorEncounteredPage(window, err, previousPage) + return + } + + databaseDirectory, err := localFilesystem.GetAppDatabaseFolderPath() + if (err != nil) { + setErrorEncounteredPage(window, err, previousPage) + return + } + + databaseSizeBytes, err := localFilesystem.GetFolderSizeInBytes(databaseDirectory) + if (err != nil) { + setErrorEncounteredPage(window, err, previousPage) + return + } + + // TODO: Change to use actual gigabyte size + databaseSizeGigabytes := float64(databaseSizeBytes)/1000000000 + + allowedStorageSpaceString := helpers.ConvertFloat64ToStringRounded(allowedStorageSpace, 2) + + databaseSizeString := helpers.ConvertFloat64ToStringRounded(databaseSizeGigabytes, 1) + + percentageOfSpaceUsed := (databaseSizeGigabytes/allowedStorageSpace)*100 + + percentageOfSpaceUsedString := helpers.ConvertFloat64ToStringRounded(percentageOfSpaceUsed, 0) + + spaceUsedTitle := getLabelCentered("Storage Space Used:") + spaceUsedLabel := getBoldLabelCentered(databaseSizeString + "/" + allowedStorageSpaceString + " gigabytes" + " (" + percentageOfSpaceUsedString + "%)") + + numberOfStoredProfiles, err := profileStorage.GetNumberOfStoredProfiles() + if (err != nil){ + setErrorEncounteredPage(window, err, previousPage) + return + } + + numberOfStoredProfilesString := helpers.ConvertInt64ToString(numberOfStoredProfiles) + numberOfStoredProfilesLabel := widget.NewLabel("Stored Profiles:") + numberOfStoredProfilesText := getBoldLabel(numberOfStoredProfilesString) + numberOfStoredProfilesRow := container.NewHBox(layout.NewSpacer(), numberOfStoredProfilesLabel, numberOfStoredProfilesText, layout.NewSpacer()) + + numberOfStoredMessages, err := chatMessageStorage.GetNumberOfStoredMessages() + if (err != nil){ + setErrorEncounteredPage(window, err, previousPage) + return + } + + numberOfStoredMessagesString := helpers.ConvertInt64ToString(numberOfStoredMessages) + + numberOfStoredMessagesLabel := widget.NewLabel("Stored Messages:") + numberOfStoredMessagesText := getBoldLabel(numberOfStoredMessagesString) + numberOfStoredMessagesRow := container.NewHBox(layout.NewSpacer(), numberOfStoredMessagesLabel, numberOfStoredMessagesText, layout.NewSpacer()) + + numberOfStoredReviews, err := reviewStorage.GetNumberOfStoredReviews() + if (err != nil){ + setErrorEncounteredPage(window, err, previousPage) + return + } + + numberOfStoredReviewsString := helpers.ConvertInt64ToString(numberOfStoredReviews) + + numberOfStoredReviewsLabel := widget.NewLabel("Stored Reviews:") + numberOfStoredReviewsText := getBoldLabel(numberOfStoredReviewsString) + numberOfStoredReviewsRow := container.NewHBox(layout.NewSpacer(), numberOfStoredReviewsLabel, numberOfStoredReviewsText, layout.NewSpacer()) + + numberOfStoredReports, err := reportStorage.GetNumberOfStoredReports() + if (err != nil){ + setErrorEncounteredPage(window, err, previousPage) + return + } + + numberOfStoredReportsString := helpers.ConvertInt64ToString(numberOfStoredReports) + + numberOfStoredReportsLabel := widget.NewLabel("Stored Reports:") + numberOfStoredReportsText := getBoldLabel(numberOfStoredReportsString) + numberOfStoredReportsRow := container.NewHBox(layout.NewSpacer(), numberOfStoredReportsLabel, numberOfStoredReportsText, layout.NewSpacer()) + + + freeUpSpaceButton := widget.NewButton("Free Up Space", func(){ + //TODO: A page to manually prune data + // This should also be happening automatically by backgroundJobs + showUnderConstructionDialog(window) + }) + + allowedSpaceButton := widget.NewButton("Manage Allowed Space", func(){ + setManageAllowedStorageSpacePage(window, currentPage) + }) + + changeDownloadsDirectoryButton := widget.NewButton("Change Database Location", func(){ + setManageDatabaseLocationPage(window, currentPage) + }) + + buttonsGrid := getContainerCentered(container.NewGridWithColumns(1, freeUpSpaceButton, allowedSpaceButton, changeDownloadsDirectoryButton)) + + page := container.NewVBox(title, backButton, widget.NewSeparator(), spaceUsedTitle, spaceUsedLabel, widget.NewSeparator(), numberOfStoredProfilesRow, numberOfStoredMessagesRow, numberOfStoredReviewsRow, numberOfStoredReportsRow, widget.NewSeparator(), buttonsGrid) + + setPageContent(page, window) +} + +func setManageAllowedStorageSpacePage(window fyne.Window, previousPage func()){ + + currentPage := func(){setManageAllowedStorageSpacePage(window, previousPage)} + + title := getPageTitleCentered(translate("Manage Allowed Storage Space")) + + backButton := getBackButtonCentered(previousPage) + + description := getLabelCentered("Select the amount of storage space Seekia is allowed to use.") + + getCurrentAllowedSpace := func()(float64, error){ + + exists, currentAllowedStorageSpace, err := globalSettings.GetSetting("AllowedStorageSpace") + if (err != nil){ return 0, err } + if (exists == false){ + // Default to 10 GB + return 10, nil + } + + currentAllowedSpaceFloat64, err := helpers.ConvertStringToFloat64(currentAllowedStorageSpace) + if (err != nil) { + return 0, errors.New("MyGlobalSettings Malformed: Contains invalid AllowedStorageSpace: " + currentAllowedStorageSpace) + } + + return currentAllowedSpaceFloat64, nil + } + + currentAllowedSpaceFloat64, err := getCurrentAllowedSpace() + if (err != nil){ + setErrorEncounteredPage(window, err, previousPage) + return + } + + currentAllowedSpaceString := helpers.ConvertFloat64ToStringRounded(currentAllowedSpaceFloat64, 2) + + allowedStorageSpaceLabel := getLabelCentered("Allowed storage space:") + allowedStorageSpaceText := getBoldLabelCentered(currentAllowedSpaceString + " gigabytes") + + allowedStorageSpaceGigabytesText := getLabelCentered("Enter a new allowed amount in gigabytes:") + + allowedStorageSpaceEntry := widget.NewEntry() + allowedStorageSpaceEntry.Text = currentAllowedSpaceString + + allowedStorageSpaceSubmitButton := widget.NewButtonWithIcon("Submit", theme.ConfirmIcon(), func(){ + + newSize := allowedStorageSpaceEntry.Text + + if (newSize == ""){ + dialogTitle := translate("Invalid Size.") + dialogMessage := getLabelCentered(translate("Your must enter a new allowed amount.")) + dialogContent := container.NewVBox(dialogMessage) + dialog.ShowCustom(dialogTitle, translate("Close"), dialogContent, window) + return + } + newSizeFloat64, err := helpers.ConvertStringToFloat64(newSize) + if (err != nil){ + dialogTitle := translate("Invalid Size.") + dialogMessage := getLabelCentered(translate("Your new maximum size must be a number.")) + dialogContent := container.NewVBox(dialogMessage) + dialog.ShowCustom(dialogTitle, translate("Close"), dialogContent, window) + return + } + + if (newSizeFloat64 < 3){ + dialogTitle := translate("Invalid Size.") + dialogMessage := getLabelCentered(translate("Your new maximum size must be at least 3 GB.")) + dialogContent := container.NewVBox(dialogMessage) + dialog.ShowCustom(dialogTitle, translate("Close"), dialogContent, window) + return + } + + err = globalSettings.SetSetting("AllowedStorageSpace", newSize) + if (err != nil){ + setErrorEncounteredPage(window, err, currentPage) + return + } + currentPage() + }) + + allowedStorageSpaceEntryRow := getContainerCentered(container.NewGridWithRows(1, allowedStorageSpaceEntry, allowedStorageSpaceSubmitButton)) + + page := container.NewVBox(title, backButton, widget.NewSeparator(), description, widget.NewSeparator(), allowedStorageSpaceLabel, allowedStorageSpaceText, widget.NewSeparator(), allowedStorageSpaceGigabytesText, allowedStorageSpaceEntryRow) + + setPageContent(page, window) +} + + +func setManageDatabaseLocationPage(window fyne.Window, previousPage func()){ + + currentPage := func(){setManageDatabaseLocationPage(window, previousPage)} + + title := getPageTitleCentered(translate("Manage Database Location")) + + backButton := getBackButtonCentered(previousPage) + + description1 := getLabelCentered("Use this page to change the location of the Seekia database.") + description2 := getLabelCentered("After changing the location, close Seekia.") + description3 := getLabelCentered("Then, you must manually move the existing database folder contents to the new location.") + description4 := getLabelCentered("Upon starting Seekia again, all existing data should be available.") + description5 := getLabelCentered("If you don't copy the folder, you will still retain your user data.") + + databaseDirectory, err := localFilesystem.GetAppDatabaseFolderPath() + if (err != nil){ + setErrorEncounteredPage(window, err, previousPage) + return + } + + currentLocationLabel := getLabelCentered("Current Database Directory:") + + currentLocationText := getBoldLabelCentered(databaseDirectory) + + resetLocation := widget.NewButtonWithIcon("Reset To Original", theme.ContentUndoIcon(), func(){ + + confirmDialogCallbackFunction := func(response bool){ + if (response == false){ + return + } + + err := globalSettings.DeleteSetting("DatabaseFolderpath") + if (err != nil){ + setErrorEncounteredPage(window, err, currentPage) + return + } + + currentPage() + } + + dialogTitle := translate("Confirm Reset Database Location?") + dialogMessageA := getLabelCentered("Confirm to reset the database location?") + dialogMessageB := getLabelCentered("You must restart Seekia for the change to take effect.") + dialogMessageC := getLabelCentered("Move your existing database to the new location before starting Seekia again.") + dialogContent := container.NewVBox(dialogMessageA, dialogMessageB, dialogMessageC) + dialog.ShowCustomConfirm(dialogTitle, translate("Yes"), translate("No"), dialogContent, confirmDialogCallbackFunction, window) + }) + + selectNewLocationButton := widget.NewButtonWithIcon("Select New Location", theme.FolderIcon(), func(){ + + folderOpenCallbackFunction := func(folderObject fyne.ListableURI, err error){ + + if (err != nil) { + title := translate("Failed To Open Folder Path.") + dialogMessage := getLabelCentered(translate("Report this error to Seekia developers: " + err.Error())) + dialogContent := container.NewVBox(dialogMessage) + dialog.ShowCustom(title, translate("Close"), dialogContent, window) + return + } + + if (folderObject == nil){ + return + } + + folderPath := folderObject.Path() + + err = globalSettings.SetSetting("DatabaseFolderpath", folderPath) + if (err != nil){ + setErrorEncounteredPage(window, err, currentPage) + return + } + currentPage() + } + + dialog.ShowFolderOpen(folderOpenCallbackFunction, window) + }) + + buttonsGrid := getContainerCentered(container.NewGridWithColumns(1, resetLocation, selectNewLocationButton)) + + page := container.NewVBox(title, backButton, widget.NewSeparator(), description1, description2, description3, description4, description5, widget.NewSeparator(), currentLocationLabel, currentLocationText, widget.NewSeparator(), buttonsGrid) + + setPageContent(page, window) +} +/* +func setSyncSettingsPage(window fyne.Window, previousPage func()){ + + title := getPageTitleCentered(translate("Sync Settings")) + backButton := getBackButtonCentered(previousPage) + + underConstructionLabel := getBoldLabelCentered("Under construction.") + + //TODO + + page := container.NewVBox(title, backButton, widget.NewSeparator(), underConstructionLabel) + + setPageContent(page, window) +} +*/ + +func setChangeAppCurrencyPage(window fyne.Window, previousPage func()){ + + getCurrentCurrencyFunction := func()(string, error){ + + exists, currentAppCurrency, err := globalSettings.GetSetting("Currency") + if (err != nil) { return "", err } + if (exists == false){ + return "USD", nil + } + + return currentAppCurrency, nil + } + + onSelectFunction := func(newCurrencyCode string)error{ + + err := globalSettings.SetSetting("Currency", newCurrencyCode) + if (err != nil){ return err } + + return nil + } + + setChooseCurrencyPage(window, getCurrentCurrencyFunction, onSelectFunction, previousPage) +} + + +func setChooseCurrencyPage(window fyne.Window, getCurrentCurrencyFunction func()(string, error), onSelectFunction func(string)error, previousPage func()){ + + currentPage := func(){setChooseCurrencyPage(window, getCurrentCurrencyFunction, onSelectFunction, previousPage)} + + title := getPageTitleCentered(translate("Choose Currency")) + + backButton := getBackButtonCentered(previousPage) + + currentCurrencyCode, err := getCurrentCurrencyFunction() + if (err != nil){ + setErrorEncounteredPage(window, err, previousPage) + return + } + + currentCurrencyName, currentCurrencySymbol, err := currencies.GetCurrencyInfoFromCurrencyCode(currentCurrencyCode) + if (err != nil) { + setErrorEncounteredPage(window, err, previousPage) + return + } + + currentCurrencyLabel := getLabelCentered("Current Currency:") + currentCurrencyText := getBoldLabelCentered(currentCurrencySymbol + " - " + currentCurrencyName + " - " + currentCurrencyCode) + + chooseCurrencyLabel := getItalicLabelCentered("Choose Currency:") + + allCurrencyObjectsList, err := currencies.GetCurrencyObjectsList() + if (err != nil) { + setErrorEncounteredPage(window, err, previousPage) + return + } + + allCurrencyDescriptionsList := make([]string, 0, len(allCurrencyObjectsList)) + allCurrencyCodesList := make([]string, 0, len(allCurrencyObjectsList)) + + for _, currencyObject := range allCurrencyObjectsList{ + + currencyName := currencyObject.Name + currencySymbol := currencyObject.Symbol + currencyCode := currencyObject.Code + + currencyNameTranslated := translate(currencyName) + + currencyDescription := currencySymbol + " - " + currencyNameTranslated + " - " + currencyCode + + allCurrencyDescriptionsList = append(allCurrencyDescriptionsList, currencyDescription) + allCurrencyCodesList = append(allCurrencyCodesList, currencyCode) + } + + onSelectedFunction := func(currencyIndex int) { + + selectedCurrencyCode := allCurrencyCodesList[currencyIndex] + + err := onSelectFunction(selectedCurrencyCode) + if (err != nil){ + setErrorEncounteredPage(window, err, currentPage) + return + } + + currentPage() + } + + widgetList, err := getFyneWidgetListFromStringList(allCurrencyDescriptionsList, onSelectedFunction) + if (err != nil){ + setErrorEncounteredPage(window, err, previousPage) + return + } + + header := container.NewVBox(title, backButton, widget.NewSeparator(), currentCurrencyLabel, currentCurrencyText, widget.NewSeparator(), chooseCurrencyLabel, widget.NewSeparator()) + + page := container.NewBorder(header, nil, nil, nil, widgetList) + + setPageContent(page, window) +} + +func setViewLogsPage(window fyne.Window, logType string, previousPage func()){ + + if (logType != "General" && logType != "Network" && logType != "BackgroundJobs"){ + setErrorEncounteredPage(window, errors.New("setViewLogsPage called with invalid logType: " + logType), previousPage) + return + } + + currentPage := func(){setViewLogsPage(window, logType, previousPage)} + + title := getPageTitleCentered("Logs") + + backButton := getBackButtonCentered(previousPage) + + description := getLabelCentered("View Seekia logs.") + + logTypeLabel := getBoldLabelCentered("Log Type:") + + logTypesList := []string{translate("General"), translate("Network"), translate("Background Jobs")} + + logTypeSelector := widget.NewSelect(logTypesList, func(selection string){ + + if (selection == translate("General")){ + + setViewLogsPage(window, "General", previousPage) + + } else if (selection == translate("Network")){ + + setViewLogsPage(window, "Network", previousPage) + + } else if (selection == translate("Background Jobs")){ + + setViewLogsPage(window, "BackgroundJobs", previousPage) + } + }) + + if (logType == "General"){ + + logTypeSelector.Selected = translate("General") + + } else if (logType == "Network"){ + + logTypeSelector.Selected = translate("Network") + + } else if (logType == "BackgroundJobs"){ + + logTypeSelector.Selected = translate("Background Jobs") + } + + logTypeSelectorCentered := getWidgetCentered(logTypeSelector) + + logList, err := logger.GetLogList(logType) + if (err != nil){ + setErrorEncounteredPage(window, err, previousPage) + return + } + + if (len(logList) == 0){ + + noLogsExistLabel := getBoldLabelCentered("No log entries exist.") + + refreshButton := getWidgetCentered(widget.NewButtonWithIcon("Refresh", theme.ViewRefreshIcon(), currentPage)) + + page := container.NewVBox(title, backButton, widget.NewSeparator(), description, widget.NewSeparator(), logTypeLabel, logTypeSelectorCentered, widget.NewSeparator(), noLogsExistLabel, refreshButton) + + setPageContent(page, window) + return + } + + trimmedLogList := make([]string, 0, len(logList)) + + for _, logText := range logList{ + + trimmedText, _, err := helpers.TrimAndFlattenString(logText, 30) + if (err != nil){ + setErrorEncounteredPage(window, err, previousPage) + return + } + + trimmedLogList = append(trimmedLogList, trimmedText) + } + + onClickedFunction := func(selectedIndex int){ + + logText := logList[selectedIndex] + + setViewTextPage(window, "Viewing Log", logText, false, currentPage) + } + + logsWidgetList, err := getFyneWidgetListFromStringList(logList, onClickedFunction) + if (err != nil){ + setErrorEncounteredPage(window, err, previousPage) + return + } + + refreshButton := getWidgetCentered(widget.NewButtonWithIcon("Refresh", theme.ViewRefreshIcon(), currentPage)) + + header := container.NewVBox(title, backButton, widget.NewSeparator(), description, widget.NewSeparator(), logTypeLabel, logTypeSelectorCentered, widget.NewSeparator()) + + page := container.NewBorder(header, refreshButton, nil, nil, logsWidgetList) + + setPageContent(page, window) +} + + +func setViewDeveloperSettingsPage(window fyne.Window, previousPage func()){ + + currentPage := func(){setViewDeveloperSettingsPage(window, previousPage)} + + title := getPageTitleCentered("Developer Settings") + + backButton := getBackButtonCentered(previousPage) + + networkTypeButton := getWidgetCentered(widget.NewButton("Network Type", func(){ + setViewAppNetworkTypePage(window, currentPage) + })) + + page := container.NewVBox(title, backButton, widget.NewSeparator(), networkTypeButton) + + setPageContent(page, window) +} + + +func setViewAppNetworkTypePage(window fyne.Window, previousPage func()){ + + currentPage := func(){setViewAppNetworkTypePage(window, previousPage)} + + title := getPageTitleCentered("Network Type") + + backButton := getBackButtonCentered(previousPage) + + description1 := getLabelCentered("Your Seekia application can interact with different network types.") + description2 := getLabelCentered("There are 2 network types: Mainnet and Testnet 1.") + description3 := getLabelCentered("You can change your app network type to interact with a different network.") + description4 := getLabelCentered("You should use Testnet 1 when testing new releases of Seekia before they are versioned.") + + currentNetworkType, err := getAppNetworkType.GetAppNetworkType() + if (err != nil){ + setErrorEncounteredPage(window, err, previousPage) + return + } + + getCurrentNetworkTypeString := func()(string, error){ + if (currentNetworkType == 1){ + return "Mainnet", nil + } + if (currentNetworkType == 2){ + return "Testnet 1", nil + } + + currentNetworkTypeString := helpers.ConvertByteToString(currentNetworkType) + + return "", errors.New("GetAppNetworkType returning invalid network type: " + currentNetworkTypeString) + } + + currentNetworkTypeString, err := getCurrentNetworkTypeString() + if (err != nil){ + setErrorEncounteredPage(window, err, previousPage) + return + } + + currentNetworkTypeLabel := widget.NewLabel("Current Network Type:") + + currentNetworkTypeText := getBoldLabel(currentNetworkTypeString) + + currentNetworkTypeRow := container.NewHBox(layout.NewSpacer(), currentNetworkTypeLabel, currentNetworkTypeText, layout.NewSpacer()) + + changeNetworkTypeButton := getWidgetCentered(widget.NewButtonWithIcon("Change Network Type", theme.NavigateNextIcon(), func(){ + setChooseNewAppNetworkTypePage(window, currentPage) + })) + + page := container.NewVBox(title, backButton, widget.NewSeparator(), description1, description2, description3, description4, widget.NewSeparator(), currentNetworkTypeRow, widget.NewSeparator(), changeNetworkTypeButton) + + setPageContent(page, window) +} + + +func setChooseNewAppNetworkTypePage(window fyne.Window, previousPage func()){ + + title := getPageTitleCentered("Choose Network Type") + + backButton := getBackButtonCentered(previousPage) + + description1 := getLabelCentered("Choose your new network type.") + description2 := getLabelCentered("This change will impact your matches and chat conversations.") + description3 := getLabelCentered("Your client will automatically delete content for other network types from the database.") + description4 := getLabelCentered("Your broadcasted content and messages will not be deleted.") + description5 := getLabelCentered("This change will take effect for all of your app users.") + + currentNetworkType, err := getAppNetworkType.GetAppNetworkType() + if (err != nil){ + setErrorEncounteredPage(window, err, previousPage) + return + } + + chooseMainnetButton := widget.NewButton("Mainnet", func(){ + if (currentNetworkType == 1){ + dialogTitle := translate("Cannot Change Network Type") + dialogMessageA := getLabelCentered("This network type is already your current app network type.") + dialogContent := container.NewVBox(dialogMessageA) + dialog.ShowCustom(dialogTitle, translate("Close"), dialogContent, window) + return + } + + setChangeAppNetworkTypePage(window, 1, previousPage) + }) + if (currentNetworkType == 1){ + chooseMainnetButton.Importance = widget.HighImportance + } + + chooseTestnet1Button := widget.NewButton("Testnet 1", func(){ + if (currentNetworkType == 2){ + dialogTitle := translate("Cannot Change Network Type") + dialogMessageA := getLabelCentered("This network type is already your current app network type.") + dialogContent := container.NewVBox(dialogMessageA) + dialog.ShowCustom(dialogTitle, translate("Close"), dialogContent, window) + return + } + + setChangeAppNetworkTypePage(window, 2, previousPage) + }) + + if (currentNetworkType == 2){ + chooseTestnet1Button.Importance = widget.HighImportance + } + + chooseNetworkTypesGrid := getContainerCentered(container.NewGridWithColumns(1, chooseMainnetButton, chooseTestnet1Button)) + + page := container.NewVBox(title, backButton, widget.NewSeparator(), description1, description2, description3, description4, description5, widget.NewSeparator(), chooseNetworkTypesGrid) + + setPageContent(page, window) +} + + +func setChangeAppNetworkTypePage(window fyne.Window, newAppNetworkType byte, nextPage func()){ + + isValid := helpers.VerifyNetworkType(newAppNetworkType) + if (isValid == false){ + newAppNetworkTypeString := helpers.ConvertByteToString(newAppNetworkType) + setErrorEncounteredPage(window, errors.New("setChangeAppNetworkTypePage called with invalid newAppNetworkType: " + newAppNetworkTypeString), nextPage) + return + } + + title := getPageTitleCentered("Changing Network Type") + + description := getLabelCentered("Changing network type...") + + progressBar := getWidgetCentered(widget.NewProgressBarInfinite()) + + page := container.NewVBox(title, widget.NewSeparator(), description, progressBar) + + window.SetContent(page) + + err := setAppNetworkType.SetAppNetworkType(newAppNetworkType) + if (err != nil){ + setErrorEncounteredPage(window, err, nextPage) + return + } + + nextPage() +} + + + diff --git a/gui/startupGui.go b/gui/startupGui.go new file mode 100644 index 0000000..cd5159f --- /dev/null +++ b/gui/startupGui.go @@ -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) +} + + + diff --git a/gui/statisticsGui.go b/gui/statisticsGui.go new file mode 100644 index 0000000..50a3e9c --- /dev/null +++ b/gui/statisticsGui.go @@ -0,0 +1,1150 @@ +package gui + +// statisticsGui.go implements pages to view statistics about the Seekia network + +import "fyne.io/fyne/v2" +import "fyne.io/fyne/v2/canvas" +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/badgerDatabase" +import "seekia/internal/createCharts" +import "seekia/internal/globalSettings" +import "seekia/internal/helpers" +import "seekia/internal/network/appNetworkType/getAppNetworkType" +import "seekia/internal/profiles/attributeDisplay" +import "seekia/internal/profiles/userStatistics" + +import "errors" +import "image" +import "strings" +import "time" +import "sync" + +func setNetworkStatisticsPage(window fyne.Window, previousPage func()){ + + currentPage := func(){setNetworkStatisticsPage(window, previousPage)} + + title := getPageTitleCentered("Network Statistics") + + backButton := getBackButtonCentered(previousPage) + + description := getLabelCentered(translate("View statistics about the Seekia network.")) + + mateStatisticsButton := widget.NewButton("Mate Statistics", func(){ + setNetworkUserStatisticsPage(window, "Mate", currentPage) + }) + + hostStatisticsButton := widget.NewButton("Host Statistics", func(){ + setNetworkUserStatisticsPage(window, "Host", currentPage) + }) + + moderatorStatisticsButton := widget.NewButton("Moderator Statistics", func(){ + setNetworkUserStatisticsPage(window, "Moderator", currentPage) + }) + + messageStatisticsButton := widget.NewButton("Message Statistics", func(){ + //TODO + showUnderConstructionDialog(window) + }) + + buttonsGrid := getContainerCentered(container.NewGridWithColumns(1, mateStatisticsButton, hostStatisticsButton, moderatorStatisticsButton, messageStatisticsButton)) + + page := container.NewVBox(title, backButton, widget.NewSeparator(), description, widget.NewSeparator(), buttonsGrid) + + setPageContent(page, window) +} + + +func setNetworkUserStatisticsPage(window fyne.Window, identityType string, previousPage func()){ + + if (identityType != "Mate" && identityType != "Host" && identityType != "Moderator"){ + setErrorEncounteredPage(window, errors.New("setUserStatisticsPage called with invalid identityType: " + identityType), previousPage) + return + } + + setLoadingScreen(window, identityType + " Statistics", "Loading " + identityType + " statistics page...") + + currentPage := func(){setNetworkUserStatisticsPage(window, identityType, previousPage)} + + title := getPageTitleCentered(identityType + " Statistics") + + backButton := getBackButtonCentered(previousPage) + + description1 := getLabelCentered("Below are statistics about " + identityType + " profiles and users.") + + description2 := widget.NewLabel("Be aware that these statistics may not represent the entire Seekia network.") + + description2HelpButton := widget.NewButtonWithIcon("", theme.QuestionIcon(), func(){ + setNetworkStatisticsInaccuracyWarningPage(window, currentPage) + }) + + description2Row := container.NewHBox(layout.NewSpacer(), description2, description2HelpButton, layout.NewSpacer()) + + profileHashesList, err := badgerDatabase.GetAllProfileHashes(identityType) + if (err != nil) { + setErrorEncounteredPage(window, err, previousPage) + return + } + + numberOfUserProfiles := len(profileHashesList) + + profileIdentityHashesList, err := badgerDatabase.GetAllProfileIdentityHashes(identityType) + if (err != nil){ + setErrorEncounteredPage(window, err, previousPage) + return + } + + numberOfUserIdentities := len(profileIdentityHashesList) + + numberOfUserProfilesString := helpers.ConvertIntToString(numberOfUserProfiles) + numberOfUserProfilesLabel := widget.NewLabel("Number of " + identityType + " Profiles:") + numberOfUserProfilesText := getBoldLabel(numberOfUserProfilesString) + numberOfUserProfilesRow := container.NewHBox(layout.NewSpacer(), numberOfUserProfilesLabel, numberOfUserProfilesText, layout.NewSpacer()) + + numberOfUserIdentitiesString := helpers.ConvertIntToString(numberOfUserIdentities) + numberOfUserIdentitiesLabel := widget.NewLabel("Number of " + identityType + " Identities:") + numberOfUserIdentitiesText := getBoldLabel(numberOfUserIdentitiesString) + numberOfUserIdentitiesRow := container.NewHBox(layout.NewSpacer(), numberOfUserIdentitiesLabel, numberOfUserIdentitiesText, layout.NewSpacer()) + + //TODO: add % of users who are disabled. + //TODO: Add more statistics + + getIdentityTypeDefaultAttribute := func()string{ + + if (identityType == "Mate"){ + return "Age" + } + if (identityType == "Host"){ + return "SeekiaVersion" + } + // identityType == "Moderator" + return "IdentityScore" + } + + identityTypeDefaultAttribute := getIdentityTypeDefaultAttribute() + + attributeStatisticsButton := getWidgetCentered(widget.NewButtonWithIcon("View Attribute Statistics", theme.VisibilityIcon(), func(){ + setViewUserAttributeStatisticsPage_BarChart(window, identityType, identityTypeDefaultAttribute, "Number Of Users", " users", true, false, false, nil, false, nil, nil, currentPage) + })) + + page := container.NewVBox(title, backButton, widget.NewSeparator(), description1, description2Row, widget.NewSeparator(), numberOfUserProfilesRow, numberOfUserIdentitiesRow, attributeStatisticsButton) + + setPageContent(page, window) +} + +func setViewUserAttributeStatisticsPage_BarChart( + window fyne.Window, + identityType string, + xAxisAttributeName string, + yAxisAttribute string, + yAxisUnits string, + showYAxisPercentage bool, + statisticsReady bool, + anyUsersExist bool, + statisticsItemsList []userStatistics.StatisticsItem, + groupingPerformed bool, + groupedStatisticsItemsList []userStatistics.StatisticsItem, + chartImage image.Image, + previousPage func()){ + + currentPage := func(){setViewUserAttributeStatisticsPage_BarChart(window, identityType, xAxisAttributeName, yAxisAttribute, yAxisUnits, showYAxisPercentage, statisticsReady, anyUsersExist, statisticsItemsList, groupingPerformed, groupedStatisticsItemsList, chartImage, previousPage)} + + 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("User Attribute Statistics - Bar Chart") + + backButton := getBackButtonCentered(previousPage) + + identityTypeLabel := getBoldLabelCentered("Identity Type") + + handleIdentityTypeSelectFunction := func(newIdentityType string){ + + if (newIdentityType == identityType){ + return + } + if (newIdentityType == "Mate"){ + + setViewUserAttributeStatisticsPage_BarChart(window, "Mate", "Age", "Number Of Users", " Users", true, false, false, nil, false, nil, nil, previousPage) + return + } + if (newIdentityType == "Host"){ + setViewUserAttributeStatisticsPage_BarChart(window, "Host", "SeekiaVersion", "Number Of Users", " Users", true, false, false, nil, false, nil, nil, previousPage) + return + } + // newIdentityType == "Moderator" + setViewUserAttributeStatisticsPage_BarChart(window, "Moderator", "IdentityScore", "Number Of Users", " Users", true, false, false, nil, false, nil, nil, previousPage) + } + + identityTypesList := []string{"Mate", "Host", "Moderator"} + + identityTypeSelector := widget.NewSelect(identityTypesList, handleIdentityTypeSelectFunction) + identityTypeSelector.Selected = identityType + + identityTypeColumn := container.NewVBox(identityTypeLabel, identityTypeSelector) + + chartTypeLabel := getBoldLabelCentered("Chart Type") + + chartTypesList := []string{"Bar Chart", "Donut Chart"} + handleChartTypeSelectFunction := func(newChartType string){ + if (newChartType == "Bar Chart"){ + return + } + // newChartType == "Donut Chart" + setViewUserAttributeStatisticsPage_DonutChart(window, identityType, xAxisAttributeName, false, false, nil, false, nil, nil, previousPage) + } + + chartTypeSelector := widget.NewSelect(chartTypesList, handleChartTypeSelectFunction) + chartTypeSelector.Selected = "Bar Chart" + + chartTypeColumn := container.NewVBox(chartTypeLabel, chartTypeSelector) + + xAxisAttributeTitle, xAxisIsNumerical, formatXAxisValuesFunction, xAxisUnits, unknownXAxisValuesTextTranslated, err := attributeDisplay.GetProfileAttributeDisplayInfo(xAxisAttributeName) + if (err != nil){ + setErrorEncounteredPage(window, err, previousPage) + return + } + + xAxisLabel := getBoldLabelCentered("X-Axis") + changeXAxisAttributeButton := widget.NewButton(xAxisAttributeTitle, func(){ + + currentPageWithNewAttributeFunction := func(newXAxisAttributeName string){ + setViewUserAttributeStatisticsPage_BarChart(window, identityType, newXAxisAttributeName, yAxisAttribute, yAxisUnits, showYAxisPercentage, false, false, nil, false, nil, nil, previousPage) + } + + setViewUserAttributeStatistics_ChooseXAxisAttributePage(window, identityType, currentPageWithNewAttributeFunction, currentPage) + }) + + xAxisColumn := container.NewVBox(xAxisLabel, changeXAxisAttributeButton) + + yAxisLabel := getBoldLabelCentered("Y-Axis") + changeYAxisAttributeButton := widget.NewButton(translate(yAxisAttribute), func(){ + + currentPageWithNewAttributeFunction := func(newYAxisAttribute string, newYAxisUnits string, showYAxisPercentage bool){ + setViewUserAttributeStatisticsPage_BarChart(window, identityType, xAxisAttributeName, newYAxisAttribute, newYAxisUnits, showYAxisPercentage, false, false, nil, false, nil, nil, previousPage) + } + + setViewUserAttributeStatistics_ChooseYAxisAttributePage(window, identityType, currentPageWithNewAttributeFunction, currentPage) + }) + + yAxisColumn := container.NewVBox(yAxisLabel, changeYAxisAttributeButton) + + chartSettingsRow := container.NewHBox(layout.NewSpacer(), widget.NewSeparator(), identityTypeColumn, widget.NewSeparator(), chartTypeColumn, widget.NewSeparator(), xAxisColumn, widget.NewSeparator(), yAxisColumn, widget.NewSeparator(), layout.NewSpacer()) + + if (statisticsReady == true){ + + if (anyUsersExist == false){ + + description1 := getBoldLabelCentered("No " + identityType + " profiles exist.") + description2 := getLabelCentered("No statistics are available to show.") + description3 := getLabelCentered("You must wait for profiles to be downloaded.") + + page := container.NewVBox(title, backButton, widget.NewSeparator(), chartSettingsRow, widget.NewSeparator(), description1, description2, description3) + + setPageContent(page, window) + return + } + + viewDataButton := widget.NewButtonWithIcon("View Data", theme.ListIcon(), func(){ + setViewUserStatisticsDataPage(window, xAxisAttributeTitle, yAxisAttribute, showYAxisPercentage, statisticsItemsList, groupingPerformed, groupedStatisticsItemsList, true, xAxisUnits, yAxisUnits, currentPage) + }) + + viewFullscreenButton := widget.NewButtonWithIcon("View Fullscreen", theme.ZoomInIcon(), func(){ + setViewFullpageImagePage(window, chartImage, currentPage) + }) + + warningButton := widget.NewButtonWithIcon("Warning", theme.WarningIcon(), func(){ + setNetworkStatisticsInaccuracyWarningPage(window, currentPage) + }) + + chartCompleteButtonsRow := getContainerCentered(container.NewGridWithRows(1, viewDataButton, viewFullscreenButton, warningButton)) + + header := container.NewVBox(title, backButton, widget.NewSeparator(), chartSettingsRow, widget.NewSeparator(), chartCompleteButtonsRow, widget.NewSeparator()) + + chartFyneImage := canvas.NewImageFromImage(chartImage) + chartFyneImage.FillMode = canvas.ImageFillContain + + page := container.NewBorder(header, nil, nil, nil, chartFyneImage) + + setPageContent(page, window) + return + } + + loadingTextBinding := binding.NewString() + loadingTextLabel := widget.NewLabelWithData(loadingTextBinding) + loadingTextLabel.TextStyle = getFyneTextStyle_Bold() + loadingTextLabelCentered := container.NewCenter(loadingTextLabel) + + appNetworkType, err := getAppNetworkType.GetAppNetworkType() + if (err != nil) { + setErrorEncounteredPage(window, err, previousPage) + return + } + + createChartAndRefreshPageFunction := func(){ + + var functionCompleteBoolMutex sync.RWMutex + functionCompleteBool := false + + updateLoadingBindingFunction := func(){ + + secondsElapsed := 0 + + for{ + + if (secondsElapsed%3 == 0){ + loadingTextBinding.Set("Loading Statistics.") + } else if (secondsElapsed%3 == 1){ + loadingTextBinding.Set("Loading Statistics..") + } else { + loadingTextBinding.Set("Loading Statistics...") + } + pageHasChanged := checkIfPageHasChangedFunction() + if (pageHasChanged == true){ + return + } + + functionCompleteBoolMutex.RLock() + functionIsComplete := functionCompleteBool + functionCompleteBoolMutex.RUnlock() + if (functionIsComplete == true){ + return + } + + secondsElapsed += 1 + time.Sleep(time.Second) + } + } + + go updateLoadingBindingFunction() + + totalAnalyzedUsers, statisticsItemsList, groupingPerformed, groupedStatisticsItemsList, formatYAxisValuesFunction, err := userStatistics.GetUserStatisticsItemsLists_BarChart(identityType, appNetworkType, xAxisAttributeName, xAxisIsNumerical, formatXAxisValuesFunction, unknownXAxisValuesTextTranslated, yAxisAttribute) + if (err != nil) { + + functionCompleteBoolMutex.Lock() + functionCompleteBool = true + functionCompleteBoolMutex.Unlock() + + pageHasChanged := checkIfPageHasChangedFunction() + if (pageHasChanged == false){ + setErrorEncounteredPage(window, err, previousPage) + } + return + } + + if (len(statisticsItemsList) == 0){ + + functionCompleteBoolMutex.Lock() + functionCompleteBool = true + functionCompleteBoolMutex.Unlock() + + pageHasChanged := checkIfPageHasChangedFunction() + if (pageHasChanged == false){ + setViewUserAttributeStatisticsPage_BarChart(window, identityType, xAxisAttributeName, yAxisAttribute, yAxisUnits, showYAxisPercentage, true, false, nil, false, nil, nil, previousPage) + } + return + } + + getChartStatisticsItemsList := func()[]userStatistics.StatisticsItem{ + if (groupingPerformed == false){ + return statisticsItemsList + } + return groupedStatisticsItemsList + } + + chartStatisticsItemsList := getChartStatisticsItemsList() + + getChartTitle := func()string{ + + totalAnalyzedUsersString := helpers.ConvertIntToString(totalAnalyzedUsers) + + chartTitle := identityType + " Statistics: " + xAxisAttributeTitle + + if (xAxisUnits != ""){ + + xAxisUnitsTrimmed := strings.TrimSpace(xAxisUnits) + + chartTitle += " (" + xAxisUnitsTrimmed + ")" + } + + chartTitle += " by " + yAxisAttribute + + if (yAxisUnits != "" && yAxisUnits != translate(" users")){ + + yAxisUnitsTrimmed := strings.TrimSpace(yAxisUnits) + + chartTitle += " (" + yAxisUnitsTrimmed + ")" + } + + chartTitle += " - " + totalAnalyzedUsersString + " Users" + + return chartTitle + } + + chartTitle := getChartTitle() + + newChartImage, err := createCharts.CreateBarChart(chartTitle, chartStatisticsItemsList, formatYAxisValuesFunction, true, yAxisUnits) + if (err != nil) { + + functionCompleteBoolMutex.Lock() + functionCompleteBool = true + functionCompleteBoolMutex.Unlock() + + pageHasChanged := checkIfPageHasChangedFunction() + if (pageHasChanged == false){ + setErrorEncounteredPage(window, err, previousPage) + } + return + } + + functionCompleteBoolMutex.Lock() + functionCompleteBool = true + functionCompleteBoolMutex.Unlock() + + pageHasChanged := checkIfPageHasChangedFunction() + if (pageHasChanged == false){ + setViewUserAttributeStatisticsPage_BarChart(window, identityType, xAxisAttributeName, yAxisAttribute, yAxisUnits, showYAxisPercentage, true, true, statisticsItemsList, groupingPerformed, groupedStatisticsItemsList, newChartImage, previousPage) + } + } + + header := container.NewVBox(title, backButton, widget.NewSeparator(), chartSettingsRow, widget.NewSeparator()) + + page := container.NewBorder(header, nil, nil, nil, loadingTextLabelCentered) + + setPageContent(page, window) + + go createChartAndRefreshPageFunction() +} + + +func setViewUserAttributeStatisticsPage_DonutChart( + window fyne.Window, + identityType string, + attributeName string, + statisticsReady bool, + anyUsersExist bool, + statisticsItemsList []userStatistics.StatisticsItem, + groupingPerformed bool, + groupedStatisticsItemsList []userStatistics.StatisticsItem, + chartImage image.Image, + previousPage func()){ + + currentPage := func(){setViewUserAttributeStatisticsPage_DonutChart(window, identityType, attributeName, statisticsReady, anyUsersExist, statisticsItemsList, groupingPerformed, groupedStatisticsItemsList, chartImage, previousPage)} + + 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("User Attribute Statistics - Donut Chart") + + backButton := getBackButtonCentered(previousPage) + + identityTypeLabel := getBoldLabelCentered("Identity Type") + + handleIdentityTypeSelectFunction := func(newIdentityType string){ + + if (newIdentityType == identityType){ + return + } + if (newIdentityType == "Mate"){ + + setViewUserAttributeStatisticsPage_DonutChart(window, "Mate", "Age", false, false, nil, false, nil, nil, previousPage) + return + } + if (newIdentityType == "Host"){ + setViewUserAttributeStatisticsPage_DonutChart(window, "Host", "SeekiaVersion", false, false, nil, false, nil, nil, previousPage) + return + } + // newIdentityType == "Moderator" + setViewUserAttributeStatisticsPage_DonutChart(window, "Moderator", "IdentityScore", false, false, nil, false, nil, nil, previousPage) + } + + identityTypesList := []string{"Mate", "Host", "Moderator"} + + identityTypeSelector := widget.NewSelect(identityTypesList, handleIdentityTypeSelectFunction) + identityTypeSelector.Selected = identityType + + identityTypeColumn := container.NewVBox(identityTypeLabel, identityTypeSelector) + + chartTypeLabel := getBoldLabelCentered("Chart Type:") + + chartTypesList := []string{"Bar Chart", "Donut Chart"} + handleSelectFunction := func(newType string){ + if (newType == "Donut Chart"){ + return + } + if (newType == "Bar Chart"){ + setViewUserAttributeStatisticsPage_BarChart(window, identityType, attributeName, "Number Of Users", " Users", true, false, false, nil, false, nil, nil, previousPage) + } + } + chartTypeSelector := widget.NewSelect(chartTypesList, handleSelectFunction) + chartTypeSelector.Selected = "Donut Chart" + + chartTypeColumn := container.NewVBox(chartTypeLabel, chartTypeSelector) + + attributeTitle, attributeIsNumerical, formatAttributeValuesFunction, attributeUnits, unknownValuesTextTranslated, err := attributeDisplay.GetProfileAttributeDisplayInfo(attributeName) + if (err != nil){ + setErrorEncounteredPage(window, err, previousPage) + return + } + + attributeLabel := getBoldLabelCentered("Attribute:") + changeAttributeButton := widget.NewButton(attributeTitle, func(){ + currentPageWithNewAttributeFunction := func(newAttributeName string){ + setViewUserAttributeStatisticsPage_DonutChart(window, identityType, newAttributeName, false, false, nil, false, nil, nil, previousPage) + } + setViewUserAttributeStatistics_ChooseXAxisAttributePage(window, identityType, currentPageWithNewAttributeFunction, currentPage) + }) + attributeColumn := container.NewVBox(attributeLabel, changeAttributeButton) + + chartSettingsRow := container.NewHBox(layout.NewSpacer(), widget.NewSeparator(), identityTypeColumn, widget.NewSeparator(), chartTypeColumn, widget.NewSeparator(), attributeColumn, widget.NewSeparator(), layout.NewSpacer()) + + if (statisticsReady == true){ + + if (anyUsersExist == false){ + + description1 := getBoldLabelCentered("No " + identityType + " profiles exist.") + description2 := getLabelCentered("No statistics are available to show.") + description3 := getLabelCentered("You must wait for profiles to be downloaded.") + + page := container.NewVBox(title, backButton, widget.NewSeparator(), chartSettingsRow, widget.NewSeparator(), description1, description2, description3) + + setPageContent(page, window) + return + } + + viewDataButton := widget.NewButtonWithIcon("View Data", theme.ListIcon(), func(){ + + setViewUserStatisticsDataPage(window, attributeTitle, "Number Of Users", true, statisticsItemsList, groupingPerformed, groupedStatisticsItemsList, true, attributeUnits, "Users", currentPage) + }) + + viewFullscreenButton := widget.NewButtonWithIcon("View Fullscreen", theme.ZoomInIcon(), func(){ + setViewFullpageImagePage(window, chartImage, currentPage) + }) + + warningButton := widget.NewButtonWithIcon("Warning", theme.WarningIcon(), func(){ + setNetworkStatisticsInaccuracyWarningPage(window, currentPage) + }) + + chartCompleteButtonsRow := getContainerCentered(container.NewGridWithRows(1, viewDataButton, viewFullscreenButton, warningButton)) + + header := container.NewVBox(title, backButton, widget.NewSeparator(), chartSettingsRow, widget.NewSeparator(), chartCompleteButtonsRow, widget.NewSeparator()) + + chartFyneImage := canvas.NewImageFromImage(chartImage) + chartFyneImage.FillMode = canvas.ImageFillContain + + page := container.NewBorder(header, nil, nil, nil, chartFyneImage) + setPageContent(page, window) + return + } + + loadingTextBinding := binding.NewString() + loadingTextLabel := widget.NewLabelWithData(loadingTextBinding) + loadingTextLabel.TextStyle = getFyneTextStyle_Bold() + loadingTextLabelCentered := container.NewCenter(loadingTextLabel) + + appNetworkType, err := getAppNetworkType.GetAppNetworkType() + if (err != nil) { + setErrorEncounteredPage(window, err, previousPage) + return + } + + createChartAndRefreshPageFunction := func(){ + + var functionCompleteBoolMutex sync.RWMutex + functionCompleteBool := false + + updateLoadingBindingFunction := func(){ + + secondsElapsed := 0 + + for{ + + if (secondsElapsed%3 == 0){ + loadingTextBinding.Set("Loading Statistics.") + } else if (secondsElapsed%3 == 1){ + loadingTextBinding.Set("Loading Statistics..") + } else { + loadingTextBinding.Set("Loading Statistics...") + } + pageHasChanged := checkIfPageHasChangedFunction() + if (pageHasChanged == true){ + return + } + + functionCompleteBoolMutex.RLock() + functionIsComplete := functionCompleteBool + functionCompleteBoolMutex.RUnlock() + if (functionIsComplete == true){ + return + } + + secondsElapsed += 1 + time.Sleep(time.Second) + } + } + + go updateLoadingBindingFunction() + + totalAnalyzedUsers, statisticsItemsList, groupingPerformed, groupedStatisticsItemsList, err := userStatistics.GetUserStatisticsItemsLists_DonutChart(identityType, appNetworkType, attributeName, attributeIsNumerical, formatAttributeValuesFunction, unknownValuesTextTranslated) + if (err != nil) { + + functionCompleteBoolMutex.Lock() + functionCompleteBool = true + functionCompleteBoolMutex.Unlock() + + pageHasChanged := checkIfPageHasChangedFunction() + if (pageHasChanged == false){ + setErrorEncounteredPage(window, err, previousPage) + } + return + } + + if (len(statisticsItemsList) == 0){ + + functionCompleteBoolMutex.Lock() + functionCompleteBool = true + functionCompleteBoolMutex.Unlock() + + pageHasChanged := checkIfPageHasChangedFunction() + if (pageHasChanged == false){ + setViewUserAttributeStatisticsPage_DonutChart(window, identityType, attributeName, true, false, nil, false, nil, nil, previousPage) + } + return + } + + getChartStatisticsItemsList := func()[]userStatistics.StatisticsItem{ + if (groupingPerformed == false){ + return statisticsItemsList + } + return groupedStatisticsItemsList + } + + chartStatisticsItemsList := getChartStatisticsItemsList() + + getChartTitle := func()string{ + + totalAnalyzedUsersString := helpers.ConvertIntToString(totalAnalyzedUsers) + + chartTitle := identityType + " Statistics: " + attributeTitle + + if (attributeUnits != ""){ + + attributeUnitsTrimmed := strings.TrimPrefix(attributeUnits, " ") + + chartTitle += " (" + attributeUnitsTrimmed + ")" + } + + chartTitle += " - " + totalAnalyzedUsersString + " Users" + + return chartTitle + } + + chartTitle := getChartTitle() + + newChartImage, err := createCharts.CreateDonutChart(chartTitle, chartStatisticsItemsList) + if (err != nil){ + + functionCompleteBoolMutex.Lock() + functionCompleteBool = true + functionCompleteBoolMutex.Unlock() + + pageHasChanged := checkIfPageHasChangedFunction() + if (pageHasChanged == false){ + setErrorEncounteredPage(window, err, previousPage) + } + return + } + + functionCompleteBoolMutex.Lock() + functionCompleteBool = true + functionCompleteBoolMutex.Unlock() + + pageHasChanged := checkIfPageHasChangedFunction() + if (pageHasChanged == false){ + setViewUserAttributeStatisticsPage_DonutChart(window, identityType, attributeName, true, true, statisticsItemsList, groupingPerformed, groupedStatisticsItemsList, newChartImage, previousPage) + } + } + + header := container.NewVBox(title, backButton, widget.NewSeparator(), chartSettingsRow, widget.NewSeparator()) + page := container.NewBorder(header, nil, nil, nil, loadingTextLabelCentered) + + setPageContent(page, window) + + go createChartAndRefreshPageFunction() +} + + +//Inputs: +// -fyne.Window +// -string: Profile Type +// -func(attributeName string) +// -func(): Previous page +func setViewUserAttributeStatistics_ChooseXAxisAttributePage(window fyne.Window, profileType string, viewStatisticsPageWithNewAttribute func(string), previousPage func()){ + + title := getPageTitleCentered(translate("Choose Attribute")) + + backButton := getBackButtonCentered(previousPage) + + attributesToShowList := make([]string, 0) + + addAttributeToListFunction := func(attributeName string){ + attributesToShowList = append(attributesToShowList, attributeName) + } + + if (profileType == "Mate"){ + + addAttributeToListFunction("Age") + addAttributeToListFunction("Height") + addAttributeToListFunction("Sex") + addAttributeToListFunction("Sexuality") + addAttributeToListFunction("Distance") + addAttributeToListFunction("WealthInGold") + } + + addAttributeToListFunction("ProfileLanguage") + + if (profileType == "Mate" || profileType == "Moderator"){ + addAttributeToListFunction("HasMessagedMe") + addAttributeToListFunction("IHaveMessaged") + } + + if (profileType == "Mate"){ + addAttributeToListFunction("HasRejectedMe") + addAttributeToListFunction("IsLiked") + addAttributeToListFunction("IsIgnored") + } + + addAttributeToListFunction("IsMyContact") + + if (profileType == "Mate"){ + addAttributeToListFunction("PrimaryLocationCountry") + addAttributeToListFunction("BodyFat") + addAttributeToListFunction("BodyMuscle") + addAttributeToListFunction("HairColor") + addAttributeToListFunction("HairTexture") + addAttributeToListFunction("EyeColor") + addAttributeToListFunction("SkinColor") + addAttributeToListFunction("RacialSimilarity") + addAttributeToListFunction("EyeColorSimilarity") + addAttributeToListFunction("EyeColorGenesSimilarity") + addAttributeToListFunction("HairColorSimilarity") + addAttributeToListFunction("HairColorGenesSimilarity") + addAttributeToListFunction("SkinColorSimilarity") + addAttributeToListFunction("SkinColorGenesSimilarity") + addAttributeToListFunction("HairTextureSimilarity") + addAttributeToListFunction("HairTextureGenesSimilarity") + addAttributeToListFunction("FacialStructureGenesSimilarity") + addAttributeToListFunction("23andMe_AncestralSimilarity") + addAttributeToListFunction("23andMe_MaternalHaplogroupSimilarity") + addAttributeToListFunction("23andMe_PaternalHaplogroupSimilarity") + addAttributeToListFunction("HasHIV") + addAttributeToListFunction("HasGenitalHerpes") + addAttributeToListFunction("GenderIdentity") + addAttributeToListFunction("23andMe_MaternalHaplogroup") + addAttributeToListFunction("23andMe_PaternalHaplogroup") + addAttributeToListFunction("23andMe_NeanderthalVariants") + addAttributeToListFunction("FruitRating") + addAttributeToListFunction("VegetablesRating") + addAttributeToListFunction("NutsRating") + addAttributeToListFunction("GrainsRating") + addAttributeToListFunction("DairyRating") + addAttributeToListFunction("SeafoodRating") + addAttributeToListFunction("BeefRating") + addAttributeToListFunction("PorkRating") + addAttributeToListFunction("PoultryRating") + addAttributeToListFunction("EggsRating") + addAttributeToListFunction("BeansRating") + addAttributeToListFunction("Fame") + addAttributeToListFunction("AlcoholFrequency") + addAttributeToListFunction("TobaccoFrequency") + addAttributeToListFunction("CannabisFrequency") + addAttributeToListFunction("PetsRating") + addAttributeToListFunction("DogsRating") + addAttributeToListFunction("CatsRating") + addAttributeToListFunction("OffspringProbabilityOfAnyMonogenicDisease") + addAttributeToListFunction("TotalPolygenicDiseaseRiskScore") + addAttributeToListFunction("OffspringTotalPolygenicDiseaseRiskScore") + } + + buttonsGrid := container.NewGridWithColumns(1) + + for _, attributeName := range attributesToShowList{ + + attributeTitle, _, _, _, _, err := attributeDisplay.GetProfileAttributeDisplayInfo(attributeName) + if (err != nil){ + setErrorEncounteredPage(window, err, previousPage) + return + } + + attributeButton := widget.NewButton(attributeTitle, func(){ + viewStatisticsPageWithNewAttribute(attributeName) + }) + + buttonsGrid.Add(attributeButton) + } + + buttonsGridCentered := getContainerCentered(buttonsGrid) + + page := container.NewVBox(title, backButton, widget.NewSeparator(), buttonsGridCentered) + + setPageContent(page, window) +} + +//Inputs: +// -fyne.Window +// -string: Profile type +// -func(attributeName string, yAxisUnits string, showYAxisPercentage bool) +// -func(): Previous page +func setViewUserAttributeStatistics_ChooseYAxisAttributePage(window fyne.Window, profileType string, viewStatisticsPageWithNewAttribute func(string, string, bool), previousPage func()){ + + title := getPageTitleCentered("Choose Y-Axis Attribute") + + backButton := getBackButtonCentered(previousPage) + + numberOfUsersButton := widget.NewButton("Number Of Users", func(){ + viewStatisticsPageWithNewAttribute("Number Of Users", translate(" Users"), true) + }) + + buttonsGrid := container.NewGridWithColumns(1, numberOfUsersButton) + + if (profileType == "Mate"){ + + averageHeightButton := widget.NewButton("Average Height", func(){ + viewStatisticsPageWithNewAttribute("Average Height", translate(" Centimeters"), false) + }) + buttonsGrid.Add(averageHeightButton) + + averageAgeButton := widget.NewButton("Average Age", func(){ + viewStatisticsPageWithNewAttribute("Average Age", translate(" Years Old"), false) + }) + buttonsGrid.Add(averageAgeButton) + + + getAppCurrency := func()(string, error){ + + exists, appCurrencyCode, err := globalSettings.GetSetting("Currency") + if (err != nil) { return "", err } + if (exists == false){ + return "USD", nil + } + return appCurrencyCode, nil + } + appCurrencyCode, err := getAppCurrency() + if (err != nil) { + setErrorEncounteredPage(window, err, previousPage) + return + } + + averageWealthButton := widget.NewButton("Average Wealth", func(){ + + wealthUnits := " " + appCurrencyCode + + viewStatisticsPageWithNewAttribute("Average Wealth", wealthUnits, false) + }) + buttonsGrid.Add(averageWealthButton) + + averageNeanderthalVariantsButton := widget.NewButton("Average 23andMe Neanderthal Variants", func(){ + viewStatisticsPageWithNewAttribute("Average 23andMe Neanderthal Variants", translate(" Variants"), false) + }) + buttonsGrid.Add(averageNeanderthalVariantsButton) + + averageBodyFatButton := widget.NewButton("Average Body Fat", func(){ + viewStatisticsPageWithNewAttribute("Average Body Fat", "/4", false) + }) + buttonsGrid.Add(averageBodyFatButton) + + averageBodyMuscleButton := widget.NewButton("Average Body Muscle", func(){ + viewStatisticsPageWithNewAttribute("Average Body Muscle", "/4", false) + }) + buttonsGrid.Add(averageBodyMuscleButton) + + oneToTenValueAttributesList := []string{ + "Fruit Rating", + "Vegetables Rating", + "Nuts Rating", + "Grains Rating", + "Dairy Rating", + "Seafood Rating", + "Beef Rating", + "Pork Rating", + "Poultry Rating", + "Eggs Rating", + "Beans Rating", + "Fame", + "Alcohol Frequency", + "Tobacco Frequency", + "Cannabis Frequency", + "Pets Rating", + "Dogs Rating", + "Cats Rating", + } + + for _, attributeTitle := range oneToTenValueAttributesList{ + + averageAttributeTitle := "Average " + attributeTitle + + averageAttributeButton := widget.NewButton(averageAttributeTitle, func(){ + viewStatisticsPageWithNewAttribute(averageAttributeTitle, "/10", false) + }) + buttonsGrid.Add(averageAttributeButton) + } + } + + buttonsGridCentered := getContainerCentered(buttonsGrid) + + page := container.NewVBox(title, backButton, widget.NewSeparator(), buttonsGridCentered) + + setPageContent(page, window) +} + + +func setViewUserStatisticsDataPage( + window fyne.Window, + attributeTitle string, + rightColumnName string, + showPercentageColumn bool, + statisticsItemsList []userStatistics.StatisticsItem, + groupingPerformed bool, + groupedStatisticsItemsList []userStatistics.StatisticsItem, + showGroupedStatistics bool, + attributeColumnUnits string, + rightColumnUnits string, + previousPage func()){ + + setLoadingScreen(window, "User Statistics Data", "Loading statistics data...") + + title := getPageTitleCentered("User Statistics Data") + + backButton := getBackButtonCentered(previousPage) + + header := container.NewVBox(title, backButton, widget.NewSeparator()) + + if (groupingPerformed == true){ + + if (showGroupedStatistics == false){ + showDataGroupedButton := getWidgetCentered(widget.NewButton("Show Data Grouped", func(){ + setViewUserStatisticsDataPage(window, attributeTitle, rightColumnName, showPercentageColumn, statisticsItemsList, true, groupedStatisticsItemsList, true, attributeColumnUnits, rightColumnUnits, previousPage) + })) + + header.Add(showDataGroupedButton) + } else { + showDataRawButton := getWidgetCentered(widget.NewButton("Show Data Raw", func(){ + setViewUserStatisticsDataPage(window, attributeTitle, rightColumnName, showPercentageColumn, statisticsItemsList, true, groupedStatisticsItemsList, false, attributeColumnUnits, rightColumnUnits, previousPage) + })) + + header.Add(showDataRawButton) + } + header.Add(widget.NewSeparator()) + } + + getStatisticsItemsListToShow := func()[]userStatistics.StatisticsItem{ + if (groupingPerformed == true && showGroupedStatistics == true){ + return groupedStatisticsItemsList + } + return statisticsItemsList + } + + statisticsItemsToShowList := getStatisticsItemsListToShow() + + getStatisticsDataGrid := func()(*fyne.Container, error){ + + allValuesSummed := float64(0) + + if (showPercentageColumn == true){ + + for _, item := range statisticsItemsToShowList{ + + itemValue := item.Value + + allValuesSummed += itemValue + } + } + + attributeColumnValuesList := make([]string, 0, len(statisticsItemsToShowList)) + rightColumnValuesList := make([]string, 0, len(statisticsItemsToShowList)) + percentageColumnValuesList := make([]string, 0, len(statisticsItemsToShowList)) + + for _, item := range statisticsItemsToShowList{ + + // Each item is represents a row in the data grid. + + itemLabelFormatted := item.LabelFormatted + + itemValueFormatted := item.ValueFormatted + + attributeColumnValuesList = append(attributeColumnValuesList, itemLabelFormatted) + rightColumnValuesList = append(rightColumnValuesList, itemValueFormatted) + + if (showPercentageColumn == true){ + + itemValue := item.Value + + getValuePercentage := func()float64{ + if (allValuesSummed == 0){ + return 0 + } + valuePercentage := (itemValue/allValuesSummed)*100 + return valuePercentage + } + + valuePercentage := getValuePercentage() + valuePercentageString := helpers.ConvertFloat64ToStringRounded(valuePercentage, 2) + valuePercentageFormatted := valuePercentageString + "%" + + percentageColumnValuesList = append(percentageColumnValuesList, valuePercentageFormatted) + } + } + + getAttributeColumnTitle := func()string{ + + if (attributeColumnUnits != ""){ + + attributeColumnUnitsTrimmed := strings.TrimSpace(attributeColumnUnits) + + attributeColumnTitle := attributeTitle + " (" + attributeColumnUnitsTrimmed + ")" + + return attributeColumnTitle + } + + return attributeTitle + } + + attributeColumnTitle := getAttributeColumnTitle() + + getRightColumnTitle := func()string{ + + if (rightColumnUnits != ""){ + + rightColumnUnitsTrimmed := strings.TrimSpace(rightColumnUnits) + + rightColumnTitle := rightColumnName + " (" + rightColumnUnitsTrimmed + ")" + + return rightColumnTitle + } + + return rightColumnName + } + + rightColumnTitle := getRightColumnTitle() + + if (showGroupedStatistics == false){ + + // Raw statistics must be rendered using a widget list + // Otherwise, rendering thousands of rows is too difficult for fyne + + headerText := attributeColumnTitle + " - " + rightColumnTitle + + if (showPercentageColumn == true){ + headerText += " - Percentage" + } + + headerLabel := getBoldLabelCentered(headerText) + + rowValuesList := make([]string, 0, len(attributeColumnValuesList)) + + for index, attributeColumnValue := range attributeColumnValuesList{ + + rightColumnValue := rightColumnValuesList[index] + + if (showPercentageColumn == false){ + + rowValue := attributeColumnValue + " - " + rightColumnValue + rowValuesList = append(rowValuesList, rowValue) + } else { + + percentageColumnValue := percentageColumnValuesList[index] + + rowValue := attributeColumnValue + " - " + rightColumnValue + " - " + percentageColumnValue + rowValuesList = append(rowValuesList, rowValue) + } + } + + onClickedFunction := func(_ int){} + + widgetList, err := getFyneWidgetListFromStringList(rowValuesList, onClickedFunction) + if (err != nil) { return nil, err } + + statisticsDataGrid := container.NewBorder(headerLabel, nil, nil, nil, widgetList) + + return statisticsDataGrid, nil + } + + attributeColumnHeader := getItalicLabelCentered(attributeColumnTitle) + rightColumnHeader := getItalicLabelCentered(rightColumnTitle) + percentageHeader := getItalicLabelCentered("Percentage") + + attributeColumn := container.NewVBox(attributeColumnHeader, widget.NewSeparator()) + rightColumn := container.NewVBox(rightColumnHeader, widget.NewSeparator()) + percentageColumn := container.NewVBox(percentageHeader, widget.NewSeparator()) + + for index, attributeColumnValue := range attributeColumnValuesList{ + + rightColumnValue := rightColumnValuesList[index] + + attributeColumnLabel := getBoldLabelCentered(attributeColumnValue) + rightColumnLabel := getBoldLabelCentered(rightColumnValue) + + attributeColumn.Add(attributeColumnLabel) + rightColumn.Add(rightColumnLabel) + + if (showPercentageColumn == true){ + + percentageColumnValue := percentageColumnValuesList[index] + + valuePercentageLabel := getBoldLabelCentered(percentageColumnValue) + percentageColumn.Add(valuePercentageLabel) + } + } + + if (showPercentageColumn == true){ + statisticsDataGrid := container.NewHBox(layout.NewSpacer(), attributeColumn, rightColumn, percentageColumn, layout.NewSpacer()) + return statisticsDataGrid, nil + } + + statisticsDataGrid := container.NewHBox(layout.NewSpacer(), attributeColumn, rightColumn, layout.NewSpacer()) + return statisticsDataGrid, nil + } + + statisticsDataGrid, err := getStatisticsDataGrid() + if (err != nil){ + setErrorEncounteredPage(window, err, previousPage) + return + } + + page := container.NewBorder(header, nil, nil, nil, statisticsDataGrid) + + setPageContent(page, window) +} + + diff --git a/gui/syncGui.go b/gui/syncGui.go new file mode 100644 index 0000000..532cd3a --- /dev/null +++ b/gui/syncGui.go @@ -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) +} + + + diff --git a/gui/themeGui.go b/gui/themeGui.go new file mode 100644 index 0000000..f5ba5cd --- /dev/null +++ b/gui/themeGui.go @@ -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 +} + + diff --git a/gui/toolsGui.go b/gui/toolsGui.go new file mode 100644 index 0000000..1962ef9 --- /dev/null +++ b/gui/toolsGui.go @@ -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) +} + + diff --git a/gui/viewAnalysisGui_Couple.go b/gui/viewAnalysisGui_Couple.go new file mode 100644 index 0000000..e3c160d --- /dev/null +++ b/gui/viewAnalysisGui_Couple.go @@ -0,0 +1,2494 @@ + +package gui + +// viewAnalysisGui_Couple.go implements pages to view a couple genetic analysis + +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/geneticReferences/monogenicDiseases" +import "seekia/resources/geneticReferences/polygenicDiseases" +import "seekia/resources/geneticReferences/traits" + +import "seekia/internal/appMemory" +import "seekia/internal/genetics/myGenomes" +import "seekia/internal/genetics/myPeople" +import "seekia/internal/genetics/readGeneticAnalysis" +import "seekia/internal/helpers" + +import "strings" +import "errors" + + +func setViewCoupleGeneticAnalysisPage(window fyne.Window, personAIdentifier string, personBIdentifier string, personAAnalysisMapList []map[string]string, personBAnalysisMapList []map[string]string, coupleAnalysisMapList []map[string]string, numberOfPersonAGenomesAnalyzed int, numberOfPersonBGenomesAnalyzed int, previousPage func()){ + + appMemory.SetMemoryEntry("CurrentViewedPage", "ViewCoupleGeneticAnalysisPage") + + currentPage := func(){setViewCoupleGeneticAnalysisPage(window, personAIdentifier, personBIdentifier, personAAnalysisMapList, personBAnalysisMapList, coupleAnalysisMapList, numberOfPersonAGenomesAnalyzed, numberOfPersonBGenomesAnalyzed, previousPage)} + + title := getPageTitleCentered("Viewing Genetic Analysis") + + backButton := getBackButtonCentered(previousPage) + + warningLabel := getBoldLabelCentered("WARNING: Results are not accurate!") + + personAFound, personAName, _, _, err := myPeople.GetPersonInfo(personAIdentifier) + if (err != nil) { + setErrorEncounteredPage(window, err, previousPage) + return + } + if (personAFound == false){ + setErrorEncounteredPage(window, errors.New("Couple person A not found."), previousPage) + return + } + + personBFound, personBName, _, _, err := myPeople.GetPersonInfo(personBIdentifier) + if (err != nil) { + setErrorEncounteredPage(window, err, previousPage) + return + } + if (personBFound == false){ + setErrorEncounteredPage(window, errors.New("Couple person B not found."), previousPage) + return + } + + coupleNameLabel := widget.NewLabel("Couple Name:") + coupleNameText := getBoldLabel(personAName + " + " + personBName) + coupleNameRow := container.NewHBox(layout.NewSpacer(), coupleNameLabel, coupleNameText, layout.NewSpacer()) + + numberOfAnalyzedGenomesLabel := getLabelCentered("Number of Analyzed Genomes:") + + personANameLabel := widget.NewLabel(personAName + ":") + personANumberOfGenomesAnalyzedString := helpers.ConvertIntToString(numberOfPersonAGenomesAnalyzed) + personANumberOfGenomesAnalyzedLabel := getBoldLabel(personANumberOfGenomesAnalyzedString) + personANumberOfAnalyzedGenomesRow := container.NewHBox(layout.NewSpacer(), personANameLabel, personANumberOfGenomesAnalyzedLabel, layout.NewSpacer()) + + personBNameLabel := widget.NewLabel(personBName + ":") + personBNumberOfGenomesAnalyzedString := helpers.ConvertIntToString(numberOfPersonBGenomesAnalyzed) + personBNumberOfGenomesAnalyzedLabel := getBoldLabel(personBNumberOfGenomesAnalyzedString) + personBNumberOfAnalyzedGenomesRow := container.NewHBox(layout.NewSpacer(), personBNameLabel, personBNumberOfGenomesAnalyzedLabel, layout.NewSpacer()) + + generalButton := widget.NewButton("General", func(){ + //TODO: Offspring inbred rating (kinship of parents), ancestry + showUnderConstructionDialog(window) + }) + monogenicDiseasesButton := widget.NewButton("Monogenic Diseases", func(){ + setViewCoupleGeneticAnalysisMonogenicDiseasesPage(window, personAName, personBName, personAAnalysisMapList, personBAnalysisMapList, coupleAnalysisMapList, currentPage) + }) + polygenicDiseasesButton := widget.NewButton("Polygenic Diseases", func(){ + setViewCoupleGeneticAnalysisPolygenicDiseasesPage(window, personAName, personBName, personAAnalysisMapList, personBAnalysisMapList, coupleAnalysisMapList, currentPage) + }) + traitsButton := widget.NewButton("Traits", func(){ + setViewCoupleGeneticAnalysisTraitsPage(window, personAName, personBName, personAAnalysisMapList, personBAnalysisMapList, coupleAnalysisMapList, currentPage) + }) + + categoryButtonsGrid := getContainerCentered(container.NewGridWithColumns(1, generalButton, monogenicDiseasesButton, polygenicDiseasesButton, traitsButton)) + + page := container.NewVBox(title, backButton, widget.NewSeparator(), warningLabel, widget.NewSeparator(), coupleNameRow, widget.NewSeparator(), numberOfAnalyzedGenomesLabel, personANumberOfAnalyzedGenomesRow, personBNumberOfAnalyzedGenomesRow, widget.NewSeparator(), categoryButtonsGrid) + + setPageContent(page, window) +} + + +func setViewCoupleGeneticAnalysisMonogenicDiseasesPage(window fyne.Window, personAName string, personBName string, personAAnalysisMapList []map[string]string, personBAnalysisMapList []map[string]string, coupleAnalysisMapList []map[string]string, previousPage func()){ + + currentPage := func(){setViewCoupleGeneticAnalysisMonogenicDiseasesPage(window, personAName, personBName, personAAnalysisMapList, personBAnalysisMapList, coupleAnalysisMapList, previousPage)} + + title := getPageTitleCentered("Viewing Genetic Analysis - Monogenic Diseases") + + backButton := getBackButtonCentered(previousPage) + + description := getLabelCentered("Below is an analysis of the monogenic disease probabilities for the couple's offspring.") + + //TODO: Sort so highest risk diseases are at the top. Everything else should be in normal order + + getMonogenicDiseasesGrid := func()(*fyne.Container, error){ + + pair1PersonAGenomeIdentifier, pair1PersonBGenomeIdentifier, secondGenomePairExists, _, _, _, _, _, _, _, _, err := readGeneticAnalysis.GetMetadataFromCoupleGeneticAnalysis(coupleAnalysisMapList) + if (err != nil){ return nil, err } + + emptyLabelA := widget.NewLabel("") + diseaseNameLabel := getItalicLabelCentered("Disease Name") + + offspringProbabilityOfLabel := getItalicLabelCentered("Offspring Probability Of") + havingDiseaseLabel := getItalicLabelCentered("Having Disease") + + emptyLabelB := widget.NewLabel("") + conflictExistsLabel := getItalicLabelCentered("Conflict Exists?") + + emptyLabelC := widget.NewLabel("") + emptyLabelD := widget.NewLabel("") + + diseaseNameColumn := container.NewVBox(emptyLabelA, diseaseNameLabel, widget.NewSeparator()) + offspringProbabilityOfHavingDiseaseColumn := container.NewVBox(offspringProbabilityOfLabel, havingDiseaseLabel, widget.NewSeparator()) + conflictExistsColumn := container.NewVBox(emptyLabelB, conflictExistsLabel, widget.NewSeparator()) + viewButtonsColumn := container.NewVBox(emptyLabelC, emptyLabelD, widget.NewSeparator()) + + monogenicDiseaseNamesList, err := monogenicDiseases.GetMonogenicDiseaseNamesList() + if (err != nil){ return nil, err } + + for _, diseaseName := range monogenicDiseaseNamesList{ + + pair1GenomeIdentifier := pair1PersonAGenomeIdentifier + "+" + pair1PersonBGenomeIdentifier + + genomeProbabilitiesKnown, _, genomePairOffspringProbabilityOfHavingDisease, _, _, conflictExists, err := readGeneticAnalysis.GetOffspringMonogenicDiseaseInfoFromGeneticAnalysis(coupleAnalysisMapList, diseaseName, pair1GenomeIdentifier, secondGenomePairExists) + if (err != nil) { return nil, err } + + getOffspringProbabilityOfHavingDiseaseText := func()string{ + if (genomeProbabilitiesKnown == false){ + result := translate("Unknown") + return result + } + + return genomePairOffspringProbabilityOfHavingDisease + } + + offspringProbabilityOfHavingDiseaseText := getOffspringProbabilityOfHavingDiseaseText() + + diseaseNameText := getBoldLabelCentered(diseaseName) + diseaseNameColumn.Add(diseaseNameText) + + offspringProbabilityOfHavingDiseaseLabel := getBoldLabelCentered(offspringProbabilityOfHavingDiseaseText) + offspringProbabilityOfHavingDiseaseColumn.Add(offspringProbabilityOfHavingDiseaseLabel) + + conflictExistsString := helpers.ConvertBoolToYesOrNoString(conflictExists) + conflictExistsLabel := getBoldLabelCentered(conflictExistsString) + conflictExistsColumn.Add(conflictExistsLabel) + + viewDetailsButton := getWidgetCentered(widget.NewButtonWithIcon("", theme.VisibilityIcon(), func(){ + setViewCoupleGeneticAnalysisMonogenicDiseaseDetailsPage(window, personAName, personBName, personAAnalysisMapList, personBAnalysisMapList, coupleAnalysisMapList, diseaseName, currentPage) + })) + viewButtonsColumn.Add(viewDetailsButton) + + diseaseNameColumn.Add(widget.NewSeparator()) + offspringProbabilityOfHavingDiseaseColumn.Add(widget.NewSeparator()) + conflictExistsColumn.Add(widget.NewSeparator()) + viewButtonsColumn.Add(widget.NewSeparator()) + } + + offspringProbabilityOfHavingDiseaseHelpButton := widget.NewButtonWithIcon("", theme.QuestionIcon(), func(){ + setOffspringProbabilityOfHavingMonogenicDiseaseExplainerPage(window, currentPage) + }) + offspringProbabilityOfHavingDiseaseColumn.Add(offspringProbabilityOfHavingDiseaseHelpButton) + + diseasesGrid := container.NewHBox(layout.NewSpacer(), diseaseNameColumn, offspringProbabilityOfHavingDiseaseColumn) + + if (secondGenomePairExists == true){ + + conflictExistsHelpButton := widget.NewButtonWithIcon("", theme.QuestionIcon(), func(){ + setCoupleGeneticAnalysisConflictExistsExplainerPage(window, currentPage) + }) + conflictExistsColumn.Add(conflictExistsHelpButton) + + diseasesGrid.Add(conflictExistsColumn) + } + + diseasesGrid.Add(viewButtonsColumn) + diseasesGrid.Add(layout.NewSpacer()) + + return diseasesGrid, nil + } + + monogenicDiseasesContainer, err := getMonogenicDiseasesGrid() + if (err != nil){ + setErrorEncounteredPage(window, err, previousPage) + return + } + + page := container.NewVBox(title, backButton, widget.NewSeparator(), description, widget.NewSeparator(), monogenicDiseasesContainer) + + setPageContent(page, window) +} + + +func setViewCoupleGeneticAnalysisMonogenicDiseaseDetailsPage(window fyne.Window, personAName string, personBName string, personAAnalysisMapList []map[string]string, personBAnalysisMapList []map[string]string, coupleAnalysisMapList []map[string]string, diseaseName string, previousPage func()){ + + currentPage := func(){setViewCoupleGeneticAnalysisMonogenicDiseaseDetailsPage(window, personAName, personBName, personAAnalysisMapList, personBAnalysisMapList, coupleAnalysisMapList, diseaseName, previousPage)} + + title := getPageTitleCentered("Viewing Couple Analysis - " + diseaseName) + + backButton := getBackButtonCentered(previousPage) + + pair1PersonAGenomeIdentifier, pair1PersonBGenomeIdentifier, secondGenomePairExists, pair2PersonAGenomeIdentifier, pair2PersonBGenomeIdentifier, _, _, _, _, _, _, err := readGeneticAnalysis.GetMetadataFromCoupleGeneticAnalysis(coupleAnalysisMapList) + if (err != nil){ + setErrorEncounteredPage(window, err, previousPage) + return + } + + getDescriptionSection := func()*fyne.Container{ + + if (secondGenomePairExists == false){ + description := getLabelCentered("Below are the disease probabilities for the couple's offspring.") + + return description + } + + description1 := getLabelCentered("Below are the disease probabilities for the couple's offspring.") + description2 := getLabelCentered("Each genome pair combines different genomes from each person.") + + descriptionsSection := container.NewVBox(description1, description2) + + return descriptionsSection + } + + descriptionSection := getDescriptionSection() + + diseaseNameLabel := widget.NewLabel("Disease:") + diseaseNameText := getBoldLabel(diseaseName) + diseaseNameInfoButton := widget.NewButtonWithIcon("", theme.InfoIcon(), func(){ + setViewMonogenicDiseaseDetailsPage(window, diseaseName, currentPage) + }) + diseaseNameRow := container.NewHBox(layout.NewSpacer(), diseaseNameLabel, diseaseNameText, diseaseNameInfoButton, layout.NewSpacer()) + + emptyLabel1 := widget.NewLabel("") + emptyLabel2 := widget.NewLabel("") + + emptyLabel3 := widget.NewLabel("") + emptyLabel4 := widget.NewLabel("") + + offspringProbabilityOfLabel1 := getItalicLabelCentered("Offspring Probability Of") + havingDiseaseLabel := getItalicLabelCentered("Having Disease") + + offspringProbabilityOfLabel2 := getItalicLabelCentered("Offspring Probability Of") + havingVariantLabel := getItalicLabelCentered("Having Variant") + + emptyLabel5 := widget.NewLabel("") + emptyLabel6 := widget.NewLabel("") + + viewGenomePairButtonsColumn := container.NewVBox(emptyLabel1, emptyLabel2, widget.NewSeparator()) + pairNameColumn := container.NewVBox(emptyLabel3, emptyLabel4, widget.NewSeparator()) + offspringProbabilityOfHavingDiseaseColumn := container.NewVBox(offspringProbabilityOfLabel1, havingDiseaseLabel, widget.NewSeparator()) + offspringProbabilityOfHavingVariantColumn := container.NewVBox(offspringProbabilityOfLabel2, havingVariantLabel, widget.NewSeparator()) + viewOffspringVariantButtonsColumn := container.NewVBox(emptyLabel5, emptyLabel6, widget.NewSeparator()) + + addGenomePairRow := func(genomePairName string, personAGenomeIdentifier string, personBGenomeIdentifier string)error{ + + genomePairIdentifier := personAGenomeIdentifier + "+" + personBGenomeIdentifier + + genomeProbabilitiesKnown, _, genomePairOffspringProbabilityOfHavingDisease, _, genomePairOffspringProbabilityOfHavingAVariant, _, err := readGeneticAnalysis.GetOffspringMonogenicDiseaseInfoFromGeneticAnalysis(coupleAnalysisMapList, diseaseName, genomePairIdentifier, secondGenomePairExists) + if (err != nil) { return err } + + getOffspringProbabilityOfHavingDiseaseText := func()string{ + if (genomeProbabilitiesKnown == false){ + result := translate("Unknown") + return result + } + + return genomePairOffspringProbabilityOfHavingDisease + } + + offspringProbabilityOfHavingDiseaseText := getOffspringProbabilityOfHavingDiseaseText() + + getOffspringProbabilityOfHavingAVariantText := func()string{ + + if (genomeProbabilitiesKnown == false){ + result := translate("Unknown") + return result + } + return genomePairOffspringProbabilityOfHavingAVariant + } + + offspringProbabilityOfHavingAVariantText := getOffspringProbabilityOfHavingAVariantText() + + genomePairNameLabel := getBoldLabelCentered(genomePairName) + + offspringProbabilityOfHavingDiseaseLabel := getBoldLabelCentered(offspringProbabilityOfHavingDiseaseText) + + offspringProbabilityOfHavingAVariantLabel := getBoldLabelCentered(offspringProbabilityOfHavingAVariantText) + + viewGenomePairButton := widget.NewButtonWithIcon("", theme.InfoIcon(), func(){ + setViewCoupleGeneticAnalysisMonogenicDiseaseGenomePairDetailsPage(window, personAName, personBName, personAAnalysisMapList, personBAnalysisMapList, coupleAnalysisMapList, diseaseName, genomePairIdentifier, genomePairName, currentPage) + }) + + viewOffspringVariantsButton := widget.NewButtonWithIcon("", theme.VisibilityIcon(), func(){ + setViewCoupleMonogenicDiseaseVariantsPage(window, personAName, personBName, personAAnalysisMapList, personBAnalysisMapList, coupleAnalysisMapList, diseaseName, genomePairIdentifier, genomePairName, currentPage) + }) + + viewGenomePairButtonsColumn.Add(viewGenomePairButton) + pairNameColumn.Add(genomePairNameLabel) + offspringProbabilityOfHavingDiseaseColumn.Add(offspringProbabilityOfHavingDiseaseLabel) + offspringProbabilityOfHavingVariantColumn.Add(offspringProbabilityOfHavingAVariantLabel) + viewOffspringVariantButtonsColumn.Add(viewOffspringVariantsButton) + + viewGenomePairButtonsColumn.Add(widget.NewSeparator()) + pairNameColumn.Add(widget.NewSeparator()) + offspringProbabilityOfHavingDiseaseColumn.Add(widget.NewSeparator()) + offspringProbabilityOfHavingVariantColumn.Add(widget.NewSeparator()) + viewOffspringVariantButtonsColumn.Add(widget.NewSeparator()) + + return nil + } + + err = addGenomePairRow("Pair 1", pair1PersonAGenomeIdentifier, pair1PersonBGenomeIdentifier) + if (err != nil) { + setErrorEncounteredPage(window, err, previousPage) + return + } + + if (secondGenomePairExists == true){ + err := addGenomePairRow("Pair 2", pair2PersonAGenomeIdentifier, pair2PersonBGenomeIdentifier) + if (err != nil) { + setErrorEncounteredPage(window, err, previousPage) + return + } + } + + offspringProbabilityOfHavingDiseaseHelpButton := widget.NewButtonWithIcon("", theme.QuestionIcon(), func(){ + setOffspringProbabilityOfHavingMonogenicDiseaseExplainerPage(window, currentPage) + }) + + offspringProbabilityOfHavingVariantHelpButton := widget.NewButtonWithIcon("", theme.QuestionIcon(), func(){ + setOffspringProbabilityOfHavingVariantExplainerPage(window, currentPage) + }) + + offspringProbabilityOfHavingDiseaseColumn.Add(offspringProbabilityOfHavingDiseaseHelpButton) + offspringProbabilityOfHavingVariantColumn.Add(offspringProbabilityOfHavingVariantHelpButton) + + genomesContainer := container.NewHBox(layout.NewSpacer(), viewGenomePairButtonsColumn, pairNameColumn, offspringProbabilityOfHavingDiseaseColumn, offspringProbabilityOfHavingVariantColumn, viewOffspringVariantButtonsColumn, layout.NewSpacer()) + + page := container.NewVBox(title, backButton, widget.NewSeparator(), descriptionSection, widget.NewSeparator(), diseaseNameRow, widget.NewSeparator(), genomesContainer) + + setPageContent(page, window) +} + + +func setViewCoupleGeneticAnalysisMonogenicDiseaseGenomePairDetailsPage(window fyne.Window, personAName string, personBName string, personAAnalysisMapList []map[string]string, personBAnalysisMapList []map[string]string, coupleAnalysisMapList []map[string]string, diseaseName string, genomePairIdentifier string, genomePairName string, previousPage func()){ + + currentPage := func(){setViewCoupleGeneticAnalysisMonogenicDiseaseGenomePairDetailsPage(window, personAName, personBName, personAAnalysisMapList, personBAnalysisMapList, coupleAnalysisMapList, diseaseName, genomePairIdentifier, genomePairName, previousPage)} + + title := getPageTitleCentered("Viewing Couple Genome Pair Info") + + backButton := getBackButtonCentered(previousPage) + + description := getLabelCentered("Below is the disease information for both genomes in the genome pair.") + + diseaseNameLabel := widget.NewLabel("Disease:") + diseaseNameText := getBoldLabel(diseaseName) + diseaseNameInfoButton := widget.NewButtonWithIcon("", theme.InfoIcon(), func(){ + setViewMonogenicDiseaseDetailsPage(window, diseaseName, currentPage) + }) + diseaseNameRow := container.NewHBox(layout.NewSpacer(), diseaseNameLabel, diseaseNameText, diseaseNameInfoButton, layout.NewSpacer()) + + genomePairLabel := widget.NewLabel("Genome Pair:") + genomePairNameLabel := getBoldLabel(genomePairName) + genomePairHelpButton := widget.NewButtonWithIcon("", theme.QuestionIcon(), func(){ + setCoupleGenomePairExplainerPage(window, currentPage) + }) + genomePairRow := container.NewHBox(layout.NewSpacer(), genomePairLabel, genomePairNameLabel, genomePairHelpButton, layout.NewSpacer()) + + emptyLabelA := widget.NewLabel("") + personNameLabel := getItalicLabelCentered("Person Name") + + emptyLabelB := widget.NewLabel("") + genomeNameLabel := getItalicLabelCentered("Genome Name") + + probabilityOfLabel := getItalicLabelCentered("Probability Of") + passingAVariantLabel := getItalicLabelCentered("Passing A Variant") + + numberOfLabel := getItalicLabelCentered("Number Of") + variantsTestedLabel := getItalicLabelCentered("Variants Tested") + + emptyLabelC := widget.NewLabel("") + emptyLabelD := widget.NewLabel("") + + personNameColumn := container.NewVBox(emptyLabelA, personNameLabel, widget.NewSeparator()) + genomeNameColumn := container.NewVBox(emptyLabelB, genomeNameLabel, widget.NewSeparator()) + probabilityOfPassingAVariantColumn := container.NewVBox(probabilityOfLabel, passingAVariantLabel, widget.NewSeparator()) + numberOfVariantsTestedColumn := container.NewVBox(numberOfLabel, variantsTestedLabel, widget.NewSeparator()) + viewGenomeButtonsColumn := container.NewVBox(emptyLabelC, emptyLabelD, widget.NewSeparator()) + + addGenomeRow := func(isPersonA bool, personName string, inputGenomeIdentifier string)error{ + + personAnalysisGenomeIdentifier, personHasMultipleGenomes, genomeIsCombined, combinedType, err := readGeneticAnalysis.GetMatchingPersonAnalysisGenomeIdentifierFromCoupleAnalysis(isPersonA, personAAnalysisMapList, personBAnalysisMapList, coupleAnalysisMapList, inputGenomeIdentifier) + if (err != nil) { return err } + + personNameTrimmed, _, err := helpers.TrimAndFlattenString(personName, 10) + if (err != nil) { return err } + + personNameLabel := getBoldLabelCentered(personNameTrimmed) + + getPersonAnalysisMapList := func()[]map[string]string{ + if (isPersonA == true){ + return personAAnalysisMapList + } + return personBAnalysisMapList + } + + personAnalysisMapList := getPersonAnalysisMapList() + + getGenomeName := func()(string, error){ + + if (genomeIsCombined == false){ + + genomeFound, _, _, _, _, _, companyName, _, _, err := myGenomes.GetMyRawGenomeMetadata(personAnalysisGenomeIdentifier) + if (err != nil) { return "", err } + if (genomeFound == false){ + return "", errors.New("MyGenomeInfo for genome from analysisMapList not found.") + } + + return companyName, nil + } + + return combinedType, nil + } + + genomeName, err := getGenomeName() + if (err != nil) { return err } + + genomeNameLabel := getBoldLabelCentered(genomeName) + + probabilitiesKnown, _, _, _, probabilityOfPassingAVariantFormatted, personNumberOfVariantsTested, _, err := readGeneticAnalysis.GetPersonMonogenicDiseaseInfoFromGeneticAnalysis(personAnalysisMapList, diseaseName, personAnalysisGenomeIdentifier, personHasMultipleGenomes) + if (err != nil) { return err } + + getProbabilityOfPassingAVariantText := func()string{ + + if (probabilitiesKnown == false){ + + result := translate("Unknown") + return result + } + + return probabilityOfPassingAVariantFormatted + } + genomeProbabilityOfPassingAVariantText := getProbabilityOfPassingAVariantText() + + genomeProbabilityOfPassingAVariantLabel := getBoldLabelCentered(genomeProbabilityOfPassingAVariantText) + + personNumberOfVariantsTestedString := helpers.ConvertIntToString(personNumberOfVariantsTested) + genomeNumberOfVariantsTestedLabel := getBoldLabelCentered(personNumberOfVariantsTestedString) + + viewGenomeButton := widget.NewButtonWithIcon("", theme.VisibilityIcon(), func(){ + setViewPersonGenomeMonogenicDiseaseVariantsPage(window, personAnalysisMapList, personAnalysisGenomeIdentifier, genomeName, diseaseName, currentPage) + }) + + personNameColumn.Add(personNameLabel) + genomeNameColumn.Add(genomeNameLabel) + probabilityOfPassingAVariantColumn.Add(genomeProbabilityOfPassingAVariantLabel) + numberOfVariantsTestedColumn.Add(genomeNumberOfVariantsTestedLabel) + viewGenomeButtonsColumn.Add(viewGenomeButton) + + personNameColumn.Add(widget.NewSeparator()) + genomeNameColumn.Add(widget.NewSeparator()) + probabilityOfPassingAVariantColumn.Add(widget.NewSeparator()) + numberOfVariantsTestedColumn.Add(widget.NewSeparator()) + viewGenomeButtonsColumn.Add(widget.NewSeparator()) + + return nil + } + + personAGenomeIdentifier, personBGenomeIdentifier, found := strings.Cut(genomePairIdentifier, "+") + if (found == false){ + setErrorEncounteredPage(window, errors.New("setViewCoupleGeneticAnalysisMonogenicDiseaseGenomePairDetailsPage called with invalid genomePairIdentifier"), previousPage) + return + } + + err := addGenomeRow(true, personAName, personAGenomeIdentifier) + if (err != nil){ + setErrorEncounteredPage(window, err, previousPage) + return + } + err = addGenomeRow(false, personBName, personBGenomeIdentifier) + if (err != nil){ + setErrorEncounteredPage(window, err, previousPage) + return + } + + probabilityOfPassingAVariantHelpButton := widget.NewButtonWithIcon("", theme.QuestionIcon(), func(){ + setPersonProbabilityOfPassingVariantExplainerPage(window, currentPage) + }) + numberOfVariantsTestedHelpButton := widget.NewButtonWithIcon("", theme.QuestionIcon(), func(){ + setNumberOfTestedVariantsExplainerPage(window, currentPage) + }) + + probabilityOfPassingAVariantColumn.Add(probabilityOfPassingAVariantHelpButton) + numberOfVariantsTestedColumn.Add(numberOfVariantsTestedHelpButton) + + genomesGrid := container.NewHBox(layout.NewSpacer(), personNameColumn, genomeNameColumn, probabilityOfPassingAVariantColumn, numberOfVariantsTestedColumn, viewGenomeButtonsColumn, layout.NewSpacer()) + + page := container.NewVBox(title, backButton, widget.NewSeparator(), description, diseaseNameRow, genomePairRow, widget.NewSeparator(), genomesGrid) + + setPageContent(page, window) +} + + +// This function provides a page to view the offspring disease variant probabilities for a particular genome pair from a couple genetic analysis +func setViewCoupleMonogenicDiseaseVariantsPage(window fyne.Window, personAName string, personBName string, personAAnalysisMapList []map[string]string, personBAnalysisMapList []map[string]string, coupleAnalysisMapList []map[string]string, diseaseName string, genomePairIdentifier string, genomePairName string, previousPage func()){ + + setLoadingScreen(window, "Loading Disease Variants", "Loading disease variants...") + + currentPage := func(){setViewCoupleMonogenicDiseaseVariantsPage(window, personAName, personBName, personAAnalysisMapList, personBAnalysisMapList, coupleAnalysisMapList, diseaseName, genomePairIdentifier, genomePairName, previousPage)} + + title := getPageTitleCentered("View Offspring Disease Variants - " + diseaseName) + + backButton := getBackButtonCentered(previousPage) + + description1 := widget.NewLabel("Below are the disease variant probabilities for offspring from this genome pair.") + variantsHelpButton := widget.NewButtonWithIcon("", theme.QuestionIcon(), func(){ + setMonogenicDiseaseVariantsExplainerPage(window, currentPage) + }) + description1Row := container.NewHBox(layout.NewSpacer(), description1, variantsHelpButton, layout.NewSpacer()) + + genomePairLabel := widget.NewLabel("Genome Pair:") + genomePairNameLabel := getBoldLabel(genomePairName) + viewGenomePairInfoButton := widget.NewButtonWithIcon("", theme.InfoIcon(), func(){ + setViewCoupleGeneticAnalysisMonogenicDiseaseGenomePairDetailsPage(window, personAName, personBName, personAAnalysisMapList, personBAnalysisMapList, coupleAnalysisMapList, diseaseName, genomePairIdentifier, genomePairName, currentPage) + }) + + genomePairRow := container.NewHBox(layout.NewSpacer(), genomePairLabel, genomePairNameLabel, viewGenomePairInfoButton, layout.NewSpacer()) + + getNumberOfVariantsTested := func()(int, error){ + + personAGenomeIdentifier, personBGenomeIdentifier, found := strings.Cut(genomePairIdentifier, "+") + if (found == false){ + return 0, errors.New("setViewCoupleMonogenicDiseaseVariantsPage called with invalid genomePairIdentifier: " + genomePairIdentifier) + } + + personAAnalysisGenomeIdentifier, personAHasMultipleGenomes, _, _, err := readGeneticAnalysis.GetMatchingPersonAnalysisGenomeIdentifierFromCoupleAnalysis(true, personAAnalysisMapList, personBAnalysisMapList, coupleAnalysisMapList, personAGenomeIdentifier) + if (err != nil) { return 0, err } + + _, _, _, _, _, personANumberOfVariantsTested, _, err := readGeneticAnalysis.GetPersonMonogenicDiseaseInfoFromGeneticAnalysis(personAAnalysisMapList, diseaseName, personAAnalysisGenomeIdentifier, personAHasMultipleGenomes) + if (err != nil) { return 0, err } + + personBAnalysisGenomeIdentifier, personBHasMultipleGenomes, _, _, err := readGeneticAnalysis.GetMatchingPersonAnalysisGenomeIdentifierFromCoupleAnalysis(false, personAAnalysisMapList, personBAnalysisMapList, coupleAnalysisMapList, personBGenomeIdentifier) + if (err != nil) { return 0, err } + + _, _, _, _, _, personBNumberOfVariantsTested, _, err := readGeneticAnalysis.GetPersonMonogenicDiseaseInfoFromGeneticAnalysis(personBAnalysisMapList, diseaseName, personBAnalysisGenomeIdentifier, personBHasMultipleGenomes) + if (err != nil) { return 0, err } + + numberOfVariantsTested := personANumberOfVariantsTested + personBNumberOfVariantsTested + + return numberOfVariantsTested, nil + } + + numberOfVariantsTested, err := getNumberOfVariantsTested() + if (err != nil){ + setErrorEncounteredPage(window, err, previousPage) + return + } + + diseaseVariantsMap, err := monogenicDiseases.GetMonogenicDiseaseVariantsMap(diseaseName) + if (err != nil){ + setErrorEncounteredPage(window, err, previousPage) + return + } + + totalNumberOfVariants := len(diseaseVariantsMap) * 2 + totalNumberOfVariantsString := helpers.ConvertIntToString(totalNumberOfVariants) + + numberOfVariantsTestedString := helpers.ConvertIntToString(numberOfVariantsTested) + + variantsTestedLabel := widget.NewLabel("Variants Tested:") + variantsTestedText := getBoldLabel(numberOfVariantsTestedString + "/" + totalNumberOfVariantsString) + variantsTestedHelpButton := widget.NewButtonWithIcon("", theme.QuestionIcon(), func(){ + setNumberOfTestedVariantsExplainerPage(window, currentPage) + }) + + variantsTestedRow := container.NewHBox(layout.NewSpacer(), variantsTestedLabel, variantsTestedText, variantsTestedHelpButton, layout.NewSpacer()) + + //TODO: Add navigation with pages + + getVariantsGrid := func()(*fyne.Container, error){ + + emptyLabelA := widget.NewLabel("") + variantNameLabel := getItalicLabelCentered("Variant Name") + + probabilityOfLabelA := getItalicLabelCentered("Probability Of") + zeroMutationsLabel := getItalicLabelCentered("0 Mutations") + + probabilityOfLabelB := getItalicLabelCentered("Probability Of") + oneMutationLabel := getItalicLabelCentered("1 Mutation") + + probabilityOfLabelC := getItalicLabelCentered("Probability Of") + twoMutationsLabel := getItalicLabelCentered("2 Mutations") + + emptyLabelB := widget.NewLabel("") + emptyLabelC := widget.NewLabel("") + + variantNameColumn := container.NewVBox(emptyLabelA, variantNameLabel, widget.NewSeparator()) + probabilityOf0MutationsColumn := container.NewVBox(probabilityOfLabelA, zeroMutationsLabel, widget.NewSeparator()) + probabilityOf1MutationColumn := container.NewVBox(probabilityOfLabelB, oneMutationLabel, widget.NewSeparator()) + probabilityOf2MutationsColumn := container.NewVBox(probabilityOfLabelC, twoMutationsLabel, widget.NewSeparator()) + variantInfoButtonsColumn := container.NewVBox(emptyLabelB, emptyLabelC, widget.NewSeparator()) + + addVariantRow := func(variantIdentifier string)error{ + + variantObject, exists := diseaseVariantsMap[variantIdentifier] + if (exists == false){ + return errors.New("Cannot add variant row: Variant missing from diseaseVariantsMap") + } + + variantName := variantObject.VariantNames[0] + + offspringProbabilitiesKnown, probabilityOf0MutationsLowerBound, probabilityOf0MutationsUpperBound, probabilityOf0MutationsFormatted, probabilityOf1MutationLowerBound, probabilityOf1MutationUpperBound, probabilityOf1MutationFormatted, probabilityOf2MutationsLowerBound, probabilityOf2MutationsUpperBound, probabilityOf2MutationsFormatted, err := readGeneticAnalysis.GetOffspringMonogenicDiseaseVariantInfoFromGeneticAnalysis(coupleAnalysisMapList, diseaseName, variantIdentifier, genomePairIdentifier) + if (err != nil) { return err } + + getProbabilityOf0MutationsText := func()string{ + if (offspringProbabilitiesKnown == false){ + + result := translate("Unknown") + return result + } + if (probabilityOf0MutationsLowerBound == 0 && probabilityOf0MutationsUpperBound == 100){ + result := translate("Unknown") + return result + } + + return probabilityOf0MutationsFormatted + } + + probabilityOf0MutationsText := getProbabilityOf0MutationsText() + + getProbabilityOf1MutationText := func()string{ + if (offspringProbabilitiesKnown == false){ + + result := translate("Unknown") + return result + } + if (probabilityOf1MutationLowerBound == 0 && probabilityOf1MutationUpperBound == 100){ + result := translate("Unknown") + return result + } + + return probabilityOf1MutationFormatted + } + + probabilityOf1MutationText := getProbabilityOf1MutationText() + + getProbabilityOf2MutationsText := func()string{ + if (offspringProbabilitiesKnown == false){ + + result := translate("Unknown") + return result + } + if (probabilityOf2MutationsLowerBound == 0 && probabilityOf2MutationsUpperBound == 100){ + result := translate("Unknown") + return result + } + + return probabilityOf2MutationsFormatted + } + + probabilityOf2MutationsText := getProbabilityOf2MutationsText() + + variantNameLabel := getBoldLabelCentered(variantName) + + probabilityOf0MutationsLabel := getBoldLabelCentered(probabilityOf0MutationsText) + probabilityOf1MutationLabel := getBoldLabelCentered(probabilityOf1MutationText) + probabilityOf2MutationsLabel := getBoldLabelCentered(probabilityOf2MutationsText) + + viewVariantDetailsButton := widget.NewButtonWithIcon("", theme.VisibilityIcon(), func(){ + setViewCoupleGeneticAnalysisMonogenicDiseaseVariantDetailsPage(window, personAName, personBName, personAAnalysisMapList, personBAnalysisMapList, coupleAnalysisMapList, diseaseName, variantIdentifier, currentPage) + }) + + variantNameColumn.Add(variantNameLabel) + probabilityOf0MutationsColumn.Add(probabilityOf0MutationsLabel) + probabilityOf1MutationColumn.Add(probabilityOf1MutationLabel) + probabilityOf2MutationsColumn.Add(probabilityOf2MutationsLabel) + variantInfoButtonsColumn.Add(viewVariantDetailsButton) + + variantNameColumn.Add(widget.NewSeparator()) + probabilityOf0MutationsColumn.Add(widget.NewSeparator()) + probabilityOf1MutationColumn.Add(widget.NewSeparator()) + probabilityOf2MutationsColumn.Add(widget.NewSeparator()) + variantInfoButtonsColumn.Add(widget.NewSeparator()) + + return nil + } + + variantsWithNonZeroRiskList := make([]string, 0) + variantsWithZeroRiskFullyKnownList := make([]string, 0) + variantsWithZeroRiskPartiallyKnownList := make([]string, 0) + variantsWithUnknownRiskList := make([]string, 0) + + for variantIdentifier, _ := range diseaseVariantsMap{ + + probabilitesKnown, probabilityOf0MutationsLowerBound, probabilityOf0MutationsUpperBound, _, probabilityOf1MutationLowerBound, probabilityOf1MutationUpperBound, _, probabilityOf2MutationsLowerBound, probabilityOf2MutationsUpperBound, _, err := readGeneticAnalysis.GetOffspringMonogenicDiseaseVariantInfoFromGeneticAnalysis(coupleAnalysisMapList, diseaseName, variantIdentifier, genomePairIdentifier) + if (err != nil) { return nil, err } + + if (probabilitesKnown == false){ + variantsWithUnknownRiskList = append(variantsWithUnknownRiskList, variantIdentifier) + continue + } + + if (probabilityOf1MutationLowerBound != 0 || probabilityOf2MutationsLowerBound != 0){ + variantsWithNonZeroRiskList = append(variantsWithNonZeroRiskList, variantIdentifier) + continue + } + + // Risk is either 0 or partially unknown + if (probabilityOf0MutationsLowerBound == 100 && probabilityOf0MutationsUpperBound == 100 && probabilityOf1MutationLowerBound == 0 && probabilityOf1MutationUpperBound == 0 && probabilityOf2MutationsLowerBound == 0 && probabilityOf2MutationsUpperBound == 0){ + variantsWithZeroRiskFullyKnownList = append(variantsWithZeroRiskFullyKnownList, variantIdentifier) + continue + } + + variantsWithZeroRiskPartiallyKnownList = append(variantsWithZeroRiskPartiallyKnownList, variantIdentifier) + } + + // We sort each list so the variants show up in the same order each time + + helpers.SortStringListToUnicodeOrder(variantsWithNonZeroRiskList) + helpers.SortStringListToUnicodeOrder(variantsWithZeroRiskFullyKnownList) + helpers.SortStringListToUnicodeOrder(variantsWithZeroRiskPartiallyKnownList) + helpers.SortStringListToUnicodeOrder(variantsWithUnknownRiskList) + + for _, variantIdentifier := range variantsWithNonZeroRiskList{ + + err = addVariantRow(variantIdentifier) + if (err != nil) { return nil, err } + } + for _, variantIdentifier := range variantsWithZeroRiskFullyKnownList{ + + err = addVariantRow(variantIdentifier) + if (err != nil) { return nil, err } + } + for _, variantIdentifier := range variantsWithZeroRiskPartiallyKnownList{ + + err = addVariantRow(variantIdentifier) + if (err != nil) { return nil, err } + } + for _, variantIdentifier := range variantsWithUnknownRiskList{ + + err = addVariantRow(variantIdentifier) + if (err != nil) { return nil, err } + } + + variantsGrid := container.NewHBox(layout.NewSpacer(), variantNameColumn, probabilityOf0MutationsColumn, probabilityOf1MutationColumn, probabilityOf2MutationsColumn, variantInfoButtonsColumn, layout.NewSpacer()) + + return variantsGrid, nil + } + + variantsGrid, err := getVariantsGrid() + if (err != nil){ + setErrorEncounteredPage(window, err, previousPage) + return + } + + page := container.NewVBox(title, backButton, widget.NewSeparator(), description1Row, widget.NewSeparator(), genomePairRow, widget.NewSeparator(), variantsTestedRow, widget.NewSeparator(), variantsGrid) + + setPageContent(page, window) +} + + + +// This function provides a page to view the details of a specific variant from a genetic analysis +// It will show the variant details for all of the couple's genome pairs +func setViewCoupleGeneticAnalysisMonogenicDiseaseVariantDetailsPage(window fyne.Window, personAName string, personBName string, personAAnalysisMapList []map[string]string, personBAnalysisMapList []map[string]string, coupleAnalysisMapList []map[string]string, diseaseName string, variantIdentifier string, previousPage func()){ + + currentPage := func(){setViewCoupleGeneticAnalysisMonogenicDiseaseVariantDetailsPage(window, personAName, personBName, personAAnalysisMapList, personBAnalysisMapList, coupleAnalysisMapList, diseaseName, variantIdentifier, previousPage)} + + title := getPageTitleCentered("Disease Variant Details - " + diseaseName) + + backButton := getBackButtonCentered(previousPage) + + variantObject, err := monogenicDiseases.GetMonogenicDiseaseVariantObject(diseaseName, variantIdentifier) + if (err != nil) { + setErrorEncounteredPage(window, err, previousPage) + return + } + + variantName := variantObject.VariantNames[0] + + description := getLabelCentered("Below is the disease variant analysis for the couple.") + + variantNameLabel := widget.NewLabel("Variant Name:") + variantNameText := getBoldLabel(variantName) + variantInfoButton := widget.NewButtonWithIcon("", theme.InfoIcon(), func(){ + setViewMonogenicDiseaseVariantDetailsPage(window, diseaseName, variantIdentifier, currentPage) + }) + variantNameRow := container.NewHBox(layout.NewSpacer(), variantNameLabel, variantNameText, variantInfoButton, layout.NewSpacer()) + + getGenomePairsHaveVariantGrid := func()(*fyne.Container, error){ + + emptyLabelA := widget.NewLabel("") + emptyLabelB := widget.NewLabel("") + + emptyLabelC := widget.NewLabel("") + genomePairLabel := getItalicLabelCentered("Genome Pair") + + probabilityOfLabelA := getItalicLabelCentered("Probability Of") + zeroMutationsLabel := getItalicLabelCentered("0 Mutations") + + probabilityOfLabelB := getItalicLabelCentered("Probability Of") + oneMutationLabel := getItalicLabelCentered("1 Mutation") + + probabilityOfLabelC := getItalicLabelCentered("Probability Of") + twoMutationsLabel := getItalicLabelCentered("2 Mutations") + + viewGenomePairInfoButtonsColumn := container.NewVBox(emptyLabelA, emptyLabelB, widget.NewSeparator()) + genomePairNameColumn := container.NewVBox(emptyLabelC, genomePairLabel, widget.NewSeparator()) + probabilityOf0MutationsColumn := container.NewVBox(probabilityOfLabelA, zeroMutationsLabel, widget.NewSeparator()) + probabilityOf1MutationColumn := container.NewVBox(probabilityOfLabelB, oneMutationLabel, widget.NewSeparator()) + probablityOf2MutationsColumn := container.NewVBox(probabilityOfLabelC, twoMutationsLabel, widget.NewSeparator()) + + addGenomePairRow := func(genomePairName string, genomePairIdentifier string)error{ + + probabilitiesKnown, probabilityOf0MutationsLowerBound, probabilityOf0MutationsUpperBound, probabilityOf0MutationsFormatted, probabilityOf1MutationLowerBound, probabilityOf1MutationUpperBound, probabilityOf1MutationFormatted, probabilityOf2MutationsLowerBound, probabilityOf2MutationsUpperBound, probabilityOf2MutationsFormatted, err := readGeneticAnalysis.GetOffspringMonogenicDiseaseVariantInfoFromGeneticAnalysis(coupleAnalysisMapList, diseaseName, variantIdentifier, genomePairIdentifier) + if (err != nil) { return err } + + getProbabilityOf0MutationsText := func()string{ + if (probabilitiesKnown == false){ + + result := translate("Unknown") + return result + } + if (probabilityOf0MutationsLowerBound == 0 && probabilityOf0MutationsUpperBound == 100){ + result := translate("Unknown") + return result + } + + return probabilityOf0MutationsFormatted + } + + probabilityOf0MutationsText := getProbabilityOf0MutationsText() + + getProbabilityOf1MutationText := func()string{ + if (probabilitiesKnown == false){ + + result := translate("Unknown") + return result + } + if (probabilityOf1MutationLowerBound == 0 && probabilityOf1MutationUpperBound == 100){ + result := translate("Unknown") + return result + } + + return probabilityOf1MutationFormatted + } + + probabilityOf1MutationText := getProbabilityOf1MutationText() + + getProbabilityOf2MutationsText := func()string{ + if (probabilitiesKnown == false){ + + result := translate("Unknown") + return result + } + if (probabilityOf2MutationsLowerBound == 0 && probabilityOf2MutationsUpperBound == 100){ + result := translate("Unknown") + return result + } + + return probabilityOf2MutationsFormatted + } + + probabilityOf2MutationsText := getProbabilityOf2MutationsText() + + viewGenomePairInfoButton := widget.NewButtonWithIcon("", theme.InfoIcon(), func(){ + setViewCoupleGeneticAnalysisMonogenicDiseaseGenomePairDetailsPage(window, personAName, personBName, personAAnalysisMapList, personBAnalysisMapList, coupleAnalysisMapList, diseaseName, genomePairIdentifier, genomePairName, currentPage) + }) + + genomePairNameLabel := getBoldLabelCentered(genomePairName) + + probabilityOf0MutationsLabel := getBoldLabelCentered(probabilityOf0MutationsText) + probabilityOf1MutationLabel := getBoldLabelCentered(probabilityOf1MutationText) + probabilityOf2MutationsLabel := getBoldLabelCentered(probabilityOf2MutationsText) + + viewGenomePairInfoButtonsColumn.Add(viewGenomePairInfoButton) + genomePairNameColumn.Add(genomePairNameLabel) + probabilityOf0MutationsColumn.Add(probabilityOf0MutationsLabel) + probabilityOf1MutationColumn.Add(probabilityOf1MutationLabel) + probablityOf2MutationsColumn.Add(probabilityOf2MutationsLabel) + + viewGenomePairInfoButtonsColumn.Add(widget.NewSeparator()) + genomePairNameColumn.Add(widget.NewSeparator()) + probabilityOf0MutationsColumn.Add(widget.NewSeparator()) + probabilityOf1MutationColumn.Add(widget.NewSeparator()) + probablityOf2MutationsColumn.Add(widget.NewSeparator()) + + return nil + } + + pair1PersonAGenomeIdentifier, pair1PersonBGenomeIdentifier, secondGenomePairExists, pair2PersonAGenomeIdentifier, pair2PersonBGenomeIdentifier, _, _, _, _, _, _, err := readGeneticAnalysis.GetMetadataFromCoupleGeneticAnalysis(coupleAnalysisMapList) + if (err != nil){ return nil, err } + + genomePair1Identifier := pair1PersonAGenomeIdentifier + "+" + pair1PersonBGenomeIdentifier + + err = addGenomePairRow("Pair 1", genomePair1Identifier) + if (err != nil) { return nil, err } + + if (secondGenomePairExists == true){ + + genomePair2Identifier := pair2PersonAGenomeIdentifier + "+" + pair2PersonBGenomeIdentifier + + err := addGenomePairRow("Pair 2", genomePair2Identifier) + if (err != nil) { return nil, err } + } + + genomesContainer := container.NewHBox(layout.NewSpacer(), viewGenomePairInfoButtonsColumn, genomePairNameColumn, probabilityOf0MutationsColumn, probabilityOf1MutationColumn, probablityOf2MutationsColumn, layout.NewSpacer()) + + return genomesContainer, nil + } + + genomePairsHaveVariantGrid, err := getGenomePairsHaveVariantGrid() + if (err != nil){ + setErrorEncounteredPage(window, err, previousPage) + return + } + + page := container.NewVBox(title, backButton, widget.NewSeparator(), description, widget.NewSeparator(), variantNameRow, widget.NewSeparator(), genomePairsHaveVariantGrid) + + setPageContent(page, window) +} + + +func setViewCoupleGeneticAnalysisPolygenicDiseasesPage(window fyne.Window, personAName string, personBName string, personAAnalysisMapList []map[string]string, personBAnalysisMapList []map[string]string, coupleAnalysisMapList []map[string]string, previousPage func()){ + + currentPage := func(){setViewCoupleGeneticAnalysisPolygenicDiseasesPage(window, personAName, personBName, personAAnalysisMapList, personBAnalysisMapList, coupleAnalysisMapList, previousPage)} + + title := getPageTitleCentered("Viewing Genetic Analysis - Polygenic Diseases") + + backButton := getBackButtonCentered(previousPage) + + description := getLabelCentered("Below is an analysis of the polygenic disease risk probabilities for the couple's offspring.") + + getDiseasesGrid := func()(*fyne.Container, error){ + + pair1PersonAGenomeIdentifier, pair1PersonBGenomeIdentifier, secondGenomePairExists, _, _, _, _, _, _, _, _, err := readGeneticAnalysis.GetMetadataFromCoupleGeneticAnalysis(coupleAnalysisMapList) + if (err != nil){ return nil, err } + + diseaseNameLabel := getItalicLabelCentered("Disease Name") + + offspringRiskScoreLabel := getItalicLabelCentered("Offspring Risk Score") + + conflictExistsLabel := getItalicLabelCentered("Conflict Exists?") + + emptyLabel := widget.NewLabel("") + + diseaseNameColumn := container.NewVBox(diseaseNameLabel, widget.NewSeparator()) + offspringRiskScoreColumn := container.NewVBox(offspringRiskScoreLabel, widget.NewSeparator()) + conflictExistsColumn := container.NewVBox(conflictExistsLabel, widget.NewSeparator()) + viewButtonsColumn := container.NewVBox(emptyLabel, widget.NewSeparator()) + + diseaseNamesList, err := polygenicDiseases.GetPolygenicDiseaseNamesList() + if (err != nil) { return nil, err } + + for _, diseaseName := range diseaseNamesList{ + + mainGenomePairIdentifier := pair1PersonAGenomeIdentifier + "+" + pair1PersonBGenomeIdentifier + + offspringRiskScoreKnown, _, offspringRiskScoreFormatted, _, conflictExists, err := readGeneticAnalysis.GetOffspringPolygenicDiseaseInfoFromGeneticAnalysis(coupleAnalysisMapList, diseaseName, mainGenomePairIdentifier, secondGenomePairExists) + if (err != nil) { return nil, err } + + getRiskScoreLabelText := func()string{ + + if (offspringRiskScoreKnown == false){ + result := translate("Unknown") + + return result + } + + return offspringRiskScoreFormatted + } + + offspringRiskScoreLabelText := getRiskScoreLabelText() + + diseaseNameText := getBoldLabelCentered(diseaseName) + diseaseNameColumn.Add(diseaseNameText) + + offspringRiskScoreText := getBoldLabelCentered(offspringRiskScoreLabelText) + offspringRiskScoreColumn.Add(offspringRiskScoreText) + + conflictExistsString := helpers.ConvertBoolToYesOrNoString(conflictExists) + conflictExistsLabel := getBoldLabelCentered(conflictExistsString) + conflictExistsColumn.Add(conflictExistsLabel) + + viewDetailsButton := getWidgetCentered(widget.NewButtonWithIcon("", theme.VisibilityIcon(), func(){ + setViewCoupleGeneticAnalysisPolygenicDiseaseDetailsPage(window, personAName, personBName, personAAnalysisMapList, personBAnalysisMapList, coupleAnalysisMapList, diseaseName, currentPage) + })) + viewButtonsColumn.Add(viewDetailsButton) + + diseaseNameColumn.Add(widget.NewSeparator()) + offspringRiskScoreColumn.Add(widget.NewSeparator()) + conflictExistsColumn.Add(widget.NewSeparator()) + viewButtonsColumn.Add(widget.NewSeparator()) + } + + offspringRiskScoreHelpButton := widget.NewButtonWithIcon("", theme.QuestionIcon(), func(){ + setOffspringPolygenicDiseaseRiskScoreExplainerPage(window, currentPage) + }) + offspringRiskScoreColumn.Add(offspringRiskScoreHelpButton) + + diseasesGrid := container.NewHBox(layout.NewSpacer(), diseaseNameColumn, offspringRiskScoreColumn) + + if (secondGenomePairExists == true){ + + conflictExistsHelpButton := widget.NewButtonWithIcon("", theme.QuestionIcon(), func(){ + setCoupleGeneticAnalysisConflictExistsExplainerPage(window, currentPage) + }) + conflictExistsColumn.Add(conflictExistsHelpButton) + + diseasesGrid.Add(conflictExistsColumn) + } + + diseasesGrid.Add(viewButtonsColumn) + diseasesGrid.Add(layout.NewSpacer()) + + return diseasesGrid, nil + } + + diseasesGrid, err := getDiseasesGrid() + if (err != nil){ + setErrorEncounteredPage(window, err, previousPage) + return + } + + page := container.NewVBox(title, backButton, widget.NewSeparator(), description, widget.NewSeparator(), diseasesGrid) + + setPageContent(page, window) +} + +func setViewCoupleGeneticAnalysisPolygenicDiseaseDetailsPage(window fyne.Window, personAName string, personBName string, personAAnalysisMapList []map[string]string, personBAnalysisMapList []map[string]string, coupleAnalysisMapList []map[string]string, diseaseName string, previousPage func()){ + + currentPage := func(){setViewCoupleGeneticAnalysisPolygenicDiseaseDetailsPage(window, personAName, personBName, personAAnalysisMapList, personBAnalysisMapList, coupleAnalysisMapList, diseaseName, previousPage)} + + title := getPageTitleCentered("Viewing Couple Analysis - " + diseaseName) + + backButton := getBackButtonCentered(previousPage) + + pair1PersonAGenomeIdentifier, pair1PersonBGenomeIdentifier, secondGenomePairExists, pair2PersonAGenomeIdentifier, pair2PersonBGenomeIdentifier, _, _, _, _, _, _, err := readGeneticAnalysis.GetMetadataFromCoupleGeneticAnalysis(coupleAnalysisMapList) + if (err != nil){ + setErrorEncounteredPage(window, err, previousPage) + return + } + + getDescriptionSection := func()*fyne.Container{ + + if (secondGenomePairExists == false){ + description := getLabelCentered("Below is the disease analysis for the couple's offspring.") + + return description + } + + description1 := getLabelCentered("Below is the disease analysis for the couple's offspring.") + description2 := getLabelCentered("Each genome pair combines different genomes from each person.") + + descriptionsSection := container.NewVBox(description1, description2) + + return descriptionsSection + } + + descriptionSection := getDescriptionSection() + + diseaseNameLabel := widget.NewLabel("Disease:") + diseaseNameText := getBoldLabel(diseaseName) + diseaseNameInfoButton := widget.NewButtonWithIcon("", theme.InfoIcon(), func(){ + setViewPolygenicDiseaseDetailsPage(window, diseaseName, currentPage) + }) + diseaseNameRow := container.NewHBox(layout.NewSpacer(), diseaseNameLabel, diseaseNameText, diseaseNameInfoButton, layout.NewSpacer()) + + emptyLabelA := widget.NewLabel("") + emptyLabelB := widget.NewLabel("") + + offspringRiskScoreLabel := getItalicLabelCentered("Offspring Risk Score") + + emptyLabelC := widget.NewLabel("") + emptyLabelD := widget.NewLabel("") + + viewGenomePairButtonsColumn := container.NewVBox(emptyLabelA, widget.NewSeparator()) + pairNameColumn := container.NewVBox(emptyLabelB, widget.NewSeparator()) + offspringRiskScoreColumn := container.NewVBox(offspringRiskScoreLabel, widget.NewSeparator()) + viewLifetimeRiskButtonsColumn := container.NewVBox(emptyLabelC, widget.NewSeparator()) + viewOffspringLociButtonsColumn := container.NewVBox(emptyLabelD, widget.NewSeparator()) + + addGenomePairRow := func(genomePairName string, personAGenomeIdentifier string, personBGenomeIdentifier string)error{ + + genomePairIdentifier := personAGenomeIdentifier + "+" + personBGenomeIdentifier + + offspringRiskScoreKnown, _, offspringRiskScoreFormatted, _, _, err := readGeneticAnalysis.GetOffspringPolygenicDiseaseInfoFromGeneticAnalysis(coupleAnalysisMapList, diseaseName, genomePairIdentifier, secondGenomePairExists) + if (err != nil) { return err } + + getRiskScoreLabelText := func()string{ + + if (offspringRiskScoreKnown == false){ + result := translate("Unknown") + + return result + } + + return offspringRiskScoreFormatted + } + + riskScoreLabelText := getRiskScoreLabelText() + + genomePairNameLabel := getBoldLabelCentered(genomePairName) + + offspringRiskScoreLabel := getBoldLabelCentered(riskScoreLabelText) + + viewGenomePairButton := widget.NewButtonWithIcon("", theme.InfoIcon(), func(){ + setViewCoupleGeneticAnalysisPolygenicDiseaseGenomePairDetailsPage(window, personAName, personBName, personAAnalysisMapList, personBAnalysisMapList, coupleAnalysisMapList, diseaseName, genomePairIdentifier, genomePairName, currentPage) + }) + + viewOffspringLifetimeRiskButton := widget.NewButtonWithIcon("", theme.HistoryIcon(), func(){ + + diseaseObject, err := polygenicDiseases.GetPolygenicDiseaseObject(diseaseName) + if (err != nil){ + setErrorEncounteredPage(window, err, currentPage) + return + } + + getPageMaleOrFemale := func()string{ + + //TODO: Get user sex from myLocalProfiles + + diseaseEffectedSex := diseaseObject.EffectedSex + if (diseaseEffectedSex == "Both"){ + return "Male" + } + + return diseaseEffectedSex + } + + pageMaleOrFemale := getPageMaleOrFemale() + + setViewPersonPolygenicDiseaseLifetimeProbabilitiesPage(window, diseaseName, genomePairName, pageMaleOrFemale, currentPage) + }) + + viewOffspringLociButton := widget.NewButtonWithIcon("", theme.VisibilityIcon(), func(){ + setViewCouplePolygenicDiseaseLociPage(window, personAName, personBName, personAAnalysisMapList, personBAnalysisMapList, coupleAnalysisMapList, diseaseName, genomePairIdentifier, genomePairName, currentPage) + }) + + viewGenomePairButtonsColumn.Add(viewGenomePairButton) + pairNameColumn.Add(genomePairNameLabel) + offspringRiskScoreColumn.Add(offspringRiskScoreLabel) + viewLifetimeRiskButtonsColumn.Add(viewOffspringLifetimeRiskButton) + viewOffspringLociButtonsColumn.Add(viewOffspringLociButton) + + viewGenomePairButtonsColumn.Add(widget.NewSeparator()) + pairNameColumn.Add(widget.NewSeparator()) + offspringRiskScoreColumn.Add(widget.NewSeparator()) + viewOffspringLociButtonsColumn.Add(widget.NewSeparator()) + + return nil + } + + err = addGenomePairRow("Pair 1", pair1PersonAGenomeIdentifier, pair1PersonBGenomeIdentifier) + if (err != nil) { + setErrorEncounteredPage(window, err, previousPage) + return + } + + if (secondGenomePairExists == true){ + err := addGenomePairRow("Pair 2", pair2PersonAGenomeIdentifier, pair2PersonBGenomeIdentifier) + if (err != nil) { + setErrorEncounteredPage(window, err, previousPage) + return + } + } + + offspringRiskScoreHelpButton := widget.NewButtonWithIcon("", theme.QuestionIcon(), func(){ + setOffspringPolygenicDiseaseRiskScoreExplainerPage(window, currentPage) + }) + + offspringRiskScoreColumn.Add(offspringRiskScoreHelpButton) + + genomesContainer := container.NewHBox(layout.NewSpacer(), viewGenomePairButtonsColumn, pairNameColumn, offspringRiskScoreColumn, viewOffspringLociButtonsColumn, layout.NewSpacer()) + + page := container.NewVBox(title, backButton, widget.NewSeparator(), descriptionSection, widget.NewSeparator(), diseaseNameRow, widget.NewSeparator(), genomesContainer) + + setPageContent(page, window) +} + + +func setViewCoupleGeneticAnalysisPolygenicDiseaseGenomePairDetailsPage(window fyne.Window, personAName string, personBName string, personAAnalysisMapList []map[string]string, personBAnalysisMapList []map[string]string, coupleAnalysisMapList []map[string]string, diseaseName string, genomePairIdentifier string, genomePairName string, previousPage func()){ + + currentPage := func(){setViewCoupleGeneticAnalysisPolygenicDiseaseGenomePairDetailsPage(window, personAName, personBName, personAAnalysisMapList, personBAnalysisMapList, coupleAnalysisMapList, diseaseName, genomePairIdentifier, genomePairName, previousPage)} + + title := getPageTitleCentered("Viewing Couple Genome Pair Info") + + backButton := getBackButtonCentered(previousPage) + + description := getLabelCentered("Below is the disease information for both genomes in the genome pair.") + + diseaseNameLabel := widget.NewLabel("Disease:") + diseaseNameText := getBoldLabel(diseaseName) + diseaseNameInfoButton := widget.NewButtonWithIcon("", theme.InfoIcon(), func(){ + setViewPolygenicDiseaseDetailsPage(window, diseaseName, currentPage) + }) + diseaseNameRow := container.NewHBox(layout.NewSpacer(), diseaseNameLabel, diseaseNameText, diseaseNameInfoButton, layout.NewSpacer()) + + genomePairLabel := widget.NewLabel("Genome Pair:") + genomePairNameLabel := getBoldLabel(genomePairName) + genomePairHelpButton := widget.NewButtonWithIcon("", theme.QuestionIcon(), func(){ + setCoupleGenomePairExplainerPage(window, currentPage) + }) + genomePairRow := container.NewHBox(layout.NewSpacer(), genomePairLabel, genomePairNameLabel, genomePairHelpButton, layout.NewSpacer()) + + emptyLabelA := widget.NewLabel("") + personNameLabel := getItalicLabelCentered("Person Name") + + emptyLabelB := widget.NewLabel("") + genomeNameLabel := getItalicLabelCentered("Genome Name") + + emptyLabelC := widget.NewLabel("") + riskScoreLabel := getItalicLabelCentered("Risk Score") + + numberOfLabel := getItalicLabelCentered("Number Of") + lociTestedLabel := getItalicLabelCentered("Loci Tested") + + emptyLabelD := widget.NewLabel("") + emptyLabelE := widget.NewLabel("") + + personNameColumn := container.NewVBox(emptyLabelA, personNameLabel, widget.NewSeparator()) + genomeNameColumn := container.NewVBox(emptyLabelB, genomeNameLabel, widget.NewSeparator()) + riskScoreColumn := container.NewVBox(emptyLabelC, riskScoreLabel, widget.NewSeparator()) + numberOfLociTestedColumn := container.NewVBox(numberOfLabel, lociTestedLabel, widget.NewSeparator()) + viewGenomeButtonsColumn := container.NewVBox(emptyLabelD, emptyLabelE, widget.NewSeparator()) + + addGenomeRow := func(isPersonA bool, personName string, inputGenomeIdentifier string)error{ + + personAnalysisGenomeIdentifier, personHasMultipleGenomes, genomeIsCombined, combinedType, err := readGeneticAnalysis.GetMatchingPersonAnalysisGenomeIdentifierFromCoupleAnalysis(isPersonA, personAAnalysisMapList, personBAnalysisMapList, coupleAnalysisMapList, inputGenomeIdentifier) + if (err != nil) { return err } + + personNameTrimmed, _, err := helpers.TrimAndFlattenString(personName, 10) + if (err != nil) { return err } + + personNameLabel := getBoldLabelCentered(personNameTrimmed) + + getPersonAnalysisMapList := func()[]map[string]string{ + if (isPersonA == true){ + return personAAnalysisMapList + } + return personBAnalysisMapList + } + + personAnalysisMapList := getPersonAnalysisMapList() + + getGenomeName := func()(string, error){ + + if (genomeIsCombined == false){ + + genomeFound, _, _, _, _, _, companyName, _, _, err := myGenomes.GetMyRawGenomeMetadata(personAnalysisGenomeIdentifier) + if (err != nil) { return "", err } + if (genomeFound == false){ + return "", errors.New("MyGenomeInfo for genome from analysisMapList not found.") + } + + return companyName, nil + } + + return combinedType, nil + } + + genomeName, err := getGenomeName() + if (err != nil) { return err } + + genomeNameLabel := getBoldLabelCentered(genomeName) + + personRiskScoreKnown, _, personRiskScoreFormatted, numberOfLociTested, _, err := readGeneticAnalysis.GetPersonPolygenicDiseaseInfoFromGeneticAnalysis(personAnalysisMapList, diseaseName, personAnalysisGenomeIdentifier, personHasMultipleGenomes) + if (err != nil) { return err } + + getPersonRiskScoreLabelText := func()string{ + if (personRiskScoreKnown == false){ + result := translate("Unknown") + return result + } + return personRiskScoreFormatted + } + + personRiskScoreLabelText := getPersonRiskScoreLabelText() + + genomeRiskScoreLabel := getBoldLabelCentered(personRiskScoreLabelText) + + numberOfLociTestedString := helpers.ConvertIntToString(numberOfLociTested) + numberOfLociTestedText := getBoldLabelCentered(numberOfLociTestedString) + + viewGenomeButton := widget.NewButtonWithIcon("", theme.VisibilityIcon(), func(){ + setViewPersonGenomePolygenicDiseaseLociPage(window, personAnalysisMapList, diseaseName, personAnalysisGenomeIdentifier, genomeName, currentPage) + }) + + personNameColumn.Add(personNameLabel) + genomeNameColumn.Add(genomeNameLabel) + riskScoreColumn.Add(genomeRiskScoreLabel) + numberOfLociTestedColumn.Add(numberOfLociTestedText) + viewGenomeButtonsColumn.Add(viewGenomeButton) + + personNameColumn.Add(widget.NewSeparator()) + genomeNameColumn.Add(widget.NewSeparator()) + riskScoreColumn.Add(widget.NewSeparator()) + numberOfLociTestedColumn.Add(widget.NewSeparator()) + viewGenomeButtonsColumn.Add(widget.NewSeparator()) + + return nil + } + + personAGenomeIdentifier, personBGenomeIdentifier, found := strings.Cut(genomePairIdentifier, "+") + if (found == false){ + setErrorEncounteredPage(window, errors.New("setViewCoupleGeneticAnalysisPolygenicDiseaseGenomePairDetailsPage called with invalid genomePairIdentifier"), previousPage) + return + } + + err := addGenomeRow(true, personAName, personAGenomeIdentifier) + if (err != nil){ + setErrorEncounteredPage(window, err, previousPage) + return + } + err = addGenomeRow(false, personBName, personBGenomeIdentifier) + if (err != nil){ + setErrorEncounteredPage(window, err, previousPage) + return + } + + riskScoreHelpButton := widget.NewButtonWithIcon("", theme.QuestionIcon(), func(){ + setPolygenicDiseaseRiskScoreExplainerPage(window, currentPage) + }) + + riskScoreColumn.Add(riskScoreHelpButton) + + numberOfLociTestedHelpButton := widget.NewButtonWithIcon("", theme.QuestionIcon(), func(){ + setPolygenicDiseaseNumberOfLociTestedExplainerPage(window, currentPage) + }) + numberOfLociTestedColumn.Add(numberOfLociTestedHelpButton) + + genomesGrid := container.NewHBox(layout.NewSpacer(), personNameColumn, genomeNameColumn, riskScoreColumn, numberOfLociTestedColumn, viewGenomeButtonsColumn, layout.NewSpacer()) + + page := container.NewVBox(title, backButton, widget.NewSeparator(), description, widget.NewSeparator(), diseaseNameRow, widget.NewSeparator(), genomePairRow, widget.NewSeparator(), genomesGrid) + + setPageContent(page, window) +} + + +// This function provides a page to view the couple offspring locus probabilities for a particular genome pair +func setViewCouplePolygenicDiseaseLociPage(window fyne.Window, personAName string, personBName string, personAAnalysisMapList []map[string]string, personBAnalysisMapList []map[string]string, coupleAnalysisMapList []map[string]string, diseaseName string, genomePairIdentifier string, genomePairName string, previousPage func()){ + + setLoadingScreen(window, "Loading Polygenic Disease Loci", "Loading disease loci...") + + currentPage := func(){setViewCouplePolygenicDiseaseLociPage(window, personAName, personBName, personAAnalysisMapList, personBAnalysisMapList, coupleAnalysisMapList, diseaseName, genomePairIdentifier, genomePairName, previousPage)} + + title := getPageTitleCentered("View Offspring Disease Loci - " + diseaseName) + + backButton := getBackButtonCentered(previousPage) + + description1 := widget.NewLabel("Below are the disease loci probabilities for offspring from this genome pair.") + diseaseLociHelpButton := widget.NewButtonWithIcon("", theme.QuestionIcon(), func(){ + setPolygenicDiseaseLociExplainerPage(window, currentPage) + }) + description1Row := container.NewHBox(layout.NewSpacer(), description1, diseaseLociHelpButton, layout.NewSpacer()) + + genomePairLabel := widget.NewLabel("Genome Pair:") + genomePairNameLabel := getBoldLabel(genomePairName) + viewGenomePairInfoButton := widget.NewButtonWithIcon("", theme.InfoIcon(), func(){ + setViewCoupleGeneticAnalysisPolygenicDiseaseGenomePairDetailsPage(window, personAName, personBName, personAAnalysisMapList, personBAnalysisMapList, coupleAnalysisMapList, diseaseName, genomePairIdentifier, genomePairName, currentPage) + }) + + genomePairRow := container.NewHBox(layout.NewSpacer(), genomePairLabel, genomePairNameLabel, viewGenomePairInfoButton, layout.NewSpacer()) + + _, _, secondGenomePairExists, _, _, _, _, _, _, _, _, err := readGeneticAnalysis.GetMetadataFromCoupleGeneticAnalysis(coupleAnalysisMapList) + if (err != nil){ + setErrorEncounteredPage(window, err, previousPage) + return + } + + _, _, _, numberOfLociTested, _, err := readGeneticAnalysis.GetOffspringPolygenicDiseaseInfoFromGeneticAnalysis(coupleAnalysisMapList, diseaseName, genomePairIdentifier, secondGenomePairExists) + if (err != nil){ + setErrorEncounteredPage(window, err, previousPage) + return + } + + numberOfLociTestedString := helpers.ConvertIntToString(numberOfLociTested) + + diseaseLociMap, err := polygenicDiseases.GetPolygenicDiseaseLociMap(diseaseName) + if (err != nil){ + setErrorEncounteredPage(window, err, previousPage) + return + } + + totalNumberOfLoci := len(diseaseLociMap) + totalNumberOfLociString := helpers.ConvertIntToString(totalNumberOfLoci) + + lociTestedLabel := widget.NewLabel("Loci Tested:") + lociTestedText := getBoldLabel(numberOfLociTestedString + "/" + totalNumberOfLociString) + lociTestedHelpButton := widget.NewButtonWithIcon("", theme.QuestionIcon(), func(){ + setOffspringPolygenicDiseaseNumberOfLociTestedExplainerPage(window, currentPage) + }) + + lociTestedRow := container.NewHBox(layout.NewSpacer(), lociTestedLabel, lociTestedText, lociTestedHelpButton, layout.NewSpacer()) + + getLociGrid := func()(*fyne.Container, error){ + + locusNameLabel := getItalicLabelCentered("Locus Name") + + offspringRiskWeightLabel := getItalicLabelCentered("Offspring Risk Weight") + + offspringOddsRatioLabel := getItalicLabelCentered("Offspring Odds Ratio") + + emptyLabel := widget.NewLabel("") + + locusNameColumn := container.NewVBox(locusNameLabel, widget.NewSeparator()) + offspringRiskWeightColumn := container.NewVBox(offspringRiskWeightLabel, widget.NewSeparator()) + offspringOddsRatioColumn := container.NewVBox(offspringOddsRatioLabel, widget.NewSeparator()) + lociInfoButtonsColumn := container.NewVBox(emptyLabel, widget.NewSeparator()) + + addLocusRow := func(locusIdentifier string)error{ + + locusObject, exists := diseaseLociMap[locusIdentifier] + if (exists == false) { + return errors.New("Cannot add locus row: Locus not found in diseaseLociMap: " + locusIdentifier) + } + + locusRSID := locusObject.LocusRSID + locusRSIDString := helpers.ConvertInt64ToString(locusRSID) + locusName := "rs" + locusRSIDString + + offspringRiskWeightKnown, offspringRiskWeight, offspringOddsRatioKnown, _, offspringOddsRatioFormatted, err := readGeneticAnalysis.GetOffspringPolygenicDiseaseLocusInfoFromGeneticAnalysis(coupleAnalysisMapList, diseaseName, locusIdentifier, genomePairIdentifier) + if (err != nil) { return err } + + getOffspringRiskWeightText := func()string{ + + if (offspringRiskWeightKnown == false){ + + unknownTextTranslated := translate("Unknown") + return unknownTextTranslated + } + + offspringRiskWeightString := helpers.ConvertIntToString(offspringRiskWeight) + + return offspringRiskWeightString + } + + offspringRiskWeightText := getOffspringRiskWeightText() + + getOffspringOddsRatioText := func()string{ + + if (offspringOddsRatioKnown == false){ + + unknownTextTranslated := translate("Unknown") + return unknownTextTranslated + } + + return offspringOddsRatioFormatted + } + + offspringOddsRatioText := getOffspringOddsRatioText() + + locusNameLabel := getBoldLabelCentered(locusName) + + locusRiskWeightLabel := getBoldLabelCentered(offspringRiskWeightText) + locusOddsRatioLabel := getBoldLabelCentered(offspringOddsRatioText) + + viewLocusDetailsButton := widget.NewButtonWithIcon("", theme.VisibilityIcon(), func(){ + setViewCoupleGeneticAnalysisPolygenicDiseaseLocusDetailsPage(window, personAName, personBName, personAAnalysisMapList, personBAnalysisMapList, coupleAnalysisMapList, diseaseName, locusIdentifier, currentPage) + }) + + locusNameColumn.Add(locusNameLabel) + offspringRiskWeightColumn.Add(locusRiskWeightLabel) + offspringOddsRatioColumn.Add(locusOddsRatioLabel) + lociInfoButtonsColumn.Add(viewLocusDetailsButton) + + locusNameColumn.Add(widget.NewSeparator()) + offspringRiskWeightColumn.Add(widget.NewSeparator()) + offspringOddsRatioColumn.Add(widget.NewSeparator()) + lociInfoButtonsColumn.Add(widget.NewSeparator()) + + return nil + } + + lociWithKnownRiskWeightList := make([]string, 0) + lociWithUnknownRiskWeightList := make([]string, 0) + + for locusIdentifier, _ := range diseaseLociMap{ + + offspringRiskWeightKnown, _, _, _, _, err := readGeneticAnalysis.GetOffspringPolygenicDiseaseLocusInfoFromGeneticAnalysis(coupleAnalysisMapList, diseaseName, locusIdentifier, genomePairIdentifier) + if (err != nil) { return nil, err } + + if (offspringRiskWeightKnown == true){ + lociWithKnownRiskWeightList = append(lociWithKnownRiskWeightList, locusIdentifier) + } else { + lociWithUnknownRiskWeightList = append(lociWithUnknownRiskWeightList, locusIdentifier) + } + } + + // We sort the lists so loci show up in the same order whenever page is refreshed + + helpers.SortStringListToUnicodeOrder(lociWithKnownRiskWeightList) + helpers.SortStringListToUnicodeOrder(lociWithUnknownRiskWeightList) + + for _, locusIdentifier := range lociWithKnownRiskWeightList{ + + err = addLocusRow(locusIdentifier) + if (err != nil) { return nil, err } + } + + for _, locusIdentifier := range lociWithUnknownRiskWeightList{ + + err = addLocusRow(locusIdentifier) + if (err != nil) { return nil, err } + } + + offspringRiskWeightHelpButton := widget.NewButtonWithIcon("", theme.QuestionIcon(), func(){ + setOffspringPolygenicDiseaseLocusRiskWeightExplainerPage(window, currentPage) + }) + offspringRiskWeightColumn.Add(offspringRiskWeightHelpButton) + + offspringOddsRatioHelpButton := widget.NewButtonWithIcon("", theme.QuestionIcon(), func(){ + //TODO + showUnderConstructionDialog(window) + }) + offspringOddsRatioColumn.Add(offspringOddsRatioHelpButton) + + lociGrid := container.NewHBox(layout.NewSpacer(), locusNameColumn, offspringRiskWeightColumn, offspringOddsRatioColumn, lociInfoButtonsColumn, layout.NewSpacer()) + + return lociGrid, nil + } + + lociGrid, err := getLociGrid() + if (err != nil){ + setErrorEncounteredPage(window, err, previousPage) + return + } + + page := container.NewVBox(title, backButton, widget.NewSeparator(), description1Row, widget.NewSeparator(), genomePairRow, widget.NewSeparator(), lociTestedRow, widget.NewSeparator(), lociGrid) + + setPageContent(page, window) +} + + +// This function provides a page to view the details of a specific locus from a genetic analysis +// It will show the locus details for all of the couple's genome pairs +func setViewCoupleGeneticAnalysisPolygenicDiseaseLocusDetailsPage(window fyne.Window, personAName string, personBName string, personAAnalysisMapList []map[string]string, personBAnalysisMapList []map[string]string, coupleAnalysisMapList []map[string]string, diseaseName string, locusIdentifier string, previousPage func()){ + + currentPage := func(){setViewCoupleGeneticAnalysisPolygenicDiseaseLocusDetailsPage(window, personAName, personBName, personAAnalysisMapList, personBAnalysisMapList, coupleAnalysisMapList, diseaseName, locusIdentifier, previousPage)} + + title := getPageTitleCentered("Disease Locus Details - " + diseaseName) + + backButton := getBackButtonCentered(previousPage) + + locusObject, err := polygenicDiseases.GetPolygenicDiseaseLocusObject(diseaseName, locusIdentifier) + if (err != nil) { + setErrorEncounteredPage(window, err, previousPage) + return + } + + locusRSID := locusObject.LocusRSID + locusRSIDString := helpers.ConvertInt64ToString(locusRSID) + locusName := "rs" + locusRSIDString + + description := getLabelCentered("Below is the locus analysis for the couple.") + + locusNameLabel := widget.NewLabel("Locus Name:") + locusNameText := getBoldLabel(locusName) + locusInfoButton := widget.NewButtonWithIcon("", theme.InfoIcon(), func(){ + setViewPolygenicDiseaseLocusDetailsPage(window, diseaseName, locusIdentifier, currentPage) + }) + locusNameRow := container.NewHBox(layout.NewSpacer(), locusNameLabel, locusNameText, locusInfoButton, layout.NewSpacer()) + + getGenomePairsLocusInfoGrid := func()(*fyne.Container, error){ + + emptyLabel := widget.NewLabel("") + + genomePairLabel := getItalicLabelCentered("Genome Pair") + + offspringRiskWeightLabel := getItalicLabelCentered("Offspring Risk Weight") + + offspringOddsRatioLabel := getItalicLabelCentered("Offspring Odds Ratio") + + viewGenomePairInfoButtonsColumn := container.NewVBox(emptyLabel, widget.NewSeparator()) + genomePairNameColumn := container.NewVBox(genomePairLabel, widget.NewSeparator()) + offspringRiskWeightColumn := container.NewVBox(offspringRiskWeightLabel, widget.NewSeparator()) + offspringOddsRatioColumn := container.NewVBox(offspringOddsRatioLabel, widget.NewSeparator()) + + addGenomePairRow := func(genomePairName string, genomePairIdentifier string)error{ + + offspringRiskWeightKnown, offspringRiskWeight, offspringOddsRatioKnown, _, offspringOddsRatioFormatted, err := readGeneticAnalysis.GetOffspringPolygenicDiseaseLocusInfoFromGeneticAnalysis(coupleAnalysisMapList, diseaseName, locusIdentifier, genomePairIdentifier) + if (err != nil) { return err } + + getOffspringRiskWeightText := func()string{ + + if (offspringRiskWeightKnown == false){ + result := translate("Unknown") + return result + } + + offspringRiskWeightString := helpers.ConvertIntToString(offspringRiskWeight) + + return offspringRiskWeightString + } + + offspringRiskWeightText := getOffspringRiskWeightText() + + getOffspringOddsRatioText := func()string{ + if (offspringOddsRatioKnown == false){ + result := translate("Unknown") + return result + } + return offspringOddsRatioFormatted + } + + offspringOddsRatioText := getOffspringOddsRatioText() + + viewGenomePairInfoButton := widget.NewButtonWithIcon("", theme.InfoIcon(), func(){ + setViewCoupleGeneticAnalysisPolygenicDiseaseGenomePairDetailsPage(window, personAName, personBName, personAAnalysisMapList, personBAnalysisMapList, coupleAnalysisMapList, diseaseName, genomePairIdentifier, genomePairName, currentPage) + }) + + genomePairNameLabel := getBoldLabelCentered(genomePairName) + + offspringRiskWeightLabel := getBoldLabelCentered(offspringRiskWeightText) + offspringOddsRatioLabel := getBoldLabelCentered(offspringOddsRatioText) + + viewGenomePairInfoButtonsColumn.Add(viewGenomePairInfoButton) + genomePairNameColumn.Add(genomePairNameLabel) + offspringRiskWeightColumn.Add(offspringRiskWeightLabel) + offspringOddsRatioColumn.Add(offspringOddsRatioLabel) + + viewGenomePairInfoButtonsColumn.Add(widget.NewSeparator()) + genomePairNameColumn.Add(widget.NewSeparator()) + offspringRiskWeightColumn.Add(widget.NewSeparator()) + offspringOddsRatioColumn.Add(widget.NewSeparator()) + + return nil + } + + pair1PersonAGenomeIdentifier, pair1PersonBGenomeIdentifier, secondGenomePairExists, pair2PersonAGenomeIdentifier, pair2PersonBGenomeIdentifier, _, _, _, _, _, _, err := readGeneticAnalysis.GetMetadataFromCoupleGeneticAnalysis(coupleAnalysisMapList) + if (err != nil){ return nil, err } + + genomePair1Identifier := pair1PersonAGenomeIdentifier + "+" + pair1PersonBGenomeIdentifier + + err = addGenomePairRow("Pair 1", genomePair1Identifier) + if (err != nil) { return nil, err } + + if (secondGenomePairExists == true){ + + genomePair2Identifier := pair2PersonAGenomeIdentifier + "+" + pair2PersonBGenomeIdentifier + + err := addGenomePairRow("Pair 2", genomePair2Identifier) + if (err != nil) { return nil, err } + } + + offspringRiskWeightHelpButton := widget.NewButtonWithIcon("", theme.QuestionIcon(), func(){ + setOffspringPolygenicDiseaseLocusRiskWeightExplainerPage(window, currentPage) + }) + + offspringOddsRatioHelpButton := widget.NewButtonWithIcon("", theme.QuestionIcon(), func(){ + //TODO + showUnderConstructionDialog(window) + }) + + offspringRiskWeightColumn.Add(offspringRiskWeightHelpButton) + offspringOddsRatioColumn.Add(offspringOddsRatioHelpButton) + + genomesContainer := container.NewHBox(layout.NewSpacer(), viewGenomePairInfoButtonsColumn, genomePairNameColumn, offspringRiskWeightColumn, offspringOddsRatioColumn, layout.NewSpacer()) + + return genomesContainer, nil + } + + genomePairsLocusInfoGrid, err := getGenomePairsLocusInfoGrid() + if (err != nil){ + setErrorEncounteredPage(window, err, previousPage) + return + } + + page := container.NewVBox(title, backButton, widget.NewSeparator(), description, widget.NewSeparator(), locusNameRow, widget.NewSeparator(), genomePairsLocusInfoGrid) + + setPageContent(page, window) +} + + +func setViewCoupleGeneticAnalysisTraitsPage(window fyne.Window, personAName string, personBName string, personAAnalysisMapList []map[string]string, personBAnalysisMapList []map[string]string, coupleAnalysisMapList []map[string]string, previousPage func()){ + + currentPage := func(){setViewCoupleGeneticAnalysisTraitsPage(window, personAName, personBName, personAAnalysisMapList, personBAnalysisMapList, coupleAnalysisMapList, previousPage)} + + title := getPageTitleCentered("Viewing Genetic Analysis - Traits") + + backButton := getBackButtonCentered(previousPage) + + description := getLabelCentered("Below is an analysis of the average trait scores for the couple's offspring.") + + getTraitsGrid := func()(*fyne.Container, error){ + + pair1PersonAGenomeIdentifier, pair1PersonBGenomeIdentifier, secondGenomePairExists, _, _, _, _, _, _, _, _, err := readGeneticAnalysis.GetMetadataFromCoupleGeneticAnalysis(coupleAnalysisMapList) + if (err != nil){ return nil, err } + + traitNameLabel := getItalicLabelCentered("Trait Name") + + offspringOutcomeScoresLabel := getItalicLabelCentered("Offspring Outcome Scores") + + conflictExistsLabel := getItalicLabelCentered("Conflict Exists?") + + emptyLabel := widget.NewLabel("") + + traitNameColumn := container.NewVBox(traitNameLabel, widget.NewSeparator()) + offspringOutcomeScoresColumn := container.NewVBox(offspringOutcomeScoresLabel, widget.NewSeparator()) + conflictExistsColumn := container.NewVBox(conflictExistsLabel, widget.NewSeparator()) + viewButtonsColumn := container.NewVBox(emptyLabel, widget.NewSeparator()) + + traitObjectsList, err := traits.GetTraitObjectsList() + if (err != nil) { return nil, err } + + for _, traitObject := range traitObjectsList{ + + traitName := traitObject.TraitName + + traitRulesList := traitObject.RulesList + + if (len(traitRulesList) == 0){ + // This trait does not have any rules + // We cannot analyze it yet + // We will add neural network prediction so we can predict these traits + continue + } + + mainGenomePairIdentifier := pair1PersonAGenomeIdentifier + "+" + pair1PersonBGenomeIdentifier + + offspringOutcomeScoresKnown, offspringAverageOutcomeScoresMap, _, conflictExists, err := readGeneticAnalysis.GetOffspringTraitInfoFromGeneticAnalysis(coupleAnalysisMapList, traitName, mainGenomePairIdentifier, secondGenomePairExists) + if (err != nil) { return nil, err } + + // We add all of the columns except for the trait outcomes column, which may be multiple rows high + + traitNameText := getBoldLabelCentered(traitName) + + conflictExistsString := helpers.ConvertBoolToYesOrNoString(conflictExists) + conflictExistsLabel := getBoldLabelCentered(conflictExistsString) + + viewDetailsButton := getWidgetCentered(widget.NewButtonWithIcon("", theme.VisibilityIcon(), func(){ + setViewCoupleGeneticAnalysisTraitDetailsPage(window, personAName, personBName, personAAnalysisMapList, personBAnalysisMapList, coupleAnalysisMapList, traitName, previousPage) + })) + + traitNameColumn.Add(traitNameText) + conflictExistsColumn.Add(conflictExistsLabel) + viewButtonsColumn.Add(viewDetailsButton) + + if (offspringOutcomeScoresKnown == false){ + + unknownTranslated := translate("Unknown") + unknownLabel := getBoldLabelCentered(unknownTranslated) + + offspringOutcomeScoresColumn.Add(unknownLabel) + } else { + + outcomeNamesList := helpers.GetListOfMapKeys(offspringAverageOutcomeScoresMap) + + // We have to sort outcome names so they always show up in the same order + helpers.SortStringListToUnicodeOrder(outcomeNamesList) + + for index, outcomeName := range outcomeNamesList{ + + outcomeScore, exists := offspringAverageOutcomeScoresMap[outcomeName] + if (exists == false){ + return nil, errors.New("Outcome name not found in outcomeScoresMap after being found already.") + } + + outcomeScoreString := helpers.ConvertFloat64ToStringRounded(outcomeScore, 2) + + outcomeRow := getBoldLabelCentered(outcomeName + ": " + outcomeScoreString) + offspringOutcomeScoresColumn.Add(outcomeRow) + + if (index > 0){ + + emptyLabelA := widget.NewLabel("") + emptyLabelB := widget.NewLabel("") + emptyLabelC := widget.NewLabel("") + + traitNameColumn.Add(emptyLabelA) + conflictExistsColumn.Add(emptyLabelB) + viewButtonsColumn.Add(emptyLabelC) + } + } + } + + traitNameColumn.Add(widget.NewSeparator()) + offspringOutcomeScoresColumn.Add(widget.NewSeparator()) + conflictExistsColumn.Add(widget.NewSeparator()) + viewButtonsColumn.Add(widget.NewSeparator()) + } + + offspringOutcomeScoresHelpButton := widget.NewButtonWithIcon("", theme.QuestionIcon(), func(){ + setOffspringTraitOutcomeScoresExplainerPage(window, currentPage) + }) + offspringOutcomeScoresColumn.Add(offspringOutcomeScoresHelpButton) + + traitsGrid := container.NewHBox(layout.NewSpacer(), traitNameColumn, offspringOutcomeScoresColumn) + + if (secondGenomePairExists == true){ + + conflictExistsHelpButton := widget.NewButtonWithIcon("", theme.QuestionIcon(), func(){ + setCoupleGeneticAnalysisConflictExistsExplainerPage(window, currentPage) + }) + conflictExistsColumn.Add(conflictExistsHelpButton) + + traitsGrid.Add(conflictExistsColumn) + } + + traitsGrid.Add(viewButtonsColumn) + traitsGrid.Add(layout.NewSpacer()) + + return traitsGrid, nil + } + + traitsGrid, err := getTraitsGrid() + if (err != nil){ + setErrorEncounteredPage(window, err, previousPage) + return + } + + page := container.NewVBox(title, backButton, widget.NewSeparator(), description, widget.NewSeparator(), traitsGrid) + + setPageContent(page, window) +} + + + +func setViewCoupleGeneticAnalysisTraitDetailsPage(window fyne.Window, personAName string, personBName string, personAAnalysisMapList []map[string]string, personBAnalysisMapList []map[string]string, coupleAnalysisMapList []map[string]string, traitName string, previousPage func()){ + + currentPage := func(){setViewCoupleGeneticAnalysisTraitDetailsPage(window, personAName, personBName, personAAnalysisMapList, personBAnalysisMapList, coupleAnalysisMapList, traitName, previousPage)} + + title := getPageTitleCentered("Viewing Couple Analysis - " + traitName) + + backButton := getBackButtonCentered(previousPage) + + pair1PersonAGenomeIdentifier, pair1PersonBGenomeIdentifier, secondGenomePairExists, pair2PersonAGenomeIdentifier, pair2PersonBGenomeIdentifier, _, _, _, _, _, _, err := readGeneticAnalysis.GetMetadataFromCoupleGeneticAnalysis(coupleAnalysisMapList) + if (err != nil){ + setErrorEncounteredPage(window, err, previousPage) + return + } + + getDescriptionSection := func()*fyne.Container{ + + if (secondGenomePairExists == false){ + description := getLabelCentered("Below is the trait analysis for the couple's offspring.") + + return description + } + + description1 := getLabelCentered("Below is the trait analysis for the couple's offspring.") + description2 := getLabelCentered("Each genome pair combines different genomes from each person.") + + descriptionsSection := container.NewVBox(description1, description2) + + return descriptionsSection + } + + descriptionSection := getDescriptionSection() + + traitNameLabel := widget.NewLabel("Trait:") + traitNameText := getBoldLabel(traitName) + traitNameInfoButton := widget.NewButtonWithIcon("", theme.InfoIcon(), func(){ + setViewTraitDetailsPage(window, traitName, currentPage) + }) + traitNameRow := container.NewHBox(layout.NewSpacer(), traitNameLabel, traitNameText, traitNameInfoButton, layout.NewSpacer()) + + emptyLabelA := widget.NewLabel("") + emptyLabelB := widget.NewLabel("") + + offspringOutcomeScoresLabel := getItalicLabelCentered("Offspring Outcome Scores") + + emptyLabelC := widget.NewLabel("") + + viewGenomePairButtonsColumn := container.NewVBox(emptyLabelA, widget.NewSeparator()) + pairNameColumn := container.NewVBox(emptyLabelB, widget.NewSeparator()) + offspringOutcomeScoresColumn := container.NewVBox(offspringOutcomeScoresLabel, widget.NewSeparator()) + viewOffspringRulesButtonsColumn := container.NewVBox(emptyLabelC, widget.NewSeparator()) + + addGenomePairRow := func(genomePairName string, personAGenomeIdentifier string, personBGenomeIdentifier string)error{ + + genomePairIdentifier := personAGenomeIdentifier + "+" + personBGenomeIdentifier + + genomePairNameLabel := getBoldLabelCentered(genomePairName) + + viewGenomePairButton := widget.NewButtonWithIcon("", theme.InfoIcon(), func(){ + setViewCoupleGeneticAnalysisTraitGenomePairDetailsPage(window, personAName, personBName, personAAnalysisMapList, personBAnalysisMapList, coupleAnalysisMapList, traitName, genomePairIdentifier, genomePairName, currentPage) + }) + + + viewOffspringRulesButton := widget.NewButtonWithIcon("", theme.VisibilityIcon(), func(){ + setViewCoupleTraitRulesPage(window, personAName, personBName, personAAnalysisMapList, personBAnalysisMapList, coupleAnalysisMapList, traitName, genomePairIdentifier, genomePairName, currentPage) + }) + + // We add all of the columns except for the trait rule column, which may be multiple rows high + + viewGenomePairButtonsColumn.Add(viewGenomePairButton) + pairNameColumn.Add(genomePairNameLabel) + viewOffspringRulesButtonsColumn.Add(viewOffspringRulesButton) + + offspringOutcomeScoresKnown, offspringOutcomeScoresMap, _, _, err := readGeneticAnalysis.GetOffspringTraitInfoFromGeneticAnalysis(coupleAnalysisMapList, traitName, genomePairIdentifier, secondGenomePairExists) + if (err != nil) { return err } + if (offspringOutcomeScoresKnown == false){ + + unknownTranslated := translate("Unknown") + unknownLabel := getBoldLabelCentered(unknownTranslated) + + offspringOutcomeScoresColumn.Add(unknownLabel) + + } else { + + outcomeNamesList := helpers.GetListOfMapKeys(offspringOutcomeScoresMap) + + // We have to sort the outcome names so they always show up in the same order + helpers.SortStringListToUnicodeOrder(outcomeNamesList) + + for index, outcomeName := range outcomeNamesList{ + + outcomeScore, exists := offspringOutcomeScoresMap[outcomeName] + if (exists == false){ + return errors.New("Outcome name not found in outcome scores map after being found already.") + } + + outcomeScoreString := helpers.ConvertFloat64ToStringRounded(outcomeScore, 2) + + outcomeRow := getBoldLabelCentered(outcomeName + ": " + outcomeScoreString) + offspringOutcomeScoresColumn.Add(outcomeRow) + + if (index > 0){ + + emptyLabelA := widget.NewLabel("") + emptyLabelB := widget.NewLabel("") + emptyLabelC := widget.NewLabel("") + + pairNameColumn.Add(emptyLabelA) + viewGenomePairButtonsColumn.Add(emptyLabelB) + viewOffspringRulesButtonsColumn.Add(emptyLabelC) + } + } + } + + viewGenomePairButtonsColumn.Add(widget.NewSeparator()) + pairNameColumn.Add(widget.NewSeparator()) + offspringOutcomeScoresColumn.Add(widget.NewSeparator()) + viewOffspringRulesButtonsColumn.Add(widget.NewSeparator()) + + return nil + } + + err = addGenomePairRow("Pair 1", pair1PersonAGenomeIdentifier, pair1PersonBGenomeIdentifier) + if (err != nil) { + setErrorEncounteredPage(window, err, previousPage) + return + } + + if (secondGenomePairExists == true){ + err := addGenomePairRow("Pair 2", pair2PersonAGenomeIdentifier, pair2PersonBGenomeIdentifier) + if (err != nil) { + setErrorEncounteredPage(window, err, previousPage) + return + } + } + + offspringOutcomeScoresHelpButton := widget.NewButtonWithIcon("", theme.QuestionIcon(), func(){ + setOffspringTraitOutcomeScoresExplainerPage(window, currentPage) + }) + + offspringOutcomeScoresColumn.Add(offspringOutcomeScoresHelpButton) + + genomesContainer := container.NewHBox(layout.NewSpacer(), viewGenomePairButtonsColumn, pairNameColumn, offspringOutcomeScoresColumn, viewOffspringRulesButtonsColumn, layout.NewSpacer()) + + page := container.NewVBox(title, backButton, widget.NewSeparator(), descriptionSection, widget.NewSeparator(), traitNameRow, widget.NewSeparator(), genomesContainer) + + setPageContent(page, window) +} + + +func setViewCoupleGeneticAnalysisTraitGenomePairDetailsPage(window fyne.Window, personAName string, personBName string, personAAnalysisMapList []map[string]string, personBAnalysisMapList []map[string]string, coupleAnalysisMapList []map[string]string, traitName string, genomePairIdentifier string, genomePairName string, previousPage func()){ + + currentPage := func(){setViewCoupleGeneticAnalysisTraitGenomePairDetailsPage(window, personAName, personBName, personAAnalysisMapList, personBAnalysisMapList, coupleAnalysisMapList, traitName, genomePairIdentifier, genomePairName, previousPage)} + + title := getPageTitleCentered("Viewing Couple Genome Pair Info") + + backButton := getBackButtonCentered(previousPage) + + description := getLabelCentered("Below is the trait information for both genomes in the genome pair.") + + traitNameLabel := widget.NewLabel("Trait:") + traitNameText := getBoldLabel(traitName) + traitNameInfoButton := widget.NewButtonWithIcon("", theme.InfoIcon(), func(){ + setViewTraitDetailsPage(window, traitName, currentPage) + }) + traitNameRow := container.NewHBox(layout.NewSpacer(), traitNameLabel, traitNameText, traitNameInfoButton, layout.NewSpacer()) + + genomePairLabel := widget.NewLabel("Genome Pair:") + genomePairNameLabel := getBoldLabel(genomePairName) + genomePairHelpButton := widget.NewButtonWithIcon("", theme.QuestionIcon(), func(){ + setCoupleGenomePairExplainerPage(window, currentPage) + }) + genomePairRow := container.NewHBox(layout.NewSpacer(), genomePairLabel, genomePairNameLabel, genomePairHelpButton, layout.NewSpacer()) + + emptyLabelA := widget.NewLabel("") + personNameLabel := getItalicLabelCentered("Person Name") + + emptyLabelB := widget.NewLabel("") + genomeNameLabel := getItalicLabelCentered("Genome Name") + + emptyLabelC := widget.NewLabel("") + outcomeScoresLabel := getItalicLabelCentered("Outcome Scores") + + numberOfLabel := getItalicLabelCentered("Number Of") + rulesTestedLabel := getItalicLabelCentered("Rules Tested") + + emptyLabelD := widget.NewLabel("") + emptyLabelE := widget.NewLabel("") + + personNameColumn := container.NewVBox(emptyLabelA, personNameLabel, widget.NewSeparator()) + genomeNameColumn := container.NewVBox(emptyLabelB, genomeNameLabel, widget.NewSeparator()) + outcomeScoresColumn := container.NewVBox(emptyLabelC, outcomeScoresLabel, widget.NewSeparator()) + numberOfRulesTestedColumn := container.NewVBox(numberOfLabel, rulesTestedLabel, widget.NewSeparator()) + viewGenomeButtonsColumn := container.NewVBox(emptyLabelD, emptyLabelE, widget.NewSeparator()) + + addGenomeRow := func(isPersonA bool, personName string, inputGenomeIdentifier string)error{ + + personAnalysisGenomeIdentifier, personHasMultipleGenomes, genomeIsCombined, combinedType, err := readGeneticAnalysis.GetMatchingPersonAnalysisGenomeIdentifierFromCoupleAnalysis(isPersonA, personAAnalysisMapList, personBAnalysisMapList, coupleAnalysisMapList, inputGenomeIdentifier) + if (err != nil) { return err } + + personNameTrimmed, _, err := helpers.TrimAndFlattenString(personName, 10) + if (err != nil) { return err } + + personNameLabel := getBoldLabelCentered(personNameTrimmed) + + getPersonAnalysisMapList := func()[]map[string]string{ + if (isPersonA == true){ + return personAAnalysisMapList + } + return personBAnalysisMapList + } + + personAnalysisMapList := getPersonAnalysisMapList() + + getGenomeName := func()(string, error){ + + if (genomeIsCombined == false){ + + genomeFound, _, _, _, _, _, companyName, _, _, err := myGenomes.GetMyRawGenomeMetadata(personAnalysisGenomeIdentifier) + if (err != nil) { return "", err } + if (genomeFound == false){ + return "", errors.New("MyGenomeInfo for genome from analysisMapList not found.") + } + + return companyName, nil + } + + return combinedType, nil + } + + genomeName, err := getGenomeName() + if (err != nil) { return err } + + genomeNameLabel := getBoldLabelCentered(genomeName) + + // We add all of the columns except for the trait rule column, which may be multiple rows high + + _, anyTraitRuleTested, outcomeScoresMap, numberOfRulesTested, _, err := readGeneticAnalysis.GetPersonTraitInfoFromGeneticAnalysis(personAnalysisMapList, traitName, personAnalysisGenomeIdentifier, personHasMultipleGenomes) + if (err != nil) { return err } + + numberOfRulesTestedString := helpers.ConvertIntToString(numberOfRulesTested) + numberOfRulesTestedText := getBoldLabelCentered(numberOfRulesTestedString) + + viewGenomeButton := widget.NewButtonWithIcon("", theme.VisibilityIcon(), func(){ + setViewPersonGenomeTraitRulesPage(window, personAnalysisMapList, traitName, personAnalysisGenomeIdentifier, genomeName, currentPage) + }) + + personNameColumn.Add(personNameLabel) + genomeNameColumn.Add(genomeNameLabel) + numberOfRulesTestedColumn.Add(numberOfRulesTestedText) + viewGenomeButtonsColumn.Add(viewGenomeButton) + + if (anyTraitRuleTested == false){ + + unknownTranslated := translate("Unknown") + unknownLabel := getBoldLabelCentered(unknownTranslated) + + outcomeScoresColumn.Add(unknownLabel) + + } else { + + outcomeNamesList := helpers.GetListOfMapKeys(outcomeScoresMap) + + // We have to sort the outcome names so they always show up in the same order + helpers.SortStringListToUnicodeOrder(outcomeNamesList) + + for index, outcomeName := range outcomeNamesList{ + + outcomeScore, exists := outcomeScoresMap[outcomeName] + if (exists == false){ + return errors.New("Outcome name not found in outcome scores map after being found already.") + } + + outcomeScoreString := helpers.ConvertIntToString(outcomeScore) + + outcomeRow := getBoldLabelCentered(outcomeName + ": " + outcomeScoreString) + outcomeScoresColumn.Add(outcomeRow) + + if (index > 0){ + + emptyLabelA := widget.NewLabel("") + emptyLabelB := widget.NewLabel("") + emptyLabelC := widget.NewLabel("") + emptyLabelD := widget.NewLabel("") + + personNameColumn.Add(emptyLabelA) + genomeNameColumn.Add(emptyLabelB) + numberOfRulesTestedColumn.Add(emptyLabelC) + viewGenomeButtonsColumn.Add(emptyLabelD) + } + } + } + + personNameColumn.Add(widget.NewSeparator()) + genomeNameColumn.Add(widget.NewSeparator()) + outcomeScoresColumn.Add(widget.NewSeparator()) + numberOfRulesTestedColumn.Add(widget.NewSeparator()) + viewGenomeButtonsColumn.Add(widget.NewSeparator()) + + return nil + } + + personAGenomeIdentifier, personBGenomeIdentifier, found := strings.Cut(genomePairIdentifier, "+") + if (found == false){ + setErrorEncounteredPage(window, errors.New("setViewCoupleGeneticAnalysisTraitGenomePairDetailsPage called with invalid genomePairIdentifier"), previousPage) + return + } + + err := addGenomeRow(true, personAName, personAGenomeIdentifier) + if (err != nil){ + setErrorEncounteredPage(window, err, previousPage) + return + } + err = addGenomeRow(false, personBName, personBGenomeIdentifier) + if (err != nil){ + setErrorEncounteredPage(window, err, previousPage) + return + } + + outcomeScoresHelpButton := widget.NewButtonWithIcon("", theme.QuestionIcon(), func(){ + setOffspringTraitOutcomeScoresExplainerPage(window, currentPage) + }) + + outcomeScoresColumn.Add(outcomeScoresHelpButton) + + numberOfRulesTestedHelpButton := widget.NewButtonWithIcon("", theme.QuestionIcon(), func(){ + setOffspringTraitNumberOfRulesTestedExplainerPage(window, currentPage) + }) + + numberOfRulesTestedColumn.Add(numberOfRulesTestedHelpButton) + + genomesGrid := container.NewHBox(layout.NewSpacer(), personNameColumn, genomeNameColumn, outcomeScoresColumn, numberOfRulesTestedColumn, viewGenomeButtonsColumn, layout.NewSpacer()) + + page := container.NewVBox(title, backButton, widget.NewSeparator(), description, traitNameRow, genomePairRow, widget.NewSeparator(), genomesGrid) + + setPageContent(page, window) +} + + + +// This function provides a page to view the couple offspring rule probabilities for a particular genome pair +func setViewCoupleTraitRulesPage(window fyne.Window, personAName string, personBName string, personAAnalysisMapList []map[string]string, personBAnalysisMapList []map[string]string, coupleAnalysisMapList []map[string]string, traitName string, genomePairIdentifier string, genomePairName string, previousPage func()){ + + setLoadingScreen(window, "Loading Trait Rules", "Loading trait rules...") + + currentPage := func(){setViewCoupleTraitRulesPage(window, personAName, personBName, personAAnalysisMapList, personBAnalysisMapList, coupleAnalysisMapList, traitName, genomePairIdentifier, genomePairName, previousPage)} + + title := getPageTitleCentered("View Offspring Trait Rules - " + traitName) + + backButton := getBackButtonCentered(previousPage) + + description1 := widget.NewLabel("Below are the trait rule probabilities for offspring from this genome pair.") + traitRulesHelpButton := widget.NewButtonWithIcon("", theme.QuestionIcon(), func(){ + setOffspringTraitRulesExplainerPage(window, currentPage) + }) + description1Row := container.NewHBox(layout.NewSpacer(), description1, traitRulesHelpButton, layout.NewSpacer()) + + genomePairLabel := widget.NewLabel("Genome Pair:") + genomePairNameLabel := getBoldLabel(genomePairName) + viewGenomePairInfoButton := widget.NewButtonWithIcon("", theme.InfoIcon(), func(){ + setViewCoupleGeneticAnalysisTraitGenomePairDetailsPage(window, personAName, personBName, personAAnalysisMapList, personBAnalysisMapList, coupleAnalysisMapList, traitName, genomePairIdentifier, genomePairName, currentPage) + }) + + genomePairRow := container.NewHBox(layout.NewSpacer(), genomePairLabel, genomePairNameLabel, viewGenomePairInfoButton, layout.NewSpacer()) + + _, _, secondGenomePairExists, _, _, _, _, _, _, _, _, err := readGeneticAnalysis.GetMetadataFromCoupleGeneticAnalysis(coupleAnalysisMapList) + if (err != nil){ + setErrorEncounteredPage(window, err, previousPage) + return + } + + _, _, numberOfRulesTested, _, err := readGeneticAnalysis.GetOffspringTraitInfoFromGeneticAnalysis(coupleAnalysisMapList, traitName, genomePairIdentifier, secondGenomePairExists) + if (err != nil){ + setErrorEncounteredPage(window, err, previousPage) + return + } + + numberOfRulesTestedString := helpers.ConvertIntToString(numberOfRulesTested) + + traitRulesMap, err := traits.GetTraitRulesMap(traitName) + if (err != nil){ + setErrorEncounteredPage(window, err, previousPage) + return + } + + totalNumberOfRules := len(traitRulesMap) + totalNumberOfRulesString := helpers.ConvertIntToString(totalNumberOfRules) + + rulesTestedLabel := widget.NewLabel("Rules Tested:") + rulesTestedText := getBoldLabel(numberOfRulesTestedString + "/" + totalNumberOfRulesString) + rulesTestedHelpButton := widget.NewButtonWithIcon("", theme.QuestionIcon(), func(){ + setOffspringTraitNumberOfRulesTestedExplainerPage(window, currentPage) + }) + + rulesTestedRow := container.NewHBox(layout.NewSpacer(), rulesTestedLabel, rulesTestedText, rulesTestedHelpButton, layout.NewSpacer()) + + getRulesGrid := func()(*fyne.Container, error){ + + emptyLabelA := widget.NewLabel("") + ruleIdentifierLabel := getItalicLabelCentered("Rule Identifier") + + emptyLabelB := widget.NewLabel("") + ruleEffectsLabel := getItalicLabelCentered("Rule Effects") + + offspringProbabilityOfLabel := getItalicLabelCentered("Offspring Probability Of") + passingRuleLabel := getItalicLabelCentered("Passing Rule") + + emptyLabelC := widget.NewLabel("") + emptyLabelD := widget.NewLabel("") + + ruleIdentifierColumn := container.NewVBox(emptyLabelA, ruleIdentifierLabel, widget.NewSeparator()) + ruleEffectsColumn := container.NewVBox(emptyLabelB, ruleEffectsLabel, widget.NewSeparator()) + offspringProbabilityOfPassingRuleColumn := container.NewVBox(offspringProbabilityOfLabel, passingRuleLabel, widget.NewSeparator()) + ruleInfoButtonsColumn := container.NewVBox(emptyLabelC, emptyLabelD, widget.NewSeparator()) + + addRuleRow := func(ruleIdentifier string)error{ + + ruleIdentifierLabel := getBoldLabelCentered(ruleIdentifier) + + offspringRuleProbabilityKnown, _, offspringProbabilityOfPassingRuleFormatted, err := readGeneticAnalysis.GetOffspringTraitRuleInfoFromGeneticAnalysis(coupleAnalysisMapList, traitName, ruleIdentifier, genomePairIdentifier) + if (err != nil) { return err } + + viewRuleDetailsButton := widget.NewButtonWithIcon("", theme.VisibilityIcon(), func(){ + setViewCoupleGeneticAnalysisTraitRuleDetailsPage(window, personAName, personBName, personAAnalysisMapList, personBAnalysisMapList, coupleAnalysisMapList, traitName, ruleIdentifier, currentPage) + }) + + getProbabilityOfPassingRuleText := func()string{ + if (offspringRuleProbabilityKnown == false){ + + result := translate("Unknown") + return result + } + return offspringProbabilityOfPassingRuleFormatted + } + + probabilityOfPassingRuleText := getProbabilityOfPassingRuleText() + probabilityOfPassingRuleTextLabel := getBoldLabelCentered(probabilityOfPassingRuleText) + + // We add all of the columns except for the rule effects column + // We do this because the rule effects column may be multiple rows tall + + ruleIdentifierColumn.Add(ruleIdentifierLabel) + offspringProbabilityOfPassingRuleColumn.Add(probabilityOfPassingRuleTextLabel) + ruleInfoButtonsColumn.Add(viewRuleDetailsButton) + + traitRuleObject, exists := traitRulesMap[ruleIdentifier] + if (exists == false){ + return errors.New("Trait rule not found after being found already: " + ruleIdentifier) + } + + ruleOutcomePointsMap := traitRuleObject.OutcomePointsMap + + outcomeNamesList := helpers.GetListOfMapKeys(ruleOutcomePointsMap) + + // We have to sort the outcome names so they always show up in the same order + helpers.SortStringListToUnicodeOrder(outcomeNamesList) + + for index, outcomeName := range outcomeNamesList{ + + outcomeChange, exists := ruleOutcomePointsMap[outcomeName] + if (exists == false){ + return errors.New("OutcomeName not found in ruleOutcomePointsMap after being found already: " + outcomeName) + } + + getOutcomeEffectString := func()string{ + + outcomeChangeString := helpers.ConvertIntToString(outcomeChange) + + if (outcomeChange < 0){ + return outcomeChangeString + } + outcomeEffect := "+" + outcomeChangeString + return outcomeEffect + } + + outcomeEffect := getOutcomeEffectString() + + outcomeRow := getBoldLabelCentered(outcomeName + ": " + outcomeEffect) + ruleEffectsColumn.Add(outcomeRow) + + if (index > 0){ + + emptyLabelA := widget.NewLabel("") + emptyLabelB := widget.NewLabel("") + emptyLabelC := widget.NewLabel("") + + ruleIdentifierColumn.Add(emptyLabelA) + offspringProbabilityOfPassingRuleColumn.Add(emptyLabelB) + ruleInfoButtonsColumn.Add(emptyLabelC) + } + } + + ruleIdentifierColumn.Add(widget.NewSeparator()) + ruleEffectsColumn.Add(widget.NewSeparator()) + offspringProbabilityOfPassingRuleColumn.Add(widget.NewSeparator()) + ruleInfoButtonsColumn.Add(widget.NewSeparator()) + + return nil + } + + rulesWithKnownProbabilityList := make([]string, 0) + rulesWithUnknownProbabilityList := make([]string, 0) + + for ruleIdentifier, _ := range traitRulesMap{ + + offspringRuleProbabilityKnown, _, _, err := readGeneticAnalysis.GetOffspringTraitRuleInfoFromGeneticAnalysis(coupleAnalysisMapList, traitName, ruleIdentifier, genomePairIdentifier) + if (err != nil) { return nil, err } + if (offspringRuleProbabilityKnown == true){ + rulesWithKnownProbabilityList = append(rulesWithKnownProbabilityList, ruleIdentifier) + } else { + rulesWithUnknownProbabilityList = append(rulesWithUnknownProbabilityList, ruleIdentifier) + } + } + + // Now we sort rules so they show up in the same order each time + + helpers.SortStringListToUnicodeOrder(rulesWithKnownProbabilityList) + helpers.SortStringListToUnicodeOrder(rulesWithUnknownProbabilityList) + + for _, ruleIdentifier := range rulesWithKnownProbabilityList{ + + err = addRuleRow(ruleIdentifier) + if (err != nil) { return nil, err } + } + + for _, ruleIdentifier := range rulesWithUnknownProbabilityList{ + + err = addRuleRow(ruleIdentifier) + if (err != nil) { return nil, err } + } + + ruleEffectsHelpButton := widget.NewButtonWithIcon("", theme.QuestionIcon(), func(){ + setTraitRuleOutcomeEffectsExplainerPage(window, currentPage) + }) + ruleEffectsColumn.Add(ruleEffectsHelpButton) + + offspringProbabilityOfPassingRuleHelpButton := widget.NewButtonWithIcon("", theme.QuestionIcon(), func(){ + setOffspringProbabilityOfPassingTraitRuleExplainerPage(window, currentPage) + }) + offspringProbabilityOfPassingRuleColumn.Add(offspringProbabilityOfPassingRuleHelpButton) + + rulesGrid := container.NewHBox(layout.NewSpacer(), ruleIdentifierColumn, ruleEffectsColumn, offspringProbabilityOfPassingRuleColumn, ruleInfoButtonsColumn, layout.NewSpacer()) + + return rulesGrid, nil + } + + rulesGrid, err := getRulesGrid() + if (err != nil){ + setErrorEncounteredPage(window, err, previousPage) + return + } + + page := container.NewVBox(title, backButton, widget.NewSeparator(), description1Row, widget.NewSeparator(), genomePairRow, widget.NewSeparator(), rulesTestedRow, widget.NewSeparator(), rulesGrid) + + setPageContent(page, window) +} + + + +// This function implements a page to view the details of a specific rule from a genetic analysis +// It will show the rule details for all of the couple's genome pairs +func setViewCoupleGeneticAnalysisTraitRuleDetailsPage(window fyne.Window, personAName string, personBName string, personAAnalysisMapList []map[string]string, personBAnalysisMapList []map[string]string, coupleAnalysisMapList []map[string]string, traitName string, ruleIdentifier string, previousPage func()){ + + currentPage := func(){setViewCoupleGeneticAnalysisTraitRuleDetailsPage(window, personAName, personBName, personAAnalysisMapList, personBAnalysisMapList, coupleAnalysisMapList, traitName, ruleIdentifier, previousPage)} + + title := getPageTitleCentered("Trait Rule Details - " + traitName) + + backButton := getBackButtonCentered(previousPage) + + description := getLabelCentered("Below is the trait rule analysis for the couple.") + + ruleIdentifierLabel := widget.NewLabel("Rule Identifier:") + ruleIdentifierText := getBoldLabel(ruleIdentifier) + ruleInfoButton := widget.NewButtonWithIcon("", theme.InfoIcon(), func(){ + setViewTraitRuleDetailsPage(window, traitName, ruleIdentifier, currentPage) + }) + ruleIdentifierRow := container.NewHBox(layout.NewSpacer(), ruleIdentifierLabel, ruleIdentifierText, ruleInfoButton, layout.NewSpacer()) + + getGenomePairsRuleInfoGrid := func()(*fyne.Container, error){ + + emptyLabelA := widget.NewLabel("") + emptyLabelB := widget.NewLabel("") + + emptyLabelC := widget.NewLabel("") + genomePairLabel := getItalicLabelCentered("Genome Pair") + + offspringProbabilityOfLabel := getItalicLabelCentered("Offspring Probability Of") + passingRuleLabel := getItalicLabelCentered("Passing Rule") + + viewGenomePairInfoButtonsColumn := container.NewVBox(emptyLabelA, emptyLabelB, widget.NewSeparator()) + genomePairNameColumn := container.NewVBox(emptyLabelC, genomePairLabel, widget.NewSeparator()) + offspringProbabilityOfPassingRuleColumn := container.NewVBox(offspringProbabilityOfLabel, passingRuleLabel, widget.NewSeparator()) + + addGenomePairRow := func(genomePairName string, genomePairIdentifier string)error{ + + offspringRuleProbabilityKnown, _, offspringProbabilityOfPassingRuleFormatted, err := readGeneticAnalysis.GetOffspringTraitRuleInfoFromGeneticAnalysis(coupleAnalysisMapList, traitName, ruleIdentifier, genomePairIdentifier) + if (err != nil) { return err } + + getOffspringProbabilityOfPassingRuleText := func()string{ + + if (offspringRuleProbabilityKnown == false){ + result := translate("Unknown") + return result + } + + return offspringProbabilityOfPassingRuleFormatted + } + + offspringProbabilityOfPassingRuleText := getOffspringProbabilityOfPassingRuleText() + + viewGenomePairInfoButton := widget.NewButtonWithIcon("", theme.InfoIcon(), func(){ + setViewCoupleGeneticAnalysisTraitGenomePairDetailsPage(window, personAName, personBName, personAAnalysisMapList, personBAnalysisMapList, coupleAnalysisMapList, traitName, genomePairIdentifier, genomePairName, currentPage) + }) + + genomePairNameLabel := getBoldLabelCentered(genomePairName) + + offspringProbabilityOfPassingRuleLabel := getBoldLabelCentered(offspringProbabilityOfPassingRuleText) + + viewGenomePairInfoButtonsColumn.Add(viewGenomePairInfoButton) + genomePairNameColumn.Add(genomePairNameLabel) + offspringProbabilityOfPassingRuleColumn.Add(offspringProbabilityOfPassingRuleLabel) + + viewGenomePairInfoButtonsColumn.Add(widget.NewSeparator()) + genomePairNameColumn.Add(widget.NewSeparator()) + offspringProbabilityOfPassingRuleColumn.Add(widget.NewSeparator()) + + return nil + } + + pair1PersonAGenomeIdentifier, pair1PersonBGenomeIdentifier, secondGenomePairExists, pair2PersonAGenomeIdentifier, pair2PersonBGenomeIdentifier, _, _, _, _, _, _, err := readGeneticAnalysis.GetMetadataFromCoupleGeneticAnalysis(coupleAnalysisMapList) + if (err != nil){ return nil, err } + + genomePair1Identifier := pair1PersonAGenomeIdentifier + "+" + pair1PersonBGenomeIdentifier + + err = addGenomePairRow("Pair 1", genomePair1Identifier) + if (err != nil) { return nil, err } + + if (secondGenomePairExists == true){ + + genomePair2Identifier := pair2PersonAGenomeIdentifier + "+" + pair2PersonBGenomeIdentifier + + err := addGenomePairRow("Pair 2", genomePair2Identifier) + if (err != nil) { return nil, err } + } + + genomesContainer := container.NewHBox(layout.NewSpacer(), viewGenomePairInfoButtonsColumn, genomePairNameColumn, offspringProbabilityOfPassingRuleColumn, layout.NewSpacer()) + + return genomesContainer, nil + } + + genomePairsRuleInfoGrid, err := getGenomePairsRuleInfoGrid() + if (err != nil){ + setErrorEncounteredPage(window, err, previousPage) + return + } + + page := container.NewVBox(title, backButton, widget.NewSeparator(), description, widget.NewSeparator(), ruleIdentifierRow, widget.NewSeparator(), genomePairsRuleInfoGrid) + + setPageContent(page, window) +} + + + + diff --git a/gui/viewAnalysisGui_Person.go b/gui/viewAnalysisGui_Person.go new file mode 100644 index 0000000..4487d75 --- /dev/null +++ b/gui/viewAnalysisGui_Person.go @@ -0,0 +1,2444 @@ + +package gui + +// viewAnalysisGui_Person.go implements pages to view a person genetic analysis + +//TODO: Use person's sex to show opposite sex diseases and traits in different section +// For example, Ovarian Cancer for men, and Testicular Cancer for women +// We still want these risks/traits to be viewable, just put into a different part of the gui + +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/geneticReferences/monogenicDiseases" +import "seekia/resources/geneticReferences/polygenicDiseases" +import "seekia/resources/geneticReferences/traits" + +import "seekia/internal/appMemory" +import "seekia/internal/genetics/myGenomes" +import "seekia/internal/genetics/myPeople" +import "seekia/internal/genetics/readGeneticAnalysis" +import "seekia/internal/helpers" + +import "errors" + +func setViewPersonGeneticAnalysisPage(window fyne.Window, personIdentifier string, analysisMapList []map[string]string, numberOfGenomesAnalyzed int, previousPage func()){ + + appMemory.SetMemoryEntry("CurrentViewedPage", "ViewGeneticAnalysisPage") + + currentPage := func(){setViewPersonGeneticAnalysisPage(window, personIdentifier, analysisMapList, numberOfGenomesAnalyzed, previousPage)} + + title := getPageTitleCentered("Viewing Genetic Analysis") + + backButton := getBackButtonCentered(previousPage) + + warningLabel := getBoldLabelCentered("WARNING: Results are not accurate!") + + personFound, personName, _, _, err := myPeople.GetPersonInfo(personIdentifier) + if (err != nil){ + setErrorEncounteredPage(window, err, previousPage) + return + } + if (personFound == false){ + setErrorEncounteredPage(window, errors.New("setViewPersonGeneticAnalysisPage called with unknown personIdentifier: " + personIdentifier), previousPage) + return + } + + personNameLabel := widget.NewLabel("Person Name:") + personNameText := getBoldLabel(personName) + personNameRow := container.NewHBox(layout.NewSpacer(), personNameLabel, personNameText, layout.NewSpacer()) + + numberOfGenomesAnalyzedString := helpers.ConvertIntToString(numberOfGenomesAnalyzed) + + numberOfAnalyzedGenomesLabel := widget.NewLabel("Number of Analyzed Genomes:") + numberOfAnalyzedGenomesText := getBoldLabel(numberOfGenomesAnalyzedString) + numberOfAnalyzedGenomesRow := container.NewHBox(layout.NewSpacer(), numberOfAnalyzedGenomesLabel, numberOfAnalyzedGenomesText, layout.NewSpacer()) + + generalButton := widget.NewButton("General", func(){ + //TODO: Inbred rating (parent relatedness), ancestry + showUnderConstructionDialog(window) + }) + monogenicDiseasesButton := widget.NewButton("Monogenic Diseases", func(){ + setViewPersonGeneticAnalysisMonogenicDiseasesPage(window, analysisMapList, currentPage) + }) + polygenicDiseasesButton := widget.NewButton("Polygenic Diseases", func(){ + setViewPersonGeneticAnalysisPolygenicDiseasesPage(window, personIdentifier, analysisMapList, currentPage) + }) + traitsButton := widget.NewButton("Traits", func(){ + setViewPersonGeneticAnalysisTraitsPage(window, personIdentifier, analysisMapList, currentPage) + }) + + categoryButtonsGrid := getContainerCentered(container.NewGridWithColumns(1, generalButton, monogenicDiseasesButton, polygenicDiseasesButton, traitsButton)) + + page := container.NewVBox(title, backButton, widget.NewSeparator(), warningLabel, widget.NewSeparator(), personNameRow, numberOfAnalyzedGenomesRow, widget.NewSeparator(), categoryButtonsGrid) + + setPageContent(page, window) +} + + +func setViewPersonGeneticAnalysisMonogenicDiseasesPage(window fyne.Window, analysisMapList []map[string]string, previousPage func()){ + + currentPage := func(){setViewPersonGeneticAnalysisMonogenicDiseasesPage(window, analysisMapList, previousPage)} + + title := getPageTitleCentered("Viewing Genetic Analysis - Monogenic Diseases") + + backButton := getBackButtonCentered(previousPage) + + description := getLabelCentered("Below is an analysis of the monogenic diseases found in the person's genome.") + + getMonogenicDiseasesContainer := func()(*fyne.Container, error){ + + allRawGenomeIdentifiersList, multipleGenomesExist, onlyExcludeConflictsGenomeIdentifier, _, err := readGeneticAnalysis.GetMetadataFromPersonGeneticAnalysis(analysisMapList) + if (err != nil){ return nil, err } + + // Outputs: + // -string: Main genome identifier (Is either the combined Only exclude conflicts genome or the only genome) + // -error + getMainGenomeIdentifier := func()(string, error){ + + if (multipleGenomesExist == true){ + return onlyExcludeConflictsGenomeIdentifier, nil + } + // Only 1 genome exists + + genomeIdentifier := allRawGenomeIdentifiersList[0] + + return genomeIdentifier, nil + } + + mainGenomeIdentifier, err := getMainGenomeIdentifier() + if (err != nil){ return nil, err } + + diseaseNameLabel := getItalicLabelCentered("Disease Name") + emptyLabelA := widget.NewLabel("") + + probabilityOfLabelA := getItalicLabelCentered("Probability of") + havingDiseaseLabel := getItalicLabelCentered("Having Disease") + + probabilityOfLabelB := getItalicLabelCentered("Probability of") + passingVariantLabel := getItalicLabelCentered("Passing Variant") + + emptyLabelB := widget.NewLabel("") + conflictExistsLabel := getItalicLabelCentered("Conflict Exists?") + + emptyLabelC := widget.NewLabel("") + emptyLabelD := widget.NewLabel("") + + diseaseNameColumn := container.NewVBox(emptyLabelA, diseaseNameLabel, widget.NewSeparator()) + probabilityOfHavingDiseaseColumn := container.NewVBox(probabilityOfLabelA, havingDiseaseLabel, widget.NewSeparator()) + probabilityOfPassingVariantColumn := container.NewVBox(probabilityOfLabelB, passingVariantLabel, widget.NewSeparator()) + conflictExistsColumn := container.NewVBox(emptyLabelB, conflictExistsLabel, widget.NewSeparator()) + viewButtonsColumn := container.NewVBox(emptyLabelC, emptyLabelD, widget.NewSeparator()) + + monogenicDiseaseNamesList, err := monogenicDiseases.GetMonogenicDiseaseNamesList() + if (err != nil) { return nil, err } + + for _, diseaseName := range monogenicDiseaseNamesList{ + + probabilitiesKnown, _, probabilityOfHavingDiseaseFormatted, _, probabilityOfPassingAVariantFormatted, _, conflictExistsBool, err := readGeneticAnalysis.GetPersonMonogenicDiseaseInfoFromGeneticAnalysis(analysisMapList, diseaseName, mainGenomeIdentifier, multipleGenomesExist) + if (err != nil) { return nil, err } + + getProbabilityOfHavingDiseaseText := func()string{ + if (probabilitiesKnown == false){ + result := translate("Unknown") + return result + } + + return probabilityOfHavingDiseaseFormatted + } + + probabilityOfHavingDiseaseText := getProbabilityOfHavingDiseaseText() + + getProbabilityOfPassingAVariantText := func()string{ + if (probabilitiesKnown == false){ + result := translate("Unknown") + return result + } + + return probabilityOfPassingAVariantFormatted + } + + probabilityOfPassingAVariantText := getProbabilityOfPassingAVariantText() + + diseaseNameText := getBoldLabelCentered(diseaseName) + diseaseNameColumn.Add(diseaseNameText) + + probabilityOfHavingDiseaseLabel := getBoldLabelCentered(probabilityOfHavingDiseaseText) + probabilityOfHavingDiseaseColumn.Add(probabilityOfHavingDiseaseLabel) + + probabilityOfPassingVariantLabel := getBoldLabelCentered(probabilityOfPassingAVariantText) + probabilityOfPassingVariantColumn.Add(probabilityOfPassingVariantLabel) + + conflictExistsString := helpers.ConvertBoolToYesOrNoString(conflictExistsBool) + conflictExistsLabel := getBoldLabelCentered(conflictExistsString) + conflictExistsColumn.Add(conflictExistsLabel) + + viewDetailsButton := getWidgetCentered(widget.NewButtonWithIcon("", theme.VisibilityIcon(), func(){ + setViewPersonGeneticAnalysisMonogenicDiseaseDetailsPage(window, analysisMapList, diseaseName, currentPage) + })) + viewButtonsColumn.Add(viewDetailsButton) + + diseaseNameColumn.Add(widget.NewSeparator()) + probabilityOfHavingDiseaseColumn.Add(widget.NewSeparator()) + probabilityOfPassingVariantColumn.Add(widget.NewSeparator()) + conflictExistsColumn.Add(widget.NewSeparator()) + viewButtonsColumn.Add(widget.NewSeparator()) + } + + probabilityOfHavingDiseaseHelpButton := widget.NewButtonWithIcon("", theme.QuestionIcon(), func(){ + setPersonProbabilityOfHavingMonogenicDiseaseExplainerPage(window, currentPage) + }) + probabilityOfHavingDiseaseColumn.Add(probabilityOfHavingDiseaseHelpButton) + + probabilityOfPassingVariantHelpButton := widget.NewButtonWithIcon("", theme.QuestionIcon(), func(){ + setPersonProbabilityOfPassingVariantExplainerPage(window, currentPage) + }) + probabilityOfPassingVariantColumn.Add(probabilityOfPassingVariantHelpButton) + + diseasesContainer := container.NewHBox(layout.NewSpacer(), diseaseNameColumn, probabilityOfHavingDiseaseColumn, probabilityOfPassingVariantColumn) + + if (multipleGenomesExist == true){ + + conflictExistsHelpButton := widget.NewButtonWithIcon("", theme.QuestionIcon(), func(){ + setPersonGeneticAnalysisConflictExistsExplainerPage(window, currentPage) + }) + + conflictExistsColumn.Add(conflictExistsHelpButton) + diseasesContainer.Add(conflictExistsColumn) + } + + diseasesContainer.Add(viewButtonsColumn) + diseasesContainer.Add(layout.NewSpacer()) + + return diseasesContainer, nil + } + + monogenicDiseasesContainer, err := getMonogenicDiseasesContainer() + if (err != nil){ + setErrorEncounteredPage(window, err, previousPage) + return + } + + page := container.NewVBox(title, backButton, widget.NewSeparator(), description, widget.NewSeparator(), monogenicDiseasesContainer) + + setPageContent(page, window) +} + + +func setViewPersonGeneticAnalysisMonogenicDiseaseDetailsPage(window fyne.Window, analysisMapList []map[string]string, diseaseName string, previousPage func()){ + + currentPage := func(){setViewPersonGeneticAnalysisMonogenicDiseaseDetailsPage(window, analysisMapList, diseaseName, previousPage)} + + title := getPageTitleCentered("Viewing Genetic Analysis - " + diseaseName) + + backButton := getBackButtonCentered(previousPage) + + allRawGenomeIdentifiersList, multipleGenomesExist, onlyExcludeConflictsGenomeIdentifier, onlyIncludeSharedGenomeIdentifier, err := readGeneticAnalysis.GetMetadataFromPersonGeneticAnalysis(analysisMapList) + if (err != nil) { + setErrorEncounteredPage(window, err, previousPage) + return + } + + getDescriptionSection := func()*fyne.Container{ + + if (multipleGenomesExist == false){ + description := getLabelCentered("Below is the disease information for this person's genome.") + + return description + } + + description1 := getLabelCentered("Below is the disease information for this person's genomes.") + description2 := getLabelCentered("The first two genomes are created by combining multiple genomes.") + + descriptionsSection := container.NewVBox(description1, description2) + + return descriptionsSection + } + + descriptionSection := getDescriptionSection() + + diseaseNameLabel := widget.NewLabel("Disease:") + diseaseNameText := getBoldLabel(diseaseName) + diseaseNameInfoButton := widget.NewButtonWithIcon("", theme.InfoIcon(), func(){ + setViewMonogenicDiseaseDetailsPage(window, diseaseName, currentPage) + }) + diseaseNameRow := container.NewHBox(layout.NewSpacer(), diseaseNameLabel, diseaseNameText, diseaseNameInfoButton, layout.NewSpacer()) + + getGenomesContainer := func()(*fyne.Container, error){ + + emptyLabelA := widget.NewLabel("") + genomeNameLabel := getItalicLabelCentered("Genome Name") + + probabilityOfLabelA := getItalicLabelCentered("Probability of") + havingDiseaseLabel := getItalicLabelCentered("Having Disease") + + probabilityOfLabelB := getItalicLabelCentered("Probability of") + passingVariantLabel := getItalicLabelCentered("Passing Variant") + + numberOfLabel := getItalicLabelCentered("Number of") + variantsTestedLabel := getItalicLabelCentered("Variants Tested") + + emptyLabelB := widget.NewLabel("") + emptyLabelC := widget.NewLabel("") + + genomeNameColumn := container.NewVBox(emptyLabelA, genomeNameLabel, widget.NewSeparator()) + probabilityOfHavingDiseaseColumn := container.NewVBox(probabilityOfLabelA, havingDiseaseLabel, widget.NewSeparator()) + probabilityOfPassingAVariantColumn := container.NewVBox(probabilityOfLabelB, passingVariantLabel, widget.NewSeparator()) + numberOfVariantsTestedColumn := container.NewVBox(numberOfLabel, variantsTestedLabel, widget.NewSeparator()) + viewButtonsColumn := container.NewVBox(emptyLabelB, emptyLabelC, widget.NewSeparator()) + + addGenomeRow := func(genomeName string, genomeIdentifier string, isACombinedGenome bool)error{ + + probabilitiesKnown, _, probabilityOfHavingDiseaseFormatted, _, probabilityOfPassingAVariantFormatted, numberOfVariantsTested, _, err := readGeneticAnalysis.GetPersonMonogenicDiseaseInfoFromGeneticAnalysis(analysisMapList, diseaseName, genomeIdentifier, multipleGenomesExist) + if (err != nil) { return err } + + getProbabilityOfHavingDiseaseText := func()string{ + if (probabilitiesKnown == false){ + result := translate("Unknown") + + return result + } + + return probabilityOfHavingDiseaseFormatted + } + + probabilityOfHavingDiseaseText := getProbabilityOfHavingDiseaseText() + + getProbabilityOfPassingAVariantText := func()string{ + + if (probabilitiesKnown == false){ + result := translate("Unknown") + + return result + } + + return probabilityOfPassingAVariantFormatted + } + + probabilityOfPassingAVariantText := getProbabilityOfPassingAVariantText() + + getGenomeNameCell := func()*fyne.Container{ + if (isACombinedGenome == false){ + + genomeNameLabel := getBoldLabelCentered(genomeName) + return genomeNameLabel + } + viewHelpButton := widget.NewButtonWithIcon("", theme.QuestionIcon(), func(){ + setCombinedGenomesExplainerPage(window, currentPage) + }) + + genomeNameLabel := getBoldLabel(genomeName) + genomeNameCell := container.NewHBox(layout.NewSpacer(), viewHelpButton, genomeNameLabel, layout.NewSpacer()) + + return genomeNameCell + } + + genomeNameCell := getGenomeNameCell() + genomeNameColumn.Add(genomeNameCell) + + probabilityOfHavingDiseaseLabel := getBoldLabelCentered(probabilityOfHavingDiseaseText) + probabilityOfHavingDiseaseColumn.Add(probabilityOfHavingDiseaseLabel) + + probabilityOfPassingAVariantLabel := getBoldLabelCentered(probabilityOfPassingAVariantText) + probabilityOfPassingAVariantColumn.Add(probabilityOfPassingAVariantLabel) + + numberOfVariantsTestedString := helpers.ConvertIntToString(numberOfVariantsTested) + numberOfVariantsTestedLabel := getBoldLabelCentered(numberOfVariantsTestedString) + numberOfVariantsTestedColumn.Add(numberOfVariantsTestedLabel) + + viewButton := widget.NewButtonWithIcon("", theme.VisibilityIcon(), func(){setViewPersonGenomeMonogenicDiseaseVariantsPage(window, analysisMapList, genomeIdentifier, genomeName, diseaseName, currentPage)}) + viewButtonsColumn.Add(viewButton) + + genomeNameColumn.Add(widget.NewSeparator()) + probabilityOfHavingDiseaseColumn.Add(widget.NewSeparator()) + probabilityOfPassingAVariantColumn.Add(widget.NewSeparator()) + numberOfVariantsTestedColumn.Add(widget.NewSeparator()) + viewButtonsColumn.Add(widget.NewSeparator()) + + return nil + } + + if (multipleGenomesExist == true){ + + err := addGenomeRow("Only Exclude Conflicts", onlyExcludeConflictsGenomeIdentifier, true) + if (err != nil){ return nil, err } + + err = addGenomeRow("Only Include Shared", onlyIncludeSharedGenomeIdentifier, true) + if (err != nil){ return nil, err } + } + + for _, genomeIdentifier := range allRawGenomeIdentifiersList{ + + getGenomeName := func()(string, error){ + + genomeFound, _, timeGenomeWasExported, _, _, _, companyName, _, _, err := myGenomes.GetMyRawGenomeMetadata(genomeIdentifier) + if (err != nil) { return "", err } + if (genomeFound == false){ + return "", errors.New("MyGenomeInfo for genome from analysisMapList not found.") + } + + if (multipleGenomesExist == false){ + return companyName, nil + } + + // We show the date that the genome was exported + + exportTimeAgo, err := helpers.ConvertUnixTimeToTimeAgoTranslated(timeGenomeWasExported, false) + if (err != nil){ return "", err } + + genomeName := companyName + " (Exported " + exportTimeAgo + ")" + + return genomeName, nil + } + + genomeName, err := getGenomeName() + if (err != nil) { return nil, err } + + err = addGenomeRow(genomeName, genomeIdentifier, false) + if (err != nil){ return nil, err } + } + + probabilityOfHavingDiseaseHelpButton := widget.NewButtonWithIcon("", theme.QuestionIcon(), func(){ + setPersonProbabilityOfHavingMonogenicDiseaseExplainerPage(window, currentPage) + }) + probabilityOfHavingDiseaseColumn.Add(probabilityOfHavingDiseaseHelpButton) + + probabilityOfPassingAVariantHelpButton := widget.NewButtonWithIcon("", theme.QuestionIcon(), func(){ + setPersonProbabilityOfPassingVariantExplainerPage(window, currentPage) + }) + probabilityOfPassingAVariantColumn.Add(probabilityOfPassingAVariantHelpButton) + + numberOfVariantsTestedHelpButton := widget.NewButtonWithIcon("", theme.QuestionIcon(), func(){ + setNumberOfTestedVariantsExplainerPage(window, currentPage) + }) + numberOfVariantsTestedColumn.Add(numberOfVariantsTestedHelpButton) + + genomesContainer := container.NewHBox(layout.NewSpacer(), genomeNameColumn, probabilityOfHavingDiseaseColumn, probabilityOfPassingAVariantColumn, numberOfVariantsTestedColumn, viewButtonsColumn, layout.NewSpacer()) + + return genomesContainer, nil + } + + genomesContainer, err := getGenomesContainer() + if (err != nil){ + setErrorEncounteredPage(window, err, previousPage) + return + } + + page := container.NewVBox(title, backButton, widget.NewSeparator(), descriptionSection, widget.NewSeparator(), diseaseNameRow, widget.NewSeparator(), genomesContainer) + + setPageContent(page, window) +} + + +// This page is used to view the person's variants for a particular genome +func setViewPersonGenomeMonogenicDiseaseVariantsPage(window fyne.Window, geneticAnalysisMapList []map[string]string, genomeIdentifier string, genomeName string, diseaseName string, previousPage func()){ + + setLoadingScreen(window, "Loading Disease Variants", "Loading disease variants...") + + currentPage := func(){setViewPersonGenomeMonogenicDiseaseVariantsPage(window, geneticAnalysisMapList, genomeIdentifier, genomeName, diseaseName, previousPage)} + + title := getPageTitleCentered("View Monogenic Disease Variants - " + diseaseName) + + backButton := getBackButtonCentered(previousPage) + + description1 := widget.NewLabel("Below are the monogenic disease variant results for this genome.") + variantsHelpButton := widget.NewButtonWithIcon("", theme.QuestionIcon(), func(){ + setMonogenicDiseaseVariantsExplainerPage(window, currentPage) + }) + description1Row := container.NewHBox(layout.NewSpacer(), description1, variantsHelpButton, layout.NewSpacer()) + + getGenomeNameRow := func()*fyne.Container{ + + genomeLabel := widget.NewLabel("Genome:") + genomeNameLabel := getBoldLabel(genomeName) + + if (genomeName == "Only Exclude Conflicts" || genomeName == "Only Include Shared"){ + genomeHelpButton := widget.NewButtonWithIcon("", theme.QuestionIcon(), func(){ + setCombinedGenomesExplainerPage(window, currentPage) + }) + genomeNameRow := container.NewHBox(layout.NewSpacer(), genomeLabel, genomeNameLabel, genomeHelpButton, layout.NewSpacer()) + return genomeNameRow + } + genomeNameRow := container.NewHBox(layout.NewSpacer(), genomeLabel, genomeNameLabel, layout.NewSpacer()) + return genomeNameRow + } + + genomeNameRow := getGenomeNameRow() + + _, multipleGenomesExist, _, _, err := readGeneticAnalysis.GetMetadataFromPersonGeneticAnalysis(geneticAnalysisMapList) + if (err != nil) { + setErrorEncounteredPage(window, err, previousPage) + return + } + + _, _, _, _, _, numberOfVariantsTested, _, err := readGeneticAnalysis.GetPersonMonogenicDiseaseInfoFromGeneticAnalysis(geneticAnalysisMapList, diseaseName, genomeIdentifier, multipleGenomesExist) + if (err != nil) { + setErrorEncounteredPage(window, err, previousPage) + return + } + + diseaseVariantsMap, err := monogenicDiseases.GetMonogenicDiseaseVariantsMap(diseaseName) + if (err != nil){ + setErrorEncounteredPage(window, err, previousPage) + return + } + + totalNumberOfVariants := len(diseaseVariantsMap) + totalNumberOfVariantsString := helpers.ConvertIntToString(totalNumberOfVariants) + + numberOfVariantsTestedString := helpers.ConvertIntToString(numberOfVariantsTested) + + variantsTestedLabel := widget.NewLabel("Variants Tested:") + variantsTestedText := getBoldLabel(numberOfVariantsTestedString + "/" + totalNumberOfVariantsString) + variantsTestedHelpButton := widget.NewButtonWithIcon("", theme.QuestionIcon(), func(){ + setNumberOfTestedVariantsExplainerPage(window, currentPage) + }) + + variantsTestedRow := container.NewHBox(layout.NewSpacer(), variantsTestedLabel, variantsTestedText, variantsTestedHelpButton, layout.NewSpacer()) + + //TODO: Add navigation buttons and pages + + getVariantsGrid := func()(*fyne.Container, error){ + + variantNameLabel := getItalicLabelCentered("Variant Name") + genomeHasMutationLabel := getItalicLabelCentered("Genome Has Mutation") + numberOfMutationsLabel := getItalicLabelCentered("Number of Mutations") + emptyLabel := widget.NewLabel("") + + variantNameColumn := container.NewVBox(variantNameLabel, widget.NewSeparator()) + genomeHasMutationColumn := container.NewVBox(genomeHasMutationLabel, widget.NewSeparator()) + numberOfMutationsColumn := container.NewVBox(numberOfMutationsLabel, widget.NewSeparator()) + viewButtonsColumn := container.NewVBox(emptyLabel, widget.NewSeparator()) + + addVariantRow := func(variantIdentifier string)error{ + + variantObject, exists := diseaseVariantsMap[variantIdentifier] + if (exists == false) { + return errors.New("Cannot add variantRow: diseaseVariantsMap missing variant.") + } + + variantName := variantObject.VariantNames[0] + + numberOfMutationsIsKnown, genomeNumberOfMutations, err := readGeneticAnalysis.GetPersonMonogenicDiseaseVariantInfoFromGeneticAnalysis(geneticAnalysisMapList, diseaseName, variantIdentifier, genomeIdentifier) + if (err != nil) { return err } + + variantNameLabel := getBoldLabelCentered(variantName) + variantNameColumn.Add(variantNameLabel) + + getGenomeHasMutationString := func()string{ + if (numberOfMutationsIsKnown == false){ + result := translate("Unknown") + return result + } + if (genomeNumberOfMutations == 0){ + result := translate("No") + return result + } + + result := translate("Yes") + return result + } + + genomeHasMutationString := getGenomeHasMutationString() + + genomeHasMutationLabel := getBoldLabelCentered(genomeHasMutationString) + genomeHasMutationColumn.Add(genomeHasMutationLabel) + + getNumberOfVariantMutationsString := func()string{ + + if (numberOfMutationsIsKnown == false){ + result := translate("Unknown") + return result + } + variantNumberOfMutationsString := helpers.ConvertIntToString(genomeNumberOfMutations) + return variantNumberOfMutationsString + } + + numberOfVariantMutationsString := getNumberOfVariantMutationsString() + + genomeNumberOfMutationsLabel := getBoldLabelCentered(numberOfVariantMutationsString) + numberOfMutationsColumn.Add(genomeNumberOfMutationsLabel) + + viewMutationButton := widget.NewButtonWithIcon("", theme.VisibilityIcon(), func(){ + setViewPersonGeneticAnalysisMonogenicDiseaseVariantDetailsPage(window, geneticAnalysisMapList, diseaseName, variantIdentifier, currentPage) + }) + viewButtonsColumn.Add(viewMutationButton) + + variantNameColumn.Add(widget.NewSeparator()) + genomeHasMutationColumn.Add(widget.NewSeparator()) + numberOfMutationsColumn.Add(widget.NewSeparator()) + viewButtonsColumn.Add(widget.NewSeparator()) + + return nil + } + + variantsList_2Mutations := make([]string, 0) + variantsList_1Mutation := make([]string, 0) + variantsList_0Mutations := make([]string, 0) + variantsList_Unknown := make([]string, 0) + + for variantIdentifier, _ := range diseaseVariantsMap{ + + numberOfMutationsIsKnown, genomeNumberOfMutations, err := readGeneticAnalysis.GetPersonMonogenicDiseaseVariantInfoFromGeneticAnalysis(geneticAnalysisMapList, diseaseName, variantIdentifier, genomeIdentifier) + if (err != nil) { return nil, err } + if (numberOfMutationsIsKnown == false){ + variantsList_Unknown = append(variantsList_Unknown, variantIdentifier) + continue + } + if (genomeNumberOfMutations == 0){ + + variantsList_0Mutations = append(variantsList_0Mutations, variantIdentifier) + + } else if (genomeNumberOfMutations == 1) { + + variantsList_1Mutation = append(variantsList_1Mutation, variantIdentifier) + + } else if (genomeNumberOfMutations == 2){ + + variantsList_2Mutations = append(variantsList_2Mutations, variantIdentifier) + + } else { + return nil, errors.New("GetPersonMonogenicDiseaseVariantInfoFromGeneticAnalysis returning invalid genome number of mutations") + } + } + + // Items within each group are sorted so they will display the same way whenever user refreshes page + + helpers.SortStringListToUnicodeOrder(variantsList_2Mutations) + helpers.SortStringListToUnicodeOrder(variantsList_1Mutation) + helpers.SortStringListToUnicodeOrder(variantsList_0Mutations) + helpers.SortStringListToUnicodeOrder(variantsList_Unknown) + + for _, variantIdentifier := range variantsList_2Mutations{ + + err = addVariantRow(variantIdentifier) + if (err != nil) { return nil, err } + } + for _, variantIdentifier := range variantsList_1Mutation{ + + err = addVariantRow(variantIdentifier) + if (err != nil) { return nil, err } + } + for _, variantIdentifier := range variantsList_0Mutations{ + + err = addVariantRow(variantIdentifier) + if (err != nil) { return nil, err } + } + for _, variantIdentifier := range variantsList_Unknown{ + + err = addVariantRow(variantIdentifier) + if (err != nil) { return nil, err } + } + + genomeHasMutationHelpButton := widget.NewButtonWithIcon("", theme.QuestionIcon(), func(){ + setGenomeHasMonogenicDiseaseVariantMutationExplainerPage(window, currentPage) + }) + genomeHasMutationColumn.Add(genomeHasMutationHelpButton) + + genomeNumberOfMutationsHelpButton := widget.NewButtonWithIcon("", theme.QuestionIcon(), func(){ + setMonogenicDiseaseVariantsExplainerPage(window, currentPage) + }) + numberOfMutationsColumn.Add(genomeNumberOfMutationsHelpButton) + + + variantsGrid := container.NewHBox(layout.NewSpacer(), variantNameColumn, genomeHasMutationColumn, numberOfMutationsColumn, viewButtonsColumn, layout.NewSpacer()) + + return variantsGrid, nil + } + + variantsGrid, err := getVariantsGrid() + if (err != nil){ + setErrorEncounteredPage(window, err, previousPage) + return + } + + page := container.NewVBox(title, backButton, widget.NewSeparator(), description1Row, widget.NewSeparator(), genomeNameRow, widget.NewSeparator(), variantsTestedRow, widget.NewSeparator(), variantsGrid) + + setPageContent(page, window) +} + + +// This page will show the details of a specific variant from a person's genetic analysis +// It will show the variant details for all of the person's genomes +func setViewPersonGeneticAnalysisMonogenicDiseaseVariantDetailsPage(window fyne.Window, geneticAnalysisMapList []map[string]string, diseaseName string, variantIdentifier string, previousPage func()){ + + currentPage := func(){setViewPersonGeneticAnalysisMonogenicDiseaseVariantDetailsPage(window, geneticAnalysisMapList, diseaseName, variantIdentifier, previousPage)} + + title := getPageTitleCentered("Monogenic Disease Variant Details - " + diseaseName) + + backButton := getBackButtonCentered(previousPage) + + variantObject, err := monogenicDiseases.GetMonogenicDiseaseVariantObject(diseaseName, variantIdentifier) + if (err != nil) { + setErrorEncounteredPage(window, err, previousPage) + return + } + + variantName := variantObject.VariantNames[0] + + description := getLabelCentered("Below is the variant status for the person's genomes.") + + variantNameLabel := widget.NewLabel("Variant Name:") + variantNameText := getBoldLabel(variantName) + variantNameHelpButton := widget.NewButtonWithIcon("", theme.InfoIcon(), func(){ + setViewMonogenicDiseaseVariantDetailsPage(window, diseaseName, variantIdentifier, currentPage) + }) + variantNameRow := container.NewHBox(layout.NewSpacer(), variantNameLabel, variantNameText, variantNameHelpButton, layout.NewSpacer()) + + getGenomesHaveVariantGrid := func()(*fyne.Container, error){ + + allRawGenomeIdentifiersList, multipleGenomesExist, onlyExcludeConflictsGenomeIdentifier, onlyIncludeSharedGenomeIdentifier, err := readGeneticAnalysis.GetMetadataFromPersonGeneticAnalysis(geneticAnalysisMapList) + if (err != nil) { return nil, err } + + genomeNameLabel := getItalicLabelCentered("Genome Name") + genomeHasMutationLabel := getItalicLabelCentered("Genome Has Mutation") + numberOfMutationsLabel := getItalicLabelCentered("Number of Mutations") + + genomeNameColumn := container.NewVBox(genomeNameLabel, widget.NewSeparator()) + genomeHasMutationColumn := container.NewVBox(genomeHasMutationLabel, widget.NewSeparator()) + numberOfMutationsColumn := container.NewVBox(numberOfMutationsLabel, widget.NewSeparator()) + + addGenomeRow := func(genomeName string, genomeIdentifier string, isACombinedGenome bool)error{ + + genomeMutationsKnown, genomeNumberOfMutations, err := readGeneticAnalysis.GetPersonMonogenicDiseaseVariantInfoFromGeneticAnalysis(geneticAnalysisMapList, diseaseName, variantIdentifier, genomeIdentifier) + if (err != nil) { return err } + + getGenomeNameCell := func()*fyne.Container{ + if (isACombinedGenome == false){ + + genomeNameLabel := getBoldLabelCentered(genomeName) + return genomeNameLabel + } + viewHelpButton := widget.NewButtonWithIcon("", theme.QuestionIcon(), func(){ + setCombinedGenomesExplainerPage(window, currentPage) + }) + + genomeNameLabel := getBoldLabel(genomeName) + genomeNameCell := container.NewHBox(layout.NewSpacer(), viewHelpButton, genomeNameLabel, layout.NewSpacer()) + + return genomeNameCell + } + + genomeNameCell := getGenomeNameCell() + genomeNameColumn.Add(genomeNameCell) + + getGenomeHasMutationText := func()string{ + if (genomeMutationsKnown == false){ + result := translate("Unknown") + return result + } + + if (genomeNumberOfMutations == 0){ + result := translate("No") + return result + } + + result := translate("Yes") + + return result + } + + genomeHasMutationText := getGenomeHasMutationText() + + getGenomeNumberOfMutationsText := func()string{ + if (genomeMutationsKnown == false){ + result := translate("Unknown") + return result + } + + genomeNumberOfMutationsString := helpers.ConvertIntToString(genomeNumberOfMutations) + + return genomeNumberOfMutationsString + } + + genomeNumberOfMutationsText := getGenomeNumberOfMutationsText() + + genomeHasMutationLabel := getBoldLabelCentered(genomeHasMutationText) + genomeHasMutationColumn.Add(genomeHasMutationLabel) + + genomeNumberOfMutationsLabel := getBoldLabelCentered(genomeNumberOfMutationsText) + numberOfMutationsColumn.Add(genomeNumberOfMutationsLabel) + + genomeNameColumn.Add(widget.NewSeparator()) + genomeHasMutationColumn.Add(widget.NewSeparator()) + numberOfMutationsColumn.Add(widget.NewSeparator()) + + return nil + } + + if (multipleGenomesExist == true){ + + err := addGenomeRow("Only Exclude Conflicts", onlyExcludeConflictsGenomeIdentifier, true) + if (err != nil){ return nil, err } + + err = addGenomeRow("Only Include Shared", onlyIncludeSharedGenomeIdentifier, true) + if (err != nil){ return nil, err } + } + + for _, genomeIdentifier := range allRawGenomeIdentifiersList{ + + getGenomeName := func()(string, error){ + + genomeFound, _, timeGenomeWasExported, _, _, _, companyName, _, _, err := myGenomes.GetMyRawGenomeMetadata(genomeIdentifier) + if (err != nil) { return "", err } + if (genomeFound == false){ + return "", errors.New("MyGenomeInfo for genome from analysisMapList not found.") + } + + if (multipleGenomesExist == false){ + return companyName, nil + } + + // We show the date that the genome was exported + + exportTimeAgo, err := helpers.ConvertUnixTimeToTimeAgoTranslated(timeGenomeWasExported, false) + if (err != nil){ return "", err } + + genomeName := companyName + " (Exported " + exportTimeAgo + ")" + + return genomeName, nil + } + + genomeName, err := getGenomeName() + if (err != nil) { return nil, err } + + err = addGenomeRow(genomeName, genomeIdentifier, false) + if (err != nil){ return nil, err } + } + + genomeHasMutationHelpButton := widget.NewButtonWithIcon("", theme.QuestionIcon(), func(){ + setGenomeHasMonogenicDiseaseVariantMutationExplainerPage(window, currentPage) + }) + genomeHasMutationColumn.Add(genomeHasMutationHelpButton) + + genomeNumberOfMutationsHelpButton := widget.NewButtonWithIcon("", theme.QuestionIcon(), func(){ + setMonogenicDiseaseVariantsExplainerPage(window, currentPage) + }) + numberOfMutationsColumn.Add(genomeNumberOfMutationsHelpButton) + + genomesContainer := container.NewHBox(layout.NewSpacer(), genomeNameColumn, genomeHasMutationColumn, numberOfMutationsColumn, layout.NewSpacer()) + + return genomesContainer, nil + } + + genomesHaveVariantGrid, err := getGenomesHaveVariantGrid() + if (err != nil){ + setErrorEncounteredPage(window, err, previousPage) + return + } + + page := container.NewVBox(title, backButton, widget.NewSeparator(), description, widget.NewSeparator(), variantNameRow, widget.NewSeparator(), genomesHaveVariantGrid) + + setPageContent(page, window) +} + + +func setViewPersonGeneticAnalysisPolygenicDiseasesPage(window fyne.Window, personIdentifier string, analysisMapList []map[string]string, previousPage func()){ + + currentPage := func(){setViewPersonGeneticAnalysisPolygenicDiseasesPage(window, personIdentifier, analysisMapList, previousPage)} + + title := getPageTitleCentered("Viewing Genetic Analysis - Polygenic Diseases") + + backButton := getBackButtonCentered(previousPage) + + description := getLabelCentered("Below is an analysis of the polygenic disease risks for this person's genome.") + + getPolygenicDiseasesContainer := func()(*fyne.Container, error){ + + allRawGenomeIdentifiersList, multipleGenomesExist, onlyExcludeConflictsGenomeIdentifier, _, err := readGeneticAnalysis.GetMetadataFromPersonGeneticAnalysis(analysisMapList) + if (err != nil){ return nil, err } + + // Outputs: + // -string: Main genome identifier (Is either the combined Only exclude conflicts genome or the only genome) + // -error + getMainGenomeIdentifier := func()(string, error){ + + if (multipleGenomesExist == true){ + return onlyExcludeConflictsGenomeIdentifier, nil + } + // Only 1 genome exists + + genomeIdentifier := allRawGenomeIdentifiersList[0] + + return genomeIdentifier, nil + } + + mainGenomeIdentifier, err := getMainGenomeIdentifier() + if (err != nil){ return nil, err } + + diseaseNameLabel := getItalicLabelCentered("Disease Name") + + riskScoreLabel := getItalicLabelCentered("Risk Score") + + conflictExistsLabel := getItalicLabelCentered("Conflict Exists?") + + emptyLabel := widget.NewLabel("") + + diseaseNameColumn := container.NewVBox(diseaseNameLabel, widget.NewSeparator()) + riskScoreColumn := container.NewVBox(riskScoreLabel, widget.NewSeparator()) + conflictExistsColumn := container.NewVBox(conflictExistsLabel, widget.NewSeparator()) + viewButtonsColumn := container.NewVBox(emptyLabel, widget.NewSeparator()) + + polygenicDiseaseNamesList, err := polygenicDiseases.GetPolygenicDiseaseNamesList() + if (err != nil) { return nil, err } + + for _, diseaseName := range polygenicDiseaseNamesList{ + + diseaseNameText := getBoldLabelCentered(diseaseName) + + personRiskScoreKnown, _, personRiskScoreFormatted, _, conflictExists, err := readGeneticAnalysis.GetPersonPolygenicDiseaseInfoFromGeneticAnalysis(analysisMapList, diseaseName, mainGenomeIdentifier, multipleGenomesExist) + if (err != nil) { return nil, err } + + getPersonRiskScoreLabelText := func()string{ + + if (personRiskScoreKnown == false){ + result := translate("Unknown") + + return result + } + + return personRiskScoreFormatted + } + + personRiskScoreLabelText := getPersonRiskScoreLabelText() + + riskScoreText := getBoldLabelCentered(personRiskScoreLabelText) + + conflictExistsString := helpers.ConvertBoolToYesOrNoString(conflictExists) + conflictExistsLabel := getBoldLabelCentered(conflictExistsString) + + viewDetailsButton := getWidgetCentered(widget.NewButtonWithIcon("", theme.VisibilityIcon(), func(){ + setViewPersonGeneticAnalysisPolygenicDiseaseDetailsPage(window, personIdentifier, analysisMapList, diseaseName, currentPage) + })) + + diseaseNameColumn.Add(diseaseNameText) + riskScoreColumn.Add(riskScoreText) + conflictExistsColumn.Add(conflictExistsLabel) + viewButtonsColumn.Add(viewDetailsButton) + + diseaseNameColumn.Add(widget.NewSeparator()) + riskScoreColumn.Add(widget.NewSeparator()) + conflictExistsColumn.Add(widget.NewSeparator()) + viewButtonsColumn.Add(widget.NewSeparator()) + } + + riskScoreHelpButton := widget.NewButtonWithIcon("", theme.QuestionIcon(), func(){ + setPolygenicDiseaseRiskScoreExplainerPage(window, currentPage) + }) + riskScoreColumn.Add(riskScoreHelpButton) + + diseasesContainer := container.NewHBox(layout.NewSpacer(), diseaseNameColumn, riskScoreColumn) + + if (multipleGenomesExist == true){ + + conflictExistsHelpButton := widget.NewButtonWithIcon("", theme.QuestionIcon(), func(){ + setPersonGeneticAnalysisConflictExistsExplainerPage(window, currentPage) + }) + + conflictExistsColumn.Add(conflictExistsHelpButton) + diseasesContainer.Add(conflictExistsColumn) + } + + diseasesContainer.Add(viewButtonsColumn) + diseasesContainer.Add(layout.NewSpacer()) + + return diseasesContainer, nil + } + + diseasesContainer, err := getPolygenicDiseasesContainer() + if (err != nil){ + setErrorEncounteredPage(window, err, previousPage) + return + } + + page := container.NewVBox(title, backButton, widget.NewSeparator(), description, widget.NewSeparator(), diseasesContainer) + + setPageContent(page, window) +} + + + +func setViewPersonGeneticAnalysisPolygenicDiseaseDetailsPage(window fyne.Window, personIdentifier string, analysisMapList []map[string]string, diseaseName string, previousPage func()){ + + currentPage := func(){setViewPersonGeneticAnalysisPolygenicDiseaseDetailsPage(window, personIdentifier, analysisMapList, diseaseName, previousPage)} + + title := getPageTitleCentered("Viewing Genetic Analysis - " + diseaseName) + + backButton := getBackButtonCentered(previousPage) + + allRawGenomeIdentifiersList, multipleGenomesExist, onlyExcludeConflictsGenomeIdentifier, onlyIncludeSharedGenomeIdentifier, err := readGeneticAnalysis.GetMetadataFromPersonGeneticAnalysis(analysisMapList) + if (err != nil) { + setErrorEncounteredPage(window, err, previousPage) + return + } + + getDescriptionSection := func()*fyne.Container{ + + if (multipleGenomesExist == false){ + description := getLabelCentered("Below is the disease information for this person's genome.") + + return description + } + + description1 := getLabelCentered("Below is the disease information for this person's genomes.") + description2 := getLabelCentered("The first two genomes are created by combining multiple genomes.") + + descriptionsSection := container.NewVBox(description1, description2) + + return descriptionsSection + } + + descriptionSection := getDescriptionSection() + + diseaseNameLabel := widget.NewLabel("Disease:") + diseaseNameText := getBoldLabel(diseaseName) + diseaseInfoButton := widget.NewButtonWithIcon("", theme.InfoIcon(), func(){ + setViewPolygenicDiseaseDetailsPage(window, diseaseName, currentPage) + }) + diseaseNameRow := container.NewHBox(layout.NewSpacer(), diseaseNameLabel, diseaseNameText, diseaseInfoButton, layout.NewSpacer()) + + getGenomesContainer := func()(*fyne.Container, error){ + + emptyLabelA := widget.NewLabel("") + genomeNameLabel := getItalicLabelCentered("Genome Name") + + emptyLabelB := widget.NewLabel("") + riskScoreLabel := getItalicLabelCentered("Risk Score") + + numberOfLabel := getItalicLabelCentered("Number of") + lociTestedLabel := getItalicLabelCentered("Loci Tested") + + emptyLabelD := widget.NewLabel("") + emptyLabelE := widget.NewLabel("") + + emptyLabelF := widget.NewLabel("") + emptyLabelG := widget.NewLabel("") + + genomeNameColumn := container.NewVBox(emptyLabelA, genomeNameLabel, widget.NewSeparator()) + riskScoreColumn := container.NewVBox(emptyLabelB, riskScoreLabel, widget.NewSeparator()) + numberOfLociTestedColumn := container.NewVBox(numberOfLabel, lociTestedLabel, widget.NewSeparator()) + viewLifetimeRiskButtonsColumn := container.NewVBox(emptyLabelD, emptyLabelE, widget.NewSeparator()) + viewLociButtonsColumn := container.NewVBox(emptyLabelF, emptyLabelG, widget.NewSeparator()) + + addGenomeRow := func(genomeName string, genomeIdentifier string, isACombinedGenome bool)error{ + + getGenomeNameCell := func()*fyne.Container{ + if (isACombinedGenome == false){ + + genomeNameLabel := getBoldLabelCentered(genomeName) + return genomeNameLabel + } + viewHelpButton := widget.NewButtonWithIcon("", theme.QuestionIcon(), func(){ + setCombinedGenomesExplainerPage(window, currentPage) + }) + + genomeNameLabel := getBoldLabel(genomeName) + genomeNameCell := container.NewHBox(layout.NewSpacer(), viewHelpButton, genomeNameLabel, layout.NewSpacer()) + + return genomeNameCell + } + + genomeNameCell := getGenomeNameCell() + + diseaseRiskScoreKnown, _, diseaseRiskScoreFormatted, numberOfLociTested, _, err := readGeneticAnalysis.GetPersonPolygenicDiseaseInfoFromGeneticAnalysis(analysisMapList, diseaseName, genomeIdentifier, multipleGenomesExist) + if (err != nil) { return err } + + getRiskScoreLabelText := func()string{ + if (diseaseRiskScoreKnown == false){ + result := translate("Unknown") + + return result + } + + return diseaseRiskScoreFormatted + } + + genomeRiskScoreLabelText := getRiskScoreLabelText() + + riskScoreLabel := getBoldLabelCentered(genomeRiskScoreLabelText) + + genomeNumberOfLociTestedString := helpers.ConvertIntToString(numberOfLociTested) + numberOfLociTestedLabel := getBoldLabelCentered(genomeNumberOfLociTestedString) + + viewLifetimeRiskButton := widget.NewButtonWithIcon("", theme.HistoryIcon(), func(){ + + personFound, _, _, personSex, err := myPeople.GetPersonInfo(personIdentifier) + if (err != nil){ + setErrorEncounteredPage(window, err, currentPage) + return + } + if (personFound == false){ + setErrorEncounteredPage(window, errors.New("setViewPersonGeneticAnalysisPolygenicDiseaseDetailsPage called with unknown personIdentifier"), currentPage) + return + } + + getSexToDisplay := func()string{ + if (personSex == "Male" || personSex == "Female"){ + return personSex + } + return "Male" + } + sexToDisplay := getSexToDisplay() + + setViewPersonPolygenicDiseaseLifetimeProbabilitiesPage(window, diseaseName, genomeName, sexToDisplay, currentPage) + }) + + viewLociButton := widget.NewButtonWithIcon("", theme.VisibilityIcon(), func(){ + setViewPersonGenomePolygenicDiseaseLociPage(window, analysisMapList, diseaseName, genomeIdentifier, genomeName, currentPage) + }) + + genomeNameColumn.Add(genomeNameCell) + riskScoreColumn.Add(riskScoreLabel) + numberOfLociTestedColumn.Add(numberOfLociTestedLabel) + viewLifetimeRiskButtonsColumn.Add(viewLifetimeRiskButton) + viewLociButtonsColumn.Add(viewLociButton) + + genomeNameColumn.Add(widget.NewSeparator()) + riskScoreColumn.Add(widget.NewSeparator()) + numberOfLociTestedColumn.Add(widget.NewSeparator()) + viewLifetimeRiskButtonsColumn.Add(widget.NewSeparator()) + viewLociButtonsColumn.Add(widget.NewSeparator()) + + return nil + } + + if (multipleGenomesExist == true){ + + err := addGenomeRow("Only Exclude Conflicts", onlyExcludeConflictsGenomeIdentifier, true) + if (err != nil){ return nil, err } + + err = addGenomeRow("Only Include Shared", onlyIncludeSharedGenomeIdentifier, true) + if (err != nil){ return nil, err } + } + + for _, genomeIdentifier := range allRawGenomeIdentifiersList{ + + getGenomeName := func()(string, error){ + genomeFound, _, timeGenomeWasExported, _, _, _, companyName, _, _, err := myGenomes.GetMyRawGenomeMetadata(genomeIdentifier) + if (err != nil) { return "", err } + if (genomeFound == false){ + return "", errors.New("MyGenomeInfo for genome from analysisMapList not found.") + } + + if (multipleGenomesExist == false){ + return companyName, nil + } + + // We show the date the genome was exported + + exportTimeAgo, err := helpers.ConvertUnixTimeToTimeAgoTranslated(timeGenomeWasExported, false) + if (err != nil){ return "", err } + + genomeName := companyName + " (Exported " + exportTimeAgo + ")" + + return genomeName, nil + } + + genomeName, err := getGenomeName() + if (err != nil) { return nil, err } + + err = addGenomeRow(genomeName, genomeIdentifier, false) + if (err != nil){ return nil, err } + } + + riskScoreHelpButton := widget.NewButtonWithIcon("", theme.QuestionIcon(), func(){ + setPolygenicDiseaseRiskScoreExplainerPage(window, currentPage) + }) + riskScoreColumn.Add(riskScoreHelpButton) + + numberOfLociTestedHelpButton := widget.NewButtonWithIcon("", theme.QuestionIcon(), func(){ + setPolygenicDiseaseNumberOfLociTestedExplainerPage(window, currentPage) + }) + numberOfLociTestedColumn.Add(numberOfLociTestedHelpButton) + + genomesContainer := container.NewHBox(layout.NewSpacer(), genomeNameColumn, riskScoreColumn, numberOfLociTestedColumn, viewLifetimeRiskButtonsColumn, viewLociButtonsColumn, layout.NewSpacer()) + + return genomesContainer, nil + } + + genomesContainer, err := getGenomesContainer() + if (err != nil){ + setErrorEncounteredPage(window, err, previousPage) + return + } + + page := container.NewVBox(title, backButton, widget.NewSeparator(), descriptionSection, widget.NewSeparator(), diseaseNameRow, widget.NewSeparator(), genomesContainer) + + setPageContent(page, window) +} + + +func setViewPersonPolygenicDiseaseLifetimeProbabilitiesPage(window fyne.Window, diseaseName string, genomeName string, maleOrFemale string, previousPage func()){ + + currentPage := func(){setViewPersonPolygenicDiseaseLifetimeProbabilitiesPage(window, diseaseName, genomeName, maleOrFemale, previousPage)} + + title := getPageTitleCentered("Viewing Disease Lifetime Probabilities") + + backButton := getBackButtonCentered(previousPage) + + description1 := getLabelCentered("Below are the lifetime probabilities for this disease.") + description2 := getLabelCentered("The average risk column describes the probability for the average person.") + + //TODO: Once we understand how to calculate it, we will add adjusted risk column with estimated probability + + getGenomeNameRow := func()*fyne.Container{ + + genomeLabel := widget.NewLabel("Genome:") + genomeNameLabel := getBoldLabel(genomeName) + + if (genomeName == "Only Exclude Conflicts" || genomeName == "Only Include Shared"){ + genomeHelpButton := widget.NewButtonWithIcon("", theme.QuestionIcon(), func(){ + setCombinedGenomesExplainerPage(window, currentPage) + }) + genomeNameRow := container.NewHBox(layout.NewSpacer(), genomeLabel, genomeNameLabel, genomeHelpButton, layout.NewSpacer()) + return genomeNameRow + } + genomeNameRow := container.NewHBox(layout.NewSpacer(), genomeLabel, genomeNameLabel, layout.NewSpacer()) + return genomeNameRow + } + + genomeNameRow := getGenomeNameRow() + + getTabContainer := func(tabMaleOrFemale string)(*container.TabItem, error){ + + ageLabel := getItalicLabelCentered("Age") + + averageRiskLabel := getItalicLabelCentered("Average Risk") + + ageColumn := container.NewVBox(ageLabel, widget.NewSeparator()) + averageRiskColumn := container.NewVBox(averageRiskLabel, widget.NewSeparator()) + + diseaseObject, err := polygenicDiseases.GetPolygenicDiseaseObject(diseaseName) + if (err != nil) { return nil, err } + + getAverageRiskProbabilitiesFunction := diseaseObject.GetAverageRiskProbabilitiesFunction + + rowInitialValuesSet := false + rowInitialAge := 0 + rowInitialAgeRisk := float64(0) + + rowSummedRisks := float64(0) + + for age := 0; age <= 110; age++{ + + ageRisk, err := getAverageRiskProbabilitiesFunction(tabMaleOrFemale, age) + if (err != nil) { return nil, err } + + if (rowInitialValuesSet == false){ + rowInitialValuesSet = true + rowInitialAge = age + rowInitialAgeRisk = ageRisk + rowSummedRisks = ageRisk + continue + } + + rowSummedRisks += ageRisk + + nextAge := age+1 + if (nextAge % 10 != 0){ + continue + } + if (age < 109 && rowInitialAgeRisk == ageRisk){ + continue + } + // We make a new row + + averageRisk := rowSummedRisks / float64(age - rowInitialAge + 1) + + if (rowInitialAge >= age){ + return nil, errors.New("rowInitialAge is >= age.") + } + + startAgeString := helpers.ConvertIntToString(rowInitialAge) + endAgeString := helpers.ConvertIntToString(age) + + averageRiskString := helpers.ConvertFloat64ToStringRounded(averageRisk, 2) + + ageRangeLabel := getBoldLabel(startAgeString + " - " + endAgeString) + averageRiskLabel := getBoldLabelCentered(averageRiskString + "%") + + ageColumn.Add(ageRangeLabel) + averageRiskColumn.Add(averageRiskLabel) + + ageColumn.Add(widget.NewSeparator()) + averageRiskColumn.Add(widget.NewSeparator()) + + rowInitialValuesSet = false + } + + averageRiskHelpButton := widget.NewButtonWithIcon("", theme.QuestionIcon(), func(){ + setPolygenicDiseaseAverageLifetimeRiskExplainerPage(window, currentPage) + }) + averageRiskColumn.Add(averageRiskHelpButton) + + ageProbabilitiesGrid := container.NewHBox(layout.NewSpacer(), ageColumn, averageRiskColumn, layout.NewSpacer()) + + ageProbabilitiesTabItem := container.NewTabItem(tabMaleOrFemale, ageProbabilitiesGrid) + + return ageProbabilitiesTabItem, nil + } + + maleTab, err := getTabContainer("Male") + if (err != nil){ + setErrorEncounteredPage(window, err, previousPage) + return + } + femaleTab, err := getTabContainer("Female") + if (err != nil){ + setErrorEncounteredPage(window, err, previousPage) + return + } + + ageProbabilityTabs := container.NewAppTabs(maleTab, femaleTab) + + if (maleOrFemale == "Male"){ + ageProbabilityTabs.Select(maleTab) + } else { + ageProbabilityTabs.Select(femaleTab) + } + + ageProbabilityTabsCentered := getContainerCentered(getAppTabsBoxed(ageProbabilityTabs)) + + page := container.NewVBox(title, backButton, widget.NewSeparator(), description1, description2, widget.NewSeparator(), genomeNameRow, widget.NewSeparator(), ageProbabilityTabsCentered) + + setPageContent(page, window) +} + + + +// This function provides a page to view the polygenic disease loci for a particular genome from a genetic analysis +func setViewPersonGenomePolygenicDiseaseLociPage(window fyne.Window, geneticAnalysisMapList []map[string]string, diseaseName string, genomeIdentifier string, genomeName string, previousPage func()){ + + setLoadingScreen(window, "Loading Polygenic Disease Loci", "Loading disease loci...") + + currentPage := func(){setViewPersonGenomePolygenicDiseaseLociPage(window, geneticAnalysisMapList, diseaseName, genomeIdentifier, genomeName, previousPage)} + + title := getPageTitleCentered("View Disease Loci - " + diseaseName) + + backButton := getBackButtonCentered(previousPage) + + description1 := widget.NewLabel("Below are the disease loci results for this genome.") + lociHelpButton := widget.NewButtonWithIcon("", theme.QuestionIcon(), func(){ + setPolygenicDiseaseLociExplainerPage(window, currentPage) + }) + description1Row := container.NewHBox(layout.NewSpacer(), description1, lociHelpButton, layout.NewSpacer()) + + getGenomeNameRow := func()*fyne.Container{ + + genomeLabel := widget.NewLabel("Genome:") + genomeNameLabel := getBoldLabel(genomeName) + + if (genomeName == "Only Exclude Conflicts" || genomeName == "Only Include Shared"){ + genomeHelpButton := widget.NewButtonWithIcon("", theme.QuestionIcon(), func(){ + setCombinedGenomesExplainerPage(window, currentPage) + }) + genomeNameRow := container.NewHBox(layout.NewSpacer(), genomeLabel, genomeNameLabel, genomeHelpButton, layout.NewSpacer()) + return genomeNameRow + } + genomeNameRow := container.NewHBox(layout.NewSpacer(), genomeLabel, genomeNameLabel, layout.NewSpacer()) + return genomeNameRow + } + + genomeNameRow := getGenomeNameRow() + + diseaseLociMap, err := polygenicDiseases.GetPolygenicDiseaseLociMap(diseaseName) + if (err != nil){ + setErrorEncounteredPage(window, err, previousPage) + return + } + + numberOfLociTested := 0 + + lociList_PositiveWeight := make([]string, 0) + lociList_ZeroWeight := make([]string, 0) + lociList_NegativeWeight := make([]string, 0) + lociList_UnknownWeight := make([]string, 0) + + for locusIdentifier, _ := range diseaseLociMap{ + + locusRiskWeightIsKnown, genomeLocusRiskWeight, _, _, _, _, err := readGeneticAnalysis.GetPersonPolygenicDiseaseLocusInfoFromGeneticAnalysis(geneticAnalysisMapList, diseaseName, locusIdentifier, genomeIdentifier) + if (err != nil) { + setErrorEncounteredPage(window, err, previousPage) + return + } + if (locusRiskWeightIsKnown == false){ + lociList_UnknownWeight = append(lociList_UnknownWeight, locusIdentifier) + continue + } + numberOfLociTested += 1 + + if (genomeLocusRiskWeight > 0){ + + lociList_PositiveWeight = append(lociList_PositiveWeight, locusIdentifier) + + } else if (genomeLocusRiskWeight == 0) { + + lociList_ZeroWeight = append(lociList_ZeroWeight, locusIdentifier) + + } else { + // genomeLocusRiskWeight < 0 + lociList_NegativeWeight = append(lociList_NegativeWeight, locusIdentifier) + } + } + + numberOfLociTestedString := helpers.ConvertIntToString(numberOfLociTested) + + totalNumberOfLoci := len(diseaseLociMap) + totalNumberOfLociString := helpers.ConvertIntToString(totalNumberOfLoci) + + lociTestedLabel := widget.NewLabel("Loci Tested:") + lociTestedText := getBoldLabel(numberOfLociTestedString + "/" + totalNumberOfLociString) + lociTestedHelpButton := widget.NewButtonWithIcon("", theme.QuestionIcon(), func(){ + setPolygenicDiseaseNumberOfLociTestedExplainerPage(window, currentPage) + }) + + lociTestedRow := container.NewHBox(layout.NewSpacer(), lociTestedLabel, lociTestedText, lociTestedHelpButton, layout.NewSpacer()) + + getDiseaseLociGrid := func()(*fyne.Container, error){ + + locusNameLabel := getItalicLabelCentered("Locus Name") + riskWeightLabel := getItalicLabelCentered("Risk Weight") + oddsRatioLabel := getItalicLabelCentered("Odds Ratio") + emptyLabel := widget.NewLabel("") + + locusNameColumn := container.NewVBox(locusNameLabel, widget.NewSeparator()) + riskWeightColumn := container.NewVBox(riskWeightLabel, widget.NewSeparator()) + oddsRatioColumn := container.NewVBox(oddsRatioLabel, widget.NewSeparator()) + viewButtonsColumn := container.NewVBox(emptyLabel, widget.NewSeparator()) + + addLocusRow := func(locusIdentifier string)error{ + + diseaseLocusObject, exists := diseaseLociMap[locusIdentifier] + if (exists == false) { + return errors.New("Cannot add locusRow: diseaseLociMap missing locus: " + locusIdentifier) + } + + locusRSID := diseaseLocusObject.LocusRSID + locusRSIDString := helpers.ConvertInt64ToString(locusRSID) + locusName := "rs" + locusRSIDString + + locusNameLabel := getBoldLabelCentered(locusName) + + locusRiskWeightIsKnown, genomeLocusRiskWeight, _, locusOddsRatioIsKnown, _, locusOddsRatioFormatted, err := readGeneticAnalysis.GetPersonPolygenicDiseaseLocusInfoFromGeneticAnalysis(geneticAnalysisMapList, diseaseName, locusIdentifier, genomeIdentifier) + if (err != nil) { return err } + + getGenomeLocusRiskWeightText := func()string{ + if (locusRiskWeightIsKnown == false){ + + result := translate("Unknown") + return result + } + locusRiskWeightString := helpers.ConvertIntToString(genomeLocusRiskWeight) + + return locusRiskWeightString + } + + locusRiskWeightText := getGenomeLocusRiskWeightText() + locusRiskWeightLabel := getBoldLabelCentered(locusRiskWeightText) + + getOddsRatioText := func()string{ + + if (locusOddsRatioIsKnown == false){ + result := translate("Unknown") + return result + } + + return locusOddsRatioFormatted + } + + locusOddsRatioText := getOddsRatioText() + genomeLocusOddsRatioLabel := getBoldLabelCentered(locusOddsRatioText) + + viewLocusButton := widget.NewButtonWithIcon("", theme.VisibilityIcon(), func(){ + setViewPersonGeneticAnalysisPolygenicDiseaseLocusDetailsPage(window, geneticAnalysisMapList, diseaseName, locusIdentifier, currentPage) + }) + + locusNameColumn.Add(locusNameLabel) + riskWeightColumn.Add(locusRiskWeightLabel) + oddsRatioColumn.Add(genomeLocusOddsRatioLabel) + viewButtonsColumn.Add(viewLocusButton) + + locusNameColumn.Add(widget.NewSeparator()) + riskWeightColumn.Add(widget.NewSeparator()) + oddsRatioColumn.Add(widget.NewSeparator()) + viewButtonsColumn.Add(widget.NewSeparator()) + + return nil + } + + // Items within each group are sorted so they will display the same way whenever user refreshes page + + helpers.SortStringListToUnicodeOrder(lociList_PositiveWeight) + helpers.SortStringListToUnicodeOrder(lociList_NegativeWeight) + helpers.SortStringListToUnicodeOrder(lociList_ZeroWeight) + helpers.SortStringListToUnicodeOrder(lociList_UnknownWeight) + + for _, locusIdentifier := range lociList_PositiveWeight{ + + err = addLocusRow(locusIdentifier) + if (err != nil) { return nil, err } + } + for _, locusIdentifier := range lociList_NegativeWeight{ + + err = addLocusRow(locusIdentifier) + if (err != nil) { return nil, err } + } + for _, locusIdentifier := range lociList_ZeroWeight{ + + err = addLocusRow(locusIdentifier) + if (err != nil) { return nil, err } + } + for _, locusIdentifier := range lociList_UnknownWeight{ + + err = addLocusRow(locusIdentifier) + if (err != nil) { return nil, err } + } + + + riskWeightHelpButton := widget.NewButtonWithIcon("", theme.QuestionIcon(), func(){ + setPolygenicDiseaseLocusRiskWeightExplainerPage(window, currentPage) + }) + riskWeightColumn.Add(riskWeightHelpButton) + + oddsRatioHelpButton := widget.NewButtonWithIcon("", theme.QuestionIcon(), func(){ + //TODO + showUnderConstructionDialog(window) + }) + + oddsRatioColumn.Add(oddsRatioHelpButton) + + diseaseLociGrid := container.NewHBox(layout.NewSpacer(), locusNameColumn, riskWeightColumn, oddsRatioColumn, viewButtonsColumn, layout.NewSpacer()) + + return diseaseLociGrid, nil + } + + diseaseLociGrid, err := getDiseaseLociGrid() + if (err != nil){ + setErrorEncounteredPage(window, err, previousPage) + return + } + + page := container.NewVBox(title, backButton, widget.NewSeparator(), description1Row, widget.NewSeparator(), genomeNameRow, widget.NewSeparator(), lociTestedRow, widget.NewSeparator(), diseaseLociGrid) + + setPageContent(page, window) +} + + +// This function provides a page to view the details of a specific locus from a person genetic analysis +// It will show the locus details for all of the person's genomes +func setViewPersonGeneticAnalysisPolygenicDiseaseLocusDetailsPage(window fyne.Window, geneticAnalysisMapList []map[string]string, diseaseName string, locusIdentifier string, previousPage func()){ + + currentPage := func(){setViewPersonGeneticAnalysisPolygenicDiseaseLocusDetailsPage(window, geneticAnalysisMapList, diseaseName, locusIdentifier, previousPage)} + + title := getPageTitleCentered("Disease Locus Details - " + diseaseName) + + backButton := getBackButtonCentered(previousPage) + + locusObject, err := polygenicDiseases.GetPolygenicDiseaseLocusObject(diseaseName, locusIdentifier) + if (err != nil){ + setErrorEncounteredPage(window, err, previousPage) + return + } + + locusRSID := locusObject.LocusRSID + locusRSIDString := helpers.ConvertInt64ToString(locusRSID) + locusName := "rs" + locusRSIDString + + description := getLabelCentered("Below is the locus result for the person's genomes.") + + locusNameLabel := widget.NewLabel("Locus Name:") + locusNameText := getBoldLabel(locusName) + locusInfoButton := widget.NewButtonWithIcon("", theme.InfoIcon(), func(){ + setViewPolygenicDiseaseLocusDetailsPage(window, diseaseName, locusIdentifier, currentPage) + }) + locusNameRow := container.NewHBox(layout.NewSpacer(), locusNameLabel, locusNameText, locusInfoButton, layout.NewSpacer()) + + getGenomesLocusInfoGrid := func()(*fyne.Container, error){ + + allRawGenomeIdentifiersList, multipleGenomesExist, onlyExcludeConflictsGenomeIdentifier, onlyIncludeSharedGenomeIdentifier, err := readGeneticAnalysis.GetMetadataFromPersonGeneticAnalysis(geneticAnalysisMapList) + if (err != nil) { return nil, err } + + genomeNameLabel := getItalicLabelCentered("Genome Name") + riskWeightLabel := getItalicLabelCentered("Risk Weight") + oddsRatioLabel := getItalicLabelCentered("Odds Ratio") + + genomeNameColumn := container.NewVBox(genomeNameLabel, widget.NewSeparator()) + riskWeightColumn := container.NewVBox(riskWeightLabel, widget.NewSeparator()) + oddsRatioColumn := container.NewVBox(oddsRatioLabel, widget.NewSeparator()) + + addGenomeRow := func(genomeName string, genomeIdentifier string, isACombinedGenome bool)error{ + + genomeRiskWeightKnown, genomeRiskWeight, _, genomeOddsRatioKnown, _, genomeOddsRatioFormatted, err := readGeneticAnalysis.GetPersonPolygenicDiseaseLocusInfoFromGeneticAnalysis(geneticAnalysisMapList, diseaseName, locusIdentifier, genomeIdentifier) + if (err != nil) { return err } + + getGenomeRiskWeightText := func()string{ + + if (genomeRiskWeightKnown == false){ + result := translate("Unknown") + + return result + } + + genomeRiskWeightString := helpers.ConvertIntToString(genomeRiskWeight) + return genomeRiskWeightString + } + + genomeRiskWeightText := getGenomeRiskWeightText() + + getGenomeOddsRatioText := func()string{ + + if (genomeOddsRatioKnown == false){ + result := translate("Unknown") + + return result + } + + return genomeOddsRatioFormatted + } + + genomeOddsRatioText := getGenomeOddsRatioText() + + getGenomeNameCell := func()*fyne.Container{ + if (isACombinedGenome == false){ + + genomeNameLabel := getBoldLabelCentered(genomeName) + return genomeNameLabel + } + viewHelpButton := widget.NewButtonWithIcon("", theme.QuestionIcon(), func(){ + setCombinedGenomesExplainerPage(window, currentPage) + }) + + genomeNameLabel := getBoldLabel(genomeName) + genomeNameCell := container.NewHBox(layout.NewSpacer(), viewHelpButton, genomeNameLabel, layout.NewSpacer()) + + return genomeNameCell + } + + genomeNameCell := getGenomeNameCell() + genomeNameColumn.Add(genomeNameCell) + + riskWeightLabel := getBoldLabelCentered(genomeRiskWeightText) + riskWeightColumn.Add(riskWeightLabel) + + oddsRatioLabel := getBoldLabelCentered(genomeOddsRatioText) + oddsRatioColumn.Add(oddsRatioLabel) + + genomeNameColumn.Add(widget.NewSeparator()) + riskWeightColumn.Add(widget.NewSeparator()) + oddsRatioColumn.Add(widget.NewSeparator()) + + return nil + } + + if (multipleGenomesExist == true){ + + err := addGenomeRow("Only Exclude Conflicts", onlyExcludeConflictsGenomeIdentifier, true) + if (err != nil){ return nil, err } + + err = addGenomeRow("Only Include Shared", onlyIncludeSharedGenomeIdentifier, true) + if (err != nil){ return nil, err } + } + + for _, genomeIdentifier := range allRawGenomeIdentifiersList{ + + getGenomeName := func()(string, error){ + + genomeFound, _, timeGenomeWasExported, _, _, _, companyName, _, _, err := myGenomes.GetMyRawGenomeMetadata(genomeIdentifier) + if (err != nil) { return "", err } + if (genomeFound == false){ + return "", errors.New("MyGenomeInfo for genome from analysisMapList not found.") + } + + if (multipleGenomesExist == false){ + return companyName, nil + } + + // We show the date that the genome was exported + + exportTimeAgo, err := helpers.ConvertUnixTimeToTimeAgoTranslated(timeGenomeWasExported, false) + if (err != nil){ return "", err } + + genomeName := companyName + " (Exported " + exportTimeAgo + ")" + + return genomeName, nil + } + + genomeName, err := getGenomeName() + if (err != nil) { return nil, err } + + err = addGenomeRow(genomeName, genomeIdentifier, false) + if (err != nil){ return nil, err } + } + + riskWeightHelpButton := widget.NewButtonWithIcon("", theme.QuestionIcon(), func(){ + setPolygenicDiseaseLocusRiskWeightExplainerPage(window, currentPage) + }) + riskWeightColumn.Add(riskWeightHelpButton) + + oddsRatioHelpButton := widget.NewButtonWithIcon("", theme.QuestionIcon(), func(){ + //TODO + showUnderConstructionDialog(window) + }) + oddsRatioColumn.Add(oddsRatioHelpButton) + + genomesContainer := container.NewHBox(layout.NewSpacer(), genomeNameColumn, riskWeightColumn, oddsRatioColumn, layout.NewSpacer()) + + return genomesContainer, nil + } + + genomesLocusInfoGrid, err := getGenomesLocusInfoGrid() + if (err != nil){ + setErrorEncounteredPage(window, err, previousPage) + return + } + + page := container.NewVBox(title, backButton, widget.NewSeparator(), description, widget.NewSeparator(), locusNameRow, widget.NewSeparator(), genomesLocusInfoGrid) + + setPageContent(page, window) +} + + +func setViewPersonGeneticAnalysisTraitsPage(window fyne.Window, personIdentifier string, analysisMapList []map[string]string, previousPage func()){ + + currentPage := func(){setViewPersonGeneticAnalysisTraitsPage(window, personIdentifier, analysisMapList, previousPage)} + + title := getPageTitleCentered("Viewing Genetic Analysis - Traits") + + backButton := getBackButtonCentered(previousPage) + + description := getLabelCentered("Below is an analysis of the traits for this person's genome.") + + getTraitsContainer := func()(*fyne.Container, error){ + + allRawGenomeIdentifiersList, multipleGenomesExist, onlyExcludeConflictsGenomeIdentifier, _, err := readGeneticAnalysis.GetMetadataFromPersonGeneticAnalysis(analysisMapList) + if (err != nil){ return nil, err } + + // Outputs: + // -string: Main genome identifier (Is either the combined Only exclude conflicts genome or the only genome) + // -error + getMainGenomeIdentifier := func()(string, error){ + + if (multipleGenomesExist == true){ + return onlyExcludeConflictsGenomeIdentifier, nil + } + // Only 1 genome exists + + genomeIdentifier := allRawGenomeIdentifiersList[0] + + return genomeIdentifier, nil + } + + mainGenomeIdentifier, err := getMainGenomeIdentifier() + if (err != nil){ return nil, err } + + traitNameLabel := getItalicLabelCentered("Trait Name") + + outcomeScoresLabel := getItalicLabelCentered("Outcome Scores") + + conflictExistsLabel := getItalicLabelCentered("Conflict Exists?") + + emptyLabel := widget.NewLabel("") + + traitNameColumn := container.NewVBox(traitNameLabel, widget.NewSeparator()) + outcomeScoresColumn := container.NewVBox(outcomeScoresLabel, widget.NewSeparator()) + conflictExistsColumn := container.NewVBox(conflictExistsLabel, widget.NewSeparator()) + viewButtonsColumn := container.NewVBox(emptyLabel, widget.NewSeparator()) + + traitObjectsList, err := traits.GetTraitObjectsList() + if (err != nil) { return nil, err } + + for _, traitObject := range traitObjectsList{ + + traitName := traitObject.TraitName + traitRulesList := traitObject.RulesList + + if (len(traitRulesList) == 0){ + // This trait does not have any rules + // We cannot analyze it yet + // We will add neural network prediction so we can predict these traits + continue + } + traitOutcomeNamesList := traitObject.OutcomesList + + _, anyTraitRuleTested, outcomeScoresMap, _, conflictExists, err := readGeneticAnalysis.GetPersonTraitInfoFromGeneticAnalysis(analysisMapList, traitName, mainGenomeIdentifier, multipleGenomesExist) + if (err != nil) { return nil, err } + + // We add all of the columns except for the trait outcomes column, which may be multiple rows high + + conflictExistsString := helpers.ConvertBoolToYesOrNoString(conflictExists) + conflictExistsLabel := getBoldLabelCentered(conflictExistsString) + + viewDetailsButton := getWidgetCentered(widget.NewButtonWithIcon("", theme.VisibilityIcon(), func(){ + setViewPersonGeneticAnalysisTraitDetailsPage(window, personIdentifier, analysisMapList, traitName, currentPage) + })) + + traitNameText := getBoldLabelCentered(traitName) + + traitNameColumn.Add(traitNameText) + conflictExistsColumn.Add(conflictExistsLabel) + viewButtonsColumn.Add(viewDetailsButton) + + if (anyTraitRuleTested == false){ + + unknownTranslated := translate("Unknown") + unknownLabel := getBoldLabelCentered(unknownTranslated) + + outcomeScoresColumn.Add(unknownLabel) + } else { + + // We have to sort outcome names so they always show up in the same order + + traitOutcomeNamesListSorted := helpers.CopyAndSortStringListToUnicodeOrder(traitOutcomeNamesList) + + for index, outcomeName := range traitOutcomeNamesListSorted{ + + outcomeScore, exists := outcomeScoresMap[outcomeName] + if (exists == false){ + return nil, errors.New("Outcome name not found in outcomeScoresMap: " + outcomeName) + } + + outcomeScoreString := helpers.ConvertIntToString(outcomeScore) + + outcomeRow := getBoldLabelCentered(outcomeName + ": " + outcomeScoreString) + outcomeScoresColumn.Add(outcomeRow) + + if (index > 0){ + + emptyLabelA := widget.NewLabel("") + emptyLabelB := widget.NewLabel("") + emptyLabelC := widget.NewLabel("") + + traitNameColumn.Add(emptyLabelA) + conflictExistsColumn.Add(emptyLabelB) + viewButtonsColumn.Add(emptyLabelC) + } + } + } + + traitNameColumn.Add(widget.NewSeparator()) + outcomeScoresColumn.Add(widget.NewSeparator()) + conflictExistsColumn.Add(widget.NewSeparator()) + viewButtonsColumn.Add(widget.NewSeparator()) + } + + outcomeScoresHelpButton := widget.NewButtonWithIcon("", theme.QuestionIcon(), func(){ + setTraitOutcomeScoresExplainerPage(window, currentPage) + }) + outcomeScoresColumn.Add(outcomeScoresHelpButton) + + traitsContainer := container.NewHBox(layout.NewSpacer(), traitNameColumn, outcomeScoresColumn) + + if (multipleGenomesExist == true){ + + conflictExistsHelpButton := widget.NewButtonWithIcon("", theme.QuestionIcon(), func(){ + setPersonGeneticAnalysisConflictExistsExplainerPage(window, currentPage) + }) + + conflictExistsColumn.Add(conflictExistsHelpButton) + traitsContainer.Add(conflictExistsColumn) + } + + traitsContainer.Add(viewButtonsColumn) + traitsContainer.Add(layout.NewSpacer()) + + return traitsContainer, nil + } + + traitsContainer, err := getTraitsContainer() + if (err != nil){ + setErrorEncounteredPage(window, err, previousPage) + return + } + + page := container.NewVBox(title, backButton, widget.NewSeparator(), description, widget.NewSeparator(), traitsContainer) + + setPageContent(page, window) +} + + + +func setViewPersonGeneticAnalysisTraitDetailsPage(window fyne.Window, personIdentifier string, analysisMapList []map[string]string, traitName string, previousPage func()){ + + currentPage := func(){setViewPersonGeneticAnalysisTraitDetailsPage(window, personIdentifier, analysisMapList, traitName, previousPage)} + + title := getPageTitleCentered("Viewing Genetic Analysis - " + traitName) + + backButton := getBackButtonCentered(previousPage) + + allRawGenomeIdentifiersList, multipleGenomesExist, onlyExcludeConflictsGenomeIdentifier, onlyIncludeSharedGenomeIdentifier, err := readGeneticAnalysis.GetMetadataFromPersonGeneticAnalysis(analysisMapList) + if (err != nil) { + setErrorEncounteredPage(window, err, previousPage) + return + } + + getDescriptionSection := func()*fyne.Container{ + + if (multipleGenomesExist == false){ + description := getLabelCentered("Below is the trait information for this person's genome.") + + return description + } + + description1 := getLabelCentered("Below is the trait information for this person's genomes.") + description2 := getLabelCentered("The first two genomes are created by combining multiple genomes.") + + descriptionsSection := container.NewVBox(description1, description2) + + return descriptionsSection + } + + descriptionSection := getDescriptionSection() + + traitNameLabel := widget.NewLabel("Trait:") + traitNameText := getBoldLabel(traitName) + traitInfoButton := widget.NewButtonWithIcon("", theme.InfoIcon(), func(){ + setViewTraitDetailsPage(window, traitName, currentPage) + }) + traitNameRow := container.NewHBox(layout.NewSpacer(), traitNameLabel, traitNameText, traitInfoButton, layout.NewSpacer()) + + getGenomesContainer := func()(*fyne.Container, error){ + + emptyLabelA := widget.NewLabel("") + genomeNameLabel := getItalicLabelCentered("Genome Name") + + emptyLabelB := widget.NewLabel("") + outcomeScoresLabel := getItalicLabelCentered("Outcome Scores") + + numberOfLabel := getItalicLabelCentered("Number of") + rulesTestedLabel := getItalicLabelCentered("Rules Tested") + + emptyLabelD := widget.NewLabel("") + emptyLabelE := widget.NewLabel("") + + genomeNameColumn := container.NewVBox(emptyLabelA, genomeNameLabel, widget.NewSeparator()) + outcomeScoresColumn := container.NewVBox(emptyLabelB, outcomeScoresLabel, widget.NewSeparator()) + numberOfRulesTestedColumn := container.NewVBox(numberOfLabel, rulesTestedLabel, widget.NewSeparator()) + viewRulesButtonsColumn := container.NewVBox(emptyLabelD, emptyLabelE, widget.NewSeparator()) + + addGenomeRow := func(genomeName string, genomeIdentifier string, isACombinedGenome bool)error{ + + getGenomeNameCell := func()*fyne.Container{ + if (isACombinedGenome == false){ + + genomeNameLabel := getBoldLabelCentered(genomeName) + return genomeNameLabel + } + viewHelpButton := widget.NewButtonWithIcon("", theme.QuestionIcon(), func(){ + setCombinedGenomesExplainerPage(window, currentPage) + }) + + genomeNameLabel := getBoldLabel(genomeName) + genomeNameCell := container.NewHBox(layout.NewSpacer(), viewHelpButton, genomeNameLabel, layout.NewSpacer()) + + return genomeNameCell + } + + genomeNameCell := getGenomeNameCell() + + _, anyTraitRuleTested, outcomeScoresMap, numberOfRulesTested, _, err := readGeneticAnalysis.GetPersonTraitInfoFromGeneticAnalysis(analysisMapList, traitName, genomeIdentifier, multipleGenomesExist) + if (err != nil) { return err } + + // We add all of the columns except for the trait rule column, which may be multiple rows high + + genomeNumberOfRulesTestedString := helpers.ConvertIntToString(numberOfRulesTested) + numberOfRulesTestedLabel := getBoldLabelCentered(genomeNumberOfRulesTestedString) + + viewRulesButton := widget.NewButtonWithIcon("", theme.VisibilityIcon(), func(){ + setViewPersonGenomeTraitRulesPage(window, analysisMapList, traitName, genomeIdentifier, genomeName, currentPage) + }) + + genomeNameColumn.Add(genomeNameCell) + numberOfRulesTestedColumn.Add(numberOfRulesTestedLabel) + viewRulesButtonsColumn.Add(viewRulesButton) + + if (anyTraitRuleTested == false){ + + unknownTranslated := translate("Unknown") + unknownLabel := getBoldLabelCentered(unknownTranslated) + + outcomeScoresColumn.Add(unknownLabel) + + } else { + + // We have to sort the outcome names so they always show up in the same order + + outcomeNamesList := helpers.GetListOfMapKeys(outcomeScoresMap) + + helpers.SortStringListToUnicodeOrder(outcomeNamesList) + + for index, outcomeName := range outcomeNamesList{ + + outcomeScore, exists := outcomeScoresMap[outcomeName] + if (exists == false){ + return errors.New("Outcome name not found in outcome scores map after being found already: " + outcomeName) + } + + outcomeScoreString := helpers.ConvertIntToString(outcomeScore) + + outcomeRow := getBoldLabelCentered(outcomeName + ": " + outcomeScoreString) + outcomeScoresColumn.Add(outcomeRow) + + if (index > 0){ + + emptyLabelA := widget.NewLabel("") + emptyLabelB := widget.NewLabel("") + emptyLabelC := widget.NewLabel("") + + genomeNameColumn.Add(emptyLabelA) + numberOfRulesTestedColumn.Add(emptyLabelB) + viewRulesButtonsColumn.Add(emptyLabelC) + } + } + } + + genomeNameColumn.Add(widget.NewSeparator()) + outcomeScoresColumn.Add(widget.NewSeparator()) + numberOfRulesTestedColumn.Add(widget.NewSeparator()) + viewRulesButtonsColumn.Add(widget.NewSeparator()) + + return nil + } + + if (multipleGenomesExist == true){ + + err := addGenomeRow("Only Exclude Conflicts", onlyExcludeConflictsGenomeIdentifier, true) + if (err != nil){ return nil, err } + + err = addGenomeRow("Only Include Shared", onlyIncludeSharedGenomeIdentifier, true) + if (err != nil){ return nil, err } + } + + for _, genomeIdentifier := range allRawGenomeIdentifiersList{ + + getGenomeName := func()(string, error){ + + genomeFound, _, timeGenomeWasExported, _, _, _, companyName, _, _, err := myGenomes.GetMyRawGenomeMetadata(genomeIdentifier) + if (err != nil) { return "", err } + if (genomeFound == false){ + return "", errors.New("MyGenomeInfo for genome from analysisMapList not found.") + } + + if (multipleGenomesExist == false){ + return companyName, nil + } + + // We show the date that the genome was exported + + exportTimeAgo, err := helpers.ConvertUnixTimeToTimeAgoTranslated(timeGenomeWasExported, false) + if (err != nil){ return "", err } + + genomeName := companyName + " (Exported " + exportTimeAgo + ")" + + return genomeName, nil + } + + genomeName, err := getGenomeName() + if (err != nil) { return nil, err } + + err = addGenomeRow(genomeName, genomeIdentifier, false) + if (err != nil){ return nil, err } + } + + outcomeScoresHelpButton := widget.NewButtonWithIcon("", theme.QuestionIcon(), func(){ + setTraitOutcomeScoresExplainerPage(window, currentPage) + }) + outcomeScoresColumn.Add(outcomeScoresHelpButton) + + numberOfRulesTestedHelpButton := widget.NewButtonWithIcon("", theme.QuestionIcon(), func(){ + setTraitNumberOfRulesTestedExplainerPage(window, currentPage) + }) + numberOfRulesTestedColumn.Add(numberOfRulesTestedHelpButton) + + genomesContainer := container.NewHBox(layout.NewSpacer(), genomeNameColumn, outcomeScoresColumn, numberOfRulesTestedColumn, viewRulesButtonsColumn, layout.NewSpacer()) + + return genomesContainer, nil + } + + genomesContainer, err := getGenomesContainer() + if (err != nil){ + setErrorEncounteredPage(window, err, previousPage) + return + } + + page := container.NewVBox(title, backButton, widget.NewSeparator(), descriptionSection, widget.NewSeparator(), traitNameRow, widget.NewSeparator(), genomesContainer) + + setPageContent(page, window) +} + + + +// Ths function provides a page to view the trait rules for a particular genome from a genetic analysis +func setViewPersonGenomeTraitRulesPage(window fyne.Window, geneticAnalysisMapList []map[string]string, traitName string, genomeIdentifier string, genomeName string, previousPage func()){ + + setLoadingScreen(window, "Loading Trait Rules", "Loading trait rules...") + + currentPage := func(){setViewPersonGenomeTraitRulesPage(window, geneticAnalysisMapList, traitName, genomeIdentifier, genomeName, previousPage)} + + title := getPageTitleCentered("View Trait Rules - " + traitName) + + backButton := getBackButtonCentered(previousPage) + + description1 := widget.NewLabel("Below are the trait rule results for this genome.") + rulesHelpButton := widget.NewButtonWithIcon("", theme.QuestionIcon(), func(){ + setTraitRulesExplainerPage(window, currentPage) + }) + description1Row := container.NewHBox(layout.NewSpacer(), description1, rulesHelpButton, layout.NewSpacer()) + + getGenomeNameRow := func()*fyne.Container{ + + genomeLabel := widget.NewLabel("Genome:") + genomeNameLabel := getBoldLabel(genomeName) + + if (genomeName == "Only Exclude Conflicts" || genomeName == "Only Include Shared"){ + genomeHelpButton := widget.NewButtonWithIcon("", theme.QuestionIcon(), func(){ + setCombinedGenomesExplainerPage(window, currentPage) + }) + genomeNameRow := container.NewHBox(layout.NewSpacer(), genomeLabel, genomeNameLabel, genomeHelpButton, layout.NewSpacer()) + return genomeNameRow + } + genomeNameRow := container.NewHBox(layout.NewSpacer(), genomeLabel, genomeNameLabel, layout.NewSpacer()) + return genomeNameRow + } + + genomeNameRow := getGenomeNameRow() + + traitRulesMap, err := traits.GetTraitRulesMap(traitName) + if (err != nil){ + setErrorEncounteredPage(window, err, previousPage) + return + } + + numberOfRulesTested := 0 + + rulesList_RulePassed := make([]string, 0) + rulesList_RuleNotPassed := make([]string, 0) + rulesList_Unknown := make([]string, 0) + + for ruleIdentifier, _ := range traitRulesMap{ + + ruleStatusIsKnown, genomePassesRule, _, err := readGeneticAnalysis.GetPersonTraitRuleInfoFromGeneticAnalysis(geneticAnalysisMapList, traitName, ruleIdentifier, genomeIdentifier) + if (err != nil){ + setErrorEncounteredPage(window, err, previousPage) + return + } + if (ruleStatusIsKnown == false){ + rulesList_Unknown = append(rulesList_Unknown, ruleIdentifier) + continue + } + numberOfRulesTested += 1 + + if (genomePassesRule == true){ + rulesList_RulePassed = append(rulesList_RulePassed, ruleIdentifier) + } else { + rulesList_RuleNotPassed = append(rulesList_RuleNotPassed, ruleIdentifier) + } + } + + numberOfRulesTestedString := helpers.ConvertIntToString(numberOfRulesTested) + + totalNumberOfRules := len(traitRulesMap) + totalNumberOfRulesString := helpers.ConvertIntToString(totalNumberOfRules) + + rulesTestedLabel := widget.NewLabel("Rules Tested:") + rulesTestedText := getBoldLabel(numberOfRulesTestedString + "/" + totalNumberOfRulesString) + rulesTestedHelpButton := widget.NewButtonWithIcon("", theme.QuestionIcon(), func(){ + setTraitNumberOfRulesTestedExplainerPage(window, currentPage) + }) + + rulesTestedRow := container.NewHBox(layout.NewSpacer(), rulesTestedLabel, rulesTestedText, rulesTestedHelpButton, layout.NewSpacer()) + + getTraitRulesGrid := func()(*fyne.Container, error){ + + ruleIdentifierLabel := getItalicLabelCentered("Rule Identifier") + ruleEffectsLabel := getItalicLabelCentered("Rule Effects") + genomePassesRuleLabel := getItalicLabelCentered("Genome Passes Rule") + emptyLabel := widget.NewLabel("") + + ruleIdentifierColumn := container.NewVBox(ruleIdentifierLabel, widget.NewSeparator()) + ruleEffectsColumn := container.NewVBox(ruleEffectsLabel, widget.NewSeparator()) + genomePassesRuleColumn := container.NewVBox(genomePassesRuleLabel, widget.NewSeparator()) + viewButtonsColumn := container.NewVBox(emptyLabel, widget.NewSeparator()) + + addRuleRow := func(ruleIdentifier string)error{ + + ruleIdentifierLabel := getBoldLabelCentered(ruleIdentifier) + + ruleStatusIsKnown, genomePassesRule, _, err := readGeneticAnalysis.GetPersonTraitRuleInfoFromGeneticAnalysis(geneticAnalysisMapList, traitName, ruleIdentifier, genomeIdentifier) + if (err != nil) { return err } + + getGenomePassesRuleText := func()string{ + if (ruleStatusIsKnown == false){ + + result := translate("Unknown") + return result + } + genomePassesRuleString := helpers.ConvertBoolToYesOrNoString(genomePassesRule) + genomePassesRuleTranslated := translate(genomePassesRuleString) + + return genomePassesRuleTranslated + } + + genomePassesRuleText := getGenomePassesRuleText() + genomePassesRuleLabel := getBoldLabelCentered(genomePassesRuleText) + + // We add all of the columns except for the rule effects column + // We do this because the rule effects column may be multiple rows tall + + viewRuleButton := widget.NewButtonWithIcon("", theme.VisibilityIcon(), func(){ + setViewPersonGeneticAnalysisTraitRuleDetailsPage(window, geneticAnalysisMapList, traitName, ruleIdentifier, currentPage) + }) + + ruleIdentifierColumn.Add(ruleIdentifierLabel) + genomePassesRuleColumn.Add(genomePassesRuleLabel) + viewButtonsColumn.Add(viewRuleButton) + + traitRuleObject, exists := traitRulesMap[ruleIdentifier] + if (exists == false){ + return errors.New("Trait rule not found after being found already.") + } + + ruleOutcomePointsMap := traitRuleObject.OutcomePointsMap + + outcomeNamesList := helpers.GetListOfMapKeys(ruleOutcomePointsMap) + + // We have to sort the outcome names so they always show up in the same order + helpers.SortStringListToUnicodeOrder(outcomeNamesList) + + for index, outcomeName := range outcomeNamesList{ + + outcomeEffectInt, exists := ruleOutcomePointsMap[outcomeName] + if (exists == false){ + return errors.New("OutcomeName not found in ruleOutcomePointsMap after being found already: " + outcomeName) + } + + getOutcomeEffectString := func()string{ + + outcomeEffectString := helpers.ConvertIntToString(outcomeEffectInt) + + if (outcomeEffectInt < 0){ + return outcomeEffectString + } + outcomeEffect := "+" + outcomeEffectString + return outcomeEffect + } + + outcomeEffect := getOutcomeEffectString() + + outcomeRow := getBoldLabelCentered(outcomeName + ": " + outcomeEffect) + ruleEffectsColumn.Add(outcomeRow) + + if (index > 0){ + + emptyLabelA := widget.NewLabel("") + emptyLabelB := widget.NewLabel("") + emptyLabelC := widget.NewLabel("") + + ruleIdentifierColumn.Add(emptyLabelA) + genomePassesRuleColumn.Add(emptyLabelB) + viewButtonsColumn.Add(emptyLabelC) + } + } + + ruleIdentifierColumn.Add(widget.NewSeparator()) + ruleEffectsColumn.Add(widget.NewSeparator()) + genomePassesRuleColumn.Add(widget.NewSeparator()) + viewButtonsColumn.Add(widget.NewSeparator()) + + return nil + } + + // Items within each group are sorted so they will display the same way whenever user refreshes page + + helpers.SortStringListToUnicodeOrder(rulesList_RulePassed) + helpers.SortStringListToUnicodeOrder(rulesList_RuleNotPassed) + helpers.SortStringListToUnicodeOrder(rulesList_Unknown) + + for _, ruleIdentifier := range rulesList_RulePassed{ + + err = addRuleRow(ruleIdentifier) + if (err != nil) { return nil, err } + } + for _, ruleIdentifier := range rulesList_RuleNotPassed{ + + err = addRuleRow(ruleIdentifier) + if (err != nil) { return nil, err } + } + for _, ruleIdentifier := range rulesList_Unknown{ + + err = addRuleRow(ruleIdentifier) + if (err != nil) { return nil, err } + } + + ruleEffectsHelpButton := widget.NewButtonWithIcon("", theme.QuestionIcon(), func(){ + setTraitRuleOutcomeEffectsExplainerPage(window, currentPage) + }) + ruleEffectsColumn.Add(ruleEffectsHelpButton) + + genomePassesRuleHelpButton := widget.NewButtonWithIcon("", theme.QuestionIcon(), func(){ + setGenomePassesTraitRuleExplainerPage(window, currentPage) + }) + + genomePassesRuleColumn.Add(genomePassesRuleHelpButton) + + traitRulesGrid := container.NewHBox(layout.NewSpacer(), ruleIdentifierColumn, ruleEffectsColumn, genomePassesRuleColumn, viewButtonsColumn, layout.NewSpacer()) + + return traitRulesGrid, nil + } + + traitRulesGrid, err := getTraitRulesGrid() + if (err != nil){ + setErrorEncounteredPage(window, err, previousPage) + return + } + + page := container.NewVBox(title, backButton, widget.NewSeparator(), description1Row, widget.NewSeparator(), genomeNameRow, widget.NewSeparator(), rulesTestedRow, widget.NewSeparator(), traitRulesGrid) + + setPageContent(page, window) +} + + + +// This function provides a page to view the details of a specific trait rule from a person genetic analysis +// The page will show the rule details for all of the person's genomes +func setViewPersonGeneticAnalysisTraitRuleDetailsPage(window fyne.Window, geneticAnalysisMapList []map[string]string, traitName string, ruleIdentifier string, previousPage func()){ + + currentPage := func(){setViewPersonGeneticAnalysisTraitRuleDetailsPage(window, geneticAnalysisMapList, traitName, ruleIdentifier, previousPage)} + + title := getPageTitleCentered("Trait Rule Details - " + traitName) + + backButton := getBackButtonCentered(previousPage) + + description := getLabelCentered("Below is the rule result for the person's genomes.") + + ruleIdentifierLabel := widget.NewLabel("Rule Identifier:") + ruleIdentifierText := getBoldLabel(ruleIdentifier) + ruleInfoButton := widget.NewButtonWithIcon("", theme.InfoIcon(), func(){ + setViewTraitRuleDetailsPage(window, traitName, ruleIdentifier, currentPage) + }) + ruleIdentifierRow := container.NewHBox(layout.NewSpacer(), ruleIdentifierLabel, ruleIdentifierText, ruleInfoButton, layout.NewSpacer()) + + getGenomesRuleInfoGrid := func()(*fyne.Container, error){ + + allRawGenomeIdentifiersList, multipleGenomesExist, onlyExcludeConflictsGenomeIdentifier, onlyIncludeSharedGenomeIdentifier, err := readGeneticAnalysis.GetMetadataFromPersonGeneticAnalysis(geneticAnalysisMapList) + if (err != nil) { return nil, err } + + genomeNameLabel := getItalicLabelCentered("Genome Name") + genomePassesRuleLabel := getItalicLabelCentered("Genome Passes Rule") + + genomeNameColumn := container.NewVBox(genomeNameLabel, widget.NewSeparator()) + genomePassesRuleColumn := container.NewVBox(genomePassesRuleLabel, widget.NewSeparator()) + + addGenomeRow := func(genomeName string, genomeIdentifier string, isACombinedGenome bool)error{ + + genomeRuleStatusKnown, genomePassesRule, _, err := readGeneticAnalysis.GetPersonTraitRuleInfoFromGeneticAnalysis(geneticAnalysisMapList, traitName, ruleIdentifier, genomeIdentifier) + if (err != nil) { return err } + + getGenomePassesRuleText := func()string{ + + if (genomeRuleStatusKnown == false){ + result := translate("Unknown") + + return result + } + + genomePassesRuleString := helpers.ConvertBoolToYesOrNoString(genomePassesRule) + + genomePassesRuleTranslated := translate(genomePassesRuleString) + return genomePassesRuleTranslated + } + + genomePassesRuleText := getGenomePassesRuleText() + + getGenomeNameCell := func()*fyne.Container{ + if (isACombinedGenome == false){ + + genomeNameLabel := getBoldLabelCentered(genomeName) + return genomeNameLabel + } + viewHelpButton := widget.NewButtonWithIcon("", theme.QuestionIcon(), func(){ + setCombinedGenomesExplainerPage(window, currentPage) + }) + + genomeNameLabel := getBoldLabel(genomeName) + genomeNameCell := container.NewHBox(layout.NewSpacer(), viewHelpButton, genomeNameLabel, layout.NewSpacer()) + + return genomeNameCell + } + + genomeNameCell := getGenomeNameCell() + genomeNameColumn.Add(genomeNameCell) + + genomePassesRuleLabel := getBoldLabelCentered(genomePassesRuleText) + genomePassesRuleColumn.Add(genomePassesRuleLabel) + + genomeNameColumn.Add(widget.NewSeparator()) + genomePassesRuleColumn.Add(widget.NewSeparator()) + + return nil + } + + if (multipleGenomesExist == true){ + + err := addGenomeRow("Only Exclude Conflicts", onlyExcludeConflictsGenomeIdentifier, true) + if (err != nil){ return nil, err } + + err = addGenomeRow("Only Include Shared", onlyIncludeSharedGenomeIdentifier, true) + if (err != nil){ return nil, err } + } + + for _, genomeIdentifier := range allRawGenomeIdentifiersList{ + + getGenomeName := func()(string, error){ + + genomeFound, _, timeGenomeWasExported, _, _, _, companyName, _, _, err := myGenomes.GetMyRawGenomeMetadata(genomeIdentifier) + if (err != nil) { return "", err } + if (genomeFound == false){ + return "", errors.New("MyGenomeInfo for genome from analysisMapList not found.") + } + + if (multipleGenomesExist == false){ + return companyName, nil + } + + // We show the date that the genome was exported + + exportTimeAgo, err := helpers.ConvertUnixTimeToTimeAgoTranslated(timeGenomeWasExported, false) + if (err != nil){ return "", err } + + genomeName := companyName + " (Exported " + exportTimeAgo + ")" + + return genomeName, nil + } + + genomeName, err := getGenomeName() + if (err != nil) { return nil, err } + + err = addGenomeRow(genomeName, genomeIdentifier, false) + if (err != nil){ return nil, err } + } + + genomePassesRuleHelpButton := widget.NewButtonWithIcon("", theme.QuestionIcon(), func(){ + setGenomePassesTraitRuleExplainerPage(window, currentPage) + }) + genomePassesRuleColumn.Add(genomePassesRuleHelpButton) + + genomesContainer := container.NewHBox(layout.NewSpacer(), genomeNameColumn, genomePassesRuleColumn, layout.NewSpacer()) + + return genomesContainer, nil + } + + genomesRuleInfoGrid, err := getGenomesRuleInfoGrid() + if (err != nil){ + setErrorEncounteredPage(window, err, previousPage) + return + } + + page := container.NewVBox(title, backButton, widget.NewSeparator(), description, widget.NewSeparator(), ruleIdentifierRow, widget.NewSeparator(), genomesRuleInfoGrid) + + setPageContent(page, window) +} + + diff --git a/gui/viewContentGui.go b/gui/viewContentGui.go new file mode 100644 index 0000000..764e02b --- /dev/null +++ b/gui/viewContentGui.go @@ -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) +} + + + diff --git a/gui/viewGeneticReferencesGui.go b/gui/viewGeneticReferencesGui.go new file mode 100644 index 0000000..b585ac5 --- /dev/null +++ b/gui/viewGeneticReferencesGui.go @@ -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) +} + + + + + diff --git a/gui/viewHostsGui.go b/gui/viewHostsGui.go new file mode 100644 index 0000000..d42a5e1 --- /dev/null +++ b/gui/viewHostsGui.go @@ -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) +} + + diff --git a/gui/viewModeratorsGui.go b/gui/viewModeratorsGui.go new file mode 100644 index 0000000..712a1d2 --- /dev/null +++ b/gui/viewModeratorsGui.go @@ -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) +} + + diff --git a/gui/viewProfileGui.go b/gui/viewProfileGui.go new file mode 100644 index 0000000..f1d9093 --- /dev/null +++ b/gui/viewProfileGui.go @@ -0,0 +1,4660 @@ +package gui + +// viewProfileGui.go implements pages to view user profiles + +import "fyne.io/fyne/v2" +import "fyne.io/fyne/v2/canvas" +import "fyne.io/fyne/v2/container" +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/resources/worldLanguages" +import "seekia/resources/worldLocations" +import "seekia/resources/geneticReferences/monogenicDiseases" +import "seekia/resources/geneticReferences/polygenicDiseases" +import "seekia/resources/geneticReferences/traits" + +import "seekia/internal/appMemory" +import "seekia/internal/encoding" +import "seekia/internal/genetics/companyAnalysis" +import "seekia/internal/genetics/createGeneticAnalysis" +import "seekia/internal/genetics/myChosenAnalysis" +import "seekia/internal/genetics/myPeople" +import "seekia/internal/genetics/readGeneticAnalysis" +import "seekia/internal/globalSettings" +import "seekia/internal/helpers" +import "seekia/internal/identity" +import "seekia/internal/imagery" +import "seekia/internal/mateQuestionnaire" +import "seekia/internal/moderation/moderatorRanking" +import "seekia/internal/moderation/trustedViewableStatus" +import "seekia/internal/moderation/verifiedStickyStatus" +import "seekia/internal/myIdentity" +import "seekia/internal/network/appNetworkType/getAppNetworkType" +import "seekia/internal/network/fundedStatus" +import "seekia/internal/network/myBroadcasts" +import "seekia/internal/profiles/attributeDisplay" +import "seekia/internal/profiles/calculatedAttributes" +import "seekia/internal/profiles/myLocalProfiles" +import "seekia/internal/profiles/myProfileExports" +import "seekia/internal/profiles/myProfileStatus" +import "seekia/internal/profiles/profileStorage" +import "seekia/internal/profiles/readProfiles" +import "seekia/internal/profiles/viewableProfiles" + +import "slices" +import "strings" +import "image" +import "time" +import "errors" + +// This page will show user the difference between local/public and let them choose +func setViewMyLocalOrPublicProfilePage(window fyne.Window, myProfileType string, previousPage func()){ + + setLoadingScreen(window, "Loading My Profile", "Loading...") + + if (myProfileType != "Mate" && myProfileType != "Host" && myProfileType != "Moderator"){ + setErrorEncounteredPage(window, errors.New("setViewMyLocalOrPublicProfilePage called with invalid profile type: " + myProfileType), previousPage) + return + } + + currentPage := func(){setViewMyLocalOrPublicProfilePage(window, myProfileType, previousPage)} + + title := getPageTitleCentered(translate("View My Profile")) + + backButton := getBackButtonCentered(previousPage) + + subtitle := getPageSubtitleCentered("View My " + myProfileType + " Profile") + + myIdentityExists, _, err := myIdentity.GetMyIdentityHash(myProfileType) + if (err != nil){ + setErrorEncounteredPage(window, err, previousPage) + return + } + if (myIdentityExists == false){ + + description1 := getBoldLabelCentered("Your " + myProfileType + " identity does not exist.") + description2 := getLabelCentered("Create your " + myProfileType + " identity below.") + + createIdentityButton := getWidgetCentered(widget.NewButtonWithIcon("Create Identity", theme.NavigateNextIcon(), func(){ + setChooseNewIdentityHashPage(window, myProfileType, currentPage, currentPage) + })) + + page := container.NewVBox(title, backButton, widget.NewSeparator(), subtitle, widget.NewSeparator(), description1, description2, createIdentityButton) + setPageContent(page, window) + return + } + + description1 := getLabelCentered("Choose which profile to view.") + description2 := getLabelCentered("Your Local profile is the profile stored on your computer.") + description3 := getLabelCentered("Your Public profile is the most recent profile that you have broadcasted.") + + localIcon, err := getFyneImageIcon("Local") + if (err != nil){ + setErrorEncounteredPage(window, err, previousPage) + return + } + + publicIcon, err := getFyneImageIcon("Broadcast") + if (err != nil){ + setErrorEncounteredPage(window, err, previousPage) + return + } + + viewLocalProfileButton := widget.NewButton("Local", func(){ + setViewMyProfilePage(window, myProfileType, "Local", currentPage) + }) + + viewPublicProfileButton := widget.NewButton("Public", func(){ + setViewMyProfilePage(window, myProfileType, "Public", currentPage) + }) + + viewLocalButtonWithIcon := container.NewGridWithColumns(1, localIcon, viewLocalProfileButton) + viewPublicButtonWithIcon := container.NewGridWithColumns(1, publicIcon, viewPublicProfileButton) + + buttonsRow := container.NewHBox(layout.NewSpacer(), viewLocalButtonWithIcon, viewPublicButtonWithIcon, layout.NewSpacer()) + + page := container.NewVBox(title, backButton, widget.NewSeparator(), subtitle, widget.NewSeparator(), description1, description2, description3, widget.NewSeparator(), buttonsRow, widget.NewSeparator()) + + setPageContent(page, window) +} + +func setViewMyProfilePage(window fyne.Window, myProfileType string, localOrPublic string, previousPage func()){ + + if (myProfileType != "Mate" && myProfileType != "Host" && myProfileType != "Moderator"){ + setErrorEncounteredPage(window, errors.New("setViewMyProfilePage called with invalid myProfileType: " + myProfileType), previousPage) + return + } + + if (localOrPublic != "Local" && localOrPublic != "Public"){ + setErrorEncounteredPage(window, errors.New("setViewMyProfilePage called with invalid localOrPublic: " + localOrPublic), previousPage) + return + } + + currentPage := func(){setViewMyProfilePage(window, myProfileType, localOrPublic, previousPage)} + + title := getPageTitleCentered("View My Profile") + + backButton := getBackButtonCentered(previousPage) + + myIdentityExists, myIdentityHash, err := myIdentity.GetMyIdentityHash(myProfileType) + if (err != nil){ + setErrorEncounteredPage(window, err, previousPage) + return + } + if (myIdentityExists == false){ + + description1 := getBoldLabelCentered("Your " + myProfileType + " identity does not exist.") + description2 := getLabelCentered("Create your " + myProfileType + " identity below.") + + createIdentityButton := getWidgetCentered(widget.NewButtonWithIcon("Create Identity", theme.NavigateNextIcon(), func(){ + setChooseNewIdentityHashPage(window, myProfileType, currentPage, currentPage) + })) + + page := container.NewVBox(title, backButton, widget.NewSeparator(), description1, description2, createIdentityButton) + setPageContent(page, window) + return + } + + if (myProfileType == "Mate"){ + + myGenomePersonIdentifierExists, myGenomePersonIdentifier, err := myLocalProfiles.GetProfileData("Mate", "GenomePersonIdentifier") + if (err != nil){ + setErrorEncounteredPage(window, err, previousPage) + return + } + if (myGenomePersonIdentifierExists == true){ + + anyGenomesExist, personAnalysisIsReady, _, err := myPeople.CheckIfPersonAnalysisIsReady(myGenomePersonIdentifier) + if (err != nil){ + setErrorEncounteredPage(window, err, previousPage) + return + } + if (anyGenomesExist == true && personAnalysisIsReady == false){ + + description1 := getBoldLabelCentered(translate("Your profile contains a linked genome person.")) + description2 := getLabelCentered(translate("You need to perform your genetic analysis.")) + description3 := getLabelCentered(translate("Only the information you choose will be shared in your profile.")) + + performAnalysisButton := getWidgetCentered(widget.NewButtonWithIcon(translate("Perform Analysis"), theme.NavigateNextIcon(), func(){ + setConfirmPerformPersonAnalysisPage(window, myGenomePersonIdentifier, currentPage, currentPage) + })) + + page := container.NewVBox(title, backButton, widget.NewSeparator(), description1, description2, description3, performAnalysisButton) + + setPageContent(page, window) + return + } + } + } + + appNetworkType, err := getAppNetworkType.GetAppNetworkType() + if (err != nil) { + setErrorEncounteredPage(window, err, previousPage) + return + } + + err = myProfileExports.UpdateMyExportedProfile(myProfileType, appNetworkType) + if (err != nil){ + setErrorEncounteredPage(window, err, previousPage) + return + } + + profileFound, profileHash, _, rawProfileMap, err := myProfileExports.GetMyExportedProfile(myProfileType, appNetworkType) + if (err != nil){ + setErrorEncounteredPage(window, err, previousPage) + return + } + if (profileFound == false){ + setErrorEncounteredPage(window, errors.New("My exported profile not found after exporting."), previousPage) + return + } + + getAnyLocalProfileAttributeFunction, err := calculatedAttributes.GetRetrieveAnyProfileAttributeIncludingCalculatedFunction(1, rawProfileMap) + if (err != nil){ + setErrorEncounteredPage(window, err, previousPage) + return + } + + exists, _, iAmDisabled, err := getAnyLocalProfileAttributeFunction("Disabled") + if (err != nil){ + setErrorEncounteredPage(window, err, previousPage) + return + } + if (exists == true && iAmDisabled == "Yes"){ + + description1 := getBoldLabelCentered("Your " + myProfileType + " profile is disabled.") + description2 := getLabelCentered("Re-enable it on the Broadcast page.") + + page := container.NewVBox(title, backButton, widget.NewSeparator(), description1, description2) + + setPageContent(page, window) + return + } + + if (localOrPublic == "Local"){ + setViewUserProfilePage(window, true, profileHash, 0, getAnyLocalProfileAttributeFunction, previousPage) + return + } + + // localOrPublic == "Public" + + identityExists, broadcastProfileExists, publicProfileHash, getAnyPublicProfileAttributeFunction, err := myBroadcasts.GetRetrieveAnyAttributeFromMyBroadcastProfileFunction(myIdentityHash, appNetworkType) + if (err != nil){ + setErrorEncounteredPage(window, err, previousPage) + return + } + if (identityExists == false){ + setErrorEncounteredPage(window, errors.New("My identity not found after being found already."), previousPage) + return + } + if (broadcastProfileExists == false){ + + description1 := getBoldLabelCentered("Your public profile does not exist.") + description2 := getLabelCentered("You have not broadcast your profile.") + description3 := getLabelCentered("Broadcast your profile on the Profile - Broadcast page.") + + page := container.NewVBox(title, backButton, widget.NewSeparator(), description1, description2, description3) + + setPageContent(page, window) + return + } + + myIdentityExists, myProfileIsActive, err := myProfileStatus.GetMyProfileIsActiveStatus(myIdentityHash, appNetworkType) + if (err != nil) { + setErrorEncounteredPage(window, err, previousPage) + return + } + if (myIdentityExists == false){ + setErrorEncounteredPage(window, errors.New("My identity not found after being found already."), previousPage) + return + } + if (myProfileIsActive == false){ + + description1 := getBoldLabelCentered("Your public profile has expired.") + description2 := getLabelCentered("You must broadcast your profile again.") + description3 := getLabelCentered("Broadcast your profile on the Profile - Broadcast page.") + + page := container.NewVBox(title, backButton, widget.NewSeparator(), description1, description2, description3) + + setPageContent(page, window) + return + } + + setViewUserProfilePage(window, true, publicProfileHash, 0, getAnyPublicProfileAttributeFunction, previousPage) +} + +func setViewPeerProfilePageFromIdentityHash(window fyne.Window, peerIdentityHash [16]byte, previousPage func()){ + + setLoadingScreen(window, "View Profile", "Loading Profile...") + + currentPage := func(){setViewPeerProfilePageFromIdentityHash(window, peerIdentityHash, previousPage)} + + title := getPageTitleCentered(translate("View Profile")) + + backButton := getBackButtonCentered(previousPage) + + userIdentityType, err := identity.GetIdentityTypeFromIdentityHash(peerIdentityHash) + if (err != nil) { + peerIdentityHashHex := encoding.EncodeBytesToHexString(peerIdentityHash[:]) + setErrorEncounteredPage(window, errors.New("setViewPeerProfilePageFromIdentityHash called with invalid peerIdentityHash: " + peerIdentityHashHex), previousPage) + return + } + + // We only allow unknown viewable status profiles to be viewed if the user profile type is host/moderator + getAllowUnknownStatusBool := func()bool{ + if (userIdentityType == "Mate"){ + return false + } + return true + } + + allowUnknownStatusBool := getAllowUnknownStatusBool() + + appNetworkType, err := getAppNetworkType.GetAppNetworkType() + if (err != nil) { + setErrorEncounteredPage(window, err, previousPage) + return + } + + viewableProfileExists, profileHash, getAnyAttributeFromViewableProfileFunction, err := viewableProfiles.GetRetrieveAnyNewestViewableUserProfileAttributeFunction(peerIdentityHash, appNetworkType, true, allowUnknownStatusBool, true) + if (err != nil){ + setErrorEncounteredPage(window, err, previousPage) + return + } + if (viewableProfileExists == true){ + + // The user has a viewable profile. + // We display it. + setViewUserProfilePage(window, false, profileHash, 0, getAnyAttributeFromViewableProfileFunction, previousPage) + return + } + + // Viewable profile does not exist + // We check to see if any profile exists + + //Outputs: + // -bool: Viewable status is known + // -bool: Identity Is Viewable status + // -error + getIdentityIsViewableStickyStatus := func()(bool, bool, error){ + + // First try verified status, then try trusted if verified is not available + + downloadingRequiredReviews, networkParametersExist, stickyConsensusEstablished, identityIsViewableStatus, err := verifiedStickyStatus.GetVerifiedIdentityIsViewableStickyStatus(peerIdentityHash, appNetworkType) + if (err != nil) { return false, false, err } + if (downloadingRequiredReviews == true && networkParametersExist == true && stickyConsensusEstablished == true){ + return true, identityIsViewableStatus, nil + } + + statusIsKnown, identityIsViewable, _, err := trustedViewableStatus.GetTrustedIdentityIsViewableStatus(peerIdentityHash, appNetworkType) + if (err != nil) { return false, false, err } + if (statusIsKnown == true){ + return true, identityIsViewable, nil + } + + return false, false, nil + } + + identityViewableStatusIsKnown, identityIsViewableStatus, err := getIdentityIsViewableStickyStatus() + if (err != nil){ + setErrorEncounteredPage(window, err, previousPage) + return + } + + theirNewestProfileExists, newestProfileVersion, newestProfileHash, _, _, newestProfileRawMap, err := profileStorage.GetNewestUserProfile(peerIdentityHash, appNetworkType) + if (err != nil) { + setErrorEncounteredPage(window, err, previousPage) + return + } + if (theirNewestProfileExists == false){ + + // Their profile does not exist. + + downloadProfileFunction := func(){setDownloadMissingUserProfilePage(window, peerIdentityHash, true, true, previousPage, currentPage, previousPage)} + + if (identityViewableStatusIsKnown == true && identityIsViewableStatus == false){ + + description1 := getLabelCentered("This user is banned.") + description2 := getLabelCentered("You do not have their profile downloaded.") + description3 := getLabelCentered("Do you want to try to download their profile?") + + downloadButton := getWidgetCentered(widget.NewButtonWithIcon("Download", theme.ConfirmIcon(), downloadProfileFunction)) + + page := container.NewVBox(title, backButton, widget.NewSeparator(), description1, description2, description3, downloadButton) + setPageContent(page, window) + return + } + + // Identity is not banned. Profile does not exist + // We check if user's identity is known to be expired. + + statusIsKnown, identityIsFunded, err := fundedStatus.GetIdentityIsFundedStatus(peerIdentityHash, appNetworkType) + if (err != nil) { + setErrorEncounteredPage(window, err, previousPage) + return + } + if (statusIsKnown == true && identityIsFunded == false){ + + // Identity is known to be expired + + description1 := getBoldLabelCentered("This user's identity is expired.") + description2 := getLabelCentered("Their profile is expired from the network.") + description3 := getLabelCentered("You can try to update this status to see if the user's identity is funded.") + + updateButton := getWidgetCentered(widget.NewButtonWithIcon("Update", theme.ViewRefreshIcon(), func(){ + //TODO: + // Use manualDownloads to update the user identity's funded status + showUnderConstructionDialog(window) + })) + + page := container.NewVBox(title, backButton, widget.NewSeparator(), description1, description2, description3, updateButton) + + setPageContent(page, window) + return + } + + // We will commence profile download without any prompt. + + downloadProfileFunction() + return + } + + // We know their profile exists, but it is not viewable + // For host/moderator profiles, this must mean that the profile/identity is banned + // For mate profiles, it could also mean that we have not downloaded the profile's sticky viewable status + + getAnyNewestProfileAttributeFunction, err := calculatedAttributes.GetRetrieveAnyProfileAttributeIncludingCalculatedFunction(newestProfileVersion, newestProfileRawMap) + if (err != nil) { + setErrorEncounteredPage(window, err, previousPage) + return + } + + viewNewestProfileFunction := func(){setViewUserProfilePage(window, false, newestProfileHash, 0, getAnyNewestProfileAttributeFunction, previousPage)} + + _, newestProfileIsDisabled, err := readProfiles.ReadProfileHashMetadata(newestProfileHash) + if (err != nil){ + setErrorEncounteredPage(window, err, previousPage) + return + } + if (newestProfileIsDisabled == true){ + + // Their newest profile is disabled. + // The user must be banned. + // We can show it to the user. + + viewNewestProfileFunction() + return + } + + if (identityViewableStatusIsKnown == true && identityIsViewableStatus == false){ + + description1 := getBoldLabelCentered("This user is banned.") + description2 := getLabelCentered("Do you want to view their profile anyway?") + + viewProfileButton := getWidgetCentered(widget.NewButtonWithIcon("View Profile", theme.VisibilityIcon(), viewNewestProfileFunction)) + + page := container.NewVBox(title, backButton, widget.NewSeparator(), description1, description2, viewProfileButton) + setPageContent(page, window) + return + } + + if (userIdentityType == "Host" || userIdentityType == "Moderator"){ + + // Host/moderator profiles are not considered unviewable until they are banned, or their author is banned. + // An unreviewed host/moderator profile is considered viewable. + // We already checked to see if the user's identity is banned + // Therefore, we know that this profile is banned. + + description1 := getBoldLabelCentered("This user's profile is banned.") + description2 := getLabelCentered("Do you want to view their profile anyway?") + + viewProfileButton := getWidgetCentered(widget.NewButtonWithIcon("View Profile", theme.VisibilityIcon(), viewNewestProfileFunction)) + + page := container.NewVBox(title, backButton, widget.NewSeparator(), description1, description2, viewProfileButton) + setPageContent(page, window) + return + } + + // userIdentityType == "Mate" + + // We know the user's identity is either viewable or unknown + // We check to see if mate profile is viewable + + //Outputs: + // -bool: Profile metadata found + // -bool: Viewable Status is known + // -bool: Profile is viewable + // -error + getNewestProfileIsViewableStatus := func()(bool, bool, bool, error){ + + profileIsDisabled, profileMetadataIsKnown, profileNetworkType, profileIdentityHash, downloadingRequiredReviews, networkParametersExist, stickyConsensusEstablished, profileIsViewableStatus, err := verifiedStickyStatus.GetVerifiedProfileIsViewableStickyStatus(newestProfileHash) + if (err != nil) { return false, false, false, err } + if (profileIsDisabled == true){ + return false, false, false, errors.New("GetVerifiedProfileIsViewableStickyStatus returning different profileIsDisabled status than ReadProfileHashMetadata.") + } + if (profileMetadataIsKnown == false){ + // Profile metadata must have been deleted after earlier check. + return false, false, false, nil + } + if (profileNetworkType != appNetworkType){ + return false, false, false, errors.New("GetVerifiedProfileIsViewableStickyStatus returning different networkType than requested from GetNewestUserProfile.") + } + + if (profileIdentityHash != peerIdentityHash){ + return false, false, false, errors.New("GetVerifiedProfileIsViewableStickyStatus returning different profileIdentityHash") + } + + if (downloadingRequiredReviews == true && networkParametersExist == true && stickyConsensusEstablished == true){ + return true, true, profileIsViewableStatus, nil + } + + return true, false, false, nil + } + + newestProfileMetadataFound, profileViewableStatusIsKnown, profileIsViewableStatus, err := getNewestProfileIsViewableStatus() + if (err != nil){ + setErrorEncounteredPage(window, err, previousPage) + return + } + + if (newestProfileMetadataFound == false){ + // This should only happen if the profile metadata was deleted after our earlier check + // We show a loading screen so this issue can be identified if it is because of an error + setLoadingScreen(window, "Loading Profile", "Looking for profile...") + time.Sleep(time.Second) + + currentPage() + return + } + + if (profileViewableStatusIsKnown == true && profileIsViewableStatus == false){ + + description1 := getBoldLabelCentered("This user's profile is not viewable.") + description2 := getLabelCentered("It may be banned, or not yet approved.") + description3 := getLabelCentered("Do you still want to view it?") + + viewProfileButton := getWidgetCentered(widget.NewButtonWithIcon("View Profile", theme.VisibilityIcon(), viewNewestProfileFunction)) + + page := container.NewVBox(title, backButton, widget.NewSeparator(), description1, description2, description3, viewProfileButton) + + setPageContent(page, window) + return + } + + // We know the profile is not considered viewable + // We know the identity is not banned, or its status is unknown + // We know the profile is not known to be unviewable + // Therefore, the profile viewable status must be unknown + + description1 := getBoldLabelCentered("This user's profile has an unknown moderation status.") + description2 := getLabelCentered("Do you still want to view their profile?") + + description3 := getLabelCentered("If not, you must wait for Seekia to download the profile's moderation status.") + description4 := getLabelCentered("After the download, you will know if the profile is banned or viewable.") + description5 := getLabelCentered("This will happen automatically in the background.") + description6 := getLabelCentered("Refresh the page to check if the status is downloaded.") + + refreshButton := getWidgetCentered(widget.NewButtonWithIcon("Refresh", theme.ViewRefreshIcon(), currentPage)) + + viewProfileButton := widget.NewButtonWithIcon("View Profile", theme.VisibilityIcon(), viewNewestProfileFunction) + + page := container.NewVBox(title, backButton, widget.NewSeparator(), description1, description2, viewProfileButton, widget.NewSeparator(), description3, description4, description5, description6, refreshButton) + + setPageContent(page, window) +} + + +func setViewUserProfilePage(window fyne.Window, profileIsMine bool, profileHash [28]byte, currentImageIndex int, getAnyUserProfileAttributeFunction func(string)(bool, int, string, error), previousPage func()) { + + pageIdentifier, err := helpers.GetNewRandomHexString(16) + if (err != nil) { + setErrorEncounteredPage(window, err, previousPage) + return + } + + appMemory.SetMemoryEntry("CurrentViewedPage", pageIdentifier) + + getTitleText := func()string{ + if (profileIsMine == true){ + return "Viewing My Profile" + } + return "Viewing Profile" + } + + titleText := getTitleText() + + setLoadingScreen(window, titleText, "Loading Profile...") + + currentPage := func(){setViewUserProfilePage(window, profileIsMine, profileHash, currentImageIndex, getAnyUserProfileAttributeFunction, previousPage)} + + userIdentityHashExists, _, userIdentityHashString, err := getAnyUserProfileAttributeFunction("IdentityHash") + if (err != nil) { + setErrorEncounteredPage(window, err, previousPage) + return + } + if (userIdentityHashExists == false) { + setErrorEncounteredPage(window, errors.New("Failed to get user profile IdentityHash"), previousPage) + return + } + + userIdentityHash, userProfileType, err := identity.ReadIdentityHashString(userIdentityHashString) + if (err != nil){ + setErrorEncounteredPage(window, errors.New("getAnyUserProfileAttributeFunction returning invalid IdentityHash: " + userIdentityHashString), previousPage) + return + } + + title := getPageTitleCentered(titleText) + + backButton := getBackButtonCentered(previousPage) + + getProfileIsDisabledStatus := func()(bool, error){ + + exists, _, disabledAttribute, err := getAnyUserProfileAttributeFunction("Disabled") + if (err != nil) { return false, err } + if (exists == true && disabledAttribute == "Yes") { + return true, nil + } + return false, nil + } + + userIsDisabled, err := getProfileIsDisabledStatus() + if (err != nil) { + setErrorEncounteredPage(window, err, previousPage) + return + } + if (userIsDisabled == true){ + + description1 := getBoldLabelCentered("User Is Disabled.") + + getDescription2Text := func()string{ + + if (profileIsMine == true){ + result := translate("You have disabled your profile.") + return result + } + result := translate("This user has disabled their profile.") + return result + } + + description2Text := getDescription2Text() + + description2 := getLabelCentered(description2Text) + + theirIdentityHashLabel := getItalicLabelCentered("User Identity Hash:") + theirIdentityHashText := getBoldLabelCentered(userIdentityHashString) + + page := container.NewVBox(title, backButton, widget.NewSeparator(), description1, description2, widget.NewSeparator(), theirIdentityHashLabel, theirIdentityHashText) + + if (profileIsMine == false){ + + page.Add(widget.NewSeparator()) + + actionsButton := getWidgetCentered(widget.NewButtonWithIcon("Actions", theme.ContentRedoIcon(), func(){ + setViewPeerActionsPage(window, userIdentityHash, currentPage) + })) + + page.Add(actionsButton) + } + + setPageContent(page, window) + return + } + + getProfileContainer := func()(*fyne.Container, error){ + + chatButton := widget.NewButtonWithIcon("Chat", theme.MailComposeIcon(), func(){ + if (profileIsMine == true){ + dialogTitle := translate("Chat Inaccessible") + dialogMessageA := getLabelCentered(translate("You are viewing your own profile.")) + dialogMessageB := getLabelCentered(translate("You cannot chat with yourself.")) + dialogContent := container.NewVBox(dialogMessageA, dialogMessageB) + dialog.ShowCustom(dialogTitle, translate("Close"), dialogContent, window) + return + } + setViewAConversationPage(window, userIdentityHash, true, currentPage) + }) + actionsButton := widget.NewButtonWithIcon("Actions", theme.ContentRedoIcon(), func(){ + if (profileIsMine == true){ + dialogTitle := translate("Peer Actions Inaccessible") + dialogMessageA := getLabelCentered(translate("You are viewing your own profile.")) + dialogMessageB := getLabelCentered(translate("You cannot access the peer actions page for your own identity.")) + dialogContent := container.NewVBox(dialogMessageA, dialogMessageB) + dialog.ShowCustom(dialogTitle, translate("Close"), dialogContent, window) + return + } + + setViewPeerActionsPage(window, userIdentityHash, currentPage) + }) + + topRowLeftButtonsColumn := container.NewGridWithColumns(1, chatButton, actionsButton) + + getPhotosColumnWithNavigationButtons := func()(*fyne.Container, error){ + + imageObjectsList := make([]image.Image, 0) + + if (userProfileType == "Mate"){ + + exists, _, webpPhotosAttribute, err := getAnyUserProfileAttributeFunction("Photos") + if (err != nil) { return nil, err } + if (exists == true){ + webpPhotosList := strings.Split(webpPhotosAttribute, "+") + + for _, base64Image := range webpPhotosList{ + + imageObject, err := imagery.ConvertWEBPBase64StringToCroppedDownsizedImageObject(base64Image) + if (err != nil) { return nil, err } + + imageObjectsList = append(imageObjectsList, imageObject) + } + } + } + + getAvatarEmojiIdentifier := func()(int, error){ + exists, _, emojiIdentifier, err := getAnyUserProfileAttributeFunction("Avatar") + if (err != nil){ return 0, err } + if (exists == false){ + // This is the avatar for users who have not selected an avatar + return 2929, nil + } + + emojiIdentifierInt, err := helpers.ConvertStringToInt(emojiIdentifier) + if (err != nil) { + return 0, errors.New("User profile is malformed: Invalid avatar: " + emojiIdentifier) + } + + return emojiIdentifierInt, nil + } + + avatarEmojiIdentifier, err := getAvatarEmojiIdentifier() + if (err != nil) { return nil, err } + + avatarImage, err := getEmojiImageObject(avatarEmojiIdentifier) + if (err != nil){ return nil, err } + + imageObjectsList = append(imageObjectsList, avatarImage) + + numberOfImages := len(imageObjectsList) + + getImageIndex := func()int{ + if (currentImageIndex <= 0){ + return 0 + } + + finalIndex := numberOfImages-1 + + if (currentImageIndex >= finalIndex){ + return finalIndex + } + return currentImageIndex + } + + imageIndex := getImageIndex() + + currentImage := imageObjectsList[imageIndex] + + currentFyneImage := canvas.NewImageFromImage(currentImage) + currentFyneImage.FillMode = canvas.ImageFillContain + currentFyneImage.SetMinSize(getCustomFyneSize(30)) + + getNavigationWithZoomRow := func()*fyne.Container{ + + zoomButton := widget.NewButtonWithIcon("", theme.ZoomInIcon(), func(){ + setViewFullpageImagesWithNavigationPage(window, imageObjectsList, imageIndex, currentPage) + }) + + if (numberOfImages == 1){ + zoomButtonRow := getWidgetCentered(zoomButton) + + return zoomButtonRow + } + + getBackButton := func()fyne.Widget{ + if (imageIndex <= 0) { + button := widget.NewButton("", nil) + return button + } + button := widget.NewButtonWithIcon("", theme.NavigateBackIcon(), func(){ + newImageIndex := imageIndex-1 + setViewUserProfilePage(window, profileIsMine, profileHash, newImageIndex, getAnyUserProfileAttributeFunction, previousPage) + }) + return button + } + + getNextButton := func()fyne.Widget{ + if (imageIndex >= numberOfImages-1) { + button := widget.NewButton("", nil) + return button + } + newImageIndex := imageIndex+1 + button := widget.NewButtonWithIcon("", theme.NavigateNextIcon(), func(){ + setViewUserProfilePage(window, profileIsMine, profileHash, newImageIndex, getAnyUserProfileAttributeFunction, previousPage) + }) + return button + } + + backButton := getBackButton() + nextButton := getNextButton() + + navigationWithZoomRow := getContainerCentered(container.NewGridWithRows(1, backButton, zoomButton, nextButton)) + + return navigationWithZoomRow + } + + navigationWithZoomRow := getNavigationWithZoomRow() + + currentFyneImageBoxed := container.NewHBox(layout.NewSpacer(), currentFyneImage, layout.NewSpacer()) + imagesColumn := container.NewVBox(currentFyneImageBoxed, navigationWithZoomRow) + + return imagesColumn, nil + } + + photosColumnWithNavButtons, err := getPhotosColumnWithNavigationButtons() + if (err != nil) { return nil, err } + + profileDetailsButton := widget.NewButtonWithIcon("Details", theme.InfoIcon(), func(){ + setViewUserProfilePage_ProfileDetails(window, profileHash, getAnyUserProfileAttributeFunction, currentPage) + }) + + reportProfileButton := widget.NewButtonWithIcon("Report", theme.WarningIcon(), func(){ + if (profileIsMine == true){ + dialogTitle := translate("Reporting Inaccessible") + dialogMessageA := getLabelCentered(translate("You are viewing your own profile.")) + dialogMessageB := getLabelCentered(translate("You cannot report your own profile.")) + dialogContent := container.NewVBox(dialogMessageA, dialogMessageB) + dialog.ShowCustom(dialogTitle, translate("Close"), dialogContent, window) + return + } + //TODO + showUnderConstructionDialog(window) + }) + + topRowRightButtonsColumn := container.NewGridWithColumns(1, profileDetailsButton, reportProfileButton) + + topRow := container.NewHBox(layout.NewSpacer(), topRowLeftButtonsColumn, widget.NewSeparator(), photosColumnWithNavButtons, widget.NewSeparator(), topRowRightButtonsColumn, layout.NewSpacer()) + + identityHashTitle := widget.NewLabel("Identity Hash:") + identityHashLabel := getBoldLabel(userIdentityHashString) + viewIdentityHashButton := widget.NewButtonWithIcon("", theme.VisibilityIcon(), func(){ + setViewIdentityHashPage(window, userIdentityHash, currentPage) + }) + identityHashRow := container.NewHBox(layout.NewSpacer(), identityHashTitle, identityHashLabel, viewIdentityHashButton, layout.NewSpacer()) + + profileContentContainer := container.NewVBox(topRow, widget.NewSeparator(), identityHashRow) + + usernameTitle := widget.NewLabel("Username:") + + getUsernameLabel := func()(fyne.Widget, error){ + + exists, _, usernameString, err := getAnyUserProfileAttributeFunction("Username") + if (err != nil) { return nil, err } + if (exists == false){ + anonymousLabel := getBoldItalicLabel(translate("Anonymous")) + + return anonymousLabel, nil + } + + usernameLabel := getBoldLabel(usernameString) + + return usernameLabel, nil + } + + usernameLabel, err := getUsernameLabel() + if (err != nil) { return nil, err } + + usernameRow := container.NewHBox(layout.NewSpacer(), usernameTitle, usernameLabel, layout.NewSpacer()) + + profileContentContainer.Add(usernameRow) + + if (profileIsMine == false && userProfileType == "Mate"){ + + exists, _, matchScore, err := getAnyUserProfileAttributeFunction("MatchScore") + if (err != nil) { return nil, err } + if (exists == false){ + return nil, errors.New("Unable to retrieve mate user match score.") + } + + matchScoreTitle := widget.NewLabel("Match Score:") + matchScoreLabel := getBoldLabel(matchScore) + viewMatchScoreDetailsButton := widget.NewButtonWithIcon("", theme.VisibilityIcon(), func(){ + // This page describes which desires a user fulfills + setViewUserMatchScoreBreakdownPage(window, userIdentityHash, currentPage) + }) + matchScoreRow := container.NewHBox(layout.NewSpacer(), matchScoreTitle, matchScoreLabel, viewMatchScoreDetailsButton, layout.NewSpacer()) + + profileContentContainer.Add(matchScoreRow) + + distanceTranslated, _, formatDistanceFunction, distanceUnits, unavailableDistanceText, err := attributeDisplay.GetProfileAttributeDisplayInfo("Distance") + if (err != nil) { return nil, err } + + distanceTitle := getLabelCentered(distanceTranslated + ":") + + getDistanceLabel := func()(fyne.Widget, error){ + + exists, _, distanceKilometersString, err := getAnyUserProfileAttributeFunction("Distance") + if (err != nil) { return nil, err } + if (exists == false){ + distanceLabel := getBoldItalicLabel(unavailableDistanceText) + return distanceLabel, nil + } + + distanceFormatted, err := formatDistanceFunction(distanceKilometersString) + if (err != nil) { return nil, err } + + distanceLabel := getBoldLabel(distanceFormatted + distanceUnits) + + return distanceLabel, nil + } + + distanceLabel, err := getDistanceLabel() + if (err != nil) { return nil, err } + + distanceRow := container.NewHBox(layout.NewSpacer(), distanceTitle, distanceLabel, layout.NewSpacer()) + + profileContentContainer.Add(distanceRow) + } + + if (userProfileType == "Moderator"){ + + appNetworkType, err := getAppNetworkType.GetAppNetworkType() + if (err != nil) { return nil, err } + + rankingKnown, moderatorRank, totalNumberOfModerators, err := moderatorRanking.GetModeratorRanking(userIdentityHash, appNetworkType) + if (err != nil) { return nil, err } + + getModeratorRankText := func()string{ + if (rankingKnown == false){ + result := translate("Unknown") + return result + } + + moderatorRankString := helpers.ConvertIntToString(moderatorRank) + totalNumberOfModeratorsString := helpers.ConvertIntToString(totalNumberOfModerators) + + moderatorRankText := moderatorRankString + "/" + totalNumberOfModeratorsString + + return moderatorRankText + } + + moderatorRankText := getModeratorRankText() + + moderatorRankTitle := widget.NewLabel(translate("Moderator Rank") + ":") + moderatorRankLabel := getBoldLabel(moderatorRankText) + + moderatorRankRow := container.NewHBox(layout.NewSpacer(), moderatorRankTitle, moderatorRankLabel, layout.NewSpacer()) + + profileContentContainer.Add(moderatorRankRow) + } + + if (userProfileType == "Mate"){ + + ageTitle := widget.NewLabel("Age:") + + getAgeLabel := func()(fyne.Widget, error){ + + exists, _, ageString, err := getAnyUserProfileAttributeFunction("Age") + if (err != nil) { return nil, err } + if (exists == false){ + noResponseLabel := getBoldItalicLabel(translate("No Response")) + return noResponseLabel, nil + } + + ageLabel := getBoldLabel(ageString) + + return ageLabel, nil + } + + ageLabel, err := getAgeLabel() + if (err != nil) { return nil, err } + + ageRow := container.NewHBox(layout.NewSpacer(), ageTitle, ageLabel, layout.NewSpacer()) + + profileContentContainer.Add(ageRow) + } + + getProfileCategorySelectButtons := func()(*fyne.Container, error){ + + generalIcon, err := getFyneImageIcon("General") + if (err != nil) { return nil, err } + generalButton := widget.NewButton("General", func(){ + setViewUserProfilePage_Category(window, profileIsMine, "General", getAnyUserProfileAttributeFunction, currentPage) + }) + + generalButtonWithIcon := container.NewGridWithRows(2, generalIcon, generalButton) + + if (userProfileType == "Host" || userProfileType == "Moderator"){ + categorySelectButtonsRow := getContainerCentered(generalButtonWithIcon) + return categorySelectButtonsRow, nil + } + + physicalIcon, err := getFyneImageIcon("Person") + if (err != nil) { return nil, err } + physicalButton := widget.NewButton("Physical", func(){ + setViewUserProfilePage_Category(window, profileIsMine, "Physical", getAnyUserProfileAttributeFunction, currentPage) + }) + + physicalButtonWithIcon := container.NewGridWithRows(2, physicalIcon, physicalButton) + + lifestyleIcon, err := getFyneImageIcon("Lifestyle") + if (err != nil) { return nil, err } + lifestyleButton := widget.NewButton("Lifestyle", func(){ + setViewUserProfilePage_Category(window, profileIsMine, "Lifestyle", getAnyUserProfileAttributeFunction, currentPage) + }) + + lifestyleButtonWithIcon := container.NewGridWithRows(2, lifestyleIcon, lifestyleButton) + + mentalIcon, err := getFyneImageIcon("Mental") + if (err != nil) { return nil, err } + mentalButton := widget.NewButton("Mental", func(){ + setViewUserProfilePage_Category(window, profileIsMine, "Mental", getAnyUserProfileAttributeFunction, currentPage) + }) + + mentalButtonWithIcon := container.NewGridWithRows(2, mentalIcon, mentalButton) + + categoriesRow := getContainerCentered(container.NewGridWithRows(1, generalButtonWithIcon, physicalButtonWithIcon, lifestyleButtonWithIcon, mentalButtonWithIcon)) + + return categoriesRow, nil + } + + selectProfileCategoryButtonsRow, err := getProfileCategorySelectButtons() + if (err != nil) { return nil, err } + + profileContentContainer.Add(widget.NewSeparator()) + profileContentContainer.Add(selectProfileCategoryButtonsRow) + profileContentContainer.Add(widget.NewSeparator()) + + return profileContentContainer, nil + } + + profileContent, err := getProfileContainer() + if (err != nil){ + setErrorEncounteredPage(window, err, previousPage) + return + } + + page := container.NewVBox(title, backButton, widget.NewSeparator(), profileContent) + + setPageContent(page, window) +} + + +func setViewUserProfilePage_ProfileDetails(window fyne.Window, profileHash [28]byte, getAnyUserProfileAttributeFunction func(string)(bool, int, string, error), previousPage func()) { + + currentPage := func(){setViewUserProfilePage_ProfileDetails(window, profileHash, getAnyUserProfileAttributeFunction, previousPage)} + + title := getPageTitleCentered(translate("View Profile Details")) + + backButton := getBackButtonCentered(previousPage) + + getPageContent := func()(*fyne.Container, error){ + + profileHashLabel := widget.NewLabel("Profile Hash:") + + profileHashHex := encoding.EncodeBytesToHexString(profileHash[:]) + + profileHashTrimmed, _, err := helpers.TrimAndFlattenString(profileHashHex, 20) + if (err != nil){ return nil, err } + + profileHashTrimmedLabel := getBoldLabel(profileHashTrimmed) + viewProfileHashButton := widget.NewButtonWithIcon("", theme.VisibilityIcon(), func(){ + setViewContentHashPage(window, "Profile", profileHash[:], currentPage) + }) + + profileHashRow := container.NewHBox(layout.NewSpacer(), profileHashLabel, profileHashTrimmedLabel, viewProfileHashButton, layout.NewSpacer()) + + exists, _, profileBroadcastTimeString, err := getAnyUserProfileAttributeFunction("BroadcastTime") + if (err != nil){ return nil, err } + if (exists == false){ + return nil, errors.New("setViewUserProfilePage_ProfileDetails called with profile missing BroadcastTime") + } + + profileBroadcastTime, err := helpers.ConvertBroadcastTimeStringToInt64(profileBroadcastTimeString) + if (err != nil){ + return nil, errors.New("setViewUserProfilePage_ProfileDetails called with profile with invalid BroadcastTime: " + profileBroadcastTimeString) + } + + broadcastTimeLabel := widget.NewLabel("Creation Time:") + + broadcastTimeString := helpers.ConvertUnixTimeToTranslatedTime(profileBroadcastTime) + + broadcastTimeAgoString, err := helpers.ConvertUnixTimeToTimeFromNowTranslated(profileBroadcastTime, true) + if (err != nil) { return nil, err } + + broadcastTimeText := getBoldLabel(broadcastTimeString + " (" + broadcastTimeAgoString + ")") + + broadcastTimeWarningButton := widget.NewButtonWithIcon("", theme.WarningIcon(), func(){ + title := translate("Creation Time Warning") + dialogMessageA := getLabelCentered("Profile creation times are not verified.") + dialogMessageB := getLabelCentered("They can be faked by the profile author.") + dialogContent := container.NewVBox(dialogMessageA, dialogMessageB) + dialog.ShowCustom(title, translate("Close"), dialogContent, window) + }) + + broadcastTimeRow := container.NewHBox(layout.NewSpacer(), broadcastTimeLabel, broadcastTimeText, broadcastTimeWarningButton, layout.NewSpacer()) + + exists, profileVersion, profileType, err := getAnyUserProfileAttributeFunction("ProfileType") + if (err != nil) { return nil, err } + if (exists == false){ + return nil, errors.New("getAnyUserProfileAttributeFunction called and ProfileType not found") + } + profileTypeLabel := widget.NewLabel("Profile Type:") + profileTypeText := getBoldLabel(profileType) + profileTypeRow := container.NewHBox(layout.NewSpacer(), profileTypeLabel, profileTypeText, layout.NewSpacer()) + + profileVersionLabel := widget.NewLabel("Profile Version:") + profileVersionString := helpers.ConvertIntToString(profileVersion) + profileVersionText := getBoldLabel(profileVersionString) + profileVersionRow := container.NewHBox(layout.NewSpacer(), profileVersionLabel, profileVersionText, layout.NewSpacer()) + + //TODO: Add moderation details (if profile/identity is banned) + + //TODO: Show when identity/profile will expire, and allow user to send funds to increase the user's identity balance/score + + //TODO: Save raw profile button (will save it as a txt file) + + pageContent := container.NewVBox(profileHashRow, widget.NewSeparator(), broadcastTimeRow, widget.NewSeparator(), profileTypeRow, widget.NewSeparator(), profileVersionRow) + + return pageContent, nil + } + + pageContent, err := getPageContent() + if (err != nil){ + setErrorEncounteredPage(window, err, previousPage) + return + } + + page := container.NewVBox(title, backButton, widget.NewSeparator(), pageContent) + + setPageContent(page, window) +} + + +func setViewUserProfilePage_Category(window fyne.Window, profileIsMine bool, categoryName string, getAnyUserProfileAttributeFunction func(string)(bool, int, string, error), previousPage func()) { + + if (categoryName != "General" && categoryName != "Physical" && categoryName != "Lifestyle" && categoryName != "Mental"){ + setErrorEncounteredPage(window, errors.New("setViewUserProfilePage_Category called with invalid profile category: " + categoryName), previousPage) + return + } + + currentPage := func(){setViewUserProfilePage_Category(window, profileIsMine, categoryName, getAnyUserProfileAttributeFunction, previousPage)} + + title := getPageTitleCentered("View Profile - " + categoryName) + + backButton := getBackButtonCentered(previousPage) + + page := container.NewVBox(title, backButton, widget.NewSeparator()) + + addAttributeRow := func(attributeName string)error{ + + attributeTitle, _, formatAttributeValue, attributeUnits, missingValueText, err := attributeDisplay.GetProfileAttributeDisplayInfo(attributeName) + if (err != nil) { return err } + + attributeTitleLabel := widget.NewLabel(attributeTitle + ":") + + exists, _, attributeValue, err := getAnyUserProfileAttributeFunction(attributeName) + if (err != nil) { return err } + if (exists == false) { + + attributeValueLabel := getBoldItalicLabel(missingValueText) + attributeRow := container.NewHBox(layout.NewSpacer(), attributeTitleLabel, attributeValueLabel, layout.NewSpacer()) + page.Add(attributeRow) + + return nil + } + + attributeValueFormatted, err := formatAttributeValue(attributeValue) + if (err != nil) { return err } + + attributeValueFormattedWithUnits := attributeValueFormatted + attributeUnits + + attributeValueTrimmed, anyChangesOccurred, err := helpers.TrimAndFlattenString(attributeValueFormattedWithUnits, 25) + if (err != nil) { return err } + if (anyChangesOccurred == false){ + + attributeValueLabel := getBoldLabel(attributeValueFormattedWithUnits) + + attributeRow := container.NewHBox(layout.NewSpacer(), attributeTitleLabel, attributeValueLabel, layout.NewSpacer()) + page.Add(attributeRow) + + return nil + } + + attributeValueTrimmedLabel := getBoldLabel(attributeValueTrimmed) + + viewAttributeValueButton := widget.NewButtonWithIcon("", theme.VisibilityIcon(), func(){ + setViewTextPage(window, "Viewing " + attributeTitle, attributeValueFormattedWithUnits, false, currentPage) + }) + + attributeValueRow := container.NewHBox(layout.NewSpacer(), attributeTitleLabel, attributeValueTrimmedLabel, viewAttributeValueButton, layout.NewSpacer()) + page.Add(attributeValueRow) + + return nil + } + + addAttributesFunction := func()error{ + + if (categoryName == "General"){ + + exists, _, userProfileType, err := getAnyUserProfileAttributeFunction("ProfileType") + if (err != nil) { return err } + if (exists == false){ + return errors.New("getAnyUserProfileAttributeFunction not able to find ProfileType") + } + + if (userProfileType == "Mate"){ + + locationButton := widget.NewButton("Location", func(){ + setViewMateProfilePage_Location(window, getAnyUserProfileAttributeFunction, currentPage) + }) + + questionnaireButton := widget.NewButton("Questionnaire", func(){ + setViewMateProfilePage_Questionnaire(window, profileIsMine, getAnyUserProfileAttributeFunction, currentPage) + }) + + tagsButton := widget.NewButton("Tags", func(){ + setViewMateProfilePage_Tags(window, getAnyUserProfileAttributeFunction, currentPage) + }) + + buttonsGrid := getContainerCentered(container.NewGridWithColumns(1, locationButton, questionnaireButton, tagsButton)) + page.Add(buttonsGrid) + + page.Add(widget.NewSeparator()) + } + + err = addAttributeRow("Description") + if (err != nil) { return err } + + err = addAttributeRow("Username") + if (err != nil) { return err } + + if (userProfileType == "Mate"){ + + err = addAttributeRow("Sexuality") + if (err != nil) { return err } + } + + return nil + } + if (categoryName == "Physical"){ + + racialSimilarityButton := widget.NewButton("Racial Similarity", func(){ + setViewMateProfilePage_RacialSimilarity(window, getAnyUserProfileAttributeFunction, currentPage) + }) + + ancestryCompositionButton := widget.NewButton("Ancestry Composition", func(){ + setViewMateProfilePage_AncestryComposition(window, "User", getAnyUserProfileAttributeFunction, currentPage) + }) + + haplogroupsButton := widget.NewButton("Haplogroups", func(){ + setViewMateProfilePage_Haplogroups(window, getAnyUserProfileAttributeFunction, currentPage) + }) + + neanderthalVariantsButton := widget.NewButton("Neanderthal Variants", func(){ + setViewMateProfilePage_NeanderthalVariants(window, getAnyUserProfileAttributeFunction, currentPage) + }) + + totalDiseaseRiskButton := widget.NewButton("Total Disease Risk", func(){ + setViewMateProfilePage_TotalDiseaseRisk(window, getAnyUserProfileAttributeFunction, currentPage) + }) + + monogenicDiseasesButton := widget.NewButton("Monogenic Diseases", func(){ + setViewMateProfilePage_MonogenicDiseases(window, "Offspring", getAnyUserProfileAttributeFunction, currentPage) + }) + + polygenicDiseasesButton := widget.NewButton("Polygenic Diseases", func(){ + setViewMateProfilePage_PolygenicDiseases(window, "Offspring", getAnyUserProfileAttributeFunction, currentPage) + }) + + geneticTraitsButton := widget.NewButton("Genetic Traits", func(){ + setViewMateProfilePage_GeneticTraits(window, "Offspring", getAnyUserProfileAttributeFunction, currentPage) + }) + + buttonsGrid := getContainerCentered(container.NewGridWithColumns(2, racialSimilarityButton, totalDiseaseRiskButton, ancestryCompositionButton, monogenicDiseasesButton, haplogroupsButton, polygenicDiseasesButton, neanderthalVariantsButton, geneticTraitsButton)) + page.Add(buttonsGrid) + + page.Add(widget.NewSeparator()) + + err := addAttributeRow("Age") + if (err != nil) { return err } + + err = addAttributeRow("Sex") + if (err != nil) { return err } + + err = addAttributeRow("Height") + if (err != nil) { return err } + + page.Add(widget.NewSeparator()) + + err = addAttributeRow("EyeColor") + if (err != nil) { return err } + + err = addAttributeRow("SkinColor") + if (err != nil) { return err } + + err = addAttributeRow("HairColor") + if (err != nil) { return err } + + err = addAttributeRow("HairTexture") + if (err != nil) { return err } + + page.Add(widget.NewSeparator()) + + err = addAttributeRow("BodyFat") + if (err != nil) { return err } + + err = addAttributeRow("BodyMuscle") + if (err != nil) { return err } + + page.Add(widget.NewSeparator()) + + err = addAttributeRow("HasHIV") + if (err != nil) { return err } + + err = addAttributeRow("HasGenitalHerpes") + if (err != nil) { return err } + + return nil + } + if (categoryName == "Lifestyle"){ + + dietButton := getWidgetCentered(widget.NewButton("Diet", func(){ + setViewMateProfilePage_Diet(window, getAnyUserProfileAttributeFunction, currentPage) + })) + page.Add(dietButton) + page.Add(widget.NewSeparator()) + + err := addAttributeRow("Hobbies") + if (err != nil) { return err } + + err = addAttributeRow("WealthInGold") + if (err != nil) { return err } + + err = addAttributeRow("Job") + if (err != nil) { return err } + + err = addAttributeRow("Fame") + if (err != nil) { return err } + + page.Add(widget.NewSeparator()) + + err = addAttributeRow("AlcoholFrequency") + if (err != nil) { return err } + + err = addAttributeRow("TobaccoFrequency") + if (err != nil) { return err } + + err = addAttributeRow("CannabisFrequency") + if (err != nil) { return err } + + return nil + } + if (categoryName == "Mental"){ + + languageButton := getWidgetCentered(widget.NewButton("Language", func(){ + setViewMateProfilePage_Language(window, getAnyUserProfileAttributeFunction, currentPage) + })) + page.Add(languageButton) + page.Add(widget.NewSeparator()) + + err := addAttributeRow("Beliefs") + if (err != nil) { return err } + + err = addAttributeRow("GenderIdentity") + if (err != nil) { return err } + + page.Add(widget.NewSeparator()) + + err = addAttributeRow("PetsRating") + if (err != nil) { return err } + + err = addAttributeRow("DogsRating") + if (err != nil) { return err } + + err = addAttributeRow("CatsRating") + if (err != nil) { return err } + + return nil + } + return errors.New("setViewUserProfilePage_Category called with invalid profile category: " + categoryName) + } + + err := addAttributesFunction() + if (err != nil){ + setErrorEncounteredPage(window, err, previousPage) + return + } + + page.Add(widget.NewSeparator()) + + setPageContent(page, window) +} + +func setViewMateProfilePage_Location(window fyne.Window, getAnyUserProfileAttributeFunction func(string)(bool, int, string, error), previousPage func()){ + + setLoadingScreen(window, "View Profile - General", "Loading user location...") + + title := getPageTitleCentered("View Profile - General") + + backButton := getBackButtonCentered(previousPage) + + subtitle := getPageSubtitleCentered(translate("Location")) + + //Outputs: + // -bool: Location exists + // -*fyne.Container + // -error + getLocationColumn := func(locationRank string)(bool, *fyne.Container, error){ + + if (locationRank != "Primary" && locationRank != "Secondary"){ + return false, nil, errors.New("getLocationColumn called with invalid locationRank: " + locationRank) + } + + exists, _, locationLatitudeString, err := getAnyUserProfileAttributeFunction(locationRank + "LocationLatitude") + if (err != nil) { return false, nil, err } + if (exists == false){ + return false, nil, nil + } + + exists, _, locationLongitudeString, err := getAnyUserProfileAttributeFunction(locationRank + "LocationLongitude") + if (err != nil) { return false, nil, err } + if (exists == false){ + return false, nil, errors.New("setViewMateProfilePage_Location called with profile containing " + locationRank + "LocationLatitude but missing " + locationRank + "LocationLongitude") + } + + locationLatitudeFloat64, err := helpers.ConvertStringToFloat64(locationLatitudeString) + if (err != nil){ + return false, nil, errors.New("setViewMateProfilePage_Location called with profile containing invalid LocationLatitude: " + locationLatitudeString) + } + + locationLongitudeFloat64, err := helpers.ConvertStringToFloat64(locationLongitudeString) + if (err != nil){ + return false, nil, errors.New("setViewMateProfilePage_Location called with profile containing invalid LocationLongitude: " + locationLongitudeString) + } + + locationRankLabel := getBoldLabelCentered(locationRank) + + locationCountryExists, _, locationCountryIdentifier, err := getAnyUserProfileAttributeFunction(locationRank + "LocationCountry") + if (err != nil) { return false, nil, err } + + getCountryTextLabel := func()(fyne.Widget, error){ + + if (locationCountryExists == false){ + noneLabel := getBoldItalicLabel(translate("None")) + return noneLabel, nil + } + + locationCountryIdentifierInt, err := helpers.ConvertStringToInt(locationCountryIdentifier) + if (err != nil) { return nil, err } + + countryObject, err := worldLocations.GetCountryObjectFromCountryIdentifier(locationCountryIdentifierInt) + if (err != nil){ return nil, err } + + countryNamesList := countryObject.NamesList + + countryDescription := helpers.TranslateAndJoinStringListItems(countryNamesList, "/") + + countryTextLabel := getBoldLabel(translate(countryDescription)) + return countryTextLabel, nil + } + + countryTextLabel, err := getCountryTextLabel() + if (err != nil) { return false, nil, err } + + countryLabel := getLabelCentered("Country:") + + coordinatesLabel := getLabelCentered("Coordinates:") + + locationCoordinatesLabel := getBoldLabel(locationLatitudeString + "°, " + locationLongitudeString + "°") + + cityLabel := getLabelCentered("City:") + + getCityContent := func()(*fyne.Container, error){ + + // We will either find the exact city or the closest city + + cityName, cityState, cityCountryIdentifier, cityDistanceKilometers, err := worldLocations.GetClosestCityFromCoordinates(locationLatitudeFloat64, locationLongitudeFloat64) + if (err != nil) { return nil, err } + + if (cityDistanceKilometers == 0){ + + locationCityFormatted := cityName + ", " + cityState + + locationCityLabel := getBoldLabelCentered(locationCityFormatted) + + return locationCityLabel, nil + } + + currentUnitsExist, currentUnits, err := globalSettings.GetSetting("MetricOrImperial") + if (err != nil){ return nil, err } + + getNearbyCityDistanceFormattedString := func()(string, error){ + + if (currentUnitsExist == true && currentUnits == "Imperial"){ + + distanceMiles, err := helpers.ConvertKilometersToMiles(cityDistanceKilometers) + if (err != nil) { return "", err } + + distanceMilesString := helpers.ConvertFloat64ToStringRounded(distanceMiles, 1) + result := distanceMilesString + " miles" + return result, nil + } + + distanceKilometersString := helpers.ConvertFloat64ToStringRounded(cityDistanceKilometers, 1) + + result := distanceKilometersString + " kilometers" + + return result, nil + } + + nearbyCityDistanceFormattedString, err := getNearbyCityDistanceFormattedString() + if (err != nil) { return nil, err } + + getNearbyCityNameFormatted := func()(string, error){ + + if (locationCountryExists == true){ + + locationCountryIdentifierInt, err := helpers.ConvertStringToInt(locationCountryIdentifier) + if (err != nil) { return "", err } + + if (cityCountryIdentifier == locationCountryIdentifierInt){ + result := cityName + ", " + cityState + return result, nil + } + } + + countryObject, err := worldLocations.GetCountryObjectFromCountryIdentifier(cityCountryIdentifier) + if (err != nil){ return "", err } + + countryNamesList := countryObject.NamesList + + countryDescription := helpers.TranslateAndJoinStringListItems(countryNamesList, "/") + + result := cityName + ", " + cityState + ", " + countryDescription + return result, nil + } + + nearbyCityNameFormatted, err := getNearbyCityNameFormatted() + if (err != nil) { return nil, err } + + nearbyCityNameFormattedAndTrimmed, _, err := helpers.TrimAndFlattenString(nearbyCityNameFormatted, 40) + if (err != nil) { return nil, err } + + locationDistanceLabel := getBoldLabelCentered(nearbyCityDistanceFormattedString + " from") + + locationCityNameLabel := getBoldLabelCentered(nearbyCityNameFormattedAndTrimmed) + + cityInfoContainer := container.NewVBox(locationDistanceLabel, locationCityNameLabel) + + return cityInfoContainer, nil + } + + cityContent, err := getCityContent() + if (err != nil){ return false, nil, err } + + locationColumn := container.NewVBox(locationRankLabel, widget.NewSeparator(), countryLabel, countryTextLabel, widget.NewSeparator(), coordinatesLabel, locationCoordinatesLabel, widget.NewSeparator(), cityLabel, cityContent) + + return true, locationColumn, nil + } + + primaryLocationExists, primaryLocationColumn, err := getLocationColumn("Primary") + if (err != nil){ + setErrorEncounteredPage(window, err, previousPage) + return + } + if (primaryLocationExists == false){ + + noLocationsLabel := getBoldLabelCentered("This user has no locations.") + page := container.NewVBox(title, backButton, widget.NewSeparator(), subtitle, widget.NewSeparator(), noLocationsLabel) + + setPageContent(page, window) + return + } + + locationColumnsGrid := container.NewGridWithRows(1, primaryLocationColumn) + + secondaryLocationExists, secondaryLocationColumn, err := getLocationColumn("Secondary") + if (err != nil){ + setErrorEncounteredPage(window, err, previousPage) + return + } + if (secondaryLocationExists == true){ + + locationColumnsGrid.Add(secondaryLocationColumn) + } + locationColumnsGridCentered := getContainerCentered(locationColumnsGrid) + + getDescriptionText := func()string{ + if (secondaryLocationExists == false){ + result := translate("Below is the user's location.") + return result + } + + result := translate("Below are the user's locations.") + return result + } + + descriptionText := getDescriptionText() + + description := getLabelCentered(descriptionText) + + page := container.NewVBox(title, backButton, widget.NewSeparator(), subtitle, widget.NewSeparator(), description, widget.NewSeparator(), locationColumnsGridCentered) + + setPageContent(page, window) +} + +func setViewMateProfilePage_Questionnaire(window fyne.Window, profileIsMine bool, getAnyUserProfileAttributeFunction func(string)(bool, int, string, error), previousPage func()){ + + currentPage := func(){setViewMateProfilePage_Questionnaire(window, profileIsMine, getAnyUserProfileAttributeFunction, previousPage)} + + title := getPageTitleCentered(translate("View Profile - General")) + + backButton := getBackButtonCentered(previousPage) + + subtitle := getPageSubtitleCentered(translate("Questionnaire")) + + questionnaireExists, _, questionnaireString, err := getAnyUserProfileAttributeFunction("Questionnaire") + if (err != nil){ + setErrorEncounteredPage(window, err, previousPage) + return + } + if (questionnaireExists == false){ + description := getBoldLabelCentered("This user does not have a questionnaire.") + + page := container.NewVBox(title, backButton, widget.NewSeparator(), subtitle, widget.NewSeparator(), description) + + setPageContent(page, window) + return + } + + description1 := getLabelCentered("This user has a questionnaire.") + description2 := getLabelCentered("You can take their questionnaire and send them your responses.") + + questionnaireObject, err := mateQuestionnaire.ReadQuestionnaireString(questionnaireString) + if (err != nil){ + setErrorEncounteredPage(window, errors.New("setViewMateProfilePage_Questionnaire called with profile containing invalid questionnaire."), previousPage) + return + } + + numberOfQuestions := len(questionnaireObject) + numberOfQuestionsString := helpers.ConvertIntToString(numberOfQuestions) + + numberOfQuestionsLabel := widget.NewLabel("Number of questions:") + numberOfQuestionsText := getBoldLabel(numberOfQuestionsString) + + numberOfQuestionsRow := container.NewHBox(layout.NewSpacer(), numberOfQuestionsLabel, numberOfQuestionsText, layout.NewSpacer()) + + exists, _, userIdentityHashString, err := getAnyUserProfileAttributeFunction("IdentityHash") + if (err != nil){ + setErrorEncounteredPage(window, err, previousPage) + return + } + if (exists == false){ + setErrorEncounteredPage(window, errors.New("getAnyUserProfileAttributeFunction failed to get IdentityHash"), previousPage) + return + } + + userIdentityHash, _, err := identity.ReadIdentityHashString(userIdentityHashString) + if (err != nil){ + setErrorEncounteredPage(window, err, previousPage) + return + } + + //TODO: Show if user has already taken the questionnaire + + submitQuestionnairePage := func(questionnaireResponse string, prevPage func()){ + if (profileIsMine == true){ + + dialogTitle := translate("Cannot Submit Questionnaire") + dialogMessageA := getLabelCentered(translate("You are viewing your own profile.")) + dialogMessageB := getLabelCentered(translate("You cannot submit a response to your own questionnaire.")) + dialogContent := container.NewVBox(dialogMessageA, dialogMessageB) + dialog.ShowCustom(dialogTitle, translate("Close"), dialogContent, window) + return + } + setSubmitQuestionnairePage(window, userIdentityHash, questionnaireResponse, prevPage, currentPage) + } + + takeQuestionnaireButton := getWidgetCentered(widget.NewButtonWithIcon("Take Questionnaire", theme.DocumentCreateIcon(), func(){ + emptyMap := make(map[string]string) + setTakeQuestionnairePage(window, questionnaireObject, 0, emptyMap, currentPage, submitQuestionnairePage) + })) + + page := container.NewVBox(title, backButton, widget.NewSeparator(), subtitle, widget.NewSeparator(), description1, description2, numberOfQuestionsRow, takeQuestionnaireButton) + + setPageContent(page, window) +} + +func setViewMateProfilePage_Tags(window fyne.Window, getAnyUserProfileAttributeFunction func(string)(bool, int, string, error), previousPage func()){ + + title := getPageTitleCentered(translate("View Profile - General")) + + backButton := getBackButtonCentered(previousPage) + + subtitle := getPageSubtitleCentered(translate("Tags")) + + anyTagsExist, _, tagsAttribute, err := getAnyUserProfileAttributeFunction("Tags") + if (err != nil){ + setErrorEncounteredPage(window, err, previousPage) + return + } + if (anyTagsExist == false){ + + description := getBoldLabelCentered("This user has no tags.") + + page := container.NewVBox(title, backButton, widget.NewSeparator(), subtitle, widget.NewSeparator(), description) + + setPageContent(page, window) + return + } + + description := getLabelCentered("Below are the user's tags.") + + page := container.NewVBox(title, backButton, widget.NewSeparator(), subtitle, widget.NewSeparator(), description, widget.NewSeparator()) + + tagsList := strings.Split(tagsAttribute, "+&") + + for _, tagName := range tagsList{ + + tagLabel := getBoldLabelCentered(tagName) + + page.Add(tagLabel) + } + + setPageContent(page, window) +} + +func setViewMateProfilePage_Haplogroups(window fyne.Window, getAnyUserProfileAttributeFunction func(string)(bool, int, string, error), previousPage func()){ + + title := getPageTitleCentered(translate("View Profile - Physical")) + + backButton := getBackButtonCentered(previousPage) + + subtitle := getPageSubtitleCentered(translate("Haplogroups")) + + description1 := getLabelCentered(translate("Below are the user's haplogroups.")) + description2 := getLabelCentered("These are reported by 23andMe. More companies will be added.") + + userMaternalHaplogroupExists, _, userMaternalHaplogroup_23andMe, err := getAnyUserProfileAttributeFunction("23andMe_MaternalHaplogroup") + if (err != nil){ + setErrorEncounteredPage(window, err, previousPage) + return + } + + maternalHaplogroupLabel := widget.NewLabel("Maternal Haplogroup:") + + getMaternalHalogroupText := func()fyne.Widget{ + + if (userMaternalHaplogroupExists == false){ + noResponseLabel := getBoldItalicLabel("No Response") + return noResponseLabel + } + + maternalHaplogroupLabel := getBoldLabel(userMaternalHaplogroup_23andMe) + + return maternalHaplogroupLabel + } + + maternalHaplogroupText := getMaternalHalogroupText() + + userMaternalHaplogroupRow := container.NewHBox(layout.NewSpacer(), maternalHaplogroupLabel, maternalHaplogroupText, layout.NewSpacer()) + + userPaternalHaplogroupExists, _, userPaternalHaplogroup_23andMe, err := getAnyUserProfileAttributeFunction("23andMe_PaternalHaplogroup") + if (err != nil){ + setErrorEncounteredPage(window, err, previousPage) + return + } + + paternalHaplogroupLabel := widget.NewLabel("Paternal Haplogroup:") + + getPaternalHalogroupText := func()fyne.Widget{ + + if (userPaternalHaplogroupExists == false){ + noResponseLabel := getBoldItalicLabel("No Response") + return noResponseLabel + } + + paternalHaplogroupLabel := getBoldLabel(userPaternalHaplogroup_23andMe) + + return paternalHaplogroupLabel + } + + paternalHaplogroupText := getPaternalHalogroupText() + + userPaternalHaplogroupRow := container.NewHBox(layout.NewSpacer(), paternalHaplogroupLabel, paternalHaplogroupText, layout.NewSpacer()) + + page := container.NewVBox(title, backButton, widget.NewSeparator(), subtitle, widget.NewSeparator(), description1, description2, widget.NewSeparator(), userMaternalHaplogroupRow, userPaternalHaplogroupRow) + + //Outputs: + // -bool: Offspring maternal haplogroup is known + // -string: Offspring maternal haplogroup + // -bool: Offspring paternal haplogroup is known + // -string: Offspring paternal haplogroup is known + // -error + getOffspringHaplogroups := func()(bool, string, bool, string, error){ + + isKnown, _, isSameSex, err := getAnyUserProfileAttributeFunction("IsSameSex") + if (err != nil){ return false, "", false, "", err } + if (isKnown == true && isSameSex == "Yes"){ + return false, "", false, "", nil + } + + mySexExists, mySex, err := myLocalProfiles.GetProfileData("Mate", "Sex") + if (err != nil) { return false, "", false, "", err } + + userSexExists, _, userSex, err := getAnyUserProfileAttributeFunction("Sex") + if (err != nil) { return false, "", false, "", err } + + if (mySexExists == false && userSexExists == false){ + return false, "", false, "", nil + } + + //Outputs: + // -bool: Haplogroup is known + // -string: Offspring maternal haplogroup + // -error + getOffspringMaternalHaplogroup := func()(bool, string, error){ + + if (mySexExists == true){ + if (mySex == "Female" || mySex == "Intersex Female"){ + + exists, myMaternalHaplogroup, err := myLocalProfiles.GetProfileData("Mate", "23andMe_MaternalHaplogroup") + if (err != nil){ return false, "", err } + if (exists == false){ + return false, "", nil + } + return true, myMaternalHaplogroup, nil + } + } + if (userSexExists == true){ + if (userSex == "Female" || userSex == "Intersex Female"){ + if (userMaternalHaplogroupExists == true){ + return true, userMaternalHaplogroup_23andMe, nil + } + } + } + return false, "", nil + } + + offspringMaternalHaplogroupIsKnown, offspringMaternalHaplogroup, err := getOffspringMaternalHaplogroup() + if (err != nil) { return false, "", false, "", err } + + //Outputs: + // -bool: Haplogroup is known + // -string: Offspring paternal haplogroup + // -error + getOffspringPaternalHaplogroup := func()(bool, string, error){ + if (mySexExists == true){ + if (mySex == "Male" || mySex == "Intersex Male"){ + + exists, myPaternalHaplogroup, err := myLocalProfiles.GetProfileData("Mate", "23andMe_PaternalHaplogroup") + if (err != nil){ return false, "", err } + if (exists == false){ + return false, "", nil + } + return true, myPaternalHaplogroup, nil + } + } + if (userSexExists == true){ + + if (userSex == "Male" || userSex == "Intersex Male"){ + if (userPaternalHaplogroupExists == true){ + return true, userPaternalHaplogroup_23andMe, nil + } + } + } + + return false, "", nil + } + + offspringPaternalHaplogroupIsKnown, offspringPaternalHaplogroup, err := getOffspringPaternalHaplogroup() + if (err != nil) { return false, "", false, "", err } + + return offspringMaternalHaplogroupIsKnown, offspringMaternalHaplogroup, offspringPaternalHaplogroupIsKnown, offspringPaternalHaplogroup, nil + } + + offspringMaternalHaplogroupIsKnown, offspringMaternalHaplogroup, offspringPaternalHaplogroupIsKnown, offspringPaternalHaplogroup, err := getOffspringHaplogroups() + if (err != nil){ + setErrorEncounteredPage(window, err, previousPage) + return + } + + if (offspringMaternalHaplogroupIsKnown == true || offspringPaternalHaplogroupIsKnown == true){ + + page.Add(widget.NewSeparator()) + + offspringDescription := getLabelCentered("Below are the haplogroups of an offspring created from you and the user.") + page.Add(offspringDescription) + page.Add(widget.NewSeparator()) + + offspringMaternalHaplogroupLabel := widget.NewLabel("Offspring Maternal Haplogroup:") + + getOffspringMaternalHaplogroupText := func()fyne.Widget{ + + if (offspringMaternalHaplogroupIsKnown == false){ + unknownLabel := getBoldItalicLabel("Unknown") + return unknownLabel + } + haplogroupLabel := getBoldLabel(offspringMaternalHaplogroup) + return haplogroupLabel + } + + offspringMaternalHaplogroupText := getOffspringMaternalHaplogroupText() + + offspringMaternalHaplogroupRow := container.NewHBox(layout.NewSpacer(), offspringMaternalHaplogroupLabel, offspringMaternalHaplogroupText, layout.NewSpacer()) + page.Add(offspringMaternalHaplogroupRow) + + offspringPaternalHaplogroupLabel := widget.NewLabel("Offspring Paternal Haplogroup:") + + getOffspringPaternalHaplogroupText := func()fyne.Widget{ + + if (offspringPaternalHaplogroupIsKnown == false){ + unknownLabel := getBoldItalicLabel("Unknown") + return unknownLabel + } + haplogroupLabel := getBoldLabel(offspringPaternalHaplogroup) + return haplogroupLabel + } + + offspringPaternalHaplogroupText := getOffspringPaternalHaplogroupText() + + offspringPaternalHaplogroupRow := container.NewHBox(layout.NewSpacer(), offspringPaternalHaplogroupLabel, offspringPaternalHaplogroupText, layout.NewSpacer()) + + page.Add(offspringPaternalHaplogroupRow) + } + + setPageContent(page, window) +} + +func setViewMateProfilePage_NeanderthalVariants(window fyne.Window, getAnyUserProfileAttributeFunction func(string)(bool, int, string, error), previousPage func()){ + + title := getPageTitleCentered(translate("View Profile - Physical")) + + backButton := getBackButtonCentered(previousPage) + + subtitle := getPageSubtitleCentered(translate("Neanderthal Variants")) + + description1 := getLabelCentered("Below is the user's neanderthal variant count.") + description2 := getLabelCentered("The information comes from 23andMe. More companies will be added.") + + userResponseExists, _, userNeanderthalVariants, err := getAnyUserProfileAttributeFunction("23andMe_NeanderthalVariants") + if (err != nil){ + setErrorEncounteredPage(window, err, previousPage) + return + } + + getUserNeanderthalVariantsLabel := func()fyne.Widget{ + + if (userResponseExists == false){ + noResponseLabel := getBoldItalicLabel(translate("No Response")) + + return noResponseLabel + } + + userNeanderthalVariantsLabel := getBoldLabel(userNeanderthalVariants + " variants") + return userNeanderthalVariantsLabel + } + + userNeanderthalVariantsLabel := getUserNeanderthalVariantsLabel() + + neanderthalVariantsLabel := widget.NewLabel("Neanderthal Variants:") + + neanderthalVariantsRow := container.NewHBox(layout.NewSpacer(), neanderthalVariantsLabel, userNeanderthalVariantsLabel, layout.NewSpacer()) + + page := container.NewVBox(title, backButton, widget.NewSeparator(), subtitle, widget.NewSeparator(), description1, description2, widget.NewSeparator(), neanderthalVariantsRow) + + isSameSexIsKnown, _, isSameSex, err := getAnyUserProfileAttributeFunction("IsSameSex") + if (err != nil){ + setErrorEncounteredPage(window, err, previousPage) + return + } + if (isSameSexIsKnown == true && isSameSex == "Yes"){ + + setPageContent(page, window) + return + } + + offspringNeanderthalVariantsKnown, _, offspringNeanderthalVariants, err := getAnyUserProfileAttributeFunction("23andMe_OffspringNeanderthalVariants") + if (err != nil){ + setErrorEncounteredPage(window, err, previousPage) + return + } + + if (offspringNeanderthalVariantsKnown == false){ + setPageContent(page, window) + return + } + + page.Add(widget.NewSeparator()) + offspringDescription := getLabelCentered("Below is the estimated neanderthal variant count of an offspring created from you and this user.") + page.Add(offspringDescription) + + offspringNeanderthalVariantsLabel := widget.NewLabel("Offspring Neanderthal Variants:") + offspringNeanderthalVariantsText := getBoldLabel(offspringNeanderthalVariants + " variants") + + offspringNeanderthalVariantsRow := container.NewHBox(layout.NewSpacer(), offspringNeanderthalVariantsLabel, offspringNeanderthalVariantsText, layout.NewSpacer()) + page.Add(offspringNeanderthalVariantsRow) + + setPageContent(page, window) +} + +func setViewMateProfilePage_RacialSimilarity(window fyne.Window, getAnyUserProfileAttributeFunction func(string)(bool, int, string, error), previousPage func()){ + + title := getPageTitleCentered(translate("View Profile - Physical")) + + backButton := getBackButtonCentered(previousPage) + + subtitle := getPageSubtitleCentered(translate("Racial Similarity")) + + description := getLabelCentered("This page describes the racial similarity between you and the user.") + + //TODO: Add help pages to explain results + + racialSimilarityTitle := widget.NewLabel("Racial Similarity:") + + getRacialSimilarityText := func()(string, error){ + + similarityIsKnown, _, racialSimilarity, err := getAnyUserProfileAttributeFunction("RacialSimilarity") + if (err != nil) { return "", err } + if (similarityIsKnown == false){ + + result := translate("Unknown") + return result, nil + } + + return racialSimilarity, nil + } + + racialSimilarityText, err := getRacialSimilarityText() + if (err != nil){ + setErrorEncounteredPage(window, err, previousPage) + return + } + + racialSimilarityLabel := getBoldLabel(racialSimilarityText) + + racialSimilarityHelpButton := widget.NewButtonWithIcon("", theme.QuestionIcon(), func(){ + //TODO + showUnderConstructionDialog(window) + }) + + racialSimilarityRow := container.NewHBox(layout.NewSpacer(), racialSimilarityTitle, racialSimilarityLabel, racialSimilarityHelpButton, layout.NewSpacer()) + + getRacialSimilarityBreakdownGrid_Traits := func()(*fyne.Container, error){ + + //TODO: A button for each trait to see what the our and the user's trait value is + + traitNameHeader := getItalicLabelCentered("Trait Name") + traitSimilarityHeader := getItalicLabelCentered("Trait Similarity") + geneticSimilarityHeader := getItalicLabelCentered("Genetic Similarity") + numberOfTestedLociHeader := getItalicLabelCentered("Number Of Tested Loci") + + traitNamesColumn := container.NewVBox(traitNameHeader, widget.NewSeparator()) + traitSimilarityColumn := container.NewVBox(traitSimilarityHeader, widget.NewSeparator()) + geneticSimilarityColumn := container.NewVBox(geneticSimilarityHeader, widget.NewSeparator()) + numberOfTestedLociColumn := container.NewVBox(numberOfTestedLociHeader, widget.NewSeparator()) + + addSimilarityRow := func(traitName string, traitSimilarityAttributeName string, geneticSimilarityAttributeName string, numberOfSimilarAllelesAttributeName string)error{ + + traitTitleLabel := getBoldLabelCentered(translate(traitName)) + + traitNamesColumn.Add(traitTitleLabel) + + if (traitName == "Facial Structure"){ + + // This trait's similarity cannot be calculated yet + // This feature is planned to be added - it will compare the faces in user profile photos. + + dashLabel := getItalicLabelCentered("-") + + traitSimilarityColumn.Add(dashLabel) + + } else { + + traitSimilarityIsKnown, _, attributeValue, err := getAnyUserProfileAttributeFunction(traitSimilarityAttributeName) + if (err != nil) { return err } + if (traitSimilarityIsKnown == false){ + + unknownLabel := getItalicLabelCentered("Unknown") + + traitSimilarityColumn.Add(unknownLabel) + } else { + + similarityFormatted := attributeValue + "%" + + similarityLabel := getBoldLabelCentered(similarityFormatted) + + traitSimilarityColumn.Add(similarityLabel) + } + } + + // We figure out how many loci exist for this trait + + traitObject, err := traits.GetTraitObject(traitName) + if (err != nil) { return err } + + traitLociList := traitObject.LociList + numberOfTraitLoci := len(traitLociList) + + numberOfTraitLociString := helpers.ConvertIntToString(numberOfTraitLoci) + + geneticSimilarityIsKnown, _, attributeValue, err := getAnyUserProfileAttributeFunction(geneticSimilarityAttributeName) + if (err != nil) { return err } + if (geneticSimilarityIsKnown == false){ + unknownLabel := getItalicLabelCentered("Unknown") + + geneticSimilarityColumn.Add(unknownLabel) + + numberOfTestedLociText := "0/" + numberOfTraitLociString + + numberOfTestedLociLabel := getBoldLabelCentered(numberOfTestedLociText) + + numberOfTestedLociColumn.Add(numberOfTestedLociLabel) + } else { + + similarityFormatted := attributeValue + "%" + + similarityLabel := getBoldLabelCentered(similarityFormatted) + + geneticSimilarityColumn.Add(similarityLabel) + + attributeIsKnown, _, numberOfSimilarAllelesValue, err := getAnyUserProfileAttributeFunction(numberOfSimilarAllelesAttributeName) + if (err != nil) { return err } + if (attributeIsKnown == false){ + return errors.New("User profile contains " + geneticSimilarityAttributeName + " but is missing " + numberOfSimilarAllelesAttributeName) + } + + // numberOfSimilarAllelesValue is represented by (number of shared alleles)/(number of tested alleles) + // Example: "52/60" + + // We extract the denominator + + _, numberOfTestedAllelesString, delimeterFound := strings.Cut(numberOfSimilarAllelesValue, "/") + if (delimeterFound == false){ + return errors.New("User profile contains invalid " + numberOfSimilarAllelesAttributeName + ": " + numberOfSimilarAllelesValue) + } + + numberOfTestedAlleles, err := helpers.ConvertStringToInt(numberOfTestedAllelesString) + if (err != nil){ + return errors.New("User profile contains invalid " + numberOfSimilarAllelesAttributeName + ": " + numberOfSimilarAllelesValue) + } + + // Each locus has 2 alleles (this will change once we include sex chromosome locations) + numberOfTestedLoci := numberOfTestedAlleles/2 + + numberOfTestedLociString := helpers.ConvertIntToString(numberOfTestedLoci) + + numberOfTestedLociLabelText := numberOfTestedLociString + "/" + numberOfTraitLociString + + numberOfTestedLociLabel := getBoldLabelCentered(numberOfTestedLociLabelText) + + numberOfTestedLociColumn.Add(numberOfTestedLociLabel) + } + + traitNamesColumn.Add(widget.NewSeparator()) + traitSimilarityColumn.Add(widget.NewSeparator()) + geneticSimilarityColumn.Add(widget.NewSeparator()) + numberOfTestedLociColumn.Add(widget.NewSeparator()) + + return nil + } + + err = addSimilarityRow("Eye Color", "EyeColorSimilarity", "EyeColorGenesSimilarity", "EyeColorGenesSimilarity_NumberOfSimilarAlleles") + if (err != nil) { return nil, err } + + err = addSimilarityRow("Hair Color", "HairColorSimilarity", "HairColorGenesSimilarity", "HairColorGenesSimilarity_NumberOfSimilarAlleles") + if (err != nil) { return nil, err } + + err = addSimilarityRow("Skin Color", "SkinColorSimilarity", "SkinColorGenesSimilarity", "SkinColorGenesSimilarity_NumberOfSimilarAlleles") + if (err != nil) { return nil, err } + + err = addSimilarityRow("Hair Texture", "HairTextureSimilarity", "HairTextureGenesSimilarity", "HairTextureGenesSimilarity_NumberOfSimilarAlleles") + if (err != nil) { return nil, err } + + err = addSimilarityRow("Facial Structure", "", "FacialStructureGenesSimilarity", "FacialStructureGenesSimilarity_NumberOfSimilarAlleles") + if (err != nil) { return nil, err } + + similarityGrid := container.NewHBox(layout.NewSpacer(), traitNamesColumn, traitSimilarityColumn, geneticSimilarityColumn, numberOfTestedLociColumn, layout.NewSpacer()) + + return similarityGrid, nil + } + + racialSimilarityBreakdownGrid_Traits, err := getRacialSimilarityBreakdownGrid_Traits() + if (err != nil){ + setErrorEncounteredPage(window, err, previousPage) + return + } + + getRacialSimilarityBreakdownGrid_GeneticAttributes := func()(*fyne.Container, error){ + + attributeTitleHeader := getItalicLabelCentered("Genetic Attribute Name") + twentyThreeAndMeHeader := getItalicLabelCentered("23andMe") + + attributeTitleColumn := container.NewVBox(attributeTitleHeader, widget.NewSeparator()) + similarityColumn_23andMe := container.NewVBox(twentyThreeAndMeHeader, widget.NewSeparator()) + + addSimilarityRow := func(attributeTitle string, attributeName_23andMe string)error{ + + attributeTitleLabel := getBoldLabelCentered(translate(attributeTitle)) + + attributeTitleColumn.Add(attributeTitleLabel) + + similarityIsKnown, _, attributeValue, err := getAnyUserProfileAttributeFunction(attributeName_23andMe) + if (err != nil) { return err } + if (similarityIsKnown == false){ + unknownLabel := getItalicLabelCentered("Unknown") + + similarityColumn_23andMe.Add(unknownLabel) + } else { + + similarityFormatted := attributeValue + "%" + + similarityLabel := getBoldLabelCentered(similarityFormatted) + + similarityColumn_23andMe.Add(similarityLabel) + } + + attributeTitleColumn.Add(widget.NewSeparator()) + similarityColumn_23andMe.Add(widget.NewSeparator()) + + return nil + } + + err = addSimilarityRow("Ancestral Similarity", "23andMe_AncestralSimilarity") + if (err != nil) { return nil, err } + + err = addSimilarityRow("Maternal Haplogroup Similarity", "23andMe_MaternalHaplogroupSimilarity") + if (err != nil) { return nil, err } + + err = addSimilarityRow("Paternal Haplogroup Similarity", "23andMe_PaternalHaplogroupSimilarity") + if (err != nil) { return nil, err } + + //TODO: Add neanderthal variants similarity? + + similarityGrid := container.NewHBox(layout.NewSpacer(), attributeTitleColumn, similarityColumn_23andMe, layout.NewSpacer()) + + return similarityGrid, nil + } + + racialSimilarityBreakdownGrid_GeneticAttributes, err := getRacialSimilarityBreakdownGrid_GeneticAttributes() + if (err != nil){ + setErrorEncounteredPage(window, err, previousPage) + return + } + + page := container.NewVBox(title, backButton, widget.NewSeparator(), subtitle, widget.NewSeparator(), description, widget.NewSeparator(), racialSimilarityRow, widget.NewSeparator(), racialSimilarityBreakdownGrid_Traits, widget.NewSeparator(), racialSimilarityBreakdownGrid_GeneticAttributes) + + setPageContent(page, window) +} + +func setViewMateProfilePage_AncestryComposition(window fyne.Window, userOrOffspring string, getAnyUserProfileAttributeFunction func(string)(bool, int, string, error), previousPage func()){ + + if (userOrOffspring != "User" && userOrOffspring != "Offspring"){ + setErrorEncounteredPage(window, errors.New("setViewMateProfilePage_AncestryComposition called with invalid userOrOffspring: " + userOrOffspring), previousPage) + return + } + + currentPage := func(){setViewMateProfilePage_AncestryComposition(window, userOrOffspring, getAnyUserProfileAttributeFunction, previousPage)} + + title := getPageTitleCentered(translate("View Profile - Physical")) + + backButton := getBackButtonCentered(previousPage) + + subtitle := getPageSubtitleCentered(translate("Ancestry Composition")) + + description1 := getLabelCentered("Below is the ancestry location composition for this user.") + description2 := getLabelCentered("The information is provided by 23andMe. More companies will be added.") + + userAncestryCompositionExists, _, userAncestryCompositionAttribute, err := getAnyUserProfileAttributeFunction("23andMe_AncestryComposition") + if (err != nil){ + setErrorEncounteredPage(window, err, previousPage) + return + } + if (userAncestryCompositionExists == false){ + noAncestryComposition := getBoldLabelCentered("This user has no ancestry composition.") + + page := container.NewVBox(title, backButton, widget.NewSeparator(), subtitle, widget.NewSeparator(), description1, description2, widget.NewSeparator(), noAncestryComposition) + + setPageContent(page, window) + return + } + + description3Label := getLabelCentered("Choose if you want to view the composition of the user or the offspring between you and the user.") + offspringAncestryCompositionHelpButton := widget.NewButtonWithIcon("", theme.QuestionIcon(), func(){ + setOffspringAncestryCompositionExplainerPage(window, currentPage) + }) + + description3Row := container.NewHBox(layout.NewSpacer(), description3Label, offspringAncestryCompositionHelpButton, layout.NewSpacer()) + + handleSelectFunction := func(newUserOrOffspring string){ + if (newUserOrOffspring == userOrOffspring){ + return + } + setViewMateProfilePage_AncestryComposition(window, newUserOrOffspring, getAnyUserProfileAttributeFunction, previousPage) + } + + userOrOffspringList := []string{"User", "Offspring"} + + userOrOffspringSelector := widget.NewSelect(userOrOffspringList, handleSelectFunction) + + userOrOffspringSelector.Selected = userOrOffspring + + userOrOffspringSelectorCentered := getWidgetCentered(userOrOffspringSelector) + + header := container.NewVBox(title, backButton, widget.NewSeparator(), subtitle, widget.NewSeparator(), description1, description2, widget.NewSeparator(), description3Row, userOrOffspringSelectorCentered, widget.NewSeparator()) + + if (userOrOffspring == "User"){ + + attributeIsValid, continentPercentagesMap, regionPercentagesMap, subregionPercentagesMap, err := companyAnalysis.ReadAncestryCompositionAttribute_23andMe(true, userAncestryCompositionAttribute) + if (err != nil){ + setErrorEncounteredPage(window, err, previousPage) + return + } + if (attributeIsValid == false){ + setErrorEncounteredPage(window, errors.New("setViewMateProfilePage_AncestryComposition called with getAnyUserProfileAttributeFunction returning invalid userAncestryCompositionAttribute"), previousPage) + return + } + + userCompositionDisplay, err := get23andMeAncestryCompositionDisplay(continentPercentagesMap, regionPercentagesMap, subregionPercentagesMap) + if (err != nil){ + setErrorEncounteredPage(window, err, previousPage) + return + } + + page := container.NewBorder(header, nil, nil, nil, userCompositionDisplay) + setPageContent(page, window) + return + } + + //userOrOffspring == "Offspring" + + myAncestryCompositionExists, myAncestryCompositionAttribute, err := myLocalProfiles.GetProfileData("Mate", "23andMe_AncestryComposition") + if (err != nil){ + setErrorEncounteredPage(window, err, previousPage) + return + } + if (myAncestryCompositionExists == false){ + + description4 := getBoldLabelCentered("Your ancestry location composition does not exist.") + description5 := getLabelCentered("Add your ancestry composition to view your offspring's ancestry composition.") + description6 := getLabelCentered("You can do this on the Build Profile - Physical - Ancestry Composition page.") + + page := container.NewVBox(header, description4, description5, description6) + + setPageContent(page, window) + return + } + + offspringContinentPercentagesMap, offspringRegionPercentagesMap, offspringSubregionPercentagesMap, err := companyAnalysis.GetOffspringAncestryComposition_23andMe(myAncestryCompositionAttribute, userAncestryCompositionAttribute) + if (err != nil){ + setErrorEncounteredPage(window, err, previousPage) + return + } + + offspringAncestryCompositionDisplay, err := get23andMeAncestryCompositionDisplay(offspringContinentPercentagesMap, offspringRegionPercentagesMap, offspringSubregionPercentagesMap) + if (err != nil){ + setErrorEncounteredPage(window, err, previousPage) + return + } + + page := container.NewBorder(header, nil, nil, nil, offspringAncestryCompositionDisplay) + + setPageContent(page, window) +} + + +// This is a page to view the total monogenic disease risk for a user's offspring +func setViewMateProfilePage_TotalDiseaseRisk(window fyne.Window, getAnyUserProfileAttributeFunction func(string)(bool, int, string, error), previousPage func()){ + + title := getPageTitleCentered(translate("View Profile - Physical")) + + backButton := getBackButtonCentered(previousPage) + + subtitle := getPageSubtitleCentered(translate("Total Disease Risk")) + + description1 := getLabelCentered("This page describes the total disease risk for this user.") + description2 := getLabelCentered("You must link your genome person in the Build Profile menu to see offspring disease information.") + + offspringProbabilityOfAnyMonogenicDiseaseTitle := widget.NewLabel("Offspring Probability Of Any Monogenic Disease:") + + getOffspringProbabilityOfAnyMonogenicDiseaseLabel := func()(fyne.Widget, error){ + + probabilityIsKnown, _, offspringProbabilityOfAnyMonogenicDisease, err := getAnyUserProfileAttributeFunction("OffspringProbabilityOfAnyMonogenicDisease") + if (err != nil) { return nil, err } + if (probabilityIsKnown == false){ + unknownLabel := getItalicLabel(translate("Unknown")) + return unknownLabel, nil + } + + probabilityFormatted := offspringProbabilityOfAnyMonogenicDisease + "%" + + probabilityLabel := getBoldLabel(probabilityFormatted) + + return probabilityLabel, nil + } + + offspringProbabilityOfAnyMonogenicDiseaseLabel, err := getOffspringProbabilityOfAnyMonogenicDiseaseLabel() + if (err != nil){ + setErrorEncounteredPage(window, err, previousPage) + return + } + + offspringProbabilityOfAnyMonogenicDiseaseRow := container.NewHBox(layout.NewSpacer(), offspringProbabilityOfAnyMonogenicDiseaseTitle, offspringProbabilityOfAnyMonogenicDiseaseLabel, layout.NewSpacer()) + + getNumberOfOffspringMonogenicDiseasesTested := func()(int, error){ + + attributeIsKnown, _, totalNumberOfMonogenicDiseasesTestedString, err := getAnyUserProfileAttributeFunction("OffspringProbabilityOfAnyMonogenicDisease_NumberOfDiseasesTested") + if (err != nil) { return 0, err } + if (attributeIsKnown == false){ + return 0, nil + } + totalNumberOfMonogenicDiseasesTested, err := helpers.ConvertStringToInt(totalNumberOfMonogenicDiseasesTestedString) + if (err != nil){ + return 0, errors.New("getAnyUserProfileAttributeFunction returning invalid OffspringProbabilityOfAnyMonogenicDisease_NumberOfDiseasesTested value: " + totalNumberOfMonogenicDiseasesTestedString) + } + + return totalNumberOfMonogenicDiseasesTested, nil + } + + numberOfOffspringMonogenicDiseasesTested, err := getNumberOfOffspringMonogenicDiseasesTested() + if (err != nil){ + setErrorEncounteredPage(window, err, previousPage) + return + } + + allMonogenicDiseaseNamesList, err := monogenicDiseases.GetMonogenicDiseaseNamesList() + if (err != nil){ + setErrorEncounteredPage(window, err, previousPage) + return + } + + totalNumberOfMonogenicDiseases := len(allMonogenicDiseaseNamesList) + + totalNumberOfMonogenicDiseasesString := helpers.ConvertIntToString(totalNumberOfMonogenicDiseases) + + numberOfOffspringMonogenicDiseasesTestedString := helpers.ConvertIntToString(numberOfOffspringMonogenicDiseasesTested) + + numberOfOffspringMonogenicDiseasesTestedLabelText := numberOfOffspringMonogenicDiseasesTestedString + "/" + totalNumberOfMonogenicDiseasesString + + numberOfOffspringMonogenicDiseasesTestedTitle := widget.NewLabel("Number Of Offspring Monogenic Diseases Tested:") + numberOfOffspringMonogenicDiseasesTestedLabel := getBoldLabel(numberOfOffspringMonogenicDiseasesTestedLabelText) + + numberOfOffspringMonogenicDiseasesTestedRow := container.NewHBox(layout.NewSpacer(), numberOfOffspringMonogenicDiseasesTestedTitle, numberOfOffspringMonogenicDiseasesTestedLabel, layout.NewSpacer()) + + + userTotalPolygenicDiseaseRiskScoreTitle := widget.NewLabel("User Total Polygenic Disease Risk Score:") + + getUserTotalPolygenicDiseaseRiskScoreLabel := func()(fyne.Widget, error){ + + riskScoreIsKnown, _, userTotalPolygenicDiseaseRiskScore, err := getAnyUserProfileAttributeFunction("TotalPolygenicDiseaseRiskScore") + if (err != nil) { return nil, err } + if (riskScoreIsKnown == false){ + unknownLabel := getItalicLabel(translate("Unknown")) + return unknownLabel, nil + } + + riskScoreFormatted := userTotalPolygenicDiseaseRiskScore + "/100" + + riskScoreLabel := getBoldLabel(riskScoreFormatted) + + return riskScoreLabel, nil + } + + userTotalPolygenicDiseaseRiskScoreLabel, err := getUserTotalPolygenicDiseaseRiskScoreLabel() + if (err != nil){ + setErrorEncounteredPage(window, err, previousPage) + return + } + + userTotalPolygenicDiseaseRiskScoreRow := container.NewHBox(layout.NewSpacer(), userTotalPolygenicDiseaseRiskScoreTitle, userTotalPolygenicDiseaseRiskScoreLabel, layout.NewSpacer()) + + getNumberOfUserPolygenicDiseasesTested := func()(int, error){ + + attributeIsKnown, _, totalNumberOfPolygenicDiseasesTestedString, err := getAnyUserProfileAttributeFunction("TotalPolygenicDiseaseRiskScore_NumberOfDiseasesTested") + if (err != nil) { return 0, err } + if (attributeIsKnown == false){ + return 0, nil + } + totalNumberOfPolygenicDiseasesTested, err := helpers.ConvertStringToInt(totalNumberOfPolygenicDiseasesTestedString) + if (err != nil){ + return 0, errors.New("getAnyUserProfileAttributeFunction returning invalid TotalPolygenicDiseaseRiskScore_NumberOfDiseasesTested value: " + totalNumberOfPolygenicDiseasesTestedString) + } + + return totalNumberOfPolygenicDiseasesTested, nil + } + + numberOfUserPolygenicDiseasesTested, err := getNumberOfUserPolygenicDiseasesTested() + if (err != nil){ + setErrorEncounteredPage(window, err, previousPage) + return + } + + allPolygenicDiseaseNamesList, err := polygenicDiseases.GetPolygenicDiseaseNamesList() + if (err != nil){ + setErrorEncounteredPage(window, err, previousPage) + return + } + + totalNumberOfPolygenicDiseases := len(allPolygenicDiseaseNamesList) + + totalNumberOfPolygenicDiseasesString := helpers.ConvertIntToString(totalNumberOfPolygenicDiseases) + + numberOfUserPolygenicDiseasesTestedString := helpers.ConvertIntToString(numberOfUserPolygenicDiseasesTested) + + numberOfUserPolygenicDiseasesTestedLabelText := numberOfUserPolygenicDiseasesTestedString + "/" + totalNumberOfPolygenicDiseasesString + + numberOfUserPolygenicDiseasesTestedTitle := widget.NewLabel("Number Of User Polygenic Diseases Tested:") + numberOfUserPolygenicDiseasesTestedLabel := getBoldLabel(numberOfUserPolygenicDiseasesTestedLabelText) + + numberOfUserPolygenicDiseasesTestedRow := container.NewHBox(layout.NewSpacer(), numberOfUserPolygenicDiseasesTestedTitle, numberOfUserPolygenicDiseasesTestedLabel, layout.NewSpacer()) + + + offspringTotalPolygenicDiseaseRiskScoreTitle := widget.NewLabel("Offspring Total Polygenic Disease Risk Score:") + + getOffspringTotalPolygenicDiseaseRiskScoreLabel := func()(fyne.Widget, error){ + + riskScoreIsKnown, _, offspringTotalPolygenicDiseaseRiskScore, err := getAnyUserProfileAttributeFunction("OffspringTotalPolygenicDiseaseRiskScore") + if (err != nil) { return nil, err } + if (riskScoreIsKnown == false){ + unknownLabel := getItalicLabel(translate("Unknown")) + return unknownLabel, nil + } + + riskScoreFormatted := offspringTotalPolygenicDiseaseRiskScore + "/100" + + riskScoreLabel := getBoldLabel(riskScoreFormatted) + + return riskScoreLabel, nil + } + + offspringTotalPolygenicDiseaseRiskScoreLabel, err := getOffspringTotalPolygenicDiseaseRiskScoreLabel() + if (err != nil){ + setErrorEncounteredPage(window, err, previousPage) + return + } + + offspringTotalPolygenicDiseaseRiskScoreRow := container.NewHBox(layout.NewSpacer(), offspringTotalPolygenicDiseaseRiskScoreTitle, offspringTotalPolygenicDiseaseRiskScoreLabel, layout.NewSpacer()) + + getNumberOfOffspringPolygenicDiseasesTested := func()(int, error){ + + attributeIsKnown, _, totalNumberOfPolygenicDiseasesTestedString, err := getAnyUserProfileAttributeFunction("OffspringTotalPolygenicDiseaseRiskScore_NumberOfDiseasesTested") + if (err != nil) { return 0, err } + if (attributeIsKnown == false){ + return 0, nil + } + totalNumberOfPolygenicDiseasesTested, err := helpers.ConvertStringToInt(totalNumberOfPolygenicDiseasesTestedString) + if (err != nil){ + return 0, errors.New("getAnyUserProfileAttributeFunction returning invalid OffspringTotalPolygenicDiseaseRiskScore_NumberOfDiseasesTested value: " + totalNumberOfPolygenicDiseasesTestedString) + } + + return totalNumberOfPolygenicDiseasesTested, nil + } + + numberOfOffspringPolygenicDiseasesTested, err := getNumberOfOffspringPolygenicDiseasesTested() + if (err != nil){ + setErrorEncounteredPage(window, err, previousPage) + return + } + + numberOfOffspringPolygenicDiseasesTestedString := helpers.ConvertIntToString(numberOfOffspringPolygenicDiseasesTested) + + numberOfOffspringPolygenicDiseasesTestedLabelText := numberOfOffspringPolygenicDiseasesTestedString + "/" + totalNumberOfPolygenicDiseasesString + + numberOfOffspringPolygenicDiseasesTestedTitle := widget.NewLabel("Number Of Offspring Polygenic Diseases Tested:") + numberOfOffspringPolygenicDiseasesTestedLabel := getBoldLabel(numberOfOffspringPolygenicDiseasesTestedLabelText) + + numberOfOffspringPolygenicDiseasesTestedRow := container.NewHBox(layout.NewSpacer(), numberOfOffspringPolygenicDiseasesTestedTitle, numberOfOffspringPolygenicDiseasesTestedLabel, layout.NewSpacer()) + + + + + //TODO: Add help buttons + + page := container.NewVBox(title, backButton, widget.NewSeparator(), subtitle, widget.NewSeparator(), description1, description2, widget.NewSeparator(), offspringProbabilityOfAnyMonogenicDiseaseRow, numberOfOffspringMonogenicDiseasesTestedRow, widget.NewSeparator(), userTotalPolygenicDiseaseRiskScoreRow, numberOfUserPolygenicDiseasesTestedRow, widget.NewSeparator(), offspringTotalPolygenicDiseaseRiskScoreRow, numberOfOffspringPolygenicDiseasesTestedRow) + + setPageContent(page, window) +} + +func setViewMateProfilePage_MonogenicDiseases(window fyne.Window, userOrOffspring string, getAnyUserProfileAttributeFunction func(string)(bool, int, string, error), previousPage func()){ + + currentPage := func(){setViewMateProfilePage_MonogenicDiseases(window, userOrOffspring, getAnyUserProfileAttributeFunction, previousPage)} + + title := getPageTitleCentered(translate("View Profile - Physical")) + + backButton := getBackButtonCentered(previousPage) + + subtitle := getPageSubtitleCentered(translate("Monogenic Diseases")) + + description1 := getLabelCentered("Below is the monogenic disease analysis for this user.") + description2 := getLabelCentered("You can choose to view the analysis of the user or an offspring between you and the user.") + description3 := getLabelCentered("You must link your genome person in the Build Profile menu to see full offspring information.") + + handleSelectButton := func(newUserOrOffspring string){ + if (userOrOffspring == newUserOrOffspring){ + return + } + setViewMateProfilePage_MonogenicDiseases(window, newUserOrOffspring, getAnyUserProfileAttributeFunction, previousPage) + } + + userOrOffspringSelector := widget.NewSelect([]string{"User", "Offspring"}, handleSelectButton) + userOrOffspringSelector.Selected = userOrOffspring + + userOrOffspringSelectorCentered := getWidgetCentered(userOrOffspringSelector) + + //TODO: Sort to show highest risk diseases first. All other diseases should be in normal order + + getDiseasesInfoGrid := func()(*fyne.Container, error){ + + emptyLabelA := widget.NewLabel("") + emptyLabelB := widget.NewLabel("") + + emptyLabelC := widget.NewLabel("") + diseaseNameLabel := getItalicLabelCentered("Disease Name") + + userProbabilityOfLabelA := getItalicLabelCentered("User Probability Of") + passingVariantLabel := getItalicLabelCentered("Passing Variant") + + userNumberOfLabel := getItalicLabelCentered("User Number Of") + variantsTestedLabelA := getItalicLabelCentered("Variants Tested") + + offspringProbabilityOfLabelA := getItalicLabelCentered("Offspring Probability Of") + havingDiseaseLabel := getItalicLabelCentered("Having Disease") + + offspringProbabilityOfLabelB := getItalicLabelCentered("Offspring Probability Of") + havingVariantLabel := getItalicLabelCentered("Having Variant") + + offspringNumberOfLabel := getItalicLabelCentered("Offspring Number Of") + variantsTestedLabelB := getItalicLabelCentered("Variants Tested") + + diseaseInfoButtonsColumn := container.NewVBox(emptyLabelA, emptyLabelB, widget.NewSeparator()) + diseaseNameColumn := container.NewVBox(emptyLabelC, diseaseNameLabel, widget.NewSeparator()) + userProbabilityOfPassingVariantColumn := container.NewVBox(userProbabilityOfLabelA, passingVariantLabel, widget.NewSeparator()) + userNumberOfVariantsTestedColumn := container.NewVBox(userNumberOfLabel, variantsTestedLabelA, widget.NewSeparator()) + offspringProbabilityOfHavingDiseaseColumn := container.NewVBox(offspringProbabilityOfLabelA, havingDiseaseLabel, widget.NewSeparator()) + offspringProbabilityOfHavingAVariantColumn := container.NewVBox(offspringProbabilityOfLabelB, havingVariantLabel, widget.NewSeparator()) + offspringNumberOfVariantsTestedColumn := container.NewVBox(offspringNumberOfLabel, variantsTestedLabelB, widget.NewSeparator()) + + myPersonChosen, myGenomesExist, myAnalysisIsReady, myAnalysisMapList, myGenomeIdentifier, iHaveMultipleGenomes, err := myChosenAnalysis.GetMyChosenMateGeneticAnalysis() + if (err != nil) { return nil, err } + + monogenicDiseaseObjectsList, err := monogenicDiseases.GetMonogenicDiseaseObjectsList() + if (err != nil) { return nil, err } + + for _, diseaseObject := range monogenicDiseaseObjectsList{ + + monogenicDiseaseName := diseaseObject.DiseaseName + diseaseIsDominantOrRecessive := diseaseObject.DominantOrRecessive + + diseaseVariantsList := diseaseObject.VariantsList + numberOfDiseaseVariants := len(diseaseVariantsList) + + //Outputs: + // -bool: User disease info exists + // -int: User probability of passing a disease variant (0-100) + // -int: User number of variants tested + // -error + getUserDiseaseInfo := func()(bool, int, int, error){ + + diseaseNameWithUnderscores := strings.ReplaceAll(monogenicDiseaseName, " ", "_") + + probabilityOfPassingAVariantAttributeName := "MonogenicDisease_" + diseaseNameWithUnderscores + "_ProbabilityOfPassingAVariant" + numberOfVariantsTestedAttributeName := "MonogenicDisease_" + diseaseNameWithUnderscores + "_NumberOfVariantsTested" + + userDiseaseInfoExists, _, userProbabilityOfPassingDiseaseVariant, err := getAnyUserProfileAttributeFunction(probabilityOfPassingAVariantAttributeName) + if (err != nil) { return false, 0, 0, err } + if (userDiseaseInfoExists == false){ + return false, 0, 0, nil + } + + userProbabilityOfPassingDiseaseVariantInt, err := helpers.ConvertStringToInt(userProbabilityOfPassingDiseaseVariant) + if (err != nil) { + return false, 0, 0, errors.New("setViewMateProfilePage_MonogenicDiseases called with profile containing invalid probabilityOfPassingAVariant: " + userProbabilityOfPassingDiseaseVariant) + } + + userVariantsTestedExists, _, userNumberOfVariantsTested, err := getAnyUserProfileAttributeFunction(numberOfVariantsTestedAttributeName) + if (err != nil) { return false, 0, 0, err } + if (userVariantsTestedExists == false){ + return false, 0, 0, errors.New("setViewMateProfilePage_MonogenicDiseases called with user profile containing probabilityOfPassingAVariant but not numberOfVariantsTested") + } + + userNumberOfVariantsTestedInt, err := helpers.ConvertStringToInt(userNumberOfVariantsTested) + if (err != nil) { + return false, 0, 0, errors.New("setViewMateProfilePage_MonogenicDiseases called with profile containing invalid numberOfVariantsTested: " + userNumberOfVariantsTested) + } + + return true, userProbabilityOfPassingDiseaseVariantInt, userNumberOfVariantsTestedInt, nil + } + + userDiseaseInfoExists, userProbabilityOfPassingAVariant, userNumberOfVariantsTested, err := getUserDiseaseInfo() + if (err != nil) { return nil, err } + + //Outputs: + // -bool: My disease info exists + // -int: My probability of passing a disease variant + // -int: Number of variants tested + // -error + getMyDiseaseInfo := func()(bool, int, int, error){ + if (myPersonChosen == false || myGenomesExist == false || myAnalysisIsReady == false){ + return false, 0, 0, nil + } + + probabilitiesKnown, _, _, probabilityOfPassingADiseaseVariant, _, numberOfVariantsTested, _, err := readGeneticAnalysis.GetPersonMonogenicDiseaseInfoFromGeneticAnalysis(myAnalysisMapList, monogenicDiseaseName, myGenomeIdentifier, iHaveMultipleGenomes) + if (err != nil) { return false, 0, 0, err } + if (probabilitiesKnown == false){ + return false, 0, 0, nil + } + + return true, probabilityOfPassingADiseaseVariant, numberOfVariantsTested, nil + } + + myDiseaseInfoExists, myProbabilityOfPassingAVariant, myNumberOfVariantsTested, err := getMyDiseaseInfo() + if (err != nil) { return nil, err } + + //Outputs: + // -bool: Disease info known + // -int: Offspring probability of having disease (0-100) + // -bool: Probability offspring has variant is known + // -int: Probability offspring has variant (0-100) + // -int: Number of variants tested + // -error + getOffspringDiseaseInfo := func()(bool, int, bool, int, int, error){ + + if (userDiseaseInfoExists == false && myDiseaseInfoExists == false){ + return false, 0, false, 0, 0, nil + } + if (userDiseaseInfoExists == true && myDiseaseInfoExists == false){ + + if (diseaseIsDominantOrRecessive == "Dominant"){ + if (userProbabilityOfPassingAVariant == 100){ + // We know the offspring will have the disease + return true, 100, true, 100, userNumberOfVariantsTested, nil + } + return false, 0, false, 0, 0, nil + } + + if (userProbabilityOfPassingAVariant == 0){ + // We know the offspring will not have the disease + return true, 0, false, 0, userNumberOfVariantsTested, nil + } + return false, 0, false, 0, 0, nil + } + if (userDiseaseInfoExists == false && myDiseaseInfoExists == true){ + + if (diseaseIsDominantOrRecessive == "Dominant"){ + if (myProbabilityOfPassingAVariant == 100){ + // We know the offspring will have the disease + return true, 100, true, 100, myNumberOfVariantsTested, nil + } + return false, 0, false, 0, 0, nil + } + + if (myProbabilityOfPassingAVariant == 0){ + // We know the offspring will not have the disease + return true, 0, false, 0, myNumberOfVariantsTested, nil + } + return false, 0, false, 0, 0, nil + } + + probabilityOffspringHasDisease, probabilityOffspringHasVariant, err := createGeneticAnalysis.GetOffspringMonogenicDiseaseProbabilities(diseaseIsDominantOrRecessive, userProbabilityOfPassingAVariant, myProbabilityOfPassingAVariant) + if (err != nil) { return false, 0, false, 0, 0, err } + + offspringNumberOfVariantsTested := userNumberOfVariantsTested + myNumberOfVariantsTested + + return true, probabilityOffspringHasDisease, true, probabilityOffspringHasVariant, offspringNumberOfVariantsTested, nil + } + + offspringDiseaseInfoIsKnown, offspringProbabilityOfHavingDisease, offspringProbabilityOfHavingAVariantIsKnown, offspringProbabilityOfHavingAVariant, offspringNumberOfVariantsTested, err := getOffspringDiseaseInfo() + if (err != nil){ return nil, err } + + getUserProbabilityOfPassingAVariantString := func()string{ + if (userDiseaseInfoExists == false){ + return "Unknown" + } + userProbabilityOfPassingAVariantString := helpers.ConvertIntToString(userProbabilityOfPassingAVariant) + resultFormatted := userProbabilityOfPassingAVariantString + "%" + return resultFormatted + } + + userProbabilityOfPassingAVariantString := getUserProbabilityOfPassingAVariantString() + + getUserNumberOfVariantsTestedString := func()string{ + numberOfDiseaseVariantsString := helpers.ConvertIntToString(numberOfDiseaseVariants) + if (userDiseaseInfoExists == false){ + result := "0/" + numberOfDiseaseVariantsString + return result + } + userNumberOfVariantsTestedString := helpers.ConvertIntToString(userNumberOfVariantsTested) + + resultFormatted := userNumberOfVariantsTestedString + "/" + numberOfDiseaseVariantsString + return resultFormatted + } + + userNumberOfVariantsTestedString := getUserNumberOfVariantsTestedString() + + getOffspringProbabilityOfHavingDiseaseString := func()string{ + + if (offspringDiseaseInfoIsKnown == false){ + result := translate("Unknown") + return result + } + offspringProbabilityOfHavingDiseaseString := helpers.ConvertIntToString(offspringProbabilityOfHavingDisease) + resultFormatted := offspringProbabilityOfHavingDiseaseString + "%" + return resultFormatted + } + + offspringProbabilityOfHavingDiseaseString := getOffspringProbabilityOfHavingDiseaseString() + + getOffspringProbabilityOfHavingAVariantString := func()string{ + + if (offspringDiseaseInfoIsKnown == false || offspringProbabilityOfHavingAVariantIsKnown == false){ + result := translate("Unknown") + return result + } + offspringProbabilityOfHavingAVariantString := helpers.ConvertIntToString(offspringProbabilityOfHavingAVariant) + resultFormatted := offspringProbabilityOfHavingAVariantString + "%" + + return resultFormatted + } + + offspringProbabilityOfHavingAVariantString := getOffspringProbabilityOfHavingAVariantString() + + getOffspringNumberOfVariantsTestedString := func()string{ + totalNumberOfOffspringDiseaseVariantsString := helpers.ConvertIntToString(numberOfDiseaseVariants*2) + if (offspringDiseaseInfoIsKnown == false){ + result := "0/" + totalNumberOfOffspringDiseaseVariantsString + return result + } + offspringNumberOfVariantsTestedString := helpers.ConvertIntToString(offspringNumberOfVariantsTested) + result := offspringNumberOfVariantsTestedString + "/" + totalNumberOfOffspringDiseaseVariantsString + return result + } + + viewDiseaseInfoButton := widget.NewButtonWithIcon("", theme.InfoIcon(), func(){ + setViewMonogenicDiseaseDetailsPage(window, monogenicDiseaseName, currentPage) + }) + + diseaseNameLabel := getBoldLabelCentered(monogenicDiseaseName) + + offspringNumberOfVariantsTestedString := getOffspringNumberOfVariantsTestedString() + + userProbabilityOfPassingAVariantLabel := getBoldLabelCentered(userProbabilityOfPassingAVariantString) + userNumberOfVariantsTestedLabel := getBoldLabelCentered(userNumberOfVariantsTestedString) + + offspringProbabilityOfHavingDiseaseLabel := getBoldLabelCentered(offspringProbabilityOfHavingDiseaseString) + offspringProbabilityOfHavingAVariantLabel := getBoldLabelCentered(offspringProbabilityOfHavingAVariantString) + offspringNumberOfVariantsTestedLabel := getBoldLabelCentered(offspringNumberOfVariantsTestedString) + + diseaseInfoButtonsColumn.Add(viewDiseaseInfoButton) + diseaseNameColumn.Add(diseaseNameLabel) + userProbabilityOfPassingVariantColumn.Add(userProbabilityOfPassingAVariantLabel) + userNumberOfVariantsTestedColumn.Add(userNumberOfVariantsTestedLabel) + offspringProbabilityOfHavingDiseaseColumn.Add(offspringProbabilityOfHavingDiseaseLabel) + offspringProbabilityOfHavingAVariantColumn.Add(offspringProbabilityOfHavingAVariantLabel) + offspringNumberOfVariantsTestedColumn.Add(offspringNumberOfVariantsTestedLabel) + + diseaseInfoButtonsColumn.Add(widget.NewSeparator()) + diseaseNameColumn.Add(widget.NewSeparator()) + userProbabilityOfPassingVariantColumn.Add(widget.NewSeparator()) + userNumberOfVariantsTestedColumn.Add(widget.NewSeparator()) + offspringProbabilityOfHavingDiseaseColumn.Add(widget.NewSeparator()) + offspringProbabilityOfHavingAVariantColumn.Add(widget.NewSeparator()) + offspringNumberOfVariantsTestedColumn.Add(widget.NewSeparator()) + } + + probabilityOfPassingVariantHelpButton := widget.NewButtonWithIcon("", theme.QuestionIcon(), func(){ + setPersonProbabilityOfPassingVariantExplainerPage(window, currentPage) + }) + numberOfVariantsTestedHelpButtonA := widget.NewButtonWithIcon("", theme.QuestionIcon(), func(){ + setNumberOfTestedVariantsExplainerPage(window, currentPage) + }) + numberOfVariantsTestedHelpButtonB := widget.NewButtonWithIcon("", theme.QuestionIcon(), func(){ + setNumberOfTestedVariantsExplainerPage(window, currentPage) + }) + probabilityOfHavingDiseaseHelpButton := widget.NewButtonWithIcon("", theme.QuestionIcon(), func(){ + setOffspringProbabilityOfHavingMonogenicDiseaseExplainerPage(window, currentPage) + }) + probabilityOfHavingAVariantHelpButton := widget.NewButtonWithIcon("", theme.QuestionIcon(), func(){ + setOffspringProbabilityOfHavingVariantExplainerPage(window, currentPage) + }) + + userProbabilityOfPassingVariantColumn.Add(probabilityOfPassingVariantHelpButton) + userNumberOfVariantsTestedColumn.Add(numberOfVariantsTestedHelpButtonA) + offspringProbabilityOfHavingDiseaseColumn.Add(probabilityOfHavingDiseaseHelpButton) + offspringProbabilityOfHavingAVariantColumn.Add(probabilityOfHavingAVariantHelpButton) + offspringNumberOfVariantsTestedColumn.Add(numberOfVariantsTestedHelpButtonB) + + if (userOrOffspring == "User"){ + + diseasesInfoGrid := container.NewHBox(layout.NewSpacer(), diseaseInfoButtonsColumn, diseaseNameColumn, userProbabilityOfPassingVariantColumn, userNumberOfVariantsTestedColumn, layout.NewSpacer()) + return diseasesInfoGrid, nil + } + + diseasesInfoGrid := container.NewHBox(layout.NewSpacer(), diseaseInfoButtonsColumn, diseaseNameColumn, offspringProbabilityOfHavingDiseaseColumn, offspringProbabilityOfHavingAVariantColumn, offspringNumberOfVariantsTestedColumn, layout.NewSpacer()) + + return diseasesInfoGrid, nil + } + + diseasesInfoGrid, err := getDiseasesInfoGrid() + if (err != nil){ + setErrorEncounteredPage(window, err, previousPage) + return + } + + page := container.NewVBox(title, backButton, widget.NewSeparator(), subtitle, widget.NewSeparator(), description1, description2, description3, widget.NewSeparator(), userOrOffspringSelectorCentered, widget.NewSeparator(), diseasesInfoGrid) + + setPageContent(page, window) +} + +func setViewMateProfilePage_PolygenicDiseases(window fyne.Window, userOrOffspring string, getAnyUserProfileAttributeFunction func(string)(bool, int, string, error), previousPage func()){ + + currentPage := func(){setViewMateProfilePage_PolygenicDiseases(window, userOrOffspring, getAnyUserProfileAttributeFunction, previousPage)} + + title := getPageTitleCentered(translate("View Profile - Physical")) + + backButton := getBackButtonCentered(previousPage) + + subtitle := getPageSubtitleCentered(translate("Polygenic Diseases")) + + description1 := getLabelCentered("Below is the polygenic disease risk analysis for this user.") + description2 := getLabelCentered("You can choose to view the analysis of the user or an offspring between you and the user.") + description3 := getLabelCentered("You must link your genome person in the Build Profile menu to see offspring information.") + + handleSelectButton := func(newUserOrOffspring string){ + if (userOrOffspring == newUserOrOffspring){ + return + } + setViewMateProfilePage_PolygenicDiseases(window, newUserOrOffspring, getAnyUserProfileAttributeFunction, previousPage) + } + + userOrOffspringSelector := widget.NewSelect([]string{"User", "Offspring"}, handleSelectButton) + userOrOffspringSelector.Selected = userOrOffspring + + userOrOffspringSelectorCentered := getWidgetCentered(userOrOffspringSelector) + + getDiseaseInfoGrid := func()(*fyne.Container, error){ + + emptyLabelA := widget.NewLabel("") + diseaseNameLabel := getItalicLabelCentered("Disease Name") + + emptyLabelB := widget.NewLabel("") + userRiskScoreLabel := getItalicLabelCentered("User Risk Score") + + emptyLabelC := widget.NewLabel("") + offspringRiskScoreLabel := getItalicLabelCentered("Offspring Risk Score") + + userNumberOfLabel := getItalicLabelCentered("User Number Of") + lociTestedLabelA := getItalicLabelCentered("Loci Tested") + + offspringNumberOfLabel := getItalicLabelCentered("Offspring Number Of") + lociTestedLabelB := getItalicLabelCentered("Loci Tested") + + emptyLabelD := widget.NewLabel("") + emptyLabelE := widget.NewLabel("") + + diseaseNameColumn := container.NewVBox(emptyLabelA, diseaseNameLabel, widget.NewSeparator()) + userRiskScoreColumn := container.NewVBox(emptyLabelB, userRiskScoreLabel, widget.NewSeparator()) + offspringRiskScoreColumn := container.NewVBox(emptyLabelC, offspringRiskScoreLabel, widget.NewSeparator()) + userNumberOfLociTestedColumn := container.NewVBox(userNumberOfLabel, lociTestedLabelA, widget.NewSeparator()) + offspringNumberOfLociTestedColumn := container.NewVBox(offspringNumberOfLabel, lociTestedLabelB, widget.NewSeparator()) + viewDiseaseInfoButtonsColumn := container.NewVBox(emptyLabelD, emptyLabelE, widget.NewSeparator()) + + myPersonChosen, myGenomesExist, myAnalysisIsReady, myAnalysisMapList, myGenomeIdentifier, _, err := myChosenAnalysis.GetMyChosenMateGeneticAnalysis() + if (err != nil) { return nil, err } + + diseaseObjectsList, err := polygenicDiseases.GetPolygenicDiseaseObjectsList() + if (err != nil) { return nil, err } + + for _, diseaseObject := range diseaseObjectsList{ + + diseaseName := diseaseObject.DiseaseName + diseaseLociList := diseaseObject.LociList + + userRiskWeightSum := 0 + userMinimumPossibleRiskWeightSum := 0 + userMaximumPossibleRiskWeightSum := 0 + userNumberOfLociTested := 0 + + offspringRiskWeightSum := 0 + offspringMinimumPossibleRiskWeightSum := 0 + offspringMaximumPossibleRiskWeightSum := 0 + offspringNumberOfLociTested := 0 + + for _, locusObject := range diseaseLociList{ + + locusIdentifier := locusObject.LocusIdentifier + locusRSID := locusObject.LocusRSID + + locusRSIDString := helpers.ConvertInt64ToString(locusRSID) + + locusRiskWeightsMap := locusObject.RiskWeightsMap + locusOddsRatiosMap := locusObject.OddsRatiosMap + locusMinimumRiskWeight := locusObject.MinimumRiskWeight + locusMaximumRiskWeight := locusObject.MaximumRiskWeight + + locusValueAttributeName := "LocusValue_rs" + locusRSIDString + + userLocusBasePairExists, _, userLocusBasePair, err := getAnyUserProfileAttributeFunction(locusValueAttributeName) + if (err != nil) { return nil, err } + if (userLocusBasePairExists == true){ + + userNumberOfLociTested += 1 + + userMinimumPossibleRiskWeightSum += locusMinimumRiskWeight + userMaximumPossibleRiskWeightSum += locusMaximumRiskWeight + + userLocusRiskWeight, exists := locusRiskWeightsMap[userLocusBasePair] + if (exists == false){ + // We do not know the risk weight for this base pair + // We treat this as a 0 risk weight + } else { + userRiskWeightSum += userLocusRiskWeight + } + } + + //Outputs: + // -bool: My locus base pair exists + // -string: My locus base pair + // -error + getMyLocusInfo := func()(bool, string, error){ + if (myPersonChosen == false || myGenomesExist == false || myAnalysisIsReady == false){ + return false, "", nil + } + + locusInfoKnown, _, locusBasePair, _, _, _, err := readGeneticAnalysis.GetPersonPolygenicDiseaseLocusInfoFromGeneticAnalysis(myAnalysisMapList, diseaseName, locusIdentifier, myGenomeIdentifier) + if (err != nil){ return false, "", err } + if (locusInfoKnown == false){ + return false, "", nil + } + return true, locusBasePair, nil + } + + myLocusBasePairExists, myLocusBasePair, err := getMyLocusInfo() + if (err != nil) { return nil, err } + + if (userLocusBasePairExists == true && myLocusBasePairExists == true){ + + offspringLocusRiskWeight, _, _, _, err := createGeneticAnalysis.GetOffspringPolygenicDiseaseLocusInfo(locusRiskWeightsMap, locusOddsRatiosMap, myLocusBasePair, userLocusBasePair) + if (err != nil) { return nil, err } + + offspringNumberOfLociTested += 1 + + offspringMinimumPossibleRiskWeightSum += locusMinimumRiskWeight + offspringMaximumPossibleRiskWeightSum += locusMaximumRiskWeight + + offspringRiskWeightSum += offspringLocusRiskWeight + } + } + + getUserDiseaseRiskScoreString := func()(string, error){ + + if (userNumberOfLociTested == 0){ + result := translate("Unknown") + + return result, nil + } + + userRiskScore, err := helpers.ScaleNumberProportionally(true, userRiskWeightSum, userMinimumPossibleRiskWeightSum, userMaximumPossibleRiskWeightSum, 0, 10) + if (err != nil) { return "", err } + + userRiskScoreString := helpers.ConvertIntToString(userRiskScore) + resultFormatted := userRiskScoreString + "/10" + + return resultFormatted, nil + } + + userDiseaseRiskScore, err := getUserDiseaseRiskScoreString() + if (err != nil) { return nil, err } + + getOffspringDiseaseRiskScoreString := func()(string, error){ + + if (offspringNumberOfLociTested == 0){ + result := translate("Unknown") + + return result, nil + } + + offspringRiskScore, err := helpers.ScaleNumberProportionally(true, offspringRiskWeightSum, offspringMinimumPossibleRiskWeightSum, offspringMaximumPossibleRiskWeightSum, 0, 10) + if (err != nil) { return "", err } + + offspringRiskScoreString := helpers.ConvertIntToString(offspringRiskScore) + resultFormatted := offspringRiskScoreString + "/10" + return resultFormatted, nil + } + + offspringDiseaseRiskScore, err := getOffspringDiseaseRiskScoreString() + if (err != nil) { return nil, err } + + totalNumberOfDiseaseLoci := len(diseaseLociList) + totalNumberOfDiseaseLociString := helpers.ConvertIntToString(totalNumberOfDiseaseLoci) + + userNumberOfLociTestedString := helpers.ConvertIntToString(userNumberOfLociTested) + userNumberOfLociTestedFormatted := userNumberOfLociTestedString + "/" + totalNumberOfDiseaseLociString + offspringNumberOfLociTestedString := helpers.ConvertIntToString(offspringNumberOfLociTested) + offspringNumberOfLociTestedFormatted := offspringNumberOfLociTestedString + "/" + totalNumberOfDiseaseLociString + + diseaseNameText := getBoldLabelCentered(diseaseName) + userRiskScoreLabel := getBoldLabelCentered(userDiseaseRiskScore) + offspringRiskScoreLabel := getBoldLabelCentered(offspringDiseaseRiskScore) + userNumberOfLociTestedLabel := getBoldLabelCentered(userNumberOfLociTestedFormatted) + offspringNumberOfLociTestedLabel := getBoldLabelCentered(offspringNumberOfLociTestedFormatted) + viewDiseaseDetailsButton := widget.NewButtonWithIcon("", theme.VisibilityIcon(), func(){ + setViewMateProfilePage_PolygenicDiseaseLoci(window, diseaseName, userOrOffspring, getAnyUserProfileAttributeFunction, currentPage) + }) + + diseaseNameColumn.Add(diseaseNameText) + userRiskScoreColumn.Add(userRiskScoreLabel) + offspringRiskScoreColumn.Add(offspringRiskScoreLabel) + userNumberOfLociTestedColumn.Add(userNumberOfLociTestedLabel) + offspringNumberOfLociTestedColumn.Add(offspringNumberOfLociTestedLabel) + viewDiseaseInfoButtonsColumn.Add(viewDiseaseDetailsButton) + + diseaseNameColumn.Add(widget.NewSeparator()) + userRiskScoreColumn.Add(widget.NewSeparator()) + offspringRiskScoreColumn.Add(widget.NewSeparator()) + userNumberOfLociTestedColumn.Add(widget.NewSeparator()) + offspringNumberOfLociTestedColumn.Add(widget.NewSeparator()) + viewDiseaseInfoButtonsColumn.Add(widget.NewSeparator()) + } + + userRiskScoreHelpButton := widget.NewButtonWithIcon("", theme.QuestionIcon(), func(){ + setPolygenicDiseaseRiskScoreExplainerPage(window, currentPage) + }) + + offspringRiskScoreHelpButton := widget.NewButtonWithIcon("", theme.QuestionIcon(), func(){ + setOffspringPolygenicDiseaseRiskScoreExplainerPage(window, currentPage) + }) + userNumberOfLociTestedButton := widget.NewButtonWithIcon("", theme.QuestionIcon(), func(){ + setPolygenicDiseaseNumberOfLociTestedExplainerPage(window, currentPage) + }) + + offspringNumberOfLociTestedButton := widget.NewButtonWithIcon("", theme.QuestionIcon(), func(){ + setOffspringPolygenicDiseaseNumberOfLociTestedExplainerPage(window, currentPage) + }) + + userRiskScoreColumn.Add(userRiskScoreHelpButton) + offspringRiskScoreColumn.Add(offspringRiskScoreHelpButton) + userNumberOfLociTestedColumn.Add(userNumberOfLociTestedButton) + offspringNumberOfLociTestedColumn.Add(offspringNumberOfLociTestedButton) + + if (userOrOffspring == "User"){ + diseaseInfoGrid := container.NewHBox(layout.NewSpacer(), diseaseNameColumn, userRiskScoreColumn, userNumberOfLociTestedColumn, viewDiseaseInfoButtonsColumn, layout.NewSpacer()) + + return diseaseInfoGrid, nil + } + + diseaseInfoGrid := container.NewHBox(layout.NewSpacer(), diseaseNameColumn, offspringRiskScoreColumn, offspringNumberOfLociTestedColumn, viewDiseaseInfoButtonsColumn, layout.NewSpacer()) + + return diseaseInfoGrid, nil + } + + diseaseInfoGrid, err := getDiseaseInfoGrid() + if (err != nil){ + setErrorEncounteredPage(window, err, previousPage) + return + } + + page := container.NewVBox(title, backButton, widget.NewSeparator(), subtitle, widget.NewSeparator(), description1, description2, description3, widget.NewSeparator(), userOrOffspringSelectorCentered, widget.NewSeparator(), diseaseInfoGrid) + + setPageContent(page, window) +} + +func setViewMateProfilePage_PolygenicDiseaseLoci(window fyne.Window, diseaseName string, userOrOffspring string, getAnyUserProfileAttributeFunction func(string)(bool, int, string, error), previousPage func()){ + + setLoadingScreen(window, translate("View Profile - Physical"), "Loading " + userOrOffspring + " Disease Loci Info") + + if (userOrOffspring != "User" && userOrOffspring != "Offspring"){ + setErrorEncounteredPage(window, errors.New("setViewMateProfilePage_DiseaseLoci called with invalid userOrOffspring: " + userOrOffspring), previousPage) + return + } + + currentPage := func(){setViewMateProfilePage_PolygenicDiseaseLoci(window, diseaseName, userOrOffspring, getAnyUserProfileAttributeFunction, previousPage)} + + title := getPageTitleCentered(translate("View Profile - Physical")) + + backButton := getBackButtonCentered(previousPage) + + subtitle := getPageSubtitleCentered(translate(userOrOffspring + " Disease Loci Info")) + + diseaseNameLabel := widget.NewLabel(translate("Disease Name:")) + + diseaseNameText := getBoldLabel(translate(diseaseName)) + + viewDiseaseInfoButton := widget.NewButtonWithIcon("", theme.InfoIcon(), func(){ + setViewPolygenicDiseaseDetailsPage(window, diseaseName, currentPage) + }) + + diseaseNameRow := container.NewHBox(layout.NewSpacer(), diseaseNameLabel, diseaseNameText, viewDiseaseInfoButton, layout.NewSpacer()) + + diseaseObject, err := polygenicDiseases.GetPolygenicDiseaseObject(diseaseName) + if (err != nil){ + setErrorEncounteredPage(window, err, previousPage) + return + } + + diseaseLocusObjectsList := diseaseObject.LociList + + numberOfDiseaseLoci := len(diseaseLocusObjectsList) + + myPersonChosen, myGenomesExist, myAnalysisIsReady, myAnalysisMapList, myGenomeIdentifier, _, err := myChosenAnalysis.GetMyChosenMateGeneticAnalysis() + if (err != nil) { + setErrorEncounteredPage(window, err, previousPage) + return + } + + //Outputs: + // -bool: User locus Info is known + // -int: User locus Risk weight + // -string: Locus base pair + // -bool: User locus odds ratio known + // -string: User locus odds ratio formatted + // -error + getUserLocusInfo := func(locusRSID int64, locusRiskWeightsMap map[string]int, locusOddsRatiosMap map[string]float64)(bool, int, string, bool, string, error){ + + locusRSIDString := helpers.ConvertInt64ToString(locusRSID) + + locusValueAttributeName := "LocusValue_rs" + locusRSIDString + + userLocusBasePairExists, _, userLocusBasePair, err := getAnyUserProfileAttributeFunction(locusValueAttributeName) + if (err != nil) { return false, 0, "", false, "", err } + if (userLocusBasePairExists == false){ + return false, 0, "", false, "", nil + } + + userLocusRiskWeight, exists := locusRiskWeightsMap[userLocusBasePair] + if (exists == false){ + // We do not know the risk weight for this base pair + // We treat this as a 0 risk weight + return true, 0, userLocusBasePair, false, "", nil + } + + locusOddsRatio, exists := locusOddsRatiosMap[userLocusBasePair] + if (exists == false){ + return true, userLocusRiskWeight, userLocusBasePair, false, "", nil + } + + locusOddsRatioString := helpers.ConvertFloat64ToStringRounded(locusOddsRatio, 2) + + locusOddsRatioFormatted := locusOddsRatioString + "x" + + return true, userLocusRiskWeight, userLocusBasePair, true, locusOddsRatioFormatted, nil + } + + //Outputs: + // -bool: My locus Info is known + // -string: My locus base pair + // -error + getMyLocusInfo := func(locusIdentifier string)(bool, string, error){ + if (myPersonChosen == false || myGenomesExist == false || myAnalysisIsReady == false){ + return false, "", nil + } + + locusInfoKnown, _, locusBasePair, _, _, _, err := readGeneticAnalysis.GetPersonPolygenicDiseaseLocusInfoFromGeneticAnalysis(myAnalysisMapList, diseaseName, locusIdentifier, myGenomeIdentifier) + if (err != nil){ return false, "", err } + if (locusInfoKnown == false){ + return false, "", nil + } + return true, locusBasePair, nil + } + + getNumberOfLociTested := func()(int, error){ + + if (userOrOffspring == "Offspring"){ + if (myPersonChosen == false || myGenomesExist == false || myAnalysisIsReady == false){ + return 0, nil + } + } + + numberOfLociTested := 0 + + for _, locusObject := range diseaseLocusObjectsList{ + + locusIdentifier := locusObject.LocusIdentifier + locusRSID := locusObject.LocusRSID + locusRiskWeightsMap := locusObject.RiskWeightsMap + locusOddsRatiosMap := locusObject.OddsRatiosMap + + userLocusInfoIsKnown, _, _, _, _, err := getUserLocusInfo(locusRSID, locusRiskWeightsMap, locusOddsRatiosMap) + if (err != nil) { return 0, err } + if (userLocusInfoIsKnown == false){ + continue + } + + if (userOrOffspring == "Offspring") { + + myLocusInfoKnown, _, err := getMyLocusInfo(locusIdentifier) + if (err != nil) { return 0, err } + if (myLocusInfoKnown == false){ + continue + } + } + + numberOfLociTested += 1 + } + + return numberOfLociTested, nil + } + + numberOfLociTested, err := getNumberOfLociTested() + if (err != nil){ + setErrorEncounteredPage(window, err, previousPage) + return + } + numberOfLociTestedString := helpers.ConvertIntToString(numberOfLociTested) + totalNumberOfDiseaseLociString := helpers.ConvertIntToString(numberOfDiseaseLoci) + lociTestedString := numberOfLociTestedString + "/" + totalNumberOfDiseaseLociString + + lociTestedLabel := widget.NewLabel("Loci Tested:") + + lociTestedText := getBoldLabel(lociTestedString) + + lociTestedHelpButton := widget.NewButtonWithIcon("", theme.QuestionIcon(), func(){ + if (userOrOffspring == "User"){ + setPolygenicDiseaseNumberOfLociTestedExplainerPage(window, currentPage) + } else { + setOffspringPolygenicDiseaseNumberOfLociTestedExplainerPage(window, currentPage) + } + }) + + lociTestedRow := container.NewHBox(layout.NewSpacer(), lociTestedLabel, lociTestedText, lociTestedHelpButton, layout.NewSpacer()) + + //TODO: Navigation buttons and multiple pages + + getDiseaseLociGrid := func()(*fyne.Container, error){ + + //TODO: Sort loci + + locusNameLabel := getItalicLabelCentered("Locus Name") + + userRiskWeightLabel := getItalicLabelCentered("User Risk Weight") + offspringRiskWeightLabel := getItalicLabelCentered("Offspring Risk Weight") + + userOddsRatioLabel := getItalicLabelCentered("User Odds Ratio") + offspringOddsRatioLabel := getItalicLabelCentered("Offspring Odds Ratio") + + emptyLabel := widget.NewLabel("") + + locusNameColumn := container.NewVBox(locusNameLabel, widget.NewSeparator()) + userRiskWeightColumn := container.NewVBox(userRiskWeightLabel, widget.NewSeparator()) + offspringRiskWeightColumn := container.NewVBox(offspringRiskWeightLabel, widget.NewSeparator()) + userOddsRatioColumn := container.NewVBox(userOddsRatioLabel, widget.NewSeparator()) + offspringOddsRatioColumn := container.NewVBox(offspringOddsRatioLabel, widget.NewSeparator()) + locusInfoButtonsColumn := container.NewVBox(emptyLabel, widget.NewSeparator()) + + for _, locusObject := range diseaseLocusObjectsList{ + + locusIdentifier := locusObject.LocusIdentifier + + locusRSID := locusObject.LocusRSID + locusRSIDString := helpers.ConvertInt64ToString(locusRSID) + locusName := "rs" + locusRSIDString + + locusRiskWeightsMap := locusObject.RiskWeightsMap + locusOddsRatiosMap := locusObject.OddsRatiosMap + + userLocusInfoIsKnown, userLocusRiskWeight, userLocusBasePair, userLocusOddsRatioKnown, userLocusOddsRatioFormatted, err := getUserLocusInfo(locusRSID, locusRiskWeightsMap, locusOddsRatiosMap) + if (err != nil) { return nil, err } + + myLocusInfoIsKnown, myLocusBasePair, err := getMyLocusInfo(locusIdentifier) + if (err != nil) { return nil, err } + + getUserRiskWeightString := func()string{ + + if (userLocusInfoIsKnown == false){ + result := translate("Unknown") + return result + } + + userRiskWeightString := helpers.ConvertIntToString(userLocusRiskWeight) + return userRiskWeightString + } + + userRiskWeightString := getUserRiskWeightString() + + getUserOddsRatioString := func()string{ + if (userLocusOddsRatioKnown == false){ + result := translate("Unknown") + return result + } + return userLocusOddsRatioFormatted + } + + userOddsRatioString := getUserOddsRatioString() + + //Outputs: + // -bool: Offspring disease locus info known + // -int: Offspring risk weight + // -bool: Offspring odds ratio known + // -string: Offspring odds ratio formatted + // -error + getOffspringDiseaseLocusInfo := func()(bool, int, bool, string, error){ + + if (userLocusInfoIsKnown == false || myLocusInfoIsKnown == false){ + return false, 0, false, "", nil + } + + offspringLocusRiskWeight, offspringOddsRatioIsKnown, offspringOddsRatio, unknownOddsRatiosWeightSum, err := createGeneticAnalysis.GetOffspringPolygenicDiseaseLocusInfo(locusRiskWeightsMap, locusOddsRatiosMap, myLocusBasePair, userLocusBasePair) + if (err != nil) { return false, 0, false, "", err } + + if (offspringOddsRatioIsKnown == false){ + return true, offspringLocusRiskWeight, false, "", nil + } + + getOddsRatioFormatted := func()string{ + + offspringOddsRatioString := helpers.ConvertFloat64ToStringRounded(offspringOddsRatio, 2) + + if (unknownOddsRatiosWeightSum > 0){ + result := offspringOddsRatioString + "x+" + return result + } + if (unknownOddsRatiosWeightSum < 0){ + result := "<" + offspringOddsRatioString + "x" + return result + } + result := offspringOddsRatioString + "x" + return result + } + + oddsRatioFormatted := getOddsRatioFormatted() + return true, offspringLocusRiskWeight, true, oddsRatioFormatted, nil + } + + offspringDiseaseLocusKnown, offspringRiskWeight, offspringOddsRatioKnown, offspringOddsRatioFormatted, err := getOffspringDiseaseLocusInfo() + if (err != nil) { return nil, err } + + getOffspringRiskWeightString := func()string{ + + if (offspringDiseaseLocusKnown == false){ + result := translate("Unknown") + return result + } + + offspringRiskWeightString := helpers.ConvertIntToString(offspringRiskWeight) + return offspringRiskWeightString + } + + offspringRiskWeightString := getOffspringRiskWeightString() + + getOffspringOddsRatioString := func()string{ + + if (offspringDiseaseLocusKnown == false || offspringOddsRatioKnown == false){ + result := translate("Unknown") + return result + } + return offspringOddsRatioFormatted + } + + offspringOddsRatioString := getOffspringOddsRatioString() + + locusNameLabel := getBoldLabelCentered(locusName) + userRiskWeightLabel := getBoldLabelCentered(userRiskWeightString) + offspringRiskWeightLabel := getBoldLabelCentered(offspringRiskWeightString) + userOddsRatioLabel := getBoldLabelCentered(userOddsRatioString) + offspringOddsRatioLabel := getBoldLabelCentered(offspringOddsRatioString) + locusInfoButton := widget.NewButtonWithIcon("", theme.InfoIcon(), func(){ + setViewPolygenicDiseaseLocusDetailsPage(window, diseaseName, locusIdentifier, currentPage) + }) + + locusNameColumn.Add(locusNameLabel) + userRiskWeightColumn.Add(userRiskWeightLabel) + offspringRiskWeightColumn.Add(offspringRiskWeightLabel) + userOddsRatioColumn.Add(userOddsRatioLabel) + offspringOddsRatioColumn.Add(offspringOddsRatioLabel) + locusInfoButtonsColumn.Add(locusInfoButton) + + locusNameColumn.Add(widget.NewSeparator()) + userRiskWeightColumn.Add(widget.NewSeparator()) + offspringRiskWeightColumn.Add(widget.NewSeparator()) + userOddsRatioColumn.Add(widget.NewSeparator()) + offspringOddsRatioColumn.Add(widget.NewSeparator()) + locusInfoButtonsColumn.Add(widget.NewSeparator()) + } + + userRiskWeightHelpButton := widget.NewButtonWithIcon("", theme.QuestionIcon(), func(){ + setPolygenicDiseaseLocusRiskWeightExplainerPage(window, currentPage) + }) + offspringRiskWeightHelpButton := widget.NewButtonWithIcon("", theme.QuestionIcon(), func(){ + setOffspringPolygenicDiseaseLocusRiskWeightExplainerPage(window, currentPage) + }) + + userRiskWeightColumn.Add(userRiskWeightHelpButton) + offspringRiskWeightColumn.Add(offspringRiskWeightHelpButton) + + if (userOrOffspring == "User"){ + + diseaseLociGrid := container.NewHBox(layout.NewSpacer(), locusNameColumn, userRiskWeightColumn, userOddsRatioColumn, locusInfoButtonsColumn, layout.NewSpacer()) + + return diseaseLociGrid, nil + } + + diseaseLociGrid := container.NewHBox(layout.NewSpacer(), locusNameColumn, offspringRiskWeightColumn, offspringOddsRatioColumn, locusInfoButtonsColumn, layout.NewSpacer()) + + return diseaseLociGrid, nil + } + + diseaseLociGrid, err := getDiseaseLociGrid() + if (err != nil){ + setErrorEncounteredPage(window, err, previousPage) + return + } + + page := container.NewVBox(title, backButton, widget.NewSeparator(), subtitle, widget.NewSeparator(), diseaseNameRow, lociTestedRow, widget.NewSeparator(), diseaseLociGrid) + + setPageContent(page, window) +} + +func setViewMateProfilePage_GeneticTraits(window fyne.Window, userOrOffspring string, getAnyUserProfileAttributeFunction func(string)(bool, int, string, error), previousPage func()){ + + currentPage := func(){setViewMateProfilePage_GeneticTraits(window, userOrOffspring, getAnyUserProfileAttributeFunction, previousPage)} + + title := getPageTitleCentered(translate("View Profile - Physical")) + + backButton := getBackButtonCentered(previousPage) + + subtitle := getPageSubtitleCentered(translate("Genetic Traits")) + + description1 := getLabelCentered("Below is the genetic trait analysis for this user.") + description2 := getLabelCentered("You can choose to view the analysis of the user or an offspring between you and the user.") + description3 := getLabelCentered("You must link your genome person in the Build Profile menu to see offspring information.") + + handleSelectButton := func(newUserOrOffspring string){ + if (userOrOffspring == newUserOrOffspring){ + return + } + setViewMateProfilePage_GeneticTraits(window, newUserOrOffspring, getAnyUserProfileAttributeFunction, previousPage) + } + + userOrOffspringSelector := widget.NewSelect([]string{"User", "Offspring"}, handleSelectButton) + userOrOffspringSelector.Selected = userOrOffspring + + userOrOffspringSelectorCentered := getWidgetCentered(userOrOffspringSelector) + + getTraitsInfoGrid := func()(*fyne.Container, error){ + + emptyLabelA := widget.NewLabel("") + traitNameLabel := getItalicLabelCentered("Trait Name") + + emptyLabelB := widget.NewLabel("") + userOutcomeScoresLabel := getItalicLabelCentered("User Outcome Scores") + + emptyLabelC := widget.NewLabel("") + offspringOutcomeScoresLabel := getItalicLabelCentered("Offspring Outcome Scores") + + numberOfLabelA := getItalicLabelCentered("Number Of") + rulesTestedLabelA := getItalicLabelCentered("Rules Tested") + + numberOfLabelB := getItalicLabelCentered("Number Of") + rulesTestedLabelB := getItalicLabelCentered("Rules Tested") + + emptyLabelD := widget.NewLabel("") + emptyLabelE := widget.NewLabel("") + + traitNameColumn := container.NewVBox(emptyLabelA, traitNameLabel, widget.NewSeparator()) + userOutcomeScoresColumn := container.NewVBox(emptyLabelB, userOutcomeScoresLabel, widget.NewSeparator()) + offspringOutcomeScoresColumn := container.NewVBox(emptyLabelC, offspringOutcomeScoresLabel, widget.NewSeparator()) + userNumberOfRulesTestedColumn := container.NewVBox(numberOfLabelA, rulesTestedLabelA, widget.NewSeparator()) + offspringNumberOfRulesTestedColumn := container.NewVBox(numberOfLabelB, rulesTestedLabelB, widget.NewSeparator()) + viewTraitDetailsButtonsColumn := container.NewVBox(emptyLabelD, emptyLabelE, widget.NewSeparator()) + + myPersonChosen, myGenomesExist, myAnalysisIsReady, myAnalysisMapList, myGenomeIdentifier, _, err := myChosenAnalysis.GetMyChosenMateGeneticAnalysis() + if (err != nil) { return nil, err } + + traitObjectsList, err := traits.GetTraitObjectsList() + if (err != nil) { return nil, err } + + for _, traitObject := range traitObjectsList{ + + traitName := traitObject.TraitName + + traitRulesList := traitObject.RulesList + totalNumberOfTraitRules := len(traitRulesList) + + if (totalNumberOfTraitRules == 0){ + // We are not able to analyze these traits yet + // We will once we add neural network prediction + continue + } + + totalNumberOfTraitRulesString := helpers.ConvertIntToString(totalNumberOfTraitRules) + + traitOutcomeNamesList := traitObject.OutcomesList + + // We have to sort outcome names so they always show up in the same order + + traitOutcomeNamesListSorted := helpers.CopyAndSortStringListToUnicodeOrder(traitOutcomeNamesList) + + traitNameText := getBoldLabelCentered(translate(traitName)) + traitNameColumn.Add(traitNameText) + + viewTraitDetailsButton := widget.NewButtonWithIcon("", theme.VisibilityIcon(), func(){ + setViewMateProfilePage_TraitRules(window, traitName, userOrOffspring, getAnyUserProfileAttributeFunction, currentPage) + }) + + viewTraitDetailsButtonsColumn.Add(viewTraitDetailsButton) + + //Outputs: + // -bool: At least 1 rule is known + // -map[string]int: Outcome name -> Outcome score + // -int: Number of rules tested + // -error + getUserTraitOutcomeScoresMap := func()(bool, map[string]int, int, error){ + + userTraitOutcomeScoresMap := make(map[string]int) + userNumberOfRulesTested := 0 + + for _, traitRuleObject := range traitRulesList{ + + // Outputs: + // -bool: Status is known + // -bool: User passes rule + // -error + getUserPassesRuleStatus := func()(bool, bool, error){ + + ruleLociList := traitRuleObject.LociList + allRuleLociKnown := true + + for _, ruleLocusObject := range ruleLociList{ + + locusRSID := ruleLocusObject.LocusRSID + + locusRSIDString := helpers.ConvertInt64ToString(locusRSID) + + userLocusValueAttributeName := "LocusValue_rs" + locusRSIDString + + userLocusBasePairIsKnown, _, userLocusBasePair, err := getAnyUserProfileAttributeFunction(userLocusValueAttributeName) + if (err != nil) { return false, false, err } + if (userLocusBasePairIsKnown == false){ + // We know rule is not passed + // We keep searching to see if ruleIsPassed status is No or Unknown + allRuleLociKnown = false + continue + } + + ruleLocusBasePairsList := ruleLocusObject.BasePairsList + + userPassesRuleLocus := slices.Contains(ruleLocusBasePairsList, userLocusBasePair) + if (userPassesRuleLocus == false){ + // We know the rule is not passed + return true, false, nil + } + } + if (allRuleLociKnown == false){ + // Rule status is unknown. Any loci which we knew must have passed. + return false, false, nil + } + // Rule is passed + return true, true, nil + } + + userRuleStatusIsKnown, userPassesRule, err := getUserPassesRuleStatus() + if (err != nil) { return false, nil, 0, err } + if (userRuleStatusIsKnown == false){ + continue + } + userNumberOfRulesTested += 1 + + if (userPassesRule == true){ + + ruleOutcomePointsMap := traitRuleObject.OutcomePointsMap + + for traitOutcome, pointsEffect := range ruleOutcomePointsMap{ + + userTraitOutcomeScoresMap[traitOutcome] += pointsEffect + } + } + } + + if (userNumberOfRulesTested == 0){ + return false, nil, 0, nil + } + + traitOutcomesList := traitObject.OutcomesList + + // We add all outcomes for which there were no points + + for _, traitOutcome := range traitOutcomesList{ + + _, exists := userTraitOutcomeScoresMap[traitOutcome] + if (exists == false){ + userTraitOutcomeScoresMap[traitOutcome] = 0 + } + } + + return true, userTraitOutcomeScoresMap, userNumberOfRulesTested, nil + } + + //Outputs: + // -bool: At least 1 rule is known + // -map[string]float64: Outcome name -> Outcome score + // -int: Number of rules tested + // -error + getOffspringTraitOutcomeScoresMap := func()(bool, map[string]float64, int, error){ + + offspringTraitOutcomeScoresMap := make(map[string]float64) + offspringNumberOfRulesTested := 0 + + for _, traitRuleObject := range traitRulesList{ + + if (myPersonChosen == false || myGenomesExist == false || myAnalysisIsReady == false){ + // Without my genome person chosen, all offspring rules and outcome scores are unknown + return false, nil, 0, nil + } + + ruleIdentifier := traitRuleObject.RuleIdentifier + + _, _, myRuleLociBasePairsMap, err := readGeneticAnalysis.GetPersonTraitRuleInfoFromGeneticAnalysis(myAnalysisMapList, traitName, ruleIdentifier, myGenomeIdentifier) + if (err != nil) { return false, nil, 0, err } + + ruleLociList := traitRuleObject.LociList + + //Outputs: + // -bool: Probability is known + // -float64: Offspring probability of passing rule + // -error + getOffspringProbabilityOfPassingRule := func()(bool, float64, error){ + + offspringProbabilityOfPassingRule := float64(1) + + for _, ruleLocusObject := range ruleLociList{ + + locusIdentifier := ruleLocusObject.LocusIdentifier + locusRSID := ruleLocusObject.LocusRSID + + locusRSIDString := helpers.ConvertInt64ToString(locusRSID) + + userLocusValueAttributeName := "LocusValue_rs" + locusRSIDString + + userLocusBasePairIsKnown, _, userLocusBasePair, err := getAnyUserProfileAttributeFunction(userLocusValueAttributeName) + if (err != nil) { return false, 0, err } + if (userLocusBasePairIsKnown == false){ + // We must know all rule loci base pairs to determine offspring probability of passing rule + return false, 0, nil + } + + myLocusBasePair, myLocusBasePairIsKnown := myRuleLociBasePairsMap[locusIdentifier] + if (myLocusBasePairIsKnown == false){ + // We must know all rule loci base pairs to determine offspring probability of passing rule + return false, 0, nil + } + + locusRequiredBasePairsList := ruleLocusObject.BasePairsList + + offspringProbabilityOfPassingRuleLocus, err := createGeneticAnalysis.GetOffspringTraitRuleLocusInfo(locusRequiredBasePairsList, userLocusBasePair, myLocusBasePair) + if (err != nil) { return false, 0, err } + + offspringProbabilityOfPassingRule *= offspringProbabilityOfPassingRuleLocus + } + + return true, offspringProbabilityOfPassingRule, nil + } + offspringProbabilityOfPassingRuleKnown, offspringProbabilityOfPassingRule, err := getOffspringProbabilityOfPassingRule() + if (err != nil) { return false, nil, 0, err } + if (offspringProbabilityOfPassingRuleKnown == false){ + continue + } + offspringNumberOfRulesTested += 1 + + ruleOutcomePointsMap := traitRuleObject.OutcomePointsMap + + for traitOutcome, pointsEffect := range ruleOutcomePointsMap{ + + pointsToAdd := float64(pointsEffect) * offspringProbabilityOfPassingRule + + offspringTraitOutcomeScoresMap[traitOutcome] += pointsToAdd + } + } + + if (offspringNumberOfRulesTested == 0){ + return false, nil, 0, nil + } + + traitOutcomesList := traitObject.OutcomesList + + // We add all outcomes for which there were no points + + for _, traitOutcome := range traitOutcomesList{ + + _, exists := offspringTraitOutcomeScoresMap[traitOutcome] + if (exists == false){ + offspringTraitOutcomeScoresMap[traitOutcome] = 0 + } + } + + return true, offspringTraitOutcomeScoresMap, offspringNumberOfRulesTested, nil + } + + if (userOrOffspring == "User"){ + + userTraitOutcomeScoresKnown, userTraitOutcomeScoresMap, userNumberOfRulesTested, err := getUserTraitOutcomeScoresMap() + if (err != nil) { return nil, err } + + numberOfRulesTestedString := helpers.ConvertIntToString(userNumberOfRulesTested) + numberOfRulesTestedFormatted := numberOfRulesTestedString + "/" + totalNumberOfTraitRulesString + numberOfRulesTestedLabel := getBoldLabelCentered(numberOfRulesTestedFormatted) + userNumberOfRulesTestedColumn.Add(numberOfRulesTestedLabel) + + if (userTraitOutcomeScoresKnown == false){ + unknownTranslated := translate("Unknown") + unknownLabel := getBoldLabelCentered(unknownTranslated) + + userOutcomeScoresColumn.Add(unknownLabel) + } else { + + for index, outcomeName := range traitOutcomeNamesListSorted{ + + outcomeScore, exists := userTraitOutcomeScoresMap[outcomeName] + if (exists == false){ + return nil, errors.New("Outcome not found in userTraitOutcomeScoresMap.") + } + + outcomeScoreString := helpers.ConvertIntToString(outcomeScore) + + outcomeRow := getBoldLabelCentered(outcomeName + ": " + outcomeScoreString) + userOutcomeScoresColumn.Add(outcomeRow) + + if (index > 0){ + + emptyLabelA := widget.NewLabel("") + emptyLabelB := widget.NewLabel("") + emptyLabelC := widget.NewLabel("") + + traitNameColumn.Add(emptyLabelA) + userNumberOfRulesTestedColumn.Add(emptyLabelB) + viewTraitDetailsButtonsColumn.Add(emptyLabelC) + } + } + } + } else if (userOrOffspring == "Offspring"){ + + offspringTraitOutcomeScoresKnown, offspringTraitOutcomeScoresMap, offspringNumberOfRulesTested, err := getOffspringTraitOutcomeScoresMap() + if (err != nil) { return nil, err } + + numberOfRulesTestedString := helpers.ConvertIntToString(offspringNumberOfRulesTested) + numberOfRulesTestedFormatted := numberOfRulesTestedString + "/" + totalNumberOfTraitRulesString + numberOfRulesTestedLabel := getBoldLabelCentered(numberOfRulesTestedFormatted) + offspringNumberOfRulesTestedColumn.Add(numberOfRulesTestedLabel) + + if (offspringTraitOutcomeScoresKnown == false){ + unknownTranslated := translate("Unknown") + unknownLabel := getBoldLabelCentered(unknownTranslated) + + offspringOutcomeScoresColumn.Add(unknownLabel) + } else { + + for index, outcomeName := range traitOutcomeNamesListSorted{ + + outcomeScore, exists := offspringTraitOutcomeScoresMap[outcomeName] + if (exists == false){ + return nil, errors.New("Outcome not found in offspringTraitOutcomeScoresMap.") + } + + outcomeScoreString := helpers.ConvertFloat64ToStringRounded(outcomeScore, 2) + + outcomeRow := getBoldLabelCentered(outcomeName + ": " + outcomeScoreString) + offspringOutcomeScoresColumn.Add(outcomeRow) + + if (index > 0){ + + emptyLabelA := widget.NewLabel("") + emptyLabelB := widget.NewLabel("") + emptyLabelC := widget.NewLabel("") + + traitNameColumn.Add(emptyLabelA) + offspringNumberOfRulesTestedColumn.Add(emptyLabelB) + viewTraitDetailsButtonsColumn.Add(emptyLabelC) + } + } + } + } + + traitNameColumn.Add(widget.NewSeparator()) + userOutcomeScoresColumn.Add(widget.NewSeparator()) + offspringOutcomeScoresColumn.Add(widget.NewSeparator()) + userNumberOfRulesTestedColumn.Add(widget.NewSeparator()) + offspringNumberOfRulesTestedColumn.Add(widget.NewSeparator()) + viewTraitDetailsButtonsColumn.Add(widget.NewSeparator()) + } + + userOutcomeScoresHelpButton := widget.NewButtonWithIcon("", theme.QuestionIcon(), func(){ + setTraitOutcomeScoresExplainerPage(window, currentPage) + }) + + offspringOutcomeScoresHelpButton := widget.NewButtonWithIcon("", theme.QuestionIcon(), func(){ + setOffspringTraitOutcomeScoresExplainerPage(window, currentPage) + }) + + userNumberOfRulesTestedHelpButton := widget.NewButtonWithIcon("", theme.QuestionIcon(), func(){ + setTraitNumberOfRulesTestedExplainerPage(window, currentPage) + }) + + offspringNumberOfRulesTestedHelpButton := widget.NewButtonWithIcon("", theme.QuestionIcon(), func(){ + setOffspringTraitNumberOfRulesTestedExplainerPage(window, currentPage) + }) + + userOutcomeScoresColumn.Add(userOutcomeScoresHelpButton) + offspringOutcomeScoresColumn.Add(offspringOutcomeScoresHelpButton) + userNumberOfRulesTestedColumn.Add(userNumberOfRulesTestedHelpButton) + offspringNumberOfRulesTestedColumn.Add(offspringNumberOfRulesTestedHelpButton) + + if (userOrOffspring == "User"){ + traitsInfoGrid := container.NewHBox(layout.NewSpacer(), traitNameColumn, userOutcomeScoresColumn, userNumberOfRulesTestedColumn, viewTraitDetailsButtonsColumn, layout.NewSpacer()) + + return traitsInfoGrid, nil + } + traitsInfoGrid := container.NewHBox(layout.NewSpacer(), traitNameColumn, offspringOutcomeScoresColumn, offspringNumberOfRulesTestedColumn, viewTraitDetailsButtonsColumn, layout.NewSpacer()) + + return traitsInfoGrid, nil + } + + traitsInfoGrid, err := getTraitsInfoGrid() + if (err != nil){ + setErrorEncounteredPage(window, err, previousPage) + return + } + + page := container.NewVBox(title, backButton, widget.NewSeparator(), subtitle, widget.NewSeparator(), description1, description2, description3, widget.NewSeparator(), userOrOffspringSelectorCentered, widget.NewSeparator(), traitsInfoGrid) + + setPageContent(page, window) +} + +func setViewMateProfilePage_TraitRules(window fyne.Window, traitName string, userOrOffspring string, getAnyUserProfileAttributeFunction func(string)(bool, int, string, error), previousPage func()){ + + currentPage := func(){setViewMateProfilePage_TraitRules(window, traitName, userOrOffspring, getAnyUserProfileAttributeFunction, previousPage)} + + title := getPageTitleCentered("View Profile - Physical") + + backButton := getBackButtonCentered(previousPage) + + subtitle := getPageSubtitleCentered(userOrOffspring + " Trait Rules") + + traitNameLabel := widget.NewLabel("Trait Name:") + + traitNameText := getBoldLabel(traitName) + + viewTraitInfoButton := widget.NewButtonWithIcon("", theme.InfoIcon(), func(){ + setViewTraitDetailsPage(window, traitName, currentPage) + }) + + traitNameRow := container.NewHBox(layout.NewSpacer(), traitNameLabel, traitNameText, viewTraitInfoButton, layout.NewSpacer()) + + //Outputs: + // -bool: Status is known + // -bool: User passes rule + // -error + getUserPassesRuleBool := func(ruleIdentifier string, ruleLociList []traits.RuleLocus)(bool, bool, error){ + + allRuleLociKnown := true + + for _, ruleLocusObject := range ruleLociList{ + + locusRSID := ruleLocusObject.LocusRSID + + locusRSIDString := helpers.ConvertInt64ToString(locusRSID) + + userLocusValueAttributeName := "LocusValue_rs" + locusRSIDString + + userLocusBasePairIsKnown, _, userLocusBasePair, err := getAnyUserProfileAttributeFunction(userLocusValueAttributeName) + if (err != nil) { return false, false, err } + if (userLocusBasePairIsKnown == false){ + // We know rule is not passed + // We keep searching to see if ruleIsPassed status is No or Unknown + allRuleLociKnown = false + continue + } + + ruleLocusBasePairsList := ruleLocusObject.BasePairsList + + userPassesRuleLocus := slices.Contains(ruleLocusBasePairsList, userLocusBasePair) + if (userPassesRuleLocus == false){ + // We know the rule is not passed + return true, false, nil + } + } + if (allRuleLociKnown == false){ + // We don't know if the user passes the rule. Any loci which we knew must have passed. + return false, false, nil + } + return true, true, nil + } + + myPersonChosen, myGenomesExist, myAnalysisIsReady, myAnalysisMapList, myGenomeIdentifier, _, err := myChosenAnalysis.GetMyChosenMateGeneticAnalysis() + if (err != nil) { + setErrorEncounteredPage(window, err, previousPage) + return + } + + //Outputs: + // -bool: Probability is known + // -int: Probability of passing rule (0-100) + // -error + getOffspringProbabilityOfPassingRule := func(ruleIdentifier string, ruleLociList []traits.RuleLocus)(bool, int, error){ + + if (myPersonChosen == false || myGenomesExist == false || myAnalysisIsReady == false){ + // Without my genome person chosen, all offspring rule probabilities are unknown + return false, 0, nil + } + + _, _, myRuleLociBasePairsMap, err := readGeneticAnalysis.GetPersonTraitRuleInfoFromGeneticAnalysis(myAnalysisMapList, traitName, ruleIdentifier, myGenomeIdentifier) + if (err != nil) { return false, 0, err } + + offspringProbabilityOfPassingRule := float64(1) + + for _, ruleLocusObject := range ruleLociList{ + + locusIdentifier := ruleLocusObject.LocusIdentifier + locusRSID := ruleLocusObject.LocusRSID + + locusRSIDString := helpers.ConvertInt64ToString(locusRSID) + + userLocusValueAttributeName := "LocusValue_rs" + locusRSIDString + + userLocusBasePairIsKnown, _, userLocusBasePair, err := getAnyUserProfileAttributeFunction(userLocusValueAttributeName) + if (err != nil) { return false, 0, err } + if (userLocusBasePairIsKnown == false){ + // We must know all rule loci base pairs to determine offspring probability of passing rule + return false, 0, nil + } + + myLocusBasePair, myLocusBasePairIsKnown := myRuleLociBasePairsMap[locusIdentifier] + if (myLocusBasePairIsKnown == false){ + // We must know all rule loci base pairs to determine offspring probability of passing rule + return false, 0, nil + } + + locusRequiredBasePairsList := ruleLocusObject.BasePairsList + + offspringProbabilityOfPassingRuleLocus, err := createGeneticAnalysis.GetOffspringTraitRuleLocusInfo(locusRequiredBasePairsList, userLocusBasePair, myLocusBasePair) + if (err != nil) { return false, 0, err } + + offspringProbabilityOfPassingRule *= offspringProbabilityOfPassingRuleLocus + } + + offspringPercentageProbabilityOfPassingRule := int(offspringProbabilityOfPassingRule * 100) + + return true, offspringPercentageProbabilityOfPassingRule, nil + } + + traitObject, err := traits.GetTraitObject(traitName) + if (err != nil){ + setErrorEncounteredPage(window, err, previousPage) + return + } + + traitRulesList := traitObject.RulesList + + totalNumberOfTraitRules := len(traitRulesList) + + getNumberOfRulesTested := func()(int, error){ + + numberOfRulesTested := 0 + + for _, ruleObject := range traitRulesList{ + + ruleIdentifier := ruleObject.RuleIdentifier + ruleLociList := ruleObject.LociList + + if (userOrOffspring == "User"){ + + ruleStatusIsKnown, _, err := getUserPassesRuleBool(ruleIdentifier, ruleLociList) + if (err != nil) { return 0, err } + if (ruleStatusIsKnown == true){ + numberOfRulesTested += 1 + } + } else if (userOrOffspring == "Offspring"){ + + ruleProbabilityIsKnown, _, err := getOffspringProbabilityOfPassingRule(ruleIdentifier, ruleLociList) + if (err != nil) { return 0, err } + if (ruleProbabilityIsKnown == true){ + numberOfRulesTested += 1 + } + } + } + + return numberOfRulesTested, nil + } + + numberOfRulesTested, err := getNumberOfRulesTested() + if (err != nil){ + setErrorEncounteredPage(window, err, previousPage) + return + } + + rulesTestedLabel := widget.NewLabel("Rules Tested:") + + numberOfRulesTestedString := helpers.ConvertIntToString(numberOfRulesTested) + totalNumberOfTraitRulesString := helpers.ConvertIntToString(totalNumberOfTraitRules) + + numberOfRulesTestedFormatted := numberOfRulesTestedString + "/" + totalNumberOfTraitRulesString + numberOfRulesTestedLabel := getBoldLabel(numberOfRulesTestedFormatted) + + numberOfRulesTestedHelpButton := widget.NewButtonWithIcon("", theme.QuestionIcon(), func(){ + if (userOrOffspring == "User"){ + setTraitNumberOfRulesTestedExplainerPage(window, currentPage) + } else { + setOffspringTraitNumberOfRulesTestedExplainerPage(window, currentPage) + } + }) + + numberOfRulesTestedRow := container.NewHBox(layout.NewSpacer(), rulesTestedLabel, numberOfRulesTestedLabel, numberOfRulesTestedHelpButton, layout.NewSpacer()) + + getRulesGrid := func()(*fyne.Container, error){ + + //TODO: Sort results + + viewRuleInfoButtonsColumn := container.NewVBox() + ruleIdentifierColumn := container.NewVBox() + ruleEffectsColumn := container.NewVBox() + userPassesRuleColumn := container.NewVBox() + offspringProbabilityOfPassingRuleColumn := container.NewVBox() + + if (userOrOffspring == "Offspring"){ + + // We need this because the header row is 2 rows tall when userOrOffspring == "Offspring" + // Otherwise, it is 1 row tall + + emptyLabelA := widget.NewLabel("") + emptyLabelB := widget.NewLabel("") + emptyLabelC := widget.NewLabel("") + emptyLabelD := widget.NewLabel("") + + viewRuleInfoButtonsColumn.Add(emptyLabelA) + ruleIdentifierColumn.Add(emptyLabelB) + ruleEffectsColumn.Add(emptyLabelC) + userPassesRuleColumn.Add(emptyLabelD) + } + + offspringProbabilityOfTitle := getItalicLabelCentered("Offspring Probability Of") + offspringProbabilityOfPassingRuleColumn.Add(offspringProbabilityOfTitle) + + emptyLabelE := widget.NewLabel("") + ruleIdentifierTitle := getItalicLabelCentered("Rule Identifier") + ruleEffectsTitle := getItalicLabelCentered("Rule Effects") + userPassesRuleTitle := getItalicLabelCentered("User Passes Rule") + passingRuleTitle := getItalicLabelCentered("Passing Rule") + + viewRuleInfoButtonsColumn.Add(emptyLabelE) + ruleIdentifierColumn.Add(ruleIdentifierTitle) + ruleEffectsColumn.Add(ruleEffectsTitle) + userPassesRuleColumn.Add(userPassesRuleTitle) + offspringProbabilityOfPassingRuleColumn.Add(passingRuleTitle) + + viewRuleInfoButtonsColumn.Add(widget.NewSeparator()) + ruleIdentifierColumn.Add(widget.NewSeparator()) + ruleEffectsColumn.Add(widget.NewSeparator()) + userPassesRuleColumn.Add(widget.NewSeparator()) + offspringProbabilityOfPassingRuleColumn.Add(widget.NewSeparator()) + + for _, ruleObject := range traitRulesList{ + + ruleIdentifier := ruleObject.RuleIdentifier + ruleLociList := ruleObject.LociList + + getUserPassesRuleString := func()(string, error){ + + userRuleStatusIsKnown, userPassesRule, err := getUserPassesRuleBool(ruleIdentifier, ruleLociList) + if (err != nil) { return "", err } + + if (userRuleStatusIsKnown == false){ + result := translate("Unknown") + return result, nil + } + userPassesRuleString := helpers.ConvertBoolToYesOrNoString(userPassesRule) + userPassesRuleTranslated := translate(userPassesRuleString) + return userPassesRuleTranslated, nil + } + + userPassesRuleString, err := getUserPassesRuleString() + if (err != nil) { return nil, err } + + getOffspringProbabilityOfPassingRuleString := func()(string, error){ + + probabilityIsKnown, probabilityOfPassingRule, err := getOffspringProbabilityOfPassingRule(ruleIdentifier, ruleLociList) + if (err != nil) { return "", err } + if (probabilityIsKnown == false){ + result := translate("Unknown") + return result, nil + } + ruleProbabilityString := helpers.ConvertIntToString(probabilityOfPassingRule) + + ruleProbabilityFormatted := ruleProbabilityString + "%" + return ruleProbabilityFormatted, nil + } + + offspringProbabilityOfPassingRuleString, err := getOffspringProbabilityOfPassingRuleString() + if (err != nil) { return nil, err } + + // We add all of the columns except for the rule effects column + // We do this because the rule effects column may be multiple rows tall + + viewRuleInfoButton := widget.NewButtonWithIcon("", theme.InfoIcon(), func(){ + setViewTraitRuleDetailsPage(window, traitName, ruleIdentifier, currentPage) + }) + ruleIdentifierLabel := getBoldLabelCentered(ruleIdentifier) + userPassesRuleLabel := getBoldLabelCentered(userPassesRuleString) + offspringProbabilityOfPassingRuleLabel := getBoldLabelCentered(offspringProbabilityOfPassingRuleString) + + viewRuleInfoButtonsColumn.Add(viewRuleInfoButton) + ruleIdentifierColumn.Add(ruleIdentifierLabel) + userPassesRuleColumn.Add(userPassesRuleLabel) + offspringProbabilityOfPassingRuleColumn.Add(offspringProbabilityOfPassingRuleLabel) + + traitOutcomesList := traitObject.OutcomesList + ruleOutcomePointsMap := ruleObject.OutcomePointsMap + + // We have to sort the outcome names so they always show up in the same order + + outcomeNamesListSorted := helpers.CopyAndSortStringListToUnicodeOrder(traitOutcomesList) + + addedOutcomes := 0 + for _, outcomeName := range outcomeNamesListSorted{ + + outcomeChange, exists := ruleOutcomePointsMap[outcomeName] + if (exists == false){ + // This rule does not effect this outcome. Skip + continue + } + + getOutcomeEffectString := func()string{ + + outcomeChangeString := helpers.ConvertIntToString(outcomeChange) + + if (outcomeChange < 0){ + return outcomeChangeString + } + outcomeEffect := "+" + outcomeChangeString + return outcomeEffect + } + + outcomeEffect := getOutcomeEffectString() + + outcomeRow := getBoldLabelCentered(outcomeName + ": " + outcomeEffect) + ruleEffectsColumn.Add(outcomeRow) + + if (addedOutcomes > 0){ + + emptyLabelA := widget.NewLabel("") + emptyLabelB := widget.NewLabel("") + emptyLabelC := widget.NewLabel("") + emptyLabelD := widget.NewLabel("") + + viewRuleInfoButtonsColumn.Add(emptyLabelA) + ruleIdentifierColumn.Add(emptyLabelB) + userPassesRuleColumn.Add(emptyLabelC) + offspringProbabilityOfPassingRuleColumn.Add(emptyLabelD) + } + addedOutcomes += 1 + } + + viewRuleInfoButtonsColumn.Add(widget.NewSeparator()) + ruleIdentifierColumn.Add(widget.NewSeparator()) + ruleEffectsColumn.Add(widget.NewSeparator()) + userPassesRuleColumn.Add(widget.NewSeparator()) + offspringProbabilityOfPassingRuleColumn.Add(widget.NewSeparator()) + } + + ruleEffectsHelpButton := widget.NewButtonWithIcon("", theme.QuestionIcon(), func(){ + setTraitRuleOutcomeEffectsExplainerPage(window, currentPage) + }) + + userPassesRuleHelpButton := widget.NewButtonWithIcon("", theme.QuestionIcon(), func(){ + setPersonPassesTraitRuleExplainerPage(window, currentPage) + }) + + offspringProbabilityOfPassingRuleHelpButton := widget.NewButtonWithIcon("", theme.QuestionIcon(), func(){ + setOffspringProbabilityOfPassingTraitRuleExplainerPage(window, currentPage) + }) + + ruleEffectsColumn.Add(ruleEffectsHelpButton) + userPassesRuleColumn.Add(userPassesRuleHelpButton) + offspringProbabilityOfPassingRuleColumn.Add(offspringProbabilityOfPassingRuleHelpButton) + + if (userOrOffspring == "User"){ + rulesGrid := container.NewHBox(layout.NewSpacer(), viewRuleInfoButtonsColumn, ruleIdentifierColumn, ruleEffectsColumn, userPassesRuleColumn, layout.NewSpacer()) + return rulesGrid, nil + } + rulesGrid := container.NewHBox(layout.NewSpacer(), viewRuleInfoButtonsColumn, ruleIdentifierColumn, ruleEffectsColumn, offspringProbabilityOfPassingRuleColumn, layout.NewSpacer()) + return rulesGrid, nil + } + + rulesGrid, err := getRulesGrid() + if (err != nil){ + setErrorEncounteredPage(window, err, previousPage) + return + } + + page := container.NewVBox(title, backButton, widget.NewSeparator(), subtitle, widget.NewSeparator(), traitNameRow, numberOfRulesTestedRow, widget.NewSeparator(), rulesGrid) + + setPageContent(page, window) +} + +func setViewMateProfilePage_Diet(window fyne.Window, getAnyUserProfileAttributeFunction func(string)(bool, int, string, error), previousPage func()){ + + title := getPageTitleCentered(translate("View Profile - Lifestyle")) + + backButton := getBackButtonCentered(previousPage) + + subtitle := getPageSubtitleCentered("Diet") + + description := getLabelCentered("Below are the user's food ratings.") + + foodNameTitle := getItalicLabelCentered("Food Name") + + ratingTitle := getItalicLabelCentered("Rating") + + foodNameColumn := container.NewVBox(foodNameTitle, widget.NewSeparator()) + + foodRatingColumn := container.NewVBox(ratingTitle, widget.NewSeparator()) + + foodsList := []string{"Fruit", "Vegetables", "Nuts", "Grains", "Dairy", "Seafood", "Beef", "Pork", "Poultry", "Eggs", "Beans"} + + for _, foodName := range foodsList{ + + foodAttributeName := foodName + "Rating" + + ratingExists, _, foodRating, err := getAnyUserProfileAttributeFunction(foodAttributeName) + if (err != nil){ + setErrorEncounteredPage(window, err, previousPage) + return + } + + foodNameLabel := getBoldLabelCentered(foodName) + + getRatingLabel := func()*fyne.Container{ + if (ratingExists == false){ + result := getBoldItalicLabelCentered(translate("Unknown")) + return result + } + + result := getBoldLabelCentered(foodRating + "/10") + return result + } + + ratingLabel := getRatingLabel() + + foodNameColumn.Add(foodNameLabel) + foodRatingColumn.Add(ratingLabel) + + foodNameColumn.Add(widget.NewSeparator()) + foodRatingColumn.Add(widget.NewSeparator()) + } + + foodsGrid := container.NewHBox(layout.NewSpacer(), foodNameColumn, foodRatingColumn, layout.NewSpacer()) + + page := container.NewVBox(title, backButton, widget.NewSeparator(), subtitle, widget.NewSeparator(), description, widget.NewSeparator(), foodsGrid) + + setPageContent(page, window) +} + +func setViewMateProfilePage_Language(window fyne.Window, getAnyUserProfileAttributeFunction func(string)(bool, int, string, error), previousPage func()){ + + title := getPageTitleCentered(translate("View Profile - Mental")) + + backButton := getBackButtonCentered(previousPage) + + subtitle := getPageSubtitleCentered(translate("Language")) + + description := getLabelCentered("Below describes the language(s) the user can speak.") + + userLanguagesExist, _, userLanguageAttribute, err := getAnyUserProfileAttributeFunction("Language") + if (err != nil){ + setErrorEncounteredPage(window, err, previousPage) + return + } + if (userLanguagesExist == false){ + + noLanguagesExistLabel := getBoldLabelCentered("This user's profile has no languages listed.") + + page := container.NewVBox(title, backButton, widget.NewSeparator(), subtitle, widget.NewSeparator(), description, widget.NewSeparator(), noLanguagesExistLabel) + + setPageContent(page, window) + return + } + + getLanguagesGrid := func()(*fyne.Container, error){ + + //TODO: Sort by fluency + + languageTitle := getItalicLabelCentered("Language") + fluencyTitle := getItalicLabelCentered("Fluency") + + languageNameColumn := container.NewVBox(languageTitle, widget.NewSeparator()) + languageFluencyColumn := container.NewVBox(fluencyTitle, widget.NewSeparator()) + + languageItemsList := strings.Split(userLanguageAttribute, "+&") + + worldLanguageObjectsMap, err := worldLanguages.GetWorldLanguageObjectsMap() + if (err != nil) { return nil, err } + + for _, languageItem := range languageItemsList{ + + languageName, languageRating, delimiterFound := strings.Cut(languageItem, "$") + if (delimiterFound == false){ + return nil, errors.New("setViewMateProfilePage_Language called with profile containing invalid language attribute item: " + languageItem) + } + + getLanguageNameFormatted := func()string{ + + // We only translate if language name is canonical + + _, languageIsCanonical := worldLanguageObjectsMap[languageName] + if (languageIsCanonical == false){ + return languageName + } + languageNameTranslated := translate(languageName) + return languageNameTranslated + } + + languageNameFormatted := getLanguageNameFormatted() + + languageFluencyFormatted := languageRating + "/5" + + languageNameLabel := getBoldLabelCentered(languageNameFormatted) + languageFluencyLabel := getBoldLabelCentered(languageFluencyFormatted) + + languageNameColumn.Add(languageNameLabel) + languageFluencyColumn.Add(languageFluencyLabel) + + languageNameColumn.Add(widget.NewSeparator()) + languageFluencyColumn.Add(widget.NewSeparator()) + } + + languagesGrid := container.NewHBox(layout.NewSpacer(), languageNameColumn, languageFluencyColumn, layout.NewSpacer()) + + return languagesGrid, nil + } + + languagesGrid, err := getLanguagesGrid() + if (err != nil){ + setErrorEncounteredPage(window, err, previousPage) + return + } + + page := container.NewVBox(title, backButton, widget.NewSeparator(), subtitle, widget.NewSeparator(), description, widget.NewSeparator(), languagesGrid) + + setPageContent(page, window) +} + + +// We use this to view an ancestry composition for moderation/comparing profile changes +func setViewUser23andMeAncestryCompositionPage(window fyne.Window, inputAncestryCompositionAttribute string, previousPage func()){ + + title := getPageTitleCentered("Viewing 23andMe Ancestry Composition") + + backButton := getBackButtonCentered(previousPage) + + attributeIsValid, continentPercentagesMap, regionPercentagesMap, subregionPercentagesMap, err := companyAnalysis.ReadAncestryCompositionAttribute_23andMe(true, inputAncestryCompositionAttribute) + if (err != nil){ + setErrorEncounteredPage(window, err, previousPage) + return + } + if (attributeIsValid == false){ + setErrorEncounteredPage(window, errors.New("setViewUser23andMeAncestryCompositionPage called with invalid inputAncestryCompositionAttribute"), previousPage) + return + } + + userCompositionDisplay, err := get23andMeAncestryCompositionDisplay(continentPercentagesMap, regionPercentagesMap, subregionPercentagesMap) + if (err != nil){ + setErrorEncounteredPage(window, err, previousPage) + return + } + + header := container.NewVBox(title, backButton, widget.NewSeparator()) + + page := container.NewBorder(header, nil, nil, nil, userCompositionDisplay) + + setPageContent(page, window) +} + +func get23andMeAncestryCompositionDisplay(inputContinentPercentagesMap map[string]float64, inputRegionPercentagesMap map[string]float64, inputSubregionPercentagesMap map[string]float64)(fyne.Widget, error){ + + mapsAreValid, continentPercentagesMap, regionPercentagesMap, subregionPercentagesMap, err := companyAnalysis.AddMissingParentsToAncestryCompositionMaps_23andMe(inputContinentPercentagesMap, inputRegionPercentagesMap, inputSubregionPercentagesMap) + if (err != nil){ return nil, err } + if (mapsAreValid == false){ + return nil, errors.New("get23andMeAncestryCompositionDisplay called with invalid ancestry location maps.") + } + + // This map is used to create a fyne tree widget + // Each item maps to a list of child items + treeMap := make(map[string][]string) + + continentsList := make([]string, 0, len(continentPercentagesMap)) + + // Map Structure: Continent Description -> Continent percentage + continentDescriptionPercentagesMap := make(map[string]float64) + + for continentName, continentPercentage := range continentPercentagesMap{ + + continentPercentageString := helpers.ConvertFloat64ToStringRounded(continentPercentage, 1) + + continentDescription := continentName + " - " + continentPercentageString + "%" + + continentDescriptionPercentagesMap[continentDescription] = continentPercentage + + continentsList = append(continentsList, continentDescription) + + currentContinentRegionsList := make([]string, 0) + + allContinentRegionsList, err := companyAnalysis.GetAncestryContinentRegionsList_23andMe(continentName) + if (err != nil) { + return nil, errors.New("continentPercentagesMap contains invalid continent.") + } + + // Map Structure: Region Description -> Region percentage + regionDescriptionPercentagesMap := make(map[string]float64) + + for regionName, regionPercentage := range regionPercentagesMap{ + + regionIsRelevant := slices.Contains(allContinentRegionsList, regionName) + if (regionIsRelevant == false){ + continue + } + + regionPercentageString := helpers.ConvertFloat64ToStringRounded(regionPercentage, 1) + + regionDescription := regionName + " - " + regionPercentageString + "%" + + regionDescriptionPercentagesMap[regionDescription] = regionPercentage + + currentContinentRegionsList = append(currentContinentRegionsList, regionDescription) + + allRegionSubregionsList, err := companyAnalysis.GetAncestryRegionSubregionsList_23andMe(continentName, regionName) + if (err != nil){ return nil, err } + + currentRegionSubregionsList := make([]string, 0, len(subregionPercentagesMap)) + + // Map Structure: Subregion description -> Subregion percentage + subregionDescriptionPercentagesMap := make(map[string]float64) + + for subregionName, subregionPercentage := range subregionPercentagesMap{ + + subregionIsRelevant := slices.Contains(allRegionSubregionsList, subregionName) + if (subregionIsRelevant == false){ + continue + } + + subregionPercentageString := helpers.ConvertFloat64ToStringRounded(subregionPercentage, 1) + + subregionDescription := subregionName + " - " + subregionPercentageString + "%" + + subregionDescriptionPercentagesMap[subregionDescription] = subregionPercentage + + currentRegionSubregionsList = append(currentRegionSubregionsList, subregionDescription) + } + + // We sort region subregions list + // We sort them from highest to lowest in percentage. + + compareSubregionsFunction := func(subregionADescription string, subregionBDescription string)int{ + + if (subregionADescription == subregionBDescription){ + panic("compareSubregionsFunction called with identical subregion descriptions.") + } + + subregionAPercentage, exists := subregionDescriptionPercentagesMap[subregionADescription] + if (exists == false){ + panic("subregionPercentagesMap missing subregion during sort.") + } + + subregionBPercentage, exists := subregionDescriptionPercentagesMap[subregionBDescription] + if (exists == false){ + panic("subregionPercentagesMap missing subregion during sort.") + } + if (subregionAPercentage == subregionBPercentage){ + // We sort subregions in unicode order + if (subregionADescription < subregionBDescription){ + return -1 + } + return 1 + } + if (subregionAPercentage > subregionBPercentage){ + return -1 + } + + return 1 + } + + slices.SortFunc(currentRegionSubregionsList, compareSubregionsFunction) + + treeMap[regionDescription] = currentRegionSubregionsList + } + + // We sort continent regions list by highest to lowest percentage. + + compareRegionsFunction := func(regionADescription string, regionBDescription string)int{ + + if (regionADescription == regionBDescription){ + panic("compareRegionsFunction called with identical regions.") + } + + regionAPercentage, exists := regionDescriptionPercentagesMap[regionADescription] + if (exists == false){ + panic("regionPercentagesMap missing subregion during sort.") + } + + regionBPercentage, exists := regionDescriptionPercentagesMap[regionBDescription] + if (exists == false){ + panic("regionPercentagesMap missing subregion during sort.") + } + if (regionAPercentage == regionBPercentage){ + // We sort regions in unicode order + if (regionADescription < regionBDescription){ + return -1 + } + return 1 + } + + if (regionAPercentage > regionBPercentage){ + return -1 + } + return 1 + } + + slices.SortFunc(currentContinentRegionsList, compareRegionsFunction) + + treeMap[continentDescription] = currentContinentRegionsList + } + + // We sort root list by highest to lowest proportions + + compareContinentsFunction := func(continentADescription string, continentBDescription string)int{ + + if (continentADescription == continentBDescription){ + panic("compareContinentsFunction called with identical continents.") + } + + continentAPercentage, exists := continentDescriptionPercentagesMap[continentADescription] + if (exists == false){ + panic("Continent percentage not found when sorting root list.") + } + + continentBPercentage, exists := continentDescriptionPercentagesMap[continentBDescription] + if (exists == false){ + panic("Continent percentage not found when sorting root list.") + } + if (continentAPercentage == continentBPercentage){ + // We sort continents in unicode order + if (continentADescription < continentBDescription){ + return -1 + } + return 1 + } + if (continentAPercentage > continentBPercentage){ + return -1 + } + return 1 + } + + slices.SortFunc(continentsList, compareContinentsFunction) + + treeMap[""] = continentsList + + resultTree := widget.NewTreeWithStrings(treeMap) + resultTree.OpenAllBranches() + + return resultTree, nil +} + + + + diff --git a/imported/geodist/geodist.go b/imported/geodist/geodist.go new file mode 100644 index 0000000..1293a7a --- /dev/null +++ b/imported/geodist/geodist.go @@ -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 +} + + diff --git a/imported/geodist/geodist_test.go b/imported/geodist/geodist_test.go new file mode 100644 index 0000000..c1b4505 --- /dev/null +++ b/imported/geodist/geodist_test.go @@ -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") + } + +} + + diff --git a/imported/goeffects/cartoon.go b/imported/goeffects/cartoon.go new file mode 100644 index 0000000..72d49e8 --- /dev/null +++ b/imported/goeffects/cartoon.go @@ -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 +} + + diff --git a/imported/goeffects/effects.go b/imported/goeffects/effects.go new file mode 100644 index 0000000..b91f048 --- /dev/null +++ b/imported/goeffects/effects.go @@ -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 + } +} diff --git a/imported/goeffects/gaussian.go b/imported/goeffects/gaussian.go new file mode 100644 index 0000000..2ecfc74 --- /dev/null +++ b/imported/goeffects/gaussian.go @@ -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)) +} diff --git a/imported/goeffects/goeffects.go b/imported/goeffects/goeffects.go new file mode 100644 index 0000000..817c0c9 --- /dev/null +++ b/imported/goeffects/goeffects.go @@ -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 +} + + + + diff --git a/imported/goeffects/grayscale.go b/imported/goeffects/grayscale.go new file mode 100644 index 0000000..8f1768a --- /dev/null +++ b/imported/goeffects/grayscale.go @@ -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} +} diff --git a/imported/goeffects/image.go b/imported/goeffects/image.go new file mode 100644 index 0000000..0a53863 --- /dev/null +++ b/imported/goeffects/image.go @@ -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 +} diff --git a/imported/goeffects/oilpainting.go b/imported/goeffects/oilpainting.go new file mode 100644 index 0000000..a0a2978 --- /dev/null +++ b/imported/goeffects/oilpainting.go @@ -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 +} + + diff --git a/imported/goeffects/pencil.go b/imported/goeffects/pencil.go new file mode 100644 index 0000000..9e090aa --- /dev/null +++ b/imported/goeffects/pencil.go @@ -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} +} diff --git a/imported/goeffects/pipeline.go b/imported/goeffects/pipeline.go new file mode 100644 index 0000000..610f5c8 --- /dev/null +++ b/imported/goeffects/pipeline.go @@ -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 +} diff --git a/imported/goeffects/rect.go b/imported/goeffects/rect.go new file mode 100644 index 0000000..fc1d8df --- /dev/null +++ b/imported/goeffects/rect.go @@ -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}, + } +} diff --git a/imported/goeffects/sobel.go b/imported/goeffects/sobel.go new file mode 100644 index 0000000..6a334ea --- /dev/null +++ b/imported/goeffects/sobel.go @@ -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 +} diff --git a/internal/allowedText/allowedText.go b/internal/allowedText/allowedText.go new file mode 100644 index 0000000..18b9010 --- /dev/null +++ b/internal/allowedText/allowedText.go @@ -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 +} + diff --git a/internal/appMemory/appMemory.go b/internal/appMemory/appMemory.go new file mode 100644 index 0000000..756ffa7 --- /dev/null +++ b/internal/appMemory/appMemory.go @@ -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() +} + + + + + diff --git a/internal/appUsers/appUsers.go b/internal/appUsers/appUsers.go new file mode 100644 index 0000000..68a4343 --- /dev/null +++ b/internal/appUsers/appUsers.go @@ -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 +} + + diff --git a/internal/appValues/appValues.go b/internal/appValues/appValues.go new file mode 100644 index 0000000..ae46fe4 --- /dev/null +++ b/internal/appValues/appValues.go @@ -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 +} + + diff --git a/internal/backgroundJobs/backgroundJobs.go b/internal/backgroundJobs/backgroundJobs.go new file mode 100644 index 0000000..6629949 --- /dev/null +++ b/internal/backgroundJobs/backgroundJobs.go @@ -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 +} + + + diff --git a/internal/badgerDatabase/badgerDatabase.go b/internal/badgerDatabase/badgerDatabase.go new file mode 100644 index 0000000..c648b60 --- /dev/null +++ b/internal/badgerDatabase/badgerDatabase.go @@ -0,0 +1,2869 @@ + +// badgerDatabase provides functions to read and write to the Badger database. +// This database is used to store profiles, messages, reviews, reports, and more. + +package badgerDatabase + +// Below are the kinds of information being stored, and the format in which they are stored. + +// Mate Profiles +// -Key = Prefix + Profile hash +// -Value = Mate profile bytes + +// Host Profiles +// -Key = Prefix + Profile hash +// -Value = Host profile bytes + +// Moderator Profiles +// -Key = Prefix + Profile hash +// -Value = Moderator profile bytes + +// Messages +// -Key = Prefix + Message Hash +// -Value = Message bytes + +// Reviews +// -Key = Prefix + Review hash +// -Value = Review bytes + +// Reports +// -Key = Prefix + Report Hash +// -Value = Report bytes + +// Profile Metadata +// -Key: Prefix + Profile Hash +// -Value: Messagepack encoded profile metadata. Described in contentMetadata.go + +// Message Metadata +// -Key: Prefix + Message Hash +// -Value: Messagepack encoded message metadata. Described in contentMetadata.go + +// The rest of the described database entries do not store data, but rather store information about the data +// These will make it faster to retrieve profiles, messages, reviews, and reports +// For example, review and report lists make it faster to find reviews/reports of a particular message/identity/profile/attribute + +// Attribute Profiles List +// -Key = Prefix + Attribute Hash +// -Value = Comma separated list of all profile hashes which contain this attribute + +// Mate Identity Profiles List: +// -Key = Prefix + Identity hash +// -Value = Comma separated list of the Mate identity's profile hashes + +// Host Identity Profiles List: +// -Key = Prefix + Identity Hash +// -Value = Comma separated list of the Host identity's profile hashes + +// Moderator Identity Profiles List: +// -Key = Prefix + Identity Hash +// -Value = Comma separated list of the Moderator identity's profile hashes + +// Inbox Messages List: +// -Key = Prefix + Inbox +// -Value = Comma separated list of message hashes of messages in a particular inbox + +// Reviewer Identity Reviews List +// -Key = Prefix + Reviewer Identity Hash +// -Value = Comma separated list of all review hashes of identity reviews authored by a reviewer (moderator) + +// Reviewer Profile Reviews List +// -Key = Prefix + Reviewer Identity Hash +// -Value = Comma separated list of all review hashes of profile reviews authored by a reviewer (moderator) + +// Reviewer Attribute Reviews List +// -Key = Prefix + Reviewer Identity Hash +// -Value = Comma separated list of all review hashes of attribute reviews authored by a reviewer (moderator) + +// Reviewer Message Reviews List +// -Key = Prefix + Reviewer Identity Hash +// -Value = Comma separated list of all review hashes of message reviews authored by a reviewer (moderator) + +// Identity Reviews List +// -Key = Prefix + Reviewed Identity Hash +// -Value = Comma separated list of all review hashes for reviews of a particular identity. +// These are the identities of the users who are being reviewed, not the identities of the moderators who created the reviews + +// Profile Reviews List: +// -Key = Prefix + Reviewed Profile Hash +// -Value = Comma separated list of all review hashes for reviews of a particular profile hash + +// Attribute Reviews List: +// -Key = Prefix + Reviewed Profile Attribute Hash +// -Value = Comma separated list of all review hashes for reviews of a particular profile attribute + +// Message Reviews List: +// -Key = Prefix + Reviewed Message hash +// -Value = Comma separated list of all reviews hashes for reviews of a particular message + + +// Identity Reports List +// -Key = Prefix + Reported Identity Hash +// -Value = Comma separated list of all report hashes for reports of a particular identity. + +// Profile Reports List: +// -Key = Prefix + Reported Profile Hash +// -Value = Comma separated list of all report hashes for reports of a particular profile + +// Attribute Reports List: +// -Key = Prefix + Reported Profile Attribute Hash +// -Value = Comma separated list of all report hashes for reports of a particular profile attribute + +// Message Reports List: +// -Key = Prefix + Reported Message Hash +// -Value = Comma separated list of all report hashes for reports of a particular message + +//TODO: Be aware that we cannot prune the attributeProfilesList until all of the attribute's profiles are expired (if mate), or if the attribute's author identity is no longer funded +// We need to do this so we can keep track of which attribute reviews can be pruned +// We can only prune an attribute review once the profile is expired (if mate), or the author identity is no longer funded +// We use the attributeProfilesList to keep track of which profile(s) the attribute belongs to +// We will delete profiles that are banned, so keeping track of the attribute's profiles is necessary +// Each profile's author information is stored in contentMetadata + +import "seekia/internal/encoding" +import "seekia/internal/helpers" +import "seekia/internal/identity" +import "seekia/internal/localFilesystem" +import "seekia/internal/moderation/readReports" +import "seekia/internal/moderation/readReviews" +import "seekia/internal/profiles/readProfiles" + +import badger "github.com/dgraph-io/badger/v4" + +import goFilepath "path/filepath" + +import "slices" +import "bytes" +import "sync" +import "errors" + +const mateProfilesPrefix byte = 1 +const hostProfilesPrefix byte = 2 +const moderatorProfilesPrefix byte = 3 + +const messagesPrefix byte = 4 +const reviewsPrefix byte = 5 +const reportsPrefix byte = 6 + +const profileMetadataPrefix byte = 7 +const messageMetadataPrefix byte = 8 +const attributeProfilesListPrefix byte = 9 + +const mateIdentityProfilesListPrefix byte = 10 +const hostIdentityProfilesListPrefix byte = 11 +const moderatorIdentityProfilesListPrefix byte = 12 + +const inboxMessagesListPrefix byte = 13 + +const reviewerIdentityReviewsListPrefix byte = 14 +const reviewerProfileReviewsListPrefix byte = 15 +const reviewerAttributeReviewsListPrefix byte = 16 +const reviewerMessageReviewsListPrefix byte = 17 + +const identityReviewsListPrefix byte = 18 +const profileReviewsListPrefix byte = 19 +const attributeReviewsListPrefix byte = 20 +const messageReviewsListPrefix byte = 21 + +const identityReportsListPrefix byte = 22 +const profileReportsListPrefix byte = 23 +const attributeReportsListPrefix byte = 24 +const messageReportsListPrefix byte = 25 + + +// We use a lock for for database entries which contain comma separated values +// This is because badger will return a ErrConflict if we do a txn.Get and txn.Set in the same operation +// List entries contain values which need to be read and edited before being committed +// For example, we need to read the exiting value "1,2,3" and append "4", so result is "1,2,3,4" +// Entries such as profiles only need to be set or deleted, so we don't need to use a Mutex + +var editingAttributeProfilesListMutex sync.Mutex + +var editingMateIdentityProfilesListMutex sync.Mutex +var editingHostIdentityProfilesListMutex sync.Mutex +var editingModeratorIdentityProfilesListMutex sync.Mutex + +var editingInboxMessagesListMutex sync.Mutex + +var editingReviewerIdentityReviewsListMutex sync.Mutex +var editingReviewerProfileReviewsListMutex sync.Mutex +var editingReviewerAttributeReviewsListMutex sync.Mutex +var editingReviewerMessageReviewsListMutex sync.Mutex + +var editingIdentityReviewsListMutex sync.Mutex +var editingProfileReviewsListMutex sync.Mutex +var editingAttributeReviewsListMutex sync.Mutex +var editingMessageReviewsListMutex sync.Mutex + +var editingIdentityReportsListMutex sync.Mutex +var editingProfileReportsListMutex sync.Mutex +var editingAttributeReportsListMutex sync.Mutex +var editingMessageReportsListMutex sync.Mutex + +var myDatabase *badger.DB + +// We use this when establishing and reading from the myDatabase object +var databaseMutex sync.RWMutex + +func startDatabase()error{ + + databaseMutex.RLock() + + if (myDatabase != nil) { + if (myDatabase.IsClosed() == false){ + databaseMutex.RUnlock() + return nil + } + } + databaseMutex.RUnlock() + + databaseDirectory, err := localFilesystem.GetAppDatabaseFolderPath() + if (err != nil) { return err } + + _, err = localFilesystem.CreateFolder(databaseDirectory) + if (err != nil) { return err } + + // We put the badger database into its own folder + // We will need to keep multiple databases and migrate data to new versions when they are released + // This also makes it easier to switch from using Badger to a different database if needed. + + databaseFolderpath := goFilepath.Join(databaseDirectory, "BadgerDatabaseVersion4") + + _, err = localFilesystem.CreateFolder(databaseFolderpath) + if (err != nil) { return err } + + databaseOptions := badger.DefaultOptions(databaseFolderpath) + + newDatabase, err := badger.Open(databaseOptions) + if (err != nil) { return err } + + databaseMutex.Lock() + myDatabase = newDatabase + databaseMutex.Unlock() + + return nil +} + + +func StopDatabase(){ + + if (myDatabase != nil) { + myDatabase.Close() + } +} + +func AddUserProfile(profileType string, profileHash [28]byte, profileBytes []byte)error{ + + hashIsValid, err := readProfiles.VerifyProfileHash(profileHash, true, profileType, false, false) + if (err != nil) { + return errors.New("AddUserProfile called with invalid profileType: " + profileType) + } + if (hashIsValid == false){ + profileHashHex := encoding.EncodeBytesToHexString(profileHash[:]) + return errors.New("AddUserProfile called with invalid profileHash: " + profileHashHex) + } + + err = startDatabase() + if (err != nil) { return err } + + getKeyPrefix := func()(byte, error){ + + if (profileType == "Mate"){ + return mateProfilesPrefix, nil + + } else if (profileType == "Host"){ + return hostProfilesPrefix, nil + + } else if (profileType == "Moderator"){ + return moderatorProfilesPrefix, nil + } + + return 0, errors.New("VerifyProfileHash not verifying profileType: " + profileType) + } + + keyPrefix, err := getKeyPrefix() + if (err != nil) { return err } + + key := []byte{keyPrefix} + key = append(key, profileHash[:]...) + + err = setDatabaseEntry(key, profileBytes) + if (err != nil) { return err } + + return nil +} + +//Outputs +// -bool: Profile exists +// -[]byte: User profile bytes +// -error +func GetUserProfile(profileType string, profileHash [28]byte)(bool, []byte, error){ + + hashIsValid, err := readProfiles.VerifyProfileHash(profileHash, true, profileType, false, false) + if (err != nil) { + return false, nil, errors.New("GetUserProfile called with invalid profileType: " + profileType) + } + if (hashIsValid == false){ + profileHashHex := encoding.EncodeBytesToHexString(profileHash[:]) + return false, nil, errors.New("GetUserProfile called with invalid profileHash: " + profileHashHex) + } + + err = startDatabase() + if (err != nil) { return false, nil, err } + + getKeyPrefix := func()(byte, error){ + + if (profileType == "Mate"){ + return mateProfilesPrefix, nil + + } else if (profileType == "Host"){ + return hostProfilesPrefix, nil + + } else if (profileType == "Moderator"){ + return moderatorProfilesPrefix, nil + } + + return 0, errors.New("VerifyProfileHash not verifying profileType: " + profileType) + } + + keyPrefix, err := getKeyPrefix() + if (err != nil) { return false, nil, err } + + key := []byte{keyPrefix} + key = append(key, profileHash[:]...) + + exists, profileData, err := getDatabaseValue(key) + if (err != nil) { return false, nil, err } + if (exists == false) { + return false, nil, nil + } + + return true, profileData, nil +} + + +func DeleteUserProfile(profileType string, profileHash [28]byte)error{ + + hashIsValid, err := readProfiles.VerifyProfileHash(profileHash, true, profileType, false, false) + if (err != nil) { + return errors.New("DeleteUserProfile called with invalid profileType: " + profileType) + } + if (hashIsValid == false){ + profileHashHex := encoding.EncodeBytesToHexString(profileHash[:]) + return errors.New("DeleteUserProfile called with invalid profileHash: " + profileHashHex) + } + + err = startDatabase() + if (err != nil) { return err } + + getKeyPrefix := func()(byte, error){ + + if (profileType == "Mate"){ + return mateProfilesPrefix, nil + + } else if (profileType == "Host"){ + return hostProfilesPrefix, nil + + } else if (profileType == "Moderator"){ + return moderatorProfilesPrefix, nil + } + + return 0, errors.New("VerifyProfileHash not verifying profileType: " + profileType) + } + + keyPrefix, err := getKeyPrefix() + if (err != nil) { return err } + + key := []byte{keyPrefix} + key = append(key, profileHash[:]...) + + err = deleteDatabaseEntry(key) + if (err != nil) { return err } + + return nil +} + +func GetNumberOfUserProfiles()(int64, error){ + + err := startDatabase() + if (err != nil) { return 0, err } + + numberOfMateProfiles, err := GetNumberOfProfiles("Mate") + if (err != nil) { return 0, err } + + numberOfHostProfiles, err := GetNumberOfProfiles("Host") + if (err != nil) { return 0, err } + + GetNumberOfModeratorProfiles, err := GetNumberOfProfiles("Moderator") + if (err != nil) { return 0, err } + + result := numberOfMateProfiles + numberOfHostProfiles + GetNumberOfModeratorProfiles + + return result, nil +} + +func GetNumberOfProfiles(profileType string)(int64, error){ + + err := startDatabase() + if (err != nil) { return 0, err } + + getKeyPrefix := func()(byte, error){ + + if (profileType == "Mate"){ + return mateProfilesPrefix, nil + + } else if (profileType == "Host"){ + return hostProfilesPrefix, nil + + } else if (profileType == "Moderator"){ + return moderatorProfilesPrefix, nil + } + + return 0, errors.New("GetNumberOfProfiles called with invalid profileType: " + profileType) + } + + keyPrefix, err := getKeyPrefix() + if (err != nil) { return 0, err } + + numberOfProfiles, err := getNumberOfKeysInDatabaseWithPrefix(keyPrefix) + if (err != nil) { return 0, err } + + return numberOfProfiles, nil +} + +func GetAllProfileHashes(profileType string)([][28]byte, error){ + + err := startDatabase() + if (err != nil) { return nil, err } + + getKeyPrefix := func()(byte, error){ + + if (profileType == "Mate"){ + return mateProfilesPrefix, nil + + } else if (profileType == "Host"){ + return hostProfilesPrefix, nil + + } else if (profileType == "Moderator"){ + return moderatorProfilesPrefix, nil + } + + return 0, errors.New("GetAllProfileHashes called with invalid profileType: " + profileType) + } + + keyPrefix, err := getKeyPrefix() + if (err != nil) { return nil, err } + + profileHashesBytesList, err := getAllKeysInDatabaseWithPrefix(keyPrefix, true) + if (err != nil) { return nil, err } + + profileHashesList := make([][28]byte, 0, len(profileHashesBytesList)) + + for _, profileHash := range profileHashesBytesList{ + + if (len(profileHash) != 28){ + return nil, errors.New("profileHashesBytesList contains invalid length profile hash.") + } + + profileHashArray := [28]byte(profileHash) + + profileHashesList = append(profileHashesList, profileHashArray) + } + + return profileHashesList, nil +} + + +func AddChatMessage(messageHash [26]byte, messageBytes []byte)error{ + + err := startDatabase() + if (err != nil) { return err } + + key := []byte{messagesPrefix} + key = append(key, messageHash[:]...) + + err = setDatabaseEntry(key, messageBytes) + if (err != nil) { return err } + + return nil +} + + +func GetChatMessage(messageHash [26]byte)(bool, []byte, error){ + + err := startDatabase() + if (err != nil) { return false, nil, err } + + key := []byte{messagesPrefix} + key = append(key, messageHash[:]...) + + exists, messageBytes, err := getDatabaseValue(key) + if (err != nil) { return false, nil, err } + if (exists == false) { + return false, nil, nil + } + + return true, messageBytes, nil +} + +func DeleteChatMessage(messageHash [26]byte)error{ + + err := startDatabase() + if (err != nil) { return err } + + key := []byte{messagesPrefix} + key = append(key, messageHash[:]...) + + err = deleteDatabaseEntry(key) + if (err != nil) { return err } + + return nil +} + +func GetNumberOfChatMessages()(int64, error){ + + err := startDatabase() + if (err != nil) { return 0, err } + + numberOfMessages, err := getNumberOfKeysInDatabaseWithPrefix(messagesPrefix) + if (err != nil) { return 0, err } + + return numberOfMessages, nil +} + +func GetAllChatMessageHashes()([][26]byte, error){ + + err := startDatabase() + if (err != nil) { return nil, err } + + messageHashesBytesList, err := getAllKeysInDatabaseWithPrefix(messagesPrefix, true) + if (err != nil) { return nil, err } + + messageHashesList := make([][26]byte, 0, len(messageHashesBytesList)) + + for _, messageHashBytes := range messageHashesBytesList{ + + if (len(messageHashBytes) != 26){ + return nil, errors.New("messageHashesBytesList contains invalid length messageHash") + } + + messageHashArray := [26]byte(messageHashBytes) + + messageHashesList = append(messageHashesList, messageHashArray) + } + + return messageHashesList, nil +} + + +func AddReview(reviewHash [29]byte, reviewBytes []byte)error{ + + isValid, err := readReviews.VerifyReviewHash(reviewHash, false, "") + if (err != nil) { return err } + if (isValid == false){ + reviewHashHex := encoding.EncodeBytesToHexString(reviewHash[:]) + return errors.New("AddReview called with invalid reviewHash: " + reviewHashHex) + } + + err = startDatabase() + if (err != nil) { return err } + + key := []byte{reviewsPrefix} + key = append(key, reviewHash[:]...) + + err = setDatabaseEntry(key, reviewBytes) + if (err != nil) { return err } + + return nil +} + + +func GetReview(reviewHash [29]byte)(bool, []byte, error){ + + isValid, err := readReviews.VerifyReviewHash(reviewHash, false, "") + if (err != nil) { return false, nil, err } + if (isValid == false){ + reviewHashHex := encoding.EncodeBytesToHexString(reviewHash[:]) + return false, nil, errors.New("GetReview called with invalid reviewHash: " + reviewHashHex) + } + + err = startDatabase() + if (err != nil) { return false, nil, err } + + key := []byte{reviewsPrefix} + key = append(key, reviewHash[:]...) + + exists, reviewBytes, err := getDatabaseValue(key) + if (err != nil) { return false, nil, err } + if (exists == false) { + return false, nil, nil + } + + return true, reviewBytes, nil +} + +func DeleteReview(reviewHash [29]byte)error{ + + isValid, err := readReviews.VerifyReviewHash(reviewHash, false, "") + if (err != nil) { return err } + if (isValid == false){ + reviewHashHex := encoding.EncodeBytesToHexString(reviewHash[:]) + return errors.New("DeleteReview called with invalid reviewHash: " + reviewHashHex) + } + + err = startDatabase() + if (err != nil) { return err } + + key := []byte{reviewsPrefix} + key = append(key, reviewHash[:]...) + + err = deleteDatabaseEntry(key) + if (err != nil) { return err } + + return nil +} + +func GetNumberOfReviews()(int64, error){ + + err := startDatabase() + if (err != nil) { return 0, err } + + numberOfReviews, err := getNumberOfKeysInDatabaseWithPrefix(reviewsPrefix) + if (err != nil) { return 0, err } + + return numberOfReviews, nil +} + + +func AddReport(reportHash [30]byte, reportBytes []byte)error{ + + isValid, err := readReports.VerifyReportHash(reportHash, false, "") + if (err != nil) { return err } + if (isValid == false){ + reportHashHex := encoding.EncodeBytesToHexString(reportHash[:]) + return errors.New("AddReport called with invalid reportHash: " + reportHashHex) + } + + err = startDatabase() + if (err != nil) { return err } + + key := []byte{reportsPrefix} + key = append(key, reportHash[:]...) + + err = setDatabaseEntry(key, reportBytes) + if (err != nil) { return err } + + return nil +} + + +func GetReport(reportHash [30]byte)(bool, []byte, error){ + + isValid, err := readReports.VerifyReportHash(reportHash, false, "") + if (err != nil) { return false, nil, err } + if (isValid == false){ + reportHashHex := encoding.EncodeBytesToHexString(reportHash[:]) + return false, nil, errors.New("GetReport called with invalid reportHash: " + reportHashHex) + } + + err = startDatabase() + if (err != nil) { return false, nil, err } + + key := []byte{reportsPrefix} + key = append(key, reportHash[:]...) + + exists, reportBytes, err := getDatabaseValue(key) + if (err != nil) { return false, nil, err } + if (exists == false) { + return false, nil, nil + } + + return true, reportBytes, nil +} + +func DeleteReport(reportHash [30]byte)error{ + + isValid, err := readReports.VerifyReportHash(reportHash, false, "") + if (err != nil) { return err } + if (isValid == false){ + reportHashHex := encoding.EncodeBytesToHexString(reportHash[:]) + return errors.New("DeleteReport called with invalid reportHash: " + reportHashHex) + } + + err = startDatabase() + if (err != nil) { return err } + + key := []byte{reportsPrefix} + key = append(key, reportHash[:]...) + + err = deleteDatabaseEntry(key) + if (err != nil) { return err } + + return nil +} + +func GetNumberOfReports()(int64, error){ + + err := startDatabase() + if (err != nil) { return 0, err } + + numberOfReports, err := getNumberOfKeysInDatabaseWithPrefix(reportsPrefix) + if (err != nil) { return 0, err } + + return numberOfReports, nil +} + + +func AddProfileMetadata(profileHash [28]byte, profileMetadata []byte)error{ + + isValid, err := readProfiles.VerifyProfileHash(profileHash, false, "", false, false) + if (err != nil) { return err } + if (isValid == false){ + profileHashHex := encoding.EncodeBytesToHexString(profileHash[:]) + return errors.New("AddProfileMetadata called with invalid profileHash: " + profileHashHex) + } + + err = startDatabase() + if (err != nil) { return err } + + entryKey := []byte{profileMetadataPrefix} + entryKey = append(entryKey, profileHash[:]...) + + err = setDatabaseEntry(entryKey, profileMetadata) + if (err != nil) { return err } + + return nil +} + +//Outputs: +// -bool: Metadata found +// -[]byte: Profile Metadata +// -error +func GetProfileMetadata(profileHash [28]byte)(bool, []byte, error){ + + isValid, err := readProfiles.VerifyProfileHash(profileHash, false, "", false, false) + if (err != nil) { return false, nil, err } + if (isValid == false){ + profileHashHex := encoding.EncodeBytesToHexString(profileHash[:]) + return false, nil, errors.New("GetProfileMetadata called with invalid profileHash: " + profileHashHex) + } + + err = startDatabase() + if (err != nil) { return false, nil, err } + + entryKey := []byte{profileMetadataPrefix} + entryKey = append(entryKey, profileHash[:]...) + + exists, entryValue, err := getDatabaseValue(entryKey) + if (err != nil) { return false, nil, err } + if (exists == false){ + return false, nil, nil + } + + return true, entryValue, nil +} + +func DeleteProfileMetadata(profileHash [28]byte)error{ + + isValid, err := readProfiles.VerifyProfileHash(profileHash, false, "", false, false) + if (err != nil) { return err } + if (isValid == false){ + profileHashHex := encoding.EncodeBytesToHexString(profileHash[:]) + return errors.New("DeleteProfileMetadata called with invalid profileHash: " + profileHashHex) + } + + err = startDatabase() + if (err != nil) { return err } + + entryKey := []byte{profileMetadataPrefix} + entryKey = append(entryKey, profileHash[:]...) + + err = deleteDatabaseEntry(entryKey) + if (err != nil) { return err } + + return nil +} + +func GetAllProfileHashesWithMetadata()([][28]byte, error){ + + err := startDatabase() + if (err != nil) { return nil, err } + + profileHashesBytesList, err := getAllKeysInDatabaseWithPrefix(profileMetadataPrefix, true) + if (err != nil) { return nil, err } + + profileHashesList := make([][28]byte, 0, len(profileHashesBytesList)) + + for _, profileHashBytes := range profileHashesBytesList{ + + if (len(profileHashBytes) != 28){ + profileHashHex := encoding.EncodeBytesToHexString(profileHashBytes) + return nil, errors.New("profileHashesBytesList contains invalid length profileHash: " + profileHashHex) + } + + profileHashArray := [28]byte(profileHashBytes) + + profileHashesList = append(profileHashesList, profileHashArray) + } + + return profileHashesList, nil +} + +func AddMessageMetadata(messageHash [26]byte, messageMetadata []byte)error{ + + err := startDatabase() + if (err != nil) { return err } + + entryKey := []byte{messageMetadataPrefix} + entryKey = append(entryKey, messageHash[:]...) + + err = setDatabaseEntry(entryKey, messageMetadata) + if (err != nil) { return err } + + return nil +} + +//Outputs: +// -bool: Message metadata exists +// -[]byte: Message Metadata +// -error +func GetMessageMetadata(messageHash [26]byte)(bool, []byte, error){ + + err := startDatabase() + if (err != nil) { return false, nil, err } + + entryKey := []byte{messageMetadataPrefix} + entryKey = append(entryKey, messageHash[:]...) + + exists, entryValue, err := getDatabaseValue(entryKey) + if (err != nil) { return false, nil, err } + if (exists == false){ + return false, nil, nil + } + + return true, entryValue, nil +} + +func DeleteMessageMetadata(messageHash [26]byte)error{ + + err := startDatabase() + if (err != nil) { return err } + + entryKey := []byte{messageMetadataPrefix} + entryKey = append(entryKey, messageHash[:]...) + + err = deleteDatabaseEntry(entryKey) + if (err != nil) { return err } + + return nil +} + +func GetAllMessageHashesWithMetadata()([][26]byte, error){ + + err := startDatabase() + if (err != nil) { return nil, err } + + messageHashesBytesList, err := getAllKeysInDatabaseWithPrefix(messageMetadataPrefix, true) + if (err != nil) { return nil, err } + + messageHashesList := make([][26]byte, 0, len(messageHashesBytesList)) + + for _, messageHashBytes := range messageHashesBytesList{ + + if (len(messageHashBytes) != 26){ + messageHashHex := encoding.EncodeBytesToHexString(messageHashBytes) + return nil, errors.New("messageHashesBytesList contains invalid length messageHash: " + messageHashHex) + } + + messageHashArray := [26]byte(messageHashBytes) + + messageHashesList = append(messageHashesList, messageHashArray) + } + + return messageHashesList, nil +} + +func AddAttributeProfile(attributeHash [27]byte, profileHash [28]byte)error{ + + attributeProfileType, _, err := readProfiles.ReadAttributeHashMetadata(attributeHash) + if (err != nil){ + attributeHashHex := encoding.EncodeBytesToHexString(attributeHash[:]) + return errors.New("AddAttributeProfile called with invalid attributeHash: " + attributeHashHex) + } + hashIsValid, err := readProfiles.VerifyProfileHash(profileHash, true, attributeProfileType, false, false) + if (err != nil) { return err } + if (hashIsValid == false){ + profileHashHex := encoding.EncodeBytesToHexString(profileHash[:]) + return errors.New("AddAttributeProfile called with invalid profileHash: " + profileHashHex) + } + + err = startDatabase() + if (err != nil) { return err } + + editingAttributeProfilesListMutex.Lock() + defer editingAttributeProfilesListMutex.Unlock() + + entryKey := []byte{attributeProfilesListPrefix} + entryKey = append(entryKey, attributeHash[:]...) + + err = addItemToEntryList(entryKey, 28, profileHash[:]) + if (err != nil) { return err } + + return nil +} + + +func GetAttributeProfilesList(attributeHash [27]byte)(bool, [][28]byte, error){ + + hashIsValid, err := readProfiles.VerifyAttributeHash(attributeHash, false, "", false, false) + if (err != nil){ return false, nil, err } + if (hashIsValid == false){ + attributeHashHex := encoding.EncodeBytesToHexString(attributeHash[:]) + return false, nil, errors.New("GetAttributeProfilesList called with invalid attributeHash: " + attributeHashHex) + } + + err = startDatabase() + if (err != nil) { return false, nil, err } + + entryKey := []byte{attributeProfilesListPrefix} + entryKey = append(entryKey, attributeHash[:]...) + + exists, profileHashesBytesList, err := getEntryList(entryKey, 28) + if (err != nil) { return false, nil, err } + if (exists == false) { + return false, nil, nil + } + + profileHashesList := make([][28]byte, 0, len(profileHashesBytesList)) + + for _, profileHashBytes := range profileHashesBytesList{ + + if (len(profileHashBytes) != 28){ + return false, nil, errors.New("getEntryList returning invalid bytes length item.") + } + + profileHashArray := [28]byte(profileHashBytes) + + profileHashesList = append(profileHashesList, profileHashArray) + } + + return true, profileHashesList, nil +} + +func DeleteAttributeProfile(attributeHash [27]byte, profileHash [28]byte)error{ + + attributeProfileType, _, err := readProfiles.ReadAttributeHashMetadata(attributeHash) + if (err != nil) { + attributeHashHex := encoding.EncodeBytesToHexString(attributeHash[:]) + return errors.New("DeleteAttributeHashProfile called with invalid attributeHash: " + attributeHashHex) + } + + hashIsValid, err := readProfiles.VerifyProfileHash(profileHash, true, attributeProfileType, false, false) + if (err != nil) { return err } + if (hashIsValid == false){ + profileHashHex := encoding.EncodeBytesToHexString(profileHash[:]) + return errors.New("DeleteAttributeHashProfile called with invalid profileHash: " + profileHashHex) + } + + err = startDatabase() + if (err != nil) { return err } + + editingAttributeProfilesListMutex.Lock() + defer editingAttributeProfilesListMutex.Unlock() + + entryKey := []byte{attributeProfilesListPrefix} + entryKey = append(entryKey, attributeHash[:]...) + + err = deleteItemFromEntryList(entryKey, 28, profileHash[:]) + if (err != nil) { return err } + + return nil +} + +func GetAllProfileAttributesHashes()([][27]byte, error){ + + err := startDatabase() + if (err != nil) { return nil, err } + + attributeHashesBytesList, err := getAllKeysInDatabaseWithPrefix(attributeProfilesListPrefix, true) + if (err != nil) { return nil, err } + + attributeHashesList := make([][27]byte, 0, len(attributeHashesBytesList)) + + for _, attributeHashBytes := range attributeHashesBytesList{ + + if (len(attributeHashBytes) != 27){ + attributeHashHex := encoding.EncodeBytesToHexString(attributeHashBytes) + return nil, errors.New("attributeHashesBytesList contains invalid length attribute hash: " + attributeHashHex) + } + + attributeHashArray := [27]byte(attributeHashBytes) + + attributeHashesList = append(attributeHashesList, attributeHashArray) + } + + return attributeHashesList, nil +} + + +func AddIdentityProfileHash(identityHash [16]byte, profileHash [28]byte)error{ + + identityType, err := identity.GetIdentityTypeFromIdentityHash(identityHash) + if (err != nil){ + identityHashHex := encoding.EncodeBytesToHexString(identityHash[:]) + return errors.New("AddIdentityProfileHash called with invalid identity hash: " + identityHashHex) + } + + hashIsValid, err := readProfiles.VerifyProfileHash(profileHash, true, identityType, false, false) + if (err != nil) { return err } + if (hashIsValid == false){ + + identityHashHex := encoding.EncodeBytesToHexString(identityHash[:]) + profileHashHex := encoding.EncodeBytesToHexString(profileHash[:]) + + return errors.New("AddIdentityProfileHash called with invalid profile hash: " + profileHashHex + ". Identity hash: " + identityHashHex) + } + + err = startDatabase() + if (err != nil) { return err } + + if (identityType == "Mate"){ + + editingMateIdentityProfilesListMutex.Lock() + defer editingMateIdentityProfilesListMutex.Unlock() + + } else if (identityType == "Host"){ + + editingHostIdentityProfilesListMutex.Lock() + defer editingHostIdentityProfilesListMutex.Unlock() + + } else if (identityType == "Moderator"){ + + editingModeratorIdentityProfilesListMutex.Lock() + defer editingModeratorIdentityProfilesListMutex.Unlock() + } else { + return errors.New("GetIdentityTypeFromIdentityHash returning invalid identityType: " + identityType) + } + + getEntryKeyPrefix := func()byte{ + + if (identityType == "Mate"){ + return mateIdentityProfilesListPrefix + } + if (identityType == "Host"){ + return hostIdentityProfilesListPrefix + } + // identityType == "Moderator" + return moderatorIdentityProfilesListPrefix + } + + entryKeyPrefix := getEntryKeyPrefix() + + entryKey := []byte{entryKeyPrefix} + entryKey = append(entryKey, identityHash[:]...) + + err = addItemToEntryList(entryKey, 28, profileHash[:]) + if (err != nil) { return err } + + return nil +} + +func GetIdentityProfileHashesList(identityHash [16]byte)(bool, [][28]byte, error){ + + identityType, err := identity.GetIdentityTypeFromIdentityHash(identityHash) + if (err != nil){ + identityHashHex := encoding.EncodeBytesToHexString(identityHash[:]) + return false, nil, errors.New("GetIdentityProfileHashesList called with invalid identity hash: " + identityHashHex) + } + + err = startDatabase() + if (err != nil) { return false, nil, err } + + getEntryKeyPrefix := func()(byte, error){ + + if (identityType == "Mate"){ + return mateIdentityProfilesListPrefix, nil + + } else if (identityType == "Host"){ + return hostIdentityProfilesListPrefix, nil + + } else if (identityType == "Moderator"){ + return moderatorIdentityProfilesListPrefix, nil + } + + return 0, errors.New("GetIdentityTypeFromIdentityHash returning invalid identityType: " + identityType) + } + + entryKeyPrefix, err := getEntryKeyPrefix() + if (err != nil) { return false, nil, err } + + entryKey := []byte{entryKeyPrefix} + entryKey = append(entryKey, identityHash[:]...) + + exists, profileHashesBytesList, err := getEntryList(entryKey, 28) + if (err != nil) { return false, nil, err } + if (exists == false){ + return false, nil, nil + } + + profileHashesList := make([][28]byte, 0, len(profileHashesBytesList)) + + for _, profileHashBytes := range profileHashesBytesList{ + + if (len(profileHashBytes) != 28){ + + profileHashHex := encoding.EncodeBytesToHexString(profileHashBytes) + return false, nil, errors.New("getEntryList returning invalid length item in list: " + profileHashHex) + } + + profileHashArray := [28]byte(profileHashBytes) + + profileHashesList = append(profileHashesList, profileHashArray) + } + + return true, profileHashesList, nil +} + +func DeleteIdentityProfileHash(identityHash [16]byte, profileHash [28]byte)error{ + + identityType, err := identity.GetIdentityTypeFromIdentityHash(identityHash) + if (err != nil){ + identityHashHex := encoding.EncodeBytesToHexString(identityHash[:]) + return errors.New("DeleteIdentityProfileHash called with invalid identity hash: " + identityHashHex) + } + + hashIsValid, err := readProfiles.VerifyProfileHash(profileHash, true, identityType, false, false) + if (err != nil) { return err } + if (hashIsValid == false){ + + identityHashHex := encoding.EncodeBytesToHexString(identityHash[:]) + profileHashHex := encoding.EncodeBytesToHexString(profileHash[:]) + + return errors.New("DeleteIdentityProfileHash called with invalid profile hash: " + profileHashHex + ". Identity hash: " + identityHashHex) + } + + err = startDatabase() + if (err != nil) { return err } + + if (identityType == "Mate"){ + + editingMateIdentityProfilesListMutex.Lock() + defer editingMateIdentityProfilesListMutex.Unlock() + + } else if (identityType == "Host"){ + + editingHostIdentityProfilesListMutex.Lock() + defer editingHostIdentityProfilesListMutex.Unlock() + + } else if (identityType == "Moderator"){ + + editingModeratorIdentityProfilesListMutex.Lock() + defer editingModeratorIdentityProfilesListMutex.Unlock() + } + + getEntryKeyPrefix := func()(byte, error){ + + if (identityType == "Mate"){ + return mateIdentityProfilesListPrefix, nil + + } else if (identityType == "Host"){ + return hostIdentityProfilesListPrefix, nil + + } else if (identityType == "Moderator"){ + return moderatorIdentityProfilesListPrefix, nil + } + + return 0, errors.New("GetIdentityTypeFromIdentityHash returning invalid identityType: " + identityType) + } + + entryKeyPrefix, err := getEntryKeyPrefix() + if (err != nil) { return err } + + entryKey := []byte{entryKeyPrefix} + entryKey = append(entryKey, identityHash[:]...) + + err = deleteItemFromEntryList(entryKey, 28, profileHash[:]) + if (err != nil) { return err } + + return nil +} + + +// This only returns identity hashes that have been added to the profile identity lists +// This means these identities have had at least 1 profile added to the database +func GetAllProfileIdentityHashes(identityType string)([][16]byte, error){ + + err := startDatabase() + if (err != nil) { return nil, err } + + getEntryKeyPrefix := func()(byte, error){ + + if (identityType == "Mate"){ + return mateIdentityProfilesListPrefix, nil + + } else if (identityType == "Host"){ + return hostIdentityProfilesListPrefix, nil + + } else if (identityType == "Moderator"){ + return moderatorIdentityProfilesListPrefix, nil + } + + return 0, errors.New("GetAllProfileIdentityHashes called with invalid identityType: " + identityType) + } + + entryKeyPrefix, err := getEntryKeyPrefix() + if (err != nil) { return nil, err } + + identityHashesBytesList, err := getAllKeysInDatabaseWithPrefix(entryKeyPrefix, true) + if (err != nil) { return nil, err } + + identityHashesList := make([][16]byte, 0, len(identityHashesBytesList)) + + for _, identityHashBytes := range identityHashesBytesList{ + + if (len(identityHashBytes) != 16){ + identityHashHex := encoding.EncodeBytesToHexString(identityHashBytes) + return nil, errors.New("Database corrupt: Identity profiles list entry contains invalid identity hash in key: " + identityHashHex) + } + + identityHashArray := [16]byte(identityHashBytes) + + identityHashesList = append(identityHashesList, identityHashArray) + } + + return identityHashesList, nil +} + + +func AddChatInboxMessage(recipientInbox [10]byte, messageHash [26]byte)error{ + + err := startDatabase() + if (err != nil) { return err } + + editingInboxMessagesListMutex.Lock() + defer editingInboxMessagesListMutex.Unlock() + + entryKey := []byte{inboxMessagesListPrefix} + entryKey = append(entryKey, recipientInbox[:]...) + + err = addItemToEntryList(entryKey, 26, messageHash[:]) + if (err != nil) { return err } + + return nil +} + +func GetChatInboxMessageHashesList(recipientInbox [10]byte)(bool, [][26]byte, error){ + + err := startDatabase() + if (err != nil) { return false, nil, err } + + entryKey := []byte{inboxMessagesListPrefix} + entryKey = append(entryKey, recipientInbox[:]...) + + exists, messageHashesBytesList, err := getEntryList(entryKey, 26) + if (err != nil) { return false, nil, err } + if (exists == false) { + return false, nil, nil + } + + messageHashesList := make([][26]byte, 0, len(messageHashesBytesList)) + + for _, messageHashBytes := range messageHashesBytesList{ + + if (len(messageHashBytes) != 26){ + return false, nil, errors.New("getEntryList returning invalid length message hash.") + } + + messageHashArray := [26]byte(messageHashBytes) + + messageHashesList = append(messageHashesList, messageHashArray) + } + + return true, messageHashesList, nil +} + +func DeleteChatInboxMessage(recipientInbox [10]byte, messageHash [26]byte)error{ + + err := startDatabase() + if (err != nil) { return err } + + editingInboxMessagesListMutex.Lock() + defer editingInboxMessagesListMutex.Unlock() + + entryKey := []byte{inboxMessagesListPrefix} + entryKey = append(entryKey, recipientInbox[:]...) + + err = deleteItemFromEntryList(entryKey, 26, messageHash[:]) + if (err != nil) { return err } + + return nil +} + + +func GetAllMessageInboxes()([][10]byte, error){ + + err := startDatabase() + if (err != nil) { return nil, err } + + inboxesBytesList, err := getAllKeysInDatabaseWithPrefix(inboxMessagesListPrefix, true) + if (err != nil) { return nil, err } + + inboxesList := make([][10]byte, 0, len(inboxesBytesList)) + + for _, inboxBytes := range inboxesBytesList{ + + if (len(inboxBytes) != 10){ + return nil, errors.New("Database corrupt: Contains inbox entry with invalid inbox: Invalid length.") + } + + inboxArray := [10]byte(inboxBytes) + + inboxesList = append(inboxesList, inboxArray) + } + + return inboxesList, nil +} + + +func AddReviewerReviewHash(reviewerIdentityHash [16]byte, reviewHash [29]byte)error{ + + identityIsValid, err := identity.VerifyIdentityHash(reviewerIdentityHash, true, "Moderator") + if (err != nil) { return err } + if (identityIsValid == false){ + reviewerIdentityHashHex := encoding.EncodeBytesToHexString(reviewerIdentityHash[:]) + return errors.New("AddReviewerReviewHash called with invalid reviewerIdentityHash: " + reviewerIdentityHashHex) + } + + reviewType, err := readReviews.GetReviewTypeFromReviewHash(reviewHash) + if (err != nil){ + reviewHashHex := encoding.EncodeBytesToHexString(reviewHash[:]) + return errors.New("AddReviewerReviewHash called with invalid reviewHash: " + reviewHashHex) + } + + err = startDatabase() + if (err != nil) { return err } + + if (reviewType == "Identity"){ + + editingReviewerIdentityReviewsListMutex.Lock() + defer editingReviewerIdentityReviewsListMutex.Unlock() + + } else if (reviewType == "Profile"){ + + editingReviewerProfileReviewsListMutex.Lock() + defer editingReviewerProfileReviewsListMutex.Unlock() + + } else if (reviewType == "Attribute"){ + + editingReviewerAttributeReviewsListMutex.Lock() + defer editingReviewerAttributeReviewsListMutex.Unlock() + + } else if (reviewType == "Message"){ + + editingReviewerMessageReviewsListMutex.Lock() + defer editingReviewerMessageReviewsListMutex.Unlock() + } else { + + return errors.New("GetReviewTypeFromReviewHash returning invalid reviewType: " + reviewType) + } + + getEntryKeyPrefix := func()byte{ + + if (reviewType == "Identity"){ + return reviewerIdentityReviewsListPrefix + } + if (reviewType == "Profile"){ + return reviewerProfileReviewsListPrefix + } + if (reviewType == "Attribute"){ + return reviewerAttributeReviewsListPrefix + } + // reviewType == "Message" + return reviewerMessageReviewsListPrefix + } + + entryKeyPrefix := getEntryKeyPrefix() + + entryKey := []byte{entryKeyPrefix} + entryKey = append(entryKey, reviewerIdentityHash[:]...) + + err = addItemToEntryList(entryKey, 29, reviewHash[:]) + if (err != nil) { return err } + + return nil +} + +// Returns all review hashes created by reviewer +func GetReviewerReviewHashesList(reviewerIdentityHash [16]byte, reviewType string)(bool, [][29]byte, error){ + + identityIsValid, err := identity.VerifyIdentityHash(reviewerIdentityHash, true, "Moderator") + if (err != nil) { return false, nil, err } + if (identityIsValid == false){ + reviewerIdentityHashHex := encoding.EncodeBytesToHexString(reviewerIdentityHash[:]) + return false, nil, errors.New("GetReviewerReviewHashesList called with invalid reviewerIdentityHash: " + reviewerIdentityHashHex) + } + + err = startDatabase() + if (err != nil) { return false, nil, err } + + getEntryKeyPrefix := func()(byte, error){ + + if (reviewType == "Identity"){ + return reviewerIdentityReviewsListPrefix, nil + } + if (reviewType == "Profile"){ + return reviewerProfileReviewsListPrefix, nil + } + if (reviewType == "Attribute"){ + return reviewerAttributeReviewsListPrefix, nil + } + if (reviewType == "Message"){ + return reviewerMessageReviewsListPrefix, nil + } + return 0, errors.New("GetReviewerReviewHashesList called with invalid reviewType: " + reviewType) + } + + entryKeyPrefix, err := getEntryKeyPrefix() + if (err != nil) { return false, nil, err } + + entryKey := []byte{entryKeyPrefix} + entryKey = append(entryKey, reviewerIdentityHash[:]...) + + exists, reviewHashesBytesList, err := getEntryList(entryKey, 29) + if (err != nil) { return false, nil, err } + if (exists == false){ + return false, nil, nil + } + + reviewHashesList := make([][29]byte, 0, len(reviewHashesBytesList)) + + for _, reviewHashBytes := range reviewHashesBytesList{ + + if (len(reviewHashBytes) != 29){ + return false, nil, errors.New("getEntryList returning invalid length review hash.") + } + + reviewHashArray := [29]byte(reviewHashBytes) + + reviewHashesList = append(reviewHashesList, reviewHashArray) + } + + return true, reviewHashesList, nil +} + +func DeleteReviewerReviewHash(reviewerIdentityHash [16]byte, reviewHash [29]byte)error{ + + identityIsValid, err := identity.VerifyIdentityHash(reviewerIdentityHash, true, "Moderator") + if (err != nil) { return err } + if (identityIsValid == false){ + reviewerIdentityHashHex := encoding.EncodeBytesToHexString(reviewerIdentityHash[:]) + return errors.New("DeleteReviewerReviewHash called with invalid reviewerIdentityHash: " + reviewerIdentityHashHex) + } + + reviewType, err := readReviews.GetReviewTypeFromReviewHash(reviewHash) + if (err != nil){ + reviewHashHex := encoding.EncodeBytesToHexString(reviewHash[:]) + return errors.New("DeleteReviewerReviewHash called with invalid reviewHash: " + reviewHashHex) + } + + err = startDatabase() + if (err != nil) { return err } + + if (reviewType == "Identity"){ + + editingReviewerIdentityReviewsListMutex.Lock() + defer editingReviewerIdentityReviewsListMutex.Unlock() + + } else if (reviewType == "Profile"){ + + editingReviewerProfileReviewsListMutex.Lock() + defer editingReviewerProfileReviewsListMutex.Unlock() + + } else if (reviewType == "Attribute"){ + + editingReviewerAttributeReviewsListMutex.Lock() + defer editingReviewerAttributeReviewsListMutex.Unlock() + + } else if (reviewType == "Message"){ + + editingReviewerMessageReviewsListMutex.Lock() + defer editingReviewerMessageReviewsListMutex.Unlock() + } else { + + return errors.New("GetReviewTypeFromReviewHash returning invalid reviewType: " + reviewType) + } + + getEntryKeyPrefix := func()byte{ + + if (reviewType == "Identity"){ + return reviewerIdentityReviewsListPrefix + } + if (reviewType == "Profile"){ + return reviewerProfileReviewsListPrefix + } + if (reviewType == "Attribute"){ + return reviewerAttributeReviewsListPrefix + } + // reviewType == "Message" + return reviewerMessageReviewsListPrefix + } + + entryKeyPrefix := getEntryKeyPrefix() + + entryKey := []byte{entryKeyPrefix} + entryKey = append(entryKey, reviewerIdentityHash[:]...) + + err = deleteItemFromEntryList(entryKey, 29, reviewHash[:]) + if (err != nil) { return err } + + return nil +} + +func AddIdentityReviewToList(identityHash [16]byte, reviewHash [29]byte)error{ + + identityIsValid, err := identity.VerifyIdentityHash(identityHash, false, "") + if (err != nil) { return err } + if (identityIsValid == false){ + identityHashHex := encoding.EncodeBytesToHexString(identityHash[:]) + return errors.New("AddIdentityReviewToList called with invalid identityHash: " + identityHashHex) + } + + hashIsValid, err := readReviews.VerifyReviewHash(reviewHash, true, "Identity") + if (err != nil) { return err } + if (hashIsValid == false){ + reviewHashHex := encoding.EncodeBytesToHexString(reviewHash[:]) + return errors.New("AddIdentityReviewToList called with invalid reviewHash: " + reviewHashHex) + } + + err = startDatabase() + if (err != nil) { return err } + + editingIdentityReviewsListMutex.Lock() + defer editingIdentityReviewsListMutex.Unlock() + + entryKey := []byte{identityReviewsListPrefix} + entryKey = append(entryKey, identityHash[:]...) + + err = addItemToEntryList(entryKey, 29, reviewHash[:]) + if (err != nil) { return err } + + return nil +} + +func GetIdentityReviewsList(identityHash [16]byte)(bool, [][29]byte, error){ + + identityIsValid, err := identity.VerifyIdentityHash(identityHash, false, "") + if (err != nil) { return false, nil, err } + if (identityIsValid == false){ + identityHashHex := encoding.EncodeBytesToHexString(identityHash[:]) + return false, nil, errors.New("GetIdentityReviewsList called with invalid identityHash: " + identityHashHex) + } + + err = startDatabase() + if (err != nil) { return false, nil, err } + + entryKey := []byte{identityReviewsListPrefix} + entryKey = append(entryKey, identityHash[:]...) + + exists, reviewHashesBytesList, err := getEntryList(entryKey, 29) + if (err != nil) { return false, nil, err } + if (exists == false) { + return false, nil, nil + } + + reviewHashesList := make([][29]byte, 0, len(reviewHashesBytesList)) + + for _, reviewHashBytes := range reviewHashesBytesList{ + + if (len(reviewHashBytes) != 29){ + return false, nil, errors.New("getEntryList returning invalid length review hash.") + } + + reviewHashArray := [29]byte(reviewHashBytes) + + reviewHashesList = append(reviewHashesList, reviewHashArray) + } + + return true, reviewHashesList, nil +} + +func DeleteIdentityReviewFromList(identityHash [16]byte, reviewHash [29]byte)error{ + + identityIsValid, err := identity.VerifyIdentityHash(identityHash, false, "") + if (err != nil) { return err } + if (identityIsValid == false){ + identityHashHex := encoding.EncodeBytesToHexString(identityHash[:]) + return errors.New("DeleteIdentityReviewFromList called with invalid identityHash: " + identityHashHex) + } + + hashIsValid, err := readReviews.VerifyReviewHash(reviewHash, true, "Identity") + if (err != nil) { return err } + if (hashIsValid == false){ + reviewHashHex := encoding.EncodeBytesToHexString(reviewHash[:]) + return errors.New("DeleteIdentityReviewFromList called with invalid reviewHash: " + reviewHashHex) + } + + err = startDatabase() + if (err != nil) { return err } + + editingIdentityReviewsListMutex.Lock() + defer editingIdentityReviewsListMutex.Unlock() + + entryKey := []byte{identityReviewsListPrefix} + entryKey = append(entryKey, identityHash[:]...) + + err = deleteItemFromEntryList(entryKey, 29, reviewHash[:]) + if (err != nil) { return err } + + return nil +} + +// Returns all identity hashes that have been reviewed, both profiles and their identity +func GetAllReviewedIdentityHashes()([][16]byte, error){ + + err := startDatabase() + if (err != nil) { return nil, err } + + identityHashesBytesList, err := getAllKeysInDatabaseWithPrefix(identityReviewsListPrefix, true) + if (err != nil) { return nil, err } + + identityHashesList := make([][16]byte, 0, len(identityHashesBytesList)) + + for _, identityHashBytes := range identityHashesBytesList{ + + if (len(identityHashBytes) != 16){ + return nil, errors.New("Database corrupt: Identtity reviews list entry contains invalid identity hash.") + } + + identityHashArray := [16]byte(identityHashBytes) + + identityHashesList = append(identityHashesList, identityHashArray) + } + + return identityHashesList, nil +} + + +func AddIdentityReportToList(identityHash [16]byte, reportHash [30]byte)error{ + + identityIsValid, err := identity.VerifyIdentityHash(identityHash, false, "") + if (err != nil) { return err } + if (identityIsValid == false){ + identityHashHex := encoding.EncodeBytesToHexString(identityHash[:]) + return errors.New("AddIdentityReportToList called with invalid identityHash: " + identityHashHex) + } + + hashIsValid, err := readReports.VerifyReportHash(reportHash, true, "Identity") + if (err != nil) { return err } + if (hashIsValid == false){ + reportHashHex := encoding.EncodeBytesToHexString(reportHash[:]) + return errors.New("AddIdentityReportToList called with invalid reportHash: " + reportHashHex) + } + + err = startDatabase() + if (err != nil) { return err } + + editingIdentityReportsListMutex.Lock() + defer editingIdentityReportsListMutex.Unlock() + + entryKey := []byte{identityReportsListPrefix} + entryKey = append(entryKey, identityHash[:]...) + + err = addItemToEntryList(entryKey, 30, reportHash[:]) + if (err != nil) { return err } + + return nil +} + +func GetIdentityReportsList(identityHash [16]byte)(bool, [][30]byte, error){ + + identityIsValid, err := identity.VerifyIdentityHash(identityHash, false, "") + if (err != nil) { return false, nil, err } + if (identityIsValid == false){ + identityHashHex := encoding.EncodeBytesToHexString(identityHash[:]) + return false, nil, errors.New("GetIdentityReportsList called with invalid identityHash: " + identityHashHex) + } + + err = startDatabase() + if (err != nil) { return false, nil, err } + + entryKey := []byte{identityReportsListPrefix} + entryKey = append(entryKey, identityHash[:]...) + + exists, reportHashesBytesList, err := getEntryList(entryKey, 30) + if (err != nil) { return false, nil, err } + if (exists == false) { + return false, nil, nil + } + + reportHashesList := make([][30]byte, 0, len(reportHashesBytesList)) + + for _, reportHashBytes := range reportHashesBytesList{ + + if (len(reportHashBytes) != 30){ + return false, nil, errors.New("getEntryList returning invalid length reportHash.") + } + + reportHashArray := [30]byte(reportHashBytes) + + reportHashesList = append(reportHashesList, reportHashArray) + } + + return true, reportHashesList, nil +} + +func DeleteIdentityReportFromList(identityHash [16]byte, reportHash [30]byte)error{ + + identityIsValid, err := identity.VerifyIdentityHash(identityHash, false, "") + if (err != nil) { return err } + if (identityIsValid == false){ + identityHashHex := encoding.EncodeBytesToHexString(identityHash[:]) + return errors.New("DeleteIdentityReportFromList called with invalid identityHash: " + identityHashHex) + } + + hashIsValid, err := readReports.VerifyReportHash(reportHash, true, "Identity") + if (err != nil) { return err } + if (hashIsValid == false){ + reportHashHex := encoding.EncodeBytesToHexString(reportHash[:]) + return errors.New("DeleteIdentityReportFromList called with invalid reportHash: " + reportHashHex) + } + + err = startDatabase() + if (err != nil) { return err } + + editingIdentityReportsListMutex.Lock() + defer editingIdentityReportsListMutex.Unlock() + + entryKey := []byte{identityReportsListPrefix} + entryKey = append(entryKey, identityHash[:]...) + + err = deleteItemFromEntryList(entryKey, 30, reportHash[:]) + if (err != nil) { return err } + + return nil +} + +func GetAllReportedIdentityHashes()([][16]byte, error){ + + err := startDatabase() + if (err != nil) { return nil, err } + + identityHashesBytesList, err := getAllKeysInDatabaseWithPrefix(identityReportsListPrefix, true) + if (err != nil) { return nil, err } + + identityHashesList := make([][16]byte, 0, len(identityHashesBytesList)) + + for _, identityHashBytes := range identityHashesBytesList{ + + if (len(identityHashBytes) != 16){ + return nil, errors.New("Database corrupt: identity reports list entry key contains invalid length identity hash.") + } + + identityHashArray := [16]byte(identityHashBytes) + + identityHashesList = append(identityHashesList, identityHashArray) + } + + return identityHashesList, nil +} + +func AddProfileReviewToList(profileHash [28]byte, reviewHash [29]byte)error{ + + profileHashIsValid, err := readProfiles.VerifyProfileHash(profileHash, false, "", false, false) + if (err != nil) { return err } + if (profileHashIsValid == false){ + profileHashHex := encoding.EncodeBytesToHexString(profileHash[:]) + return errors.New("AddProfileReviewToList called with invalid profileHash: " + profileHashHex) + } + + reviewHashIsValid, err := readReviews.VerifyReviewHash(reviewHash, true, "Profile") + if (err != nil) { return err } + if (reviewHashIsValid == false){ + reviewHashHex := encoding.EncodeBytesToHexString(reviewHash[:]) + return errors.New("AddProfileReviewToList called with invalid reviewHash: " + reviewHashHex) + } + + err = startDatabase() + if (err != nil) { return err } + + editingProfileReviewsListMutex.Lock() + defer editingProfileReviewsListMutex.Unlock() + + entryKey := []byte{profileReviewsListPrefix} + entryKey = append(entryKey, profileHash[:]...) + + err = addItemToEntryList(entryKey, 29, reviewHash[:]) + if (err != nil) { return err } + + return nil +} + +func GetProfileReviewsList(profileHash [28]byte)(bool, [][29]byte, error){ + + profileHashIsValid, err := readProfiles.VerifyProfileHash(profileHash, false, "", false, false) + if (err != nil) { return false, nil, err } + if (profileHashIsValid == false){ + profileHashHex := encoding.EncodeBytesToHexString(profileHash[:]) + return false, nil, errors.New("GetProfileReviewsList called with invalid profileHash: " + profileHashHex) + } + + err = startDatabase() + if (err != nil) { return false, nil, err } + + entryKey := []byte{profileReviewsListPrefix} + entryKey = append(entryKey, profileHash[:]...) + + exists, reviewHashesBytesList, err := getEntryList(entryKey, 29) + if (err != nil) { return false, nil, err } + if (exists == false) { + return false, nil, nil + } + + reviewHashesList := make([][29]byte, 0, len(reviewHashesBytesList)) + + for _, reviewHashBytes := range reviewHashesBytesList{ + + if (len(reviewHashBytes) != 29){ + return false, nil, errors.New("getEntryList returning invalid review hash.") + } + + reviewHashArray := [29]byte(reviewHashBytes) + + reviewHashesList = append(reviewHashesList, reviewHashArray) + } + + return true, reviewHashesList, nil +} + +func DeleteProfileReviewFromList(profileHash [28]byte, reviewHash [29]byte)error{ + + profileHashIsValid, err := readProfiles.VerifyProfileHash(profileHash, false, "", false, false) + if (err != nil) { return err } + if (profileHashIsValid == false){ + profileHashHex := encoding.EncodeBytesToHexString(profileHash[:]) + return errors.New("DeleteProfileReviewFromList called with invalid profileHash: " + profileHashHex) + } + + reviewHashIsValid, err := readReviews.VerifyReviewHash(reviewHash, true, "Profile") + if (err != nil) { return err } + if (reviewHashIsValid == false){ + reviewHashHex := encoding.EncodeBytesToHexString(reviewHash[:]) + return errors.New("DeleteProfileReviewFromList called with invalid reviewHash: " + reviewHashHex) + } + + err = startDatabase() + if (err != nil) { return err } + + editingProfileReviewsListMutex.Lock() + defer editingProfileReviewsListMutex.Unlock() + + entryKey := []byte{profileReviewsListPrefix} + entryKey = append(entryKey, profileHash[:]...) + + err = deleteItemFromEntryList(entryKey, 29, reviewHash[:]) + if (err != nil) { return err } + + return nil +} + +// Returns all Profile hashes that have been reviewed +func GetAllReviewedProfileHashes()([][28]byte, error){ + + err := startDatabase() + if (err != nil) { return nil, err } + + profileHashesBytesList, err := getAllKeysInDatabaseWithPrefix(profileReviewsListPrefix, true) + if (err != nil) { return nil, err } + + profileHashesList := make([][28]byte, 0, len(profileHashesBytesList)) + + for _, profileHashBytes := range profileHashesBytesList{ + + if (len(profileHashBytes) != 28){ + return nil, errors.New("Database corrupt: Profile reviews list entry key contains invalid profile hash.") + } + + profileHashArray := [28]byte(profileHashBytes) + + profileHashesList = append(profileHashesList, profileHashArray) + } + + return profileHashesList, nil +} + + +func AddProfileReportToList(profileHash [28]byte, reportHash [30]byte)error{ + + profileHashIsValid, err := readProfiles.VerifyProfileHash(profileHash, false, "", false, false) + if (err != nil) { return err } + if (profileHashIsValid == false){ + profileHashHex := encoding.EncodeBytesToHexString(profileHash[:]) + return errors.New("AddProfileReportToList called with invalid profileHash: " + profileHashHex) + } + + reportHashIsValid, err := readReports.VerifyReportHash(reportHash, true, "Profile") + if (err != nil) { return err } + if (reportHashIsValid == false){ + reportHashHex := encoding.EncodeBytesToHexString(reportHash[:]) + return errors.New("AddProfileReportToList called with invalid reportHash: " + reportHashHex) + } + + err = startDatabase() + if (err != nil) { return err } + + editingProfileReportsListMutex.Lock() + defer editingProfileReportsListMutex.Unlock() + + entryKey := []byte{profileReportsListPrefix} + entryKey = append(entryKey, profileHash[:]...) + + err = addItemToEntryList(entryKey, 30, reportHash[:]) + if (err != nil) { return err } + + return nil +} + +func GetProfileReportsList(profileHash [28]byte)(bool, [][30]byte, error){ + + profileHashIsValid, err := readProfiles.VerifyProfileHash(profileHash, false, "", false, false) + if (err != nil) { return false, nil, err } + if (profileHashIsValid == false){ + profileHashHex := encoding.EncodeBytesToHexString(profileHash[:]) + return false, nil, errors.New("GetProfileReportsList called with invalid profileHash: " + profileHashHex) + } + + err = startDatabase() + if (err != nil) { return false, nil, err } + + entryKey := []byte{profileReportsListPrefix} + entryKey = append(entryKey, profileHash[:]...) + + exists, reportHashesBytesList, err := getEntryList(entryKey, 30) + if (err != nil) { return false, nil, err } + if (exists == false) { + return false, nil, nil + } + + reportHashesList := make([][30]byte, 0, len(reportHashesBytesList)) + + for _, reportHashBytes := range reportHashesBytesList{ + + if (len(reportHashBytes) != 30){ + return false, nil, errors.New("getEntryList returning invalid length reportHash.") + } + + reportHashArray := [30]byte(reportHashBytes) + + reportHashesList = append(reportHashesList, reportHashArray) + } + + return true, reportHashesList, nil +} + + +func DeleteProfileReportFromList(profileHash [28]byte, reportHash [30]byte)error{ + + profileHashIsValid, err := readProfiles.VerifyProfileHash(profileHash, false, "", false, false) + if (err != nil) { return err } + if (profileHashIsValid == false){ + profileHashHex := encoding.EncodeBytesToHexString(profileHash[:]) + return errors.New("DeleteProfileReportFromList called with invalid profileHash: " + profileHashHex) + } + + reportHashIsValid, err := readReports.VerifyReportHash(reportHash, true, "Profile") + if (err != nil) { return err } + if (reportHashIsValid == false){ + reportHashHex := encoding.EncodeBytesToHexString(reportHash[:]) + return errors.New("DeleteProfileReportFromList called with invalid reportHash: " + reportHashHex) + } + + err = startDatabase() + if (err != nil) { return err } + + editingProfileReportsListMutex.Lock() + defer editingProfileReportsListMutex.Unlock() + + entryKey := []byte{profileReportsListPrefix} + entryKey = append(entryKey, profileHash[:]...) + + err = deleteItemFromEntryList(entryKey, 30, reportHash[:]) + if (err != nil) { return err } + + return nil +} + +// Returns all Profile hashes that have been reported +func GetAllReportedProfileHashes()([][28]byte, error){ + + err := startDatabase() + if (err != nil) { return nil, err } + + profileHashesBytesList, err := getAllKeysInDatabaseWithPrefix(profileReportsListPrefix, true) + if (err != nil) { return nil, err } + + profileHashesList := make([][28]byte, 0, len(profileHashesBytesList)) + + for _, profileHashBytes := range profileHashesBytesList{ + + if (len(profileHashBytes) != 28){ + return nil, errors.New("Database corrupt: Profile reports list entry contains invalid profileHash.") + } + + profileHashArray := [28]byte(profileHashBytes) + + profileHashesList = append(profileHashesList, profileHashArray) + } + + return profileHashesList, nil +} + + +func AddProfileAttributeReviewToList(attributeHash [27]byte, reviewHash [29]byte)error{ + + attributeHashIsValid, err := readProfiles.VerifyAttributeHash(attributeHash, false, "", false, false) + if (err != nil) { return err } + if (attributeHashIsValid == false){ + attributeHashHex := encoding.EncodeBytesToHexString(attributeHash[:]) + return errors.New("AddProfileAttributeReviewToList called with invalid attributeHash: " + attributeHashHex) + } + reviewHashIsValid, err := readReviews.VerifyReviewHash(reviewHash, true, "Attribute") + if (err != nil) { return err } + if (reviewHashIsValid == false){ + reviewHashHex := encoding.EncodeBytesToHexString(reviewHash[:]) + return errors.New("AddProfileAttributeReviewToList called with invalid reviewHash: " + reviewHashHex) + } + + err = startDatabase() + if (err != nil) { return err } + + editingAttributeReviewsListMutex.Lock() + defer editingAttributeReviewsListMutex.Unlock() + + entryKey := []byte{attributeReviewsListPrefix} + entryKey = append(entryKey, attributeHash[:]...) + + err = addItemToEntryList(entryKey, 29, reviewHash[:]) + if (err != nil) { return err } + + return nil +} + +func GetProfileAttributeReviewsList(attributeHash [27]byte)(bool, [][29]byte, error){ + + attributeHashIsValid, err := readProfiles.VerifyAttributeHash(attributeHash, false, "", false, false) + if (err != nil) { return false, nil, err } + if (attributeHashIsValid == false){ + attributeHashHex := encoding.EncodeBytesToHexString(attributeHash[:]) + return false, nil, errors.New("GetProfileAttributeReviewsList called with invalid attributeHash: " + attributeHashHex) + } + + err = startDatabase() + if (err != nil) { return false, nil, err } + + entryKey := []byte{attributeReviewsListPrefix} + entryKey = append(entryKey, attributeHash[:]...) + + exists, reviewHashesBytesList, err := getEntryList(entryKey, 29) + if (err != nil) { return false, nil, err } + if (exists == false) { + return false, nil, nil + } + + reviewHashesList := make([][29]byte, 0, len(reviewHashesBytesList)) + + for _, reviewHashBytes := range reviewHashesBytesList{ + + if (len(reviewHashBytes) != 29){ + return false, nil, errors.New("getEntryList returning invalid length review hash.") + } + + reviewHashArray := [29]byte(reviewHashBytes) + + reviewHashesList = append(reviewHashesList, reviewHashArray) + } + + return true, reviewHashesList, nil +} + +func DeleteProfileAttributeReviewFromList(attributeHash [27]byte, reviewHash [29]byte)error{ + + attributeHashIsValid, err := readProfiles.VerifyAttributeHash(attributeHash, false, "", false, false) + if (err != nil) { return err } + if (attributeHashIsValid == false){ + attributeHashHex := encoding.EncodeBytesToHexString(attributeHash[:]) + return errors.New("DeleteProfileAttributeReviewFromList called with invalid attributeHash: " + attributeHashHex) + } + + reviewHashIsValid, err := readReviews.VerifyReviewHash(reviewHash, true, "Attribute") + if (err != nil) { return err } + if (reviewHashIsValid == false){ + reviewHashHex := encoding.EncodeBytesToHexString(reviewHash[:]) + return errors.New("DeleteProfileAttributeReviewFromList called with invalid reviewHash: " + reviewHashHex) + } + + err = startDatabase() + if (err != nil) { return err } + + editingAttributeReviewsListMutex.Lock() + defer editingAttributeReviewsListMutex.Unlock() + + entryKey := []byte{attributeReviewsListPrefix} + entryKey = append(entryKey, attributeHash[:]...) + + err = deleteItemFromEntryList(entryKey, 29, reviewHash[:]) + if (err != nil) { return err } + + return nil +} + +// Returns all attribute hashes that have been reviewed +func GetAllReviewedProfileAttributeHashes()([][27]byte, error){ + + err := startDatabase() + if (err != nil) { return nil, err } + + attributeHashesBytesList, err := getAllKeysInDatabaseWithPrefix(attributeReviewsListPrefix, true) + if (err != nil) { return nil, err } + + attributeHashesList := make([][27]byte, 0, len(attributeHashesBytesList)) + + for _, attributeHashBytes := range attributeHashesBytesList{ + + if (len(attributeHashBytes) != 27){ + return nil, errors.New("Database corrupt: Attribute reviews list entry key contains invalid length attribute hash.") + } + + attributeHashArray := [27]byte(attributeHashBytes) + + attributeHashesList = append(attributeHashesList, attributeHashArray) + } + + return attributeHashesList, nil +} + +func AddProfileAttributeReportToList(attributeHash [27]byte, reportHash [30]byte)error{ + + attributeHashIsValid, err := readProfiles.VerifyAttributeHash(attributeHash, false, "", false, false) + if (err != nil) { return err } + if (attributeHashIsValid == false){ + attributeHashHex := encoding.EncodeBytesToHexString(attributeHash[:]) + return errors.New("AddProfileAttributeReportToList called with invalid attributeHash: " + attributeHashHex) + } + reportHashIsValid, err := readReports.VerifyReportHash(reportHash, true, "Attribute") + if (err != nil) { return err } + if (reportHashIsValid == false){ + reportHashHex := encoding.EncodeBytesToHexString(reportHash[:]) + return errors.New("AddProfileAttributeReportToList called with invalid reportHash: " + reportHashHex) + } + + err = startDatabase() + if (err != nil) { return err } + + editingAttributeReportsListMutex.Lock() + defer editingAttributeReportsListMutex.Unlock() + + entryKey := []byte{attributeReportsListPrefix} + entryKey = append(entryKey, attributeHash[:]...) + + err = addItemToEntryList(entryKey, 30, reportHash[:]) + if (err != nil) { return err } + + return nil +} + + +func GetProfileAttributeReportsList(attributeHash [27]byte)(bool, [][30]byte, error){ + + attributeHashIsValid, err := readProfiles.VerifyAttributeHash(attributeHash, false, "", false, false) + if (err != nil) { return false, nil, err } + if (attributeHashIsValid == false){ + attributeHashHex := encoding.EncodeBytesToHexString(attributeHash[:]) + return false, nil, errors.New("GetProfileAttributeReportsList called with invalid attributeHash: " + attributeHashHex) + } + + err = startDatabase() + if (err != nil) { return false, nil, err } + + entryKey := []byte{attributeReportsListPrefix} + entryKey = append(entryKey, attributeHash[:]...) + + exists, reportHashesBytesList, err := getEntryList(entryKey, 30) + if (err != nil) { return false, nil, err } + if (exists == false) { + return false, nil, nil + } + + reportHashesList := make([][30]byte, 0, len(reportHashesBytesList)) + + for _, reportHashBytes := range reportHashesBytesList{ + + if (len(reportHashBytes) != 30){ + return false, nil, errors.New("getEntryList returning invalid length report hash.") + } + + reportHashArray := [30]byte(reportHashBytes) + + reportHashesList = append(reportHashesList, reportHashArray) + } + + return true, reportHashesList, nil +} + +func DeleteProfileAttributeReportFromList(attributeHash [27]byte, reportHash [30]byte)error{ + + attributeHashIsValid, err := readProfiles.VerifyAttributeHash(attributeHash, false, "", false, false) + if (err != nil){ return err } + if (attributeHashIsValid == false){ + attributeHashHex := encoding.EncodeBytesToHexString(attributeHash[:]) + return errors.New("DeleteProfileAttributeReportFromList called with invalid attributeHash: " + attributeHashHex) + } + reportHashIsValid, err := readReports.VerifyReportHash(reportHash, true, "Attribute") + if (err != nil){ return err } + if (reportHashIsValid == false){ + reportHashHex := encoding.EncodeBytesToHexString(reportHash[:]) + return errors.New("DeleteProfileAttributeReportFromList called with invalid reportHash: " + reportHashHex) + } + + err = startDatabase() + if (err != nil) { return err } + + editingAttributeReportsListMutex.Lock() + defer editingAttributeReportsListMutex.Unlock() + + entryKey := []byte{attributeReportsListPrefix} + entryKey = append(entryKey, attributeHash[:]...) + + err = deleteItemFromEntryList(entryKey, 30, reportHash[:]) + if (err != nil) { return err } + + return nil +} + + +// Returns all profile attribute hashes that have been reported +func GetAllReportedProfileAttributeHashes()([][27]byte, error){ + + err := startDatabase() + if (err != nil) { return nil, err } + + attributeHashesBytesList, err := getAllKeysInDatabaseWithPrefix(attributeReportsListPrefix, true) + if (err != nil) { return nil, err } + + attributeHashesList := make([][27]byte, 0, len(attributeHashesBytesList)) + + for _, attributeHashBytes := range attributeHashesBytesList{ + + if (len(attributeHashBytes) != 27){ + return nil, errors.New("Database corrupt: Attribute reports list entry key contains invalid attribute hash") + } + + attributeHashArray := [27]byte(attributeHashBytes) + + attributeHashesList = append(attributeHashesList, attributeHashArray) + } + + return attributeHashesList, nil +} + + +func AddMessageReviewToList(messageHash [26]byte, reviewHash [29]byte)error{ + + reviewHashIsValid, err := readReviews.VerifyReviewHash(reviewHash, true, "Message") + if (err != nil) { return err } + if (reviewHashIsValid == false){ + reviewHashHex := encoding.EncodeBytesToHexString(reviewHash[:]) + return errors.New("AddMessageReviewToList called with invalid reviewHash: " + reviewHashHex) + } + + err = startDatabase() + if (err != nil) { return err } + + editingMessageReviewsListMutex.Lock() + defer editingMessageReviewsListMutex.Unlock() + + entryKey := []byte{messageReviewsListPrefix} + entryKey = append(entryKey, messageHash[:]...) + + err = addItemToEntryList(entryKey, 29, reviewHash[:]) + if (err != nil) { return err } + + return nil +} + +func GetMessageReviewsList(messageHash [26]byte)(bool, [][29]byte, error){ + + err := startDatabase() + if (err != nil) { return false, nil, err } + + entryKey := []byte{messageReviewsListPrefix} + entryKey = append(entryKey, messageHash[:]...) + + exists, reviewHashesBytesList, err := getEntryList(entryKey, 29) + if (err != nil) { return false, nil, err } + if (exists == false) { + return false, nil, nil + } + + reviewHashesList := make([][29]byte, 0, len(reviewHashesBytesList)) + + for _, reviewHashBytes := range reviewHashesBytesList{ + + if (len(reviewHashBytes) != 29){ + return false, nil, errors.New("getEntryList returning invalid length reviewHash.") + } + + reviewHashArray := [29]byte(reviewHashBytes) + + reviewHashesList = append(reviewHashesList, reviewHashArray) + } + + return true, reviewHashesList, nil +} + + +func DeleteMessageReviewFromList(messageHash [26]byte, reviewHash [29]byte)error{ + + reviewHashIsValid, err := readReviews.VerifyReviewHash(reviewHash, true, "Message") + if (err != nil){ return err } + if (reviewHashIsValid == false){ + reviewHashHex := encoding.EncodeBytesToHexString(reviewHash[:]) + return errors.New("DeleteMessageReviewFromList called with invalid reviewHash: " + reviewHashHex) + } + + err = startDatabase() + if (err != nil) { return err } + + editingMessageReviewsListMutex.Lock() + defer editingMessageReviewsListMutex.Unlock() + + entryKey := []byte{messageReviewsListPrefix} + entryKey = append(entryKey, messageHash[:]...) + + err = deleteItemFromEntryList(entryKey, 29, reviewHash[:]) + if (err != nil) { return err } + + return nil +} + +// Returns all message hashes that have been reviewed +func GetAllReviewedMessageHashes()([][26]byte, error){ + + err := startDatabase() + if (err != nil) { return nil, err } + + messageHashesBytesList, err := getAllKeysInDatabaseWithPrefix(messageReviewsListPrefix, true) + if (err != nil) { return nil, err } + + messageHashesList := make([][26]byte, 0, len(messageHashesBytesList)) + + for _, messageHashBytes := range messageHashesBytesList{ + + if (len(messageHashBytes) != 26){ + return nil, errors.New("Database corrupt: Contains invalid message reviews list entry key message hash.") + } + + messageHashArray := [26]byte(messageHashBytes) + + messageHashesList = append(messageHashesList, messageHashArray) + } + + return messageHashesList, nil +} + + +func AddMessageReportToList(messageHash [26]byte, reportHash [30]byte)error{ + + reportHashIsValid, err := readReports.VerifyReportHash(reportHash, true, "Message") + if (err != nil) { return err } + if (reportHashIsValid == false){ + reportHashHex := encoding.EncodeBytesToHexString(reportHash[:]) + return errors.New("AddMessageReportToList called with invalid reportHash: " + reportHashHex) + } + + err = startDatabase() + if (err != nil) { return err } + + editingMessageReportsListMutex.Lock() + defer editingMessageReportsListMutex.Unlock() + + entryKey := []byte{messageReportsListPrefix} + entryKey = append(entryKey, messageHash[:]...) + + err = addItemToEntryList(entryKey, 30, reportHash[:]) + if (err != nil) { return err } + + return nil +} + +func GetMessageReportsList(messageHash [26]byte)(bool, [][30]byte, error){ + + err := startDatabase() + if (err != nil) { return false, nil, err } + + entryKey := []byte{messageReportsListPrefix} + entryKey = append(entryKey, messageHash[:]...) + + exists, reportHashesBytesList, err := getEntryList(entryKey, 30) + if (err != nil) { return false, nil, err } + if (exists == false) { + return false, nil, nil + } + + reportHashesList := make([][30]byte, 0, len(reportHashesBytesList)) + + for _, reportHashBytes := range reportHashesBytesList{ + + if (len(reportHashBytes) != 30){ + return false, nil, errors.New("getEntryList returning invalid length report hash.") + } + + reportHashArray := [30]byte(reportHashBytes) + + reportHashesList = append(reportHashesList, reportHashArray) + } + + return true, reportHashesList, nil +} + +func DeleteMessageReportFromList(messageHash [26]byte, reportHash [30]byte)error{ + + reportHashIsValid, err := readReports.VerifyReportHash(reportHash, true, "Message") + if (err != nil) { return err } + if (reportHashIsValid == false){ + reportHashHex := encoding.EncodeBytesToHexString(reportHash[:]) + return errors.New("DeleteMessageReportFromList called with invalid reportHash: " + reportHashHex) + } + + err = startDatabase() + if (err != nil) { return err } + + editingMessageReportsListMutex.Lock() + defer editingMessageReportsListMutex.Unlock() + + entryKey := []byte{messageReportsListPrefix} + entryKey = append(entryKey, messageHash[:]...) + + err = deleteItemFromEntryList(entryKey, 30, reportHash[:]) + if (err != nil) { return err } + + return nil +} + +// Returns all message hashes that have been reported +func GetAllReportedMessageHashes()([][26]byte, error){ + + err := startDatabase() + if (err != nil) { return nil, err } + + messageHashesBytesList, err := getAllKeysInDatabaseWithPrefix(messageReportsListPrefix, true) + if (err != nil) { return nil, err } + + messageHashesList := make([][26]byte, 0, len(messageHashesBytesList)) + + for _, messageHashBytes := range messageHashesBytesList{ + + if (len(messageHashBytes) != 26){ + return nil, errors.New("Database corrupt: Contains message reports list entry with invalid message hash.") + } + + messageHashArray := [26]byte(messageHashBytes) + + messageHashesList = append(messageHashesList, messageHashArray) + } + + return messageHashesList, nil +} + +//Ouputs: +// -bool: Entry exists +// -[][]byte: Items list +// -error +func getEntryList(entryKey []byte, lengthOfEachItem int)(bool, [][]byte, error){ + + exists, entryValue, err := getDatabaseValue(entryKey) + if (err != nil) { return false, nil, err } + if (exists == false){ + return false, nil, nil + } + + itemsList, err := splitByteSliceIntoEqualParts(entryValue, lengthOfEachItem) + if (err != nil){ + entryKeyHex := encoding.EncodeBytesToHexString(entryKey) + return false, nil, errors.New("Database entry list is invalid for entry key: " + entryKeyHex) + } + + return true, itemsList, nil +} + +// This adds an item to an entry whose value is a list of equal-length byte slices +// Duplicates items will be avoided +func addItemToEntryList(entryKey []byte, lengthOfEachItem int, itemToAdd []byte)error{ + + if (len(itemToAdd) != lengthOfEachItem){ + lengthString := helpers.ConvertIntToString(lengthOfEachItem) + return errors.New("addItemToEntryList called with invalid length itemToAdd: Not " + lengthString) + } + + //Outputs: + // -bool: Update needed + // -bool: New value exists (false = delete entry value) + // -[]byte: New value + // -error + updateValueFunction := func(existingValueExists bool, existingValue []byte)(bool, bool, []byte, error){ + + if (existingValueExists == false){ + return true, true, itemToAdd, nil + } + + //TODO: Do we need to copy existingValue? because we use it as an input to slices.Concat() + + currentList, err := splitByteSliceIntoEqualParts(existingValue, lengthOfEachItem) + if (err != nil) { return false, false, nil, err } + + for _, element := range currentList{ + + areEqual := bytes.Equal(itemToAdd, element) + if (areEqual == true){ + // The item to add already exists. + // No update is needed. + return false, false, nil, nil + } + } + + newValue := slices.Concat(existingValue, itemToAdd) + + return true, true, newValue, nil + } + + err := updateDatabaseEntry(entryKey, updateValueFunction) + if (err != nil) { return err } + + return nil +} + +// This deletes a value from an entry whose value is a list of equal-length byte slices +// All byte slices are concatenated +func deleteItemFromEntryList(entryKey []byte, lengthOfEachItem int, itemToDelete []byte)error{ + + //Outputs: + // -bool: Update needed + // -bool: New value exists (false == delete entry) + // -[]byte: New value + // -error + updateValueFunction := func(existingValueExists bool, existingValue []byte)(bool, bool, []byte, error){ + + if (existingValueExists == false){ + // Nothing to delete + return false, false, nil, nil + } + + currentList, err := splitByteSliceIntoEqualParts(existingValue, lengthOfEachItem) + if (err != nil) { return false, false, nil, err } + + newValue := make([]byte, 0) + + deletedAny := false + + for _, element := range currentList{ + + areEqual := bytes.Equal(element, itemToDelete) + if (areEqual == true){ + // We are deleting this element + deletedAny = true + continue + } + + newValue = append(newValue, element...) + } + + if (deletedAny == false){ + // Nothing to delete + return false, false, nil, nil + } + + if (len(newValue) == 0){ + // New value is empty + return true, false, nil, nil + } + + return true, true, newValue, nil + } + + err := updateDatabaseEntry(entryKey, updateValueFunction) + if (err != nil) { return err } + + return nil +} + +func setDatabaseEntry(key []byte, value []byte)error{ + + if (myDatabase == nil) { + return errors.New("setDatabaseEntry called when database not started.") + } + + //TODO: Do we need to copy value? + // Copy is never needed for key because it will always be passed in as an array + + valueCopy := slices.Clone(value) + + updateValueFunction := func(txn *badger.Txn) error { + err := txn.Set(key, valueCopy) + return err + } + + databaseMutex.RLock() + defer databaseMutex.RUnlock() + + err := myDatabase.Update(updateValueFunction) + if (err != nil) { return err } + + return nil +} + +func getDatabaseValue(key []byte)(bool, []byte, error){ + + if (myDatabase == nil) { + return false, nil, errors.New("getDatabaseValue called when database is not started.") + } + + valueFound := false + var valueBytes []byte + + getEntryValueFunction := func(txn *badger.Txn)error{ + + item, err := txn.Get(key) + if (err != nil) { + if (err == badger.ErrKeyNotFound){ + return nil + } + return err + } + + valueFound = true + valueBytes, err = item.ValueCopy(nil) + if (err != nil) { return err } + + return nil + } + + databaseMutex.RLock() + defer databaseMutex.RUnlock() + + err := myDatabase.View(getEntryValueFunction) + if (err != nil){ return false, nil, err } + + if (valueFound == false){ + return false, nil, nil + } + + return true, valueBytes, nil +} + + +func deleteDatabaseEntry(key []byte)error{ + + if (myDatabase == nil) { + return errors.New("deleteDatabaseEntry called when database is not started.") + } + + deleteFunction := func(txn *badger.Txn) error { + err := txn.Delete(key) + if (err != nil) { return err } + + return nil + } + + databaseMutex.RLock() + defer databaseMutex.RUnlock() + + err := myDatabase.Update(deleteFunction) + if (err != nil) { return err } + + return nil +} + +// This function will get the database entry and replace the value, conditional on what the existing value is +// +//Inputs: +// -[]byte: The key we are editing +// -func(currentValueExists bool, currentValue []byte)(bool, bool, []byte, error) +// -Outputs: +// -bool: Change to key value is needed +// -bool: New value exists (false = delete key value) +// -[]byte: New value for key entry +// -error +// Outputs: +// -error +func updateDatabaseEntry(key []byte, updateEntryValueFunction func(bool, []byte)(bool, bool, []byte, error))error{ + + updateFunction := func(txn *badger.Txn)error{ + + //Outputs: + // -bool: Existing value exists + // -string: Existing value + // -error + getExistingValue := func()(bool, []byte, error){ + + item, err := txn.Get(key) + if (err != nil) { + if (err == badger.ErrKeyNotFound){ + return false, nil, nil + } + return false, nil, err + } + + existingValue, err := item.ValueCopy(nil) + if (err != nil) { return false, nil, err } + + return true, existingValue, nil + } + + existingValueExists, existingValue, err := getExistingValue() + if (err != nil){ return err } + + needsUpdate, newValueExists, newValue, err := updateEntryValueFunction(existingValueExists, existingValue) + if (err != nil){ return err } + if (needsUpdate == false){ + return nil + } + if (newValueExists == false){ + err := txn.Delete(key) + if (err != nil) { return err } + + return nil + } + + err = txn.Set(key, newValue) + if (err != nil) { return err } + + return nil + } + + databaseMutex.RLock() + defer databaseMutex.RUnlock() + + err := myDatabase.Update(updateFunction) + if (err != nil){ return err } + + return nil +} + + +func getNumberOfKeysInDatabaseWithPrefix(prefixToCheck byte)(int64, error){ + + if (myDatabase == nil) { + return 0, errors.New("getNumberOfKeysInDatabaseWithPrefix called when database is not started.") + } + + counter := int64(0) + + getNumberOfKeysFunction := func(txn *badger.Txn) error { + + options := badger.DefaultIteratorOptions + options.PrefetchValues = false + iterator := txn.NewIterator(options) + + for iterator.Rewind(); iterator.Valid(); iterator.Next() { + + item := iterator.Item() + entryKey := item.KeyCopy(nil) + + if (len(entryKey) == 0){ + return errors.New("Database corrupt: Contains empty entry key.") + } + + if (entryKey[0] == prefixToCheck){ + counter += 1 + } + } + + iterator.Close() + return nil + } + + databaseMutex.RLock() + defer databaseMutex.RUnlock() + + err := myDatabase.View(getNumberOfKeysFunction) + if (err != nil) { return 0, err } + + return counter, nil +} + + +func getAllKeysInDatabaseWithPrefix(prefixToCheck byte, trimPrefix bool)([][]byte, error){ + + if (myDatabase == nil) { + return nil, errors.New("getAllKeysInDatabaseWithPrefix called when database is not started.") + } + + keysList := make([][]byte, 0) + + getAllKeysFunction := func(txn *badger.Txn) error { + options := badger.DefaultIteratorOptions + options.PrefetchValues = false + iterator := txn.NewIterator(options) + + for iterator.Rewind(); iterator.Valid(); iterator.Next() { + item := iterator.Item() + + entryKey := item.KeyCopy(nil) + + if (len(entryKey) < 2){ + return errors.New("Database corrupt: Contains entry key less than 2 bytes in length.") + } + + if (entryKey[0] == prefixToCheck){ + + if (trimPrefix == false){ + keysList = append(keysList, entryKey) + + } else { + + keyWithoutPrefix := entryKey[1:] + keysList = append(keysList, keyWithoutPrefix) + } + } + } + + iterator.Close() + return nil + } + + databaseMutex.RLock() + defer databaseMutex.RUnlock() + + err := myDatabase.View(getAllKeysFunction) + if (err != nil) { return nil, err } + + return keysList, nil +} + + +func splitByteSliceIntoEqualParts(inputSlice []byte, lengthOfEachSubslice int)([][]byte, error){ + + inputLength := len(inputSlice) + + if (inputLength % lengthOfEachSubslice != 0){ + // The input slice should be a list of uniform-length byte arrays appended together + inputSliceHex := encoding.EncodeBytesToHexString(inputSlice) + return nil, errors.New("splitByteSliceIntoEqualParts called with invalid inputSlice: " + inputSliceHex) + } + + numberOfSubslices := inputLength/lengthOfEachSubslice + + subslicesList := make([][]byte, 0) + + for i := 0; i < numberOfSubslices; i++{ + + index := i * lengthOfEachSubslice + + subsliceBytes := inputSlice[index:index+lengthOfEachSubslice] + + subslicesList = append(subslicesList, subsliceBytes) + } + + return subslicesList, nil +} + + + diff --git a/internal/badgerDatabase/badgerDatabase_test.go b/internal/badgerDatabase/badgerDatabase_test.go new file mode 100644 index 0000000..158d83b --- /dev/null +++ b/internal/badgerDatabase/badgerDatabase_test.go @@ -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() +} + + + diff --git a/internal/byteRange/byteRange.go b/internal/byteRange/byteRange.go new file mode 100644 index 0000000..c0eb0fd --- /dev/null +++ b/internal/byteRange/byteRange.go @@ -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 +} + + + + diff --git a/internal/byteRange/byteRange_test.go b/internal/byteRange/byteRange_test.go new file mode 100644 index 0000000..90806ae --- /dev/null +++ b/internal/byteRange/byteRange_test.go @@ -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.") + } + + +} diff --git a/internal/contentMetadata/contentMetadata.go b/internal/contentMetadata/contentMetadata.go new file mode 100644 index 0000000..8c62d4c --- /dev/null +++ b/internal/contentMetadata/contentMetadata.go @@ -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 +} + + + diff --git a/internal/convertCurrencies/convertCurrencies.go b/internal/convertCurrencies/convertCurrencies.go new file mode 100644 index 0000000..2656d5b --- /dev/null +++ b/internal/convertCurrencies/convertCurrencies.go @@ -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 +} + + + + diff --git a/internal/convertCurrencies/convertCurrencies_test.go b/internal/convertCurrencies/convertCurrencies_test.go new file mode 100644 index 0000000..2991993 --- /dev/null +++ b/internal/convertCurrencies/convertCurrencies_test.go @@ -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) + } + } +} + + + diff --git a/internal/createCharts/createCharts.go b/internal/createCharts/createCharts.go new file mode 100644 index 0000000..2af6450 --- /dev/null +++ b/internal/createCharts/createCharts.go @@ -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 +} + + + + diff --git a/internal/cryptocurrency/cardanoAddress/cardanoAddress.go b/internal/cryptocurrency/cardanoAddress/cardanoAddress.go new file mode 100644 index 0000000..4197670 --- /dev/null +++ b/internal/cryptocurrency/cardanoAddress/cardanoAddress.go @@ -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 +} + + diff --git a/internal/cryptocurrency/cardanoAddress/cardanoAddress_test.go b/internal/cryptocurrency/cardanoAddress/cardanoAddress_test.go new file mode 100644 index 0000000..c9d7a2a --- /dev/null +++ b/internal/cryptocurrency/cardanoAddress/cardanoAddress_test.go @@ -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.") + } + } +} + + + diff --git a/internal/cryptocurrency/cardanoNode/cardanoNode.go b/internal/cryptocurrency/cardanoNode/cardanoNode.go new file mode 100644 index 0000000..18d2b27 --- /dev/null +++ b/internal/cryptocurrency/cardanoNode/cardanoNode.go @@ -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 +} + + + diff --git a/internal/cryptocurrency/ethereumAddress/ethereumAddress.go b/internal/cryptocurrency/ethereumAddress/ethereumAddress.go new file mode 100644 index 0000000..91c303e --- /dev/null +++ b/internal/cryptocurrency/ethereumAddress/ethereumAddress.go @@ -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 +} + + diff --git a/internal/cryptocurrency/ethereumAddress/ethereumAddress_test.go b/internal/cryptocurrency/ethereumAddress/ethereumAddress_test.go new file mode 100644 index 0000000..34e99ac --- /dev/null +++ b/internal/cryptocurrency/ethereumAddress/ethereumAddress_test.go @@ -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.") + } + } +} + + + diff --git a/internal/cryptocurrency/ethereumNode/ethereumNode.go b/internal/cryptocurrency/ethereumNode/ethereumNode.go new file mode 100644 index 0000000..1ff0a80 --- /dev/null +++ b/internal/cryptocurrency/ethereumNode/ethereumNode.go @@ -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 +} + + + diff --git a/internal/cryptography/blake3/blake3.go b/internal/cryptography/blake3/blake3.go new file mode 100644 index 0000000..5c2a3bc --- /dev/null +++ b/internal/cryptography/blake3/blake3.go @@ -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 +} + + + diff --git a/internal/cryptography/blake3/blake3_test.go b/internal/cryptography/blake3/blake3_test.go new file mode 100644 index 0000000..b24c67f --- /dev/null +++ b/internal/cryptography/blake3/blake3_test.go @@ -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) + } +} + diff --git a/internal/cryptography/chaPolyShrink/chaPolyShrink.go b/internal/cryptography/chaPolyShrink/chaPolyShrink.go new file mode 100644 index 0000000..77af9d0 --- /dev/null +++ b/internal/cryptography/chaPolyShrink/chaPolyShrink.go @@ -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 +} + + diff --git a/internal/cryptography/chaPolyShrink/chaPolyShrink_test.go b/internal/cryptography/chaPolyShrink/chaPolyShrink_test.go new file mode 100644 index 0000000..5329c29 --- /dev/null +++ b/internal/cryptography/chaPolyShrink/chaPolyShrink_test.go @@ -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.") + } + } + +} + + + + diff --git a/internal/cryptography/edwardsKeys/edwardsKeys.go b/internal/cryptography/edwardsKeys/edwardsKeys.go new file mode 100644 index 0000000..76455c5 --- /dev/null +++ b/internal/cryptography/edwardsKeys/edwardsKeys.go @@ -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 +} + + + diff --git a/internal/cryptography/edwardsKeys/edwardsKeys_test.go b/internal/cryptography/edwardsKeys/edwardsKeys_test.go new file mode 100644 index 0000000..46ad23e --- /dev/null +++ b/internal/cryptography/edwardsKeys/edwardsKeys_test.go @@ -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.") + } +} + + + diff --git a/internal/cryptography/kyber/kyber.go b/internal/cryptography/kyber/kyber.go new file mode 100644 index 0000000..f02eefb --- /dev/null +++ b/internal/cryptography/kyber/kyber.go @@ -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 +} + + diff --git a/internal/cryptography/kyber/kyber_test.go b/internal/cryptography/kyber/kyber_test.go new file mode 100644 index 0000000..5ba2ef4 --- /dev/null +++ b/internal/cryptography/kyber/kyber_test.go @@ -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.") + } +} + + + diff --git a/internal/cryptography/nacl/nacl.go b/internal/cryptography/nacl/nacl.go new file mode 100644 index 0000000..a22c686 --- /dev/null +++ b/internal/cryptography/nacl/nacl.go @@ -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 +} + + diff --git a/internal/cryptography/nacl/nacl_test.go b/internal/cryptography/nacl/nacl_test.go new file mode 100644 index 0000000..c3a9604 --- /dev/null +++ b/internal/cryptography/nacl/nacl_test.go @@ -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.") + } +} + + + diff --git a/internal/databaseJobs/databaseJobs.go b/internal/databaseJobs/databaseJobs.go new file mode 100644 index 0000000..5881463 --- /dev/null +++ b/internal/databaseJobs/databaseJobs.go @@ -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 +} + + diff --git a/internal/desires/mateDesires/mateDesires.go b/internal/desires/mateDesires/mateDesires.go new file mode 100644 index 0000000..6b6233f --- /dev/null +++ b/internal/desires/mateDesires/mateDesires.go @@ -0,0 +1,1119 @@ + +// mateDesires provides functions to see if a profile passes a user's desires +// This package is used for users to find their mate matches, and for hosts serving mate profiles which fulfill a provided criteria + +package mateDesires + +import "seekia/resources/worldLanguages" + +import "seekia/imported/geodist" + +import "seekia/internal/convertCurrencies" +import "seekia/internal/encoding" +import "seekia/internal/helpers" +import "seekia/internal/genetics/companyAnalysis" + +import "strings" +import "errors" +import "slices" + +// We use this function to initialize the numericalDesiresMap and choiceDesiresMap +func init(){ + + numericalDesiresMap = make(map[string]struct{}) + + for _, desireName := range numericalDesiresList{ + numericalDesiresMap[desireName] = struct{}{} + } + + choiceDesiresMap = make(map[string]struct{}) + + for _, desireName := range choiceDesiresList{ + choiceDesiresMap[desireName] = struct{}{} + } +} + +func GetAllDesiresList(copyList bool)[]string{ + + if (copyList == false){ + // List only needs to be copied if we are sorting and/or editing its elements afterwards + return allDesiresList + } + + listCopy := slices.Clone(allDesiresList) + + return listCopy +} + +var allDesiresList []string = []string{ + "ProfileLanguage", + "PrimaryLocationCountry", + "SearchTerms", + "HasMessagedMe", // Never used in criteria + "IHaveMessaged", // Never used in criteria + "HasRejectedMe", // Never used in criteria + "IsLiked", // Never used in criteria + "IsIgnored", // Never used in criteria + "IsMyContact", // Never used in criteria + "Age", + "Height", + "Wealth", + "WealthInGold", // Only used in criteria + "Distance", // Never used in criteria + "DistanceFrom", // Only used in criteria + "Sex", + "Sexuality", + "BodyFat", + "BodyMuscle", + "EyeColor", + "SkinColor", + "HairColor", + "HairTexture", + "HasHIV", + "HasGenitalHerpes", + "FruitRating", + "VegetablesRating", + "NutsRating", + "GrainsRating", + "DairyRating", + "SeafoodRating", + "BeefRating", + "PorkRating", + "PoultryRating", + "EggsRating", + "BeansRating", + "Fame", + "AlcoholFrequency", + "TobaccoFrequency", + "CannabisFrequency", + "GenderIdentity", + "23andMe_AncestryComposition", + "23andMe_AncestryComposition_Restrictive", + "23andMe_MaternalHaplogroup", + "23andMe_PaternalHaplogroup", + "23andMe_NeanderthalVariants", + "Language", + "PetsRating", + "DogsRating", + "CatsRating", + "OffspringProbabilityOfAnyMonogenicDisease", // Never used in criteria + "MonogenicDisease_Cystic_Fibrosis_ProbabilityOfPassingAVariant", // Only used in criteria + "MonogenicDisease_Sickle_Cell_Anemia_ProbabilityOfPassingAVariant", // Only used in criteria +} + +// A desire being Numerical or Choice depends on some factors +// For example, BodyFat/BodyMuscle are numerical attributes, but we treat them as choice desires +// The end user only has to select between the 4 possible options (1/4, 2/4, 3/4, 4/4) +// Some attributes are not numerical or choice, and require a more complex desire analysis + +func GetNumericalDesiresList(copyList bool)[]string{ + + if (copyList == false){ + + return numericalDesiresList + } + + listCopy := slices.Clone(numericalDesiresList) + + return listCopy +} + + +// We use these maps to make lookups faster when checking to see if a desire is numerical/choice/other +var numericalDesiresMap map[string]struct{} +var choiceDesiresMap map[string]struct{} + +var numericalDesiresList []string = []string{ + "Age", + "Height", + "Distance", + "Wealth", + "WealthInGold", + "23andMe_NeanderthalVariants", + "OffspringProbabilityOfAnyMonogenicDisease", + "MonogenicDisease_Cystic_Fibrosis_ProbabilityOfPassingAVariant", + "MonogenicDisease_Sickle_Cell_Anemia_ProbabilityOfPassingAVariant", +} + +var choiceDesiresList []string = []string{ + "ProfileLanguage", + "HasMessagedMe", + "IHaveMessaged", + "HasRejectedMe", + "IsLiked", + "IsMyContact", + "IsIgnored", + "PrimaryLocationCountry", + "Sex", + "Sexuality", + "BodyFat", + "BodyMuscle", + "HairColor", + "HairTexture", + "SkinColor", + "EyeColor", + "HasHIV", + "HasGenitalHerpes", + "GenderIdentity", + "23andMe_MaternalHaplogroup", + "23andMe_PaternalHaplogroup", + "FruitRating", + "VegetablesRating", + "NutsRating", + "GrainsRating", + "DairyRating", + "SeafoodRating", + "BeefRating", + "PorkRating", + "PoultryRating", + "EggsRating", + "BeansRating", + "Fame", + "AlcoholFrequency", + "TobaccoFrequency", + "CannabisFrequency", + "PetsRating", + "DogsRating", + "CatsRating", +} + + +// These are desires that are not used by the end user through the GUI +// They are only used in Criteria, which are the mate desires sent to hosts +func CheckIfDesireIsCriteriaOnly(desireName string)bool{ + + desireHasMonogenicDiseasePrefix := strings.HasPrefix(desireName, "MonogenicDisease_") + if (desireHasMonogenicDiseasePrefix == true){ + return true + } + if (desireName == "DistanceFrom"){ + return true + } + if (desireName == "WealthInGold"){ + return true + } + return false +} + +// This function will tell us if a desire allows the use of the RequireResponse setting +// If true, it also return an attribute that a profile must contain to check if they fulfill the desire +//Outputs: +// -bool: Desire allows the use of RequireResponse +// -string: An attribute name we can check to see if a profile has provided the information that the desire relies upon +// -There may be other attributes, but they will all be mandatory alongside the attribute we return +func CheckIfDesireAllowsRequireResponse(desireName string)(bool, string){ + + switch desireName{ + + case "HasMessagedMe", "IHaveMessaged", "HasRejectedMe", "IsLiked", "IsMyContact", "IsIgnored":{ + // These attributes are not constructed from information that is provided within the user's profile + // Thus, we do not allow ResponseRequired for them because the fulfillsDesire status will be known for all users + return false, "" + } + case "SearchTerms", "OffspringProbabilityOfAnyMonogenicDisease":{ + // We do not allow RequireResponse for these attributes + return false, "" + } + case "WealthInGold":{ + return true, "Wealth" + } + case "Distance", "DistanceFrom":{ + return true, "PrimaryLocationLatitude" + } + case "23andMe_AncestryComposition", "23andMe_AncestryComposition_Restrictive":{ + return true, "23andMe_AncestryComposition" + } + } + hasPrefix := strings.HasPrefix(desireName, "MonogenicDisease_") + if (hasPrefix == true){ + // We do not allow RequireResponse for these attributes + return false, "" + } + + return true, desireName +} + +func CheckIfDesireIsNumerical(desireName string)bool{ + + _, isNumericalDesire := numericalDesiresMap[desireName] + + return isNumericalDesire +} + +func GetDesireTitleFromDesireName(desireName string)(string, error){ + + switch desireName{ + + case "ProfileLanguage":{ + return "Profile Language", nil + } + case "PrimaryLocationCountry":{ + return "Country", nil + } + case "SearchTerms":{ + return "Search Terms", nil + } + case "HasMessagedMe":{ + return "Has Messaged Me", nil + } + case "IHaveMessaged":{ + return "I Have Messaged", nil + } + case "HasRejectedMe":{ + return "Has Rejected Me", nil + } + case "IsLiked":{ + return "Is Liked", nil + } + case "IsIgnored":{ + return "Is Ignored", nil + } + case "IsMyContact":{ + return "Is My Contact", nil + } + case "Age":{ + return "Age", nil + } + case "Height":{ + return "Height", nil + } + case "Wealth":{ + return "Wealth", nil + } + case "Distance":{ + return "Distance", nil + } + case "Sex":{ + return "Sex", nil + } + case "Sexuality":{ + return "Sexuality", nil + } + case "Fame":{ + return "Fame", nil + } + case "BodyFat":{ + return "Body Fat", nil + } + case "BodyMuscle":{ + return "Body Muscle", nil + } + case "EyeColor":{ + return "Eye Color", nil + } + case "SkinColor":{ + return "Skin Color", nil + } + case "HairColor":{ + return "Hair Color", nil + } + case "HairTexture":{ + return "Hair Texture", nil + } + case "HasHIV":{ + return "Has HIV", nil + } + case "HasGenitalHerpes":{ + return "Has Genital Herpes", nil + } + case "FruitRating":{ + return "Fruit Rating", nil + } + case "VegetablesRating":{ + return "Vegetables Rating", nil + } + case "NutsRating":{ + return "Nuts Rating", nil + } + case "GrainsRating":{ + return "Grains Rating", nil + } + case "DairyRating":{ + return "Dairy Rating", nil + } + case "SeafoodRating":{ + return "Seafood Rating", nil + } + case "BeefRating":{ + return "Beef Rating", nil + } + case "PorkRating":{ + return "Pork Rating", nil + } + case "PoultryRating":{ + return "Poultry Rating", nil + } + case "EggsRating":{ + return "Eggs Rating", nil + } + case "BeansRating":{ + return "Beans Rating", nil + } + case "AlcoholFrequency":{ + return "Alcohol Frequency", nil + } + case "TobaccoFrequency":{ + return "Tobacco Frequency", nil + } + case "CannabisFrequency":{ + return "Cannabis Frequency", nil + } + case "GenderIdentity":{ + return "Gender Identity", nil + } + case "23andMe_AncestryComposition":{ + return "23andMe Ancestry Composition", nil + } + case "23andMe_MaternalHaplogroup":{ + return "23andMe Maternal Haplogroup", nil + } + case "23andMe_PaternalHaplogroup":{ + return "23andMe Paternal Haplogroup", nil + } + case "23andMe_NeanderthalVariants":{ + return "23andMe Neanderthal Variants", nil + } + case "OffspringProbabilityOfAnyMonogenicDisease":{ + return "Offspring Probability Of Any Monogenic Disease", nil + } + case "Language":{ + return "Language", nil + } + case "PetsRating":{ + return "Pets Rating", nil + } + case "DogsRating":{ + return "Dogs Rating", nil + } + case "CatsRating":{ + return "Cats Rating", nil + } + } + + return "", errors.New("GetDesireTitleFromDesireName called with invalid desireName: " + desireName) +} + +//Inputs: +// -string: Desire name +// -string: Desire type +// -func(attributeName string)(bool, error): The function to retrieve any attribute from the profile +//Outputs: +// -bool: Any desire exists +// -bool: Desire is valid +// -Will be false if requestor is malicious. Example: non-numerical desired value for numerical desire +// -We will not always verify that the desire is valid. +// -We just need to be able to know when the desire is invalid, rather than throwing an error +// -bool: User response exists (if response is possible) +// -bool: Profile fulfills desire +// -error +func CheckIfMateProfileFulfillsDesire(desireName string, getAnyDesireFunction func(string)(bool, string, error), getAnyProfileAttributeFunction func(string)(bool, int, string, error))(bool, bool, bool, bool, error){ + + isNumericDesire := CheckIfDesireIsNumerical(desireName) + if (isNumericDesire == true){ + + desireMinimumExists, minimumString, err := getAnyDesireFunction(desireName + "_Minimum") + if (err != nil) { return false, false, false, false, err } + + desireMaximumExists, maximumString, err := getAnyDesireFunction(desireName + "_Maximum") + if (err != nil) { return false, false, false, false, err } + + if (desireMinimumExists == false && desireMaximumExists == false){ + return false, false, false, false, nil + } + + //Outputs: + // -bool: Value exists + // -float64: Value + // -error + getAttributeValueToCompare := func()(bool, float64, error){ + + if (desireName == "Wealth"){ + + profileWealthExists, _, profileWealthInGold, err := getAnyProfileAttributeFunction("WealthInGold") + if (err != nil) { return false, 0, err } + if (profileWealthExists == false){ + return false, 0, nil + } + + profileWealthInGoldFloat64, err := helpers.ConvertStringToFloat64(profileWealthInGold) + if (err != nil) { return false, 0, err } + + // We must convert their wealth to our desired currency for the comparison + + getMyWealthCurrencyCode := func()(string, error){ + + currencyExists, currencyCode, err := getAnyDesireFunction("WealthCurrency") + if (err != nil) { return "", err } + if (currencyExists == false){ + return "USD", nil + } + return currencyCode, nil + } + + myCurrencyCode, err := getMyWealthCurrencyCode() + if (err != nil) { return false, 0, err } + + profileNetworkTypeExists, _, profileNetworkTypeString, err := getAnyProfileAttributeFunction("NetworkType") + if (err != nil) { return false, 0, err } + if (profileNetworkTypeExists == false){ + return false, 0, errors.New("CheckIfMateProfileFulfillsDesire called with profile missing NetworkType.") + } + + profileNetworkType, err := helpers.ConvertNetworkTypeStringToByte(profileNetworkTypeString) + if (err != nil) { + return false, 0, errors.New("CheckIfMateProfileFulfillsDesire called with profile containing invalid NetworkType: " + profileNetworkTypeString) + } + + _, convertedProfileWealth, err := convertCurrencies.ConvertKilogramsOfGoldToAnyCurrency(profileNetworkType, profileWealthInGoldFloat64, myCurrencyCode) + if (err != nil) { return false, 0, err } + + return true, convertedProfileWealth, nil + } + + exists, _, profileAttributeValue, err := getAnyProfileAttributeFunction(desireName) + if (err != nil) { return false, 0, err } + if (exists == false){ + return false, 0, nil + } + + profileAttributeValueFloat64, err := helpers.ConvertStringToFloat64(profileAttributeValue) + if (err != nil) { return false, 0, err } + + return true, profileAttributeValueFloat64, nil + } + + attributeValueKnown, attributeValueToCompare, err := getAttributeValueToCompare() + if (err != nil) { return false, false, false, false, err } + if (attributeValueKnown == false){ + + return true, true, false, false, nil + } + + if (desireMinimumExists == true){ + minimumFloat64, err := helpers.ConvertStringToFloat64(minimumString) + if (err != nil) { + // Desire is invalid + return true, false, false, false, nil + } + + if (attributeValueToCompare < minimumFloat64){ + return true, true, true, false, nil + } + } + + if (desireMaximumExists == true){ + maximumFloat64, err := helpers.ConvertStringToFloat64(maximumString) + if (err != nil) { + // Desire is invalid + return true, false, false, false, nil + } + + if (attributeValueToCompare > maximumFloat64){ + return true, true, true, false, nil + } + } + + return true, true, true, true, nil + } + + _, isChoiceDesire := choiceDesiresMap[desireName] + if (isChoiceDesire == true){ + + myDesireExists, myDesiredChoicesListString, err := getAnyDesireFunction(desireName) + if (err != nil) { return false, false, false, false, err } + if (myDesireExists == false){ + // Users must select at least one option for choice filtering to take effect. + // Otherwise, users will get confused. + // The user must specify they want non-canonical attributes using the "Other" choice + return false, false, false, false, nil + } + myDesiredChoicesList := strings.Split(myDesiredChoicesListString, "+") + + exists, _, profileAttributeValue, err := getAnyProfileAttributeFunction(desireName) + if (err != nil) { return false, false, false, false, err } + if (exists == false){ + return true, true, false, false, nil + } + + // This is the attribute of the profile + // We are checking to see if this attribute fulfills our desires + profileAttributeValueBase64 := encoding.EncodeBytesToBase64String([]byte(profileAttributeValue)) + + // The profile attribute must be one of the items in the choices list + // Each choice is encoded in base64 + + fulfillsDesire := slices.Contains(myDesiredChoicesList, profileAttributeValueBase64) + if (fulfillsDesire == true){ + return true, true, true, true, nil + } + + containsOther := slices.Contains(myDesiredChoicesList, "Other") + if (containsOther == false){ + return true, true, true, false, nil + } + + // The user has selected Other + // Some desires allow custom values as well as having some canonical attributes + // We must allow a special choice called "Other" + // If the "Other" choice is chosen, we will allow attributes except for the canonical attributes + // We already checked for all explicit attributes, so all we must do is make sure the user's profile attribute is not canonical + // If it is not, we know the user fulfills the desire + + // Here is an example: + // Desire Options: Man, Woman, Other + // User selects Other, does not select Man or Woman + // This means the user wants to allow all responses except for "Man" and "Woman" + + getDesireCanonicalAttributesList := func()([]string, error){ + + switch desireName{ + + case "GenderIdentity":{ + + canonicalAttributesList := []string{"Man", "Woman"} + return canonicalAttributesList, nil + } + + case "23andMe_MaternalHaplogroup":{ + + knownMaternalHaplogroupsList := companyAnalysis.GetKnownMaternalHaplogroupsList_23andMe() + + return knownMaternalHaplogroupsList, nil + } + case "23andMe_PaternalHaplogroup":{ + + knownPaternalHaplogroupsList := companyAnalysis.GetKnownPaternalHaplogroupsList_23andMe() + + return knownPaternalHaplogroupsList, nil + } + case "Language":{ + + worldLanguagesList, err := worldLanguages.GetWorldLanguagePrimaryNamesList() + if (err != nil) { return nil, err } + + return worldLanguagesList, nil + } + } + + return nil, errors.New("My Desired Choices List contains Other choice for invalid desire: " + desireName) + } + + desireCanonicalAttributesList, err := getDesireCanonicalAttributesList() + if (err != nil) { return false, false, false, false, err } + + profileAttributeIsCanonical := slices.Contains(desireCanonicalAttributesList, profileAttributeValue) + if (profileAttributeIsCanonical == true){ + // Profile contains a canonical attribute that the user did not select + return true, true, true, false, nil + } + + // User selected Other, and the profile has a custom value + return true, true, true, true, nil + } + + switch desireName{ + + case "23andMe_AncestryComposition", "23andMe_AncestryComposition_Restrictive":{ + + exists, ancestryCompositionDesire, err := getAnyDesireFunction(desireName) + if (exists == false){ + // The user has not selected any ancestry composition desires + // We treat this as a desire being disabled + return false, false, false, false, nil + } + + exists, _, userAncestryComposition, err := getAnyProfileAttributeFunction("23andMe_AncestryComposition") + if (err != nil) { return false, false, false, false, err } + if (exists == false){ + return true, true, false, false, nil + } + + // These are the user's desired bounds + continentMinimumBoundsMap, continentMaximumBoundsMap, regionMinimumBoundsMap, regionMaximumBoundsMap, subregionMinimumBoundsMap, subregionMaximumBoundsMap, err := ReadAncestryCompositionDesire_23andMe(ancestryCompositionDesire) + if (err != nil){ + //Desire is invalid + return true, false, false, false, nil + } + + userAttributeIsValid, userContinentPercentagesMap, userRegionPercentagesMap, userSubregionPercentagesMap, err := companyAnalysis.ReadAncestryCompositionAttribute_23andMe(true, userAncestryComposition) + if (err != nil){ return false, false, false, false, err } + if (userAttributeIsValid == false){ + return false, false, false, false, errors.New("CheckIfMateProfileFulfillsDesire called with profile containing invalid 23andMe_AncestryComposition attribute.") + } + + desireIsRestrictive := strings.HasSuffix(desireName, "_Restrictive") + + // Now we see if profile fulfills the desire + + type locationTypeStruct struct{ + + // These are the ancestry location percentages of the user for all locations of this locationType + locationTypePercentagesMap map[string]float64 + + // These are the minimum bounds which we desire for all locations of this locationType + locationTypeMinimumBoundsMap map[string]float64 + + // These are the maximum bounds which we desire for all location of this locationType + locationTypeMaximumBoundsMap map[string]float64 + } + + continentsObject := locationTypeStruct{ + locationTypePercentagesMap: userContinentPercentagesMap, + locationTypeMinimumBoundsMap: continentMinimumBoundsMap, + locationTypeMaximumBoundsMap: continentMaximumBoundsMap, + } + + regionsObject := locationTypeStruct{ + locationTypePercentagesMap: userRegionPercentagesMap, + locationTypeMinimumBoundsMap: regionMinimumBoundsMap, + locationTypeMaximumBoundsMap: regionMaximumBoundsMap, + } + + subregionsObject := locationTypeStruct{ + locationTypePercentagesMap: userSubregionPercentagesMap, + locationTypeMinimumBoundsMap: subregionMinimumBoundsMap, + locationTypeMaximumBoundsMap: subregionMaximumBoundsMap, + } + + locationTypeObjectsList := []locationTypeStruct{continentsObject, regionsObject, subregionsObject} + + for _, locationTypeObject := range locationTypeObjectsList{ + + userLocationTypePercentagesMap := locationTypeObject.locationTypePercentagesMap + desiredMinimumBoundsMap := locationTypeObject.locationTypeMinimumBoundsMap + desiredMaximumBoundsMap := locationTypeObject.locationTypeMaximumBoundsMap + + for locationName, locationPercentage := range userLocationTypePercentagesMap{ + + desiredMinimumBound, exists := desiredMinimumBoundsMap[locationName] + if (exists == false){ + if (desireIsRestrictive == true){ + // Profile has a location that we have not explicitly allowed + // User does not fulfill our desire + return true, true, true, false, nil + } + // We do not desire this location + // We skip it + continue + } + desiredMaximumBound, exists := desiredMaximumBoundsMap[locationName] + if (exists == false){ + return false, false, false, false, errors.New("ReadAncestryCompositionDesire_23andMe returning maximumBoundsMap missing location from minimumBounds map") + } + if (locationPercentage >= desiredMinimumBound && locationPercentage <= desiredMaximumBound){ + // Profile fulfills this location + // If we are not in restrictive mode, then the user fulfills the desires + // If we are in restrictive mode, then we must make sure the rest of the user's locations are desired + if (desireIsRestrictive == false){ + return true, true, true, true, nil + } + } + } + } + + // We have analyzed all locations + // If we are in restrictive mode, this means that all of the user's profile locations were desired + // If we are not in restrictive mode, it means that they did not fulfill any of our desired locations + if (desireIsRestrictive == true){ + return true, true, true, true, nil + } + return true, true, true, false, nil + } + case "DistanceFrom":{ + + desireExists, distanceFromDesire, err := getAnyDesireFunction("DistanceFrom") + if (desireExists == false){ + return false, false, false, false, nil + } + + desiredDistanceRange, desiredCoordinates, delimiterFound := strings.Cut(distanceFromDesire, "@") + if (delimiterFound == false){ + // Desire value is malformed + return true, false, false, false, nil + } + + desiredDistanceMinimum, desiredDistanceMaximum, delimiterFound := strings.Cut(desiredDistanceRange, "$") + if (delimiterFound == false){ + // Desire value is malformed + return true, false, false, false, nil + } + + desiredDistanceMinimumFloat64, err := helpers.ConvertStringToFloat64(desiredDistanceMinimum) + if (err != nil){ + // Desire value is malformed + return true, false, false, false, nil + } + + if (desiredDistanceMinimumFloat64 < 0){ + // Desire value is malformed + return true, false, false, false, nil + } + + desiredDistanceMaximumFloat64, err := helpers.ConvertStringToFloat64(desiredDistanceMaximum) + if (err != nil){ + // Desire value is malformed + return true, false, false, false, nil + } + + if (desiredDistanceMaximumFloat64 < desiredDistanceMinimumFloat64){ + // Desire value is malformed + return true, false, false, false, nil + } + + desiredLatitude, desiredLongitude, delimiterFound := strings.Cut(desiredCoordinates, "+") + if (delimiterFound == false){ + // Desire value is malformed + return true, false, false, false, nil + } + desiredLocationLatitudeFloat64, err := helpers.ConvertStringToFloat64(desiredLatitude) + if (err != nil){ + // Desire value is malformed + return true, false, false, false, nil + } + desiredLocationLongitudeFloat64, err := helpers.ConvertStringToFloat64(desiredLongitude) + if (err != nil){ + // Desire value is malformed + return true, false, false, false, nil + } + + isValid := helpers.VerifyLatitude(desiredLocationLatitudeFloat64) + if (isValid == false){ + // Desire value is malformed + return true, false, false, false, nil + } + + isValid = helpers.VerifyLongitude(desiredLocationLongitudeFloat64) + if (isValid == false){ + // Desire value is malformed + return true, false, false, false, nil + } + + exists, _, userLocationLatitude, err := getAnyProfileAttributeFunction("PrimaryLocationLatitude") + if (err != nil) { return false, false, false, false, err } + if (exists == false){ + // User has no location provided, desire status is unknown + return true, true, false, false, nil + } + exists, _, userLocationLongitude, err := getAnyProfileAttributeFunction("PrimaryLocationLongitude") + if (err != nil) { return false, false, false, false, err } + if (exists == false){ + return false, true, false, false, errors.New("CheckIfMateProfileFulfillsDesire called with profile containing PrimaryLocationLatitude, but missing PrimaryLocationLongitude") + } + + userLocationLatitudeFloat64, err := helpers.ConvertStringToFloat64(userLocationLatitude) + if (err != nil){ + return false, false, false, false, errors.New("CheckIfMateProfileFulfillsDesire called with profile containing invalid PrimaryLocationLatitude: " + userLocationLatitude) + } + userLocationLongitudeFloat64, err := helpers.ConvertStringToFloat64(userLocationLongitude) + if (err != nil){ + return false, false, false, false, errors.New("CheckIfMateProfileFulfillsDesire called with profile containing invalid PrimaryLocationLongitude: " + userLocationLongitude) + } + + distanceInKilometers, err := geodist.GetDistanceBetweenCoordinates(desiredLocationLatitudeFloat64, desiredLocationLongitudeFloat64, userLocationLatitudeFloat64, userLocationLongitudeFloat64) + if (err != nil){ return false, false, false, false, err } + + if (distanceInKilometers < desiredDistanceMinimumFloat64 || distanceInKilometers > desiredDistanceMaximumFloat64){ + return true, true, true, false, nil + } + + return true, true, true, true, nil + } + case "Language":{ + + myDesireExists, myDesiredChoicesListString, err := getAnyDesireFunction("Language") + if (err != nil) { return false, false, false, false, err } + if (myDesireExists == false){ + return false, false, false, false, nil + } + + myDesiredChoicesList := strings.Split(myDesiredChoicesListString, "+") + + exists, _, profileAttributeValue, err := getAnyProfileAttributeFunction("Language") + if (err != nil) { return false, false, false, false, err } + if (exists == false){ + return true, true, false, false, nil + } + + //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 + + profileLanguageItemsList := strings.Split(profileAttributeValue, "+&") + + profileLanguagesList := make([]string, 0) + + for _, languageItem := range profileLanguageItemsList{ + + languageName, _, delimiterFound := strings.Cut(languageItem, "$") + if (delimiterFound == false){ + return false, false, false, false, errors.New("CheckIfMateProfileFulfillsDesire called with profile containing invalid Language attribute: Invalid item: " + languageItem) + } + + languageNameBase64 := encoding.EncodeBytesToBase64String([]byte(languageName)) + + languageIsDesired := slices.Contains(myDesiredChoicesList, languageNameBase64) + if (languageIsDesired == true){ + // This user has a language which we desire + return true, true, true, true, nil + } + + profileLanguagesList = append(profileLanguagesList, languageName) + } + + otherIsChosen := slices.Contains(myDesiredChoicesList, "Other") + if (otherIsChosen == false){ + return true, true, true, false, nil + } + + // If the user has any non-canonical languages, we will consider this user as having fulfilled our desires + canonicalLanguagesMap, err := worldLanguages.GetWorldLanguageObjectsMap() + if (err != nil) { return false, false, false, false, err } + + for _, languageName := range profileLanguagesList{ + + _, exists := canonicalLanguagesMap[languageName] + if (exists == false){ + // This user has a non-canonical language, thus fulfilling our desire of "Other" + return true, true, true, true, nil + } + } + return true, true, true, false, nil + } + case "SearchTerms":{ + + myDesireExists, myDesiredChoicesListString, err := getAnyDesireFunction("SearchTerms") + if (err != nil) { return false, false, false, false, err } + if (myDesireExists == false){ + return false, false, false, false, nil + } + + myDesiredChoicesList := strings.Split(myDesiredChoicesListString, "+") + + myDesiredSearchTermsList := make([]string, 0, len(myDesiredChoicesList)) + + for _, searchTermBase64 := range myDesiredChoicesList{ + + searchTerm, err := encoding.DecodeBase64StringToUnicodeString(searchTermBase64) + if (err != nil){ + return false, false, false, false, errors.New("My search term desires contains invalid term: " + searchTermBase64) + } + + myDesiredSearchTermsList = append(myDesiredSearchTermsList, searchTerm) + } + + profileAttributesToCheckList := []string{"Description", "Tags", "Hobbies", "Beliefs"} + + for _, attributeName := range profileAttributesToCheckList{ + + attributeExists, _, attributeValue, err := getAnyProfileAttributeFunction(attributeName) + if (err != nil) { return false, false, false, false, err } + if (attributeExists == false){ + continue + } + for _, searchTerm := range myDesiredSearchTermsList{ + containsTerm := strings.Contains(attributeValue, searchTerm) + if (containsTerm == true){ + // This user fulfills our search term desires. + return true, true, true, true, nil + } + } + } + // None of our search terms were found in this user's profile + // They do not fulfill our search term desires. + return true, true, true, false, nil + } + } + + return false, false, false, false, errors.New("CheckIfMateProfileFulfillsDesire called with unknown desire: " + desireName) +} + +func CreateAncestryCompositionDesire_23andMe( + continentMinimumBoundsMap map[string]float64, + continentMaximumBoundsMap map[string]float64, + regionMinimumBoundsMap map[string]float64, + regionMaximumBoundsMap map[string]float64, + subregionMinimumBoundsMap map[string]float64, + subregionMaximumBoundsMap map[string]float64)(string, error){ + + if (len(continentMinimumBoundsMap) != len(continentMaximumBoundsMap)){ + return "", errors.New("CreateAncestryCompositionDesire_23andMe called with continent bounds maps of differing length") + } + if (len(regionMinimumBoundsMap) != len(regionMaximumBoundsMap)){ + return "", errors.New("CreateAncestryCompositionDesire_23andMe called with region bounds maps of differing length") + } + if (len(subregionMinimumBoundsMap) != len(subregionMaximumBoundsMap)){ + return "", errors.New("CreateAncestryCompositionDesire_23andMe called with subregion bounds maps of differing length") + } + + // We will round down all percentages to 1 decimal, because that is the highest precision that 23andMe offers + // Seekia will round to 1 decimal whenever it displays the desire, so the user would not be able to see why they did not add up to 100. + + getLocationSection := func(locationMinimumBoundsMap map[string]float64, locationMaximumBoundsMap map[string]float64)(string, error){ + + if (len(locationMinimumBoundsMap) == 0){ + return "None", nil + } + + locationItemsList := make([]string, 0, len(locationMinimumBoundsMap)) + + for locationName, locationMinimumBound := range locationMinimumBoundsMap{ + + locationMaximumBound, exists := locationMaximumBoundsMap[locationName] + if (exists == false){ + return "", errors.New("locationMinimumBoundsMap contains location, but locationMaximumBoundsMap does not.") + } + + minimumBoundString := helpers.ConvertFloat64ToStringRounded(locationMinimumBound, 1) + maximumBoundString := helpers.ConvertFloat64ToStringRounded(locationMaximumBound, 1) + + locationItem := locationName + "$" + minimumBoundString + "@" + maximumBoundString + + locationItemsList = append(locationItemsList, locationItem) + } + + locationSection := strings.Join(locationItemsList, "#") + + return locationSection, nil + } + + continentsSection, err := getLocationSection(continentMinimumBoundsMap, continentMaximumBoundsMap) + if (err != nil) { return "", err } + regionsSection, err := getLocationSection(regionMinimumBoundsMap, regionMaximumBoundsMap) + if (err != nil) { return "", err } + subregionsSection, err := getLocationSection(subregionMinimumBoundsMap, subregionMaximumBoundsMap) + if (err != nil) { return "", err } + + compositionDesire := continentsSection + "+" + regionsSection + "+" + subregionsSection + + _, _, _, _, _, _, err = ReadAncestryCompositionDesire_23andMe(compositionDesire) + if (err != nil){ + return "", errors.New("CreateAncestryCompositionDesire_23andMe failed: Result is invalid: " + err.Error()) + } + + return compositionDesire, nil +} + +// These maps will only contain bounds for locations with no sublocations +// The maps will not contain the parent locations for each location +//Outputs: +// -map[string]float64: Continent Minimum bounds map +// -map[string]float64: Continent Maximum bounds map +// -map[string]float64: Region Minimum bounds map +// -map[string]float64: Region maximum bounds map +// -map[string]float64: Subregion minimum bounds map +// -map[string]float64: Subregion maximum bounds map +// -error (will return err if desire is invalid) +func ReadAncestryCompositionDesire_23andMe(ancestryCompositionDesire string)(map[string]float64, map[string]float64, map[string]float64, map[string]float64, map[string]float64, map[string]float64, error){ + + // An ancestry composition desire value is formatted as follows: + + // Continent section + "+" + Region Section + "+" + Sub-Region section + + // Continents section is made up of continent items or "None" + // Region section is made up of region items or "None" + // Subregion section is made up of subregion items or "None" + // Each section's items are separated by "#" + + // Continent Item: Continent name + "$" + Minimum percentage of whole + "@" + Maximum Percentage of whole + // Region Item: Region name + "$" + Minimum percentage of whole + "@" + Maximum Percentage of whole + // Subregion Item: Subregion name + "$" + Minimum percentage of whole + "@" + Maximum Percentage of whole + + // Each continent/region/subregion must be locations that have no sublocations + // For example, "Unassigned" has no regions/subregions, so it is a continent that can be included + + // Any Continents/Regions/Subregions that do not exist are considered undesired + // 0%-0% ranges are not permitted. All ranges must represent a non-zero desired proportion + + locationSectionsList := strings.Split(ancestryCompositionDesire, "+") + + if (len(locationSectionsList) != 3){ + return nil, nil, nil, nil, nil, nil, errors.New("Invalid 23andMe ancestry composition desire: " + ancestryCompositionDesire) + } + + continentMinimumBoundsMap := make(map[string]float64) + continentMaximumBoundsMap := make(map[string]float64) + regionMinimumBoundsMap := make(map[string]float64) + regionMaximumBoundsMap := make(map[string]float64) + subregionMinimumBoundsMap := make(map[string]float64) + subregionMaximumBoundsMap := make(map[string]float64) + + continentsSection := locationSectionsList[0] + regionsSection := locationSectionsList[1] + subregionsSection := locationSectionsList[2] + + if (continentsSection == "None" && regionsSection == "None" && subregionsSection == "None"){ + return nil, nil, nil, nil, nil, nil, errors.New("ReadAncestryCompositionDesire_23andMe called with desire containing all-None sections.") + } + + addLocationSectionToMaps := func(locationSection string, minimumBoundsMap map[string]float64, maximumBoundsMap map[string]float64)error{ + + if (locationSection == "None"){ + return nil + } + + locationItemsList := strings.Split(locationSection, "#") + + for _, locationItem := range locationItemsList{ + + locationName, desiredRange, delimiterFound := strings.Cut(locationItem, "$") + if (delimiterFound == false){ + return errors.New("Invalid 23andMe ancestry composition desire Contains invalid continent item: " + locationName) + } + + desiredRangeMinimum, desiredRangeMaximum, delimiterFound := strings.Cut(desiredRange, "@") + if (delimiterFound == false){ + return errors.New("Invalid 23andMe ancestry composition desire: contains invalid desired range: " + desiredRange) + } + + desiredRangeMinimumFloat64, err := helpers.ConvertStringToFloat64(desiredRangeMinimum) + if (err != nil){ + return errors.New("Invalid 23andMe ancestry composition desire: contains invalid minimum range bound: " + desiredRangeMinimum) + } + + desiredRangeMaximumFloat64, err := helpers.ConvertStringToFloat64(desiredRangeMaximum) + if (err != nil){ + return errors.New("Invalid 23andMe ancestry composition desire: contains invalid maximum range bound: " + desiredRangeMaximum) + } + + _, exists := minimumBoundsMap[locationName] + if (exists == true){ + return errors.New("Invalid 23andMe ancestry composition desire: contains duplicate location name: " + locationName) + } + + _, exists = maximumBoundsMap[locationName] + if (exists == true){ + return errors.New("Invalid 23andMe ancestry composition desire: contains duplicate location name: " + locationName) + } + + if (desiredRangeMinimumFloat64 < 0 || desiredRangeMinimumFloat64 > 100){ + return errors.New("Invalid 23andMe ancestry composition desire: minimum bound out of range: " + desiredRangeMinimum) + } + + if (desiredRangeMaximumFloat64 < 0 || desiredRangeMaximumFloat64 > 100){ + return errors.New("Invalid 23andMe ancestry composition desire: maximum bound out of range: " + desiredRangeMaximum) + } + + if (desiredRangeMinimumFloat64 > desiredRangeMaximumFloat64){ + return errors.New("Invalid 23andMe ancestry composition desire: minimum is greater than maximum.") + } + + minimumBoundsMap[locationName] = desiredRangeMinimumFloat64 + maximumBoundsMap[locationName] = desiredRangeMaximumFloat64 + } + + return nil + } + + err := addLocationSectionToMaps(continentsSection, continentMinimumBoundsMap, continentMaximumBoundsMap) + if (err != nil) { return nil, nil, nil, nil, nil, nil, err } + + err = addLocationSectionToMaps(regionsSection, regionMinimumBoundsMap, regionMaximumBoundsMap) + if (err != nil) { return nil, nil, nil, nil, nil, nil, err } + + err = addLocationSectionToMaps(subregionsSection, subregionMinimumBoundsMap, subregionMaximumBoundsMap) + if (err != nil) { return nil, nil, nil, nil, nil, nil, err } + + return continentMinimumBoundsMap, continentMaximumBoundsMap, regionMinimumBoundsMap, regionMaximumBoundsMap, subregionMinimumBoundsMap, subregionMaximumBoundsMap, nil +} + + + diff --git a/internal/desires/myDesireStatistics/myDesireStatistics.go b/internal/desires/myDesireStatistics/myDesireStatistics.go new file mode 100644 index 0000000..4d8cbdf --- /dev/null +++ b/internal/desires/myDesireStatistics/myDesireStatistics.go @@ -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 +} + + + + diff --git a/internal/desires/myLocalDesires/myLocalDesires.go b/internal/desires/myLocalDesires/myLocalDesires.go new file mode 100644 index 0000000..f3e9fc4 --- /dev/null +++ b/internal/desires/myLocalDesires/myLocalDesires.go @@ -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 +} + + + diff --git a/internal/desires/myMateDesires/myMateDesires.go b/internal/desires/myMateDesires/myMateDesires.go new file mode 100644 index 0000000..6ca45be --- /dev/null +++ b/internal/desires/myMateDesires/myMateDesires.go @@ -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 +} + + + diff --git a/internal/encoding/encoding.go b/internal/encoding/encoding.go new file mode 100644 index 0000000..5b13bdb --- /dev/null +++ b/internal/encoding/encoding.go @@ -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 +} + + + diff --git a/internal/encoding/encoding_test.go b/internal/encoding/encoding_test.go new file mode 100644 index 0000000..99fe1dd --- /dev/null +++ b/internal/encoding/encoding_test.go @@ -0,0 +1,71 @@ +package encoding_test + +import "seekia/internal/encoding" + +import "testing" + +func TestEncoding(t *testing.T){ + + isBase32, _ := encoding.VerifyStringContainsOnlyBase32Charset("abcd") + if (isBase32 == false){ + t.Fatalf("VerifyStringContainsOnlyBase32Charset failed test 1.") + } + + isBase32, invalidCharacter := encoding.VerifyStringContainsOnlyBase32Charset("1234") + if (isBase32 == true){ + t.Fatalf("VerifyStringContainsOnlyBase32Charset failed test 2.") + } + if (invalidCharacter != "1"){ + t.Fatalf("VerifyStringContainsOnlyBase32Charset failed test 3.") + } + + isBase32, _ = encoding.VerifyStringContainsOnlyBase32Charset("abcdefghijklmnopqrstuvwxyz234567") + if (isBase32 == false){ + t.Fatalf("VerifyStringContainsOnlyBase32Charset failed test 4.") + } + + isBase32, invalidCharacter = encoding.VerifyStringContainsOnlyBase32Charset("abcdefghijklmnopqrstuvwxyz1234567890") + if (isBase32 == true){ + t.Fatalf("VerifyStringContainsOnlyBase32Charset failed test 5.") + } + if (invalidCharacter != "1"){ + t.Fatalf("VerifyStringContainsOnlyBase32Charset failed test 6.") + } + + testString1 := "abcdefghijklmnopqrstuvwxyz234567\r" + + _, err := encoding.DecodeBase32StringToBytes(testString1) + if (err == nil){ + t.Fatalf(`DecodeBase32StringToBytes allowing \r.`) + } + + testString2 := "abcdefghijklmnopqrstuvwxyz234567\n" + + _, err = encoding.DecodeBase32StringToBytes(testString2) + if (err == nil){ + t.Fatalf(`DecodeBase32StringToBytes allowing \n.`) + } + + testString3 := "abcdefghijklmnopqrstuvwxyz234567" + + isValid, badCharacter := encoding.VerifyStringContainsOnlyBase32Charset(testString3) + if (isValid == false){ + t.Fatalf("VerifyStringContainsOnlyBase32Charset claims testString3 is invalid: " + badCharacter) + } + + resultBytes, err := encoding.DecodeBase32StringToBytes(testString3) + if (err != nil){ + t.Fatalf("DecodeBase32StringToBytes failed to read testString3: " + err.Error()) + } + + hexEncoded := encoding.EncodeBytesToHexString(resultBytes) + if (hexEncoded != "00443214c74254b635cf84653a56d7c675be77df"){ + t.Fatalf("EncodeBytesToHexString result does not match expected result: " + hexEncoded) + } + + base64Encoded := encoding.EncodeBytesToBase64String(resultBytes) + if (base64Encoded != "AEQyFMdCVLY1z4RlOlbXxnW-d98="){ + t.Fatalf("EncodeBytesToBase64String result does not match expected result: " + base64Encoded) + } +} + diff --git a/internal/generate/generate.go b/internal/generate/generate.go new file mode 100644 index 0000000..199a48c --- /dev/null +++ b/internal/generate/generate.go @@ -0,0 +1,1972 @@ + +// generate provides functions to generate fake content +// Examples of fake content include random text, messages, profiles, reviews and reports +// This package is used for testing and debugging purposes +// Add fake content to the database using /utilities/generateContent/generateContent.go + +package generate + +import "seekia/resources/wordLists" +import "seekia/resources/imageFiles" +import "seekia/resources/currencies" + +import "seekia/resources/geneticReferences/traits" +import "seekia/resources/geneticReferences/monogenicDiseases" +import "seekia/resources/geneticReferences/polygenicDiseases" + +import "seekia/internal/contentMetadata" +import "seekia/internal/cryptography/blake3" +import "seekia/internal/cryptography/edwardsKeys" +import "seekia/internal/cryptography/kyber" +import "seekia/internal/cryptography/nacl" +import "seekia/internal/encoding" +import "seekia/internal/genetics/companyAnalysis" +import "seekia/internal/helpers" +import "seekia/internal/identity" +import "seekia/internal/imagery" +import "seekia/internal/messaging/chatMessageStorage" +import "seekia/internal/messaging/createMessages" +import "seekia/internal/messaging/inbox" +import "seekia/internal/messaging/myChatKeys" +import "seekia/internal/messaging/readMessages" +import "seekia/internal/moderation/createReports" +import "seekia/internal/moderation/createReviews" +import "seekia/internal/moderation/reportStorage" +import "seekia/internal/moderation/reviewStorage" +import "seekia/internal/myIdentity" +import "seekia/internal/parameters/createParameters" +import "seekia/internal/profiles/createProfiles" +import "seekia/internal/profiles/profileStorage" + +import "errors" +import "strings" +import "time" +import "unicode" + + +func GetFakeParameters(parametersType string, networkType byte)([]byte, error){ + + isValid := helpers.VerifyNetworkType(networkType) + if (isValid == false){ + networkTypeString := helpers.ConvertByteToString(networkType) + return nil, errors.New("GetFakeParameters called with invalid networkType: " + networkTypeString) + } + + networkTypeString := helpers.ConvertByteToString(networkType) + + parametersMap := make(map[string]string) + + //TODO: Build the parameters + + parametersMap["ParametersType"] = parametersType + parametersMap["NetworkType"] = networkTypeString + + newIdentityPublicKey, newIdentityPrivateKey, err := identity.GetNewRandomPublicPrivateIdentityKeys() + if (err != nil) { return nil, err } + + adminPublicKeysList := [][32]byte{newIdentityPublicKey} + + parametersContentBytes, err := createParameters.CreateParametersContentBytes(adminPublicKeysList, parametersMap) + if (err != nil) { return nil, err } + + contentHash, err := blake3.Get32ByteBlake3Hash(parametersContentBytes) + if (err != nil) { return nil, err } + + signatureBytes := edwardsKeys.CreateSignature(newIdentityPrivateKey, contentHash) + + adminSignaturesMap := map[[32]byte][64]byte{ + newIdentityPublicKey: signatureBytes, + } + + parametersBytes, err := createParameters.CreateParameters(adminSignaturesMap, parametersContentBytes) + if (err != nil) { return nil, err } + + return parametersBytes, nil +} + +// This function will add fake profiles to the database +// They will never be disabled profiles +func GenerateFakeProfiles(profileType string, networkType byte, profilesToGenerate int)error{ + + if (profileType != "Mate" && profileType != "Host" && profileType != "Moderator"){ + return errors.New("GenerateFakeProfiles called with invalid profileType: " + profileType) + } + + isValid := helpers.VerifyNetworkType(networkType) + if (isValid == false){ + networkTypeString := helpers.ConvertByteToString(networkType) + return errors.New("GenerateFakeProfiles called with invalid networkType: " + networkTypeString) + } + + for i := 0; i < profilesToGenerate; i++ { + + newIdentityPublicKey, newIdentityPrivateKey, err := identity.GetNewRandomPublicPrivateIdentityKeys() + if (err != nil) { return err } + + fakeProfile, err := GetFakeProfile(profileType, newIdentityPublicKey, newIdentityPrivateKey, networkType) + if (err != nil) { return err } + + wellFormed, _, err := profileStorage.AddUserProfile(fakeProfile) + if (err != nil) { return err } + if (wellFormed == false){ + return errors.New("Profile to add is malformed.") + } + } + + //TODO: Add parameter to approve profiles so they will be visible for the user + + return nil +} + +// This function will add disabled profiles to the database +func GenerateDisabledProfiles(profileType string, networkType byte, profilesToGenerate int)error{ + + if (profileType != "Mate" && profileType != "Host" && profileType != "Moderator"){ + return errors.New("GenerateDisabledProfiles called with invalid profileType: " + profileType) + } + + isValid := helpers.VerifyNetworkType(networkType) + if (isValid == false){ + networkTypeString := helpers.ConvertByteToString(networkType) + return errors.New("GenerateDisabledProfiles called with invalid networkType: " + networkTypeString) + } + + networkTypeString := helpers.ConvertByteToString(networkType) + + for i := 0; i < profilesToGenerate; i++ { + + newIdentityPublicKey, newIdentityPrivateKey, err := identity.GetNewRandomPublicPrivateIdentityKeys() + if (err != nil) { return err } + + profileMap := map[string]string{ + "NetworkType": networkTypeString, + "ProfileType": profileType, + "Disabled": "Yes", + } + + newProfile, err := createProfiles.CreateProfile(newIdentityPublicKey, newIdentityPrivateKey, profileMap) + if (err != nil) { return err } + + wellFormed, _, err := profileStorage.AddUserProfile(newProfile) + if (err != nil) { return err } + if (wellFormed == false){ + return errors.New("Profile to add is malformed.") + } + } + + return nil +} + +func GetRandomText(maximumCharacters int, addNewlines bool)(string, error){ + + if (maximumCharacters == 0){ + return "", errors.New("GetRandomText called with 0 maximumCharacters") + } + if (maximumCharacters == 1){ + return "!", nil + } + if (maximumCharacters == 2){ + return "Hi", nil + } + if (maximumCharacters == 3){ + return "Hi!", nil + } + + englishWordList, err := wordLists.GetWordListFromLanguage("English") + if (err != nil) { return "", err } + + // This is the length of the randomText we will generate + textCharacterLength := helpers.GetRandomIntWithinRange(1, maximumCharacters) + + addedCharactersCount := 0 + + var newRandomTextBuilder strings.Builder + + for { + + neededCharacters := textCharacterLength - addedCharactersCount + + if (neededCharacters < 0){ + return "", errors.New("Failed to get random text: added too many characters.") + } else if (neededCharacters == 0){ + break + } else if (neededCharacters == 1){ + newRandomTextBuilder.WriteString("!") + break + } else if (neededCharacters == 2){ + newRandomTextBuilder.WriteString("!!") + break + } else if (neededCharacters == 3){ + newRandomTextBuilder.WriteString("!!!") + break + } + + if (addNewlines == true){ + + // We will add a newline with a probability of 3% + + addNewline, err := helpers.GetRandomBoolWithProbability(.03) + if (err != nil) { return "", err } + if (addNewline == true){ + + newRandomTextBuilder.WriteString("\n") + + addedCharactersCount += 1 + continue + } + } + + randomWord, err := helpers.GetRandomItemFromList(englishWordList) + if (err != nil) { return "", err } + + wordWithWhitespace := randomWord + " " + wordWithWhitespaceCharacterCount := len(wordWithWhitespace) + + if (wordWithWhitespaceCharacterCount > neededCharacters){ + // Word is too long, get shorter word + continue + } + + newRandomTextBuilder.WriteString(wordWithWhitespace) + + addedCharactersCount += wordWithWhitespaceCharacterCount + } + + newRandomText := newRandomTextBuilder.String() + + return newRandomText, nil +} + +func GetFakeProfile(profileType string, identityPublicKey [32]byte, identityPrivateKey [64]byte, networkType byte)([]byte, error){ + + if (profileType != "Mate" && profileType != "Host" && profileType != "Moderator"){ + return nil, errors.New("GetFakeProfile called with invalid profileType: " + profileType) + } + + isValid := helpers.VerifyNetworkType(networkType) + if (isValid == false){ + networkTypeString := helpers.ConvertByteToString(networkType) + return nil, errors.New("GetFakeProfile called with invalid networkType: " + networkTypeString) + } + + networkTypeString := helpers.ConvertByteToString(networkType) + + profileMap := map[string]string{ + "ProfileType": profileType, + "NetworkType": networkTypeString, + } + + // This will return true 80% of the time + getIncludeAttributeBool := func()(bool, error){ + + includeAttribute, err := helpers.GetRandomBoolWithProbability(.8) + + return includeAttribute, err + } + + getRandomNumberWithinRangeAsString := func(minValue int, maxValue int)string{ + + newNumber := helpers.GetRandomIntWithinRange(minValue, maxValue) + + result := helpers.ConvertIntToString(newNumber) + + return result + } + + includeUsername, err := getIncludeAttributeBool() + if (err != nil) { return nil, err } + if (includeUsername == true){ + + englishWordList, err := wordLists.GetWordListFromLanguage("English") + if (err != nil) { return nil, err } + + var newUsernameBuilder strings.Builder + + for i:=0; i < 3; i++{ + + randomWord, err := helpers.GetRandomItemFromList(englishWordList) + if (err != nil) { return nil, err } + + // We captialize the first letter of the word + + wordRunes := []rune(randomWord) + wordFirstCharacter := wordRunes[0] + wordFirstCharacterCapitalized := unicode.ToUpper(wordFirstCharacter) + wordRunes[0] = wordFirstCharacterCapitalized + + randomWordCapitalized := string(wordRunes) + + newUsernameBuilder.WriteString(randomWordCapitalized) + } + + profileUsername := newUsernameBuilder.String() + + profileMap["Username"] = profileUsername + } + + includeDescription, err := getIncludeAttributeBool() + if (err != nil) { return nil, err } + if (includeDescription == true){ + + getDescriptionMaximumLength := func()int{ + if (profileType == "Mate"){ + return 3000 + } + if (profileType == "Host"){ + return 300 + } + //profileType == "Moderator" + return 500 + } + + descriptionMaximumLength := getDescriptionMaximumLength() + + profileDescription, err := GetRandomText(descriptionMaximumLength, true) + if (err != nil) { return nil, err } + + profileMap["Description"] = profileDescription + } + + if (profileType == "Mate" || profileType == "Moderator"){ + + // We add chat keys + + randomNaclKey, err := nacl.GetNewRandomPublicNaclKey() + if (err != nil) { return nil, err } + + randomKyberKey, err := kyber.GetNewRandomPublicKyberKey() + if (err != nil) { return nil, err } + + randomNaclKeyString := encoding.EncodeBytesToBase64String(randomNaclKey[:]) + randomKyberKeyString := encoding.EncodeBytesToBase64String(randomKyberKey[:]) + + profileMap["NaclKey"] = randomNaclKeyString + profileMap["KyberKey"] = randomKyberKeyString + + deviceIdentifier, err := helpers.GetNewRandomHexString(11) + if (err != nil) { return nil, err } + + profileMap["DeviceIdentifier"] = deviceIdentifier + + chatKeysLatestUpdateTime := time.Now().Unix() + chatKeysLatestUpdateTimeString := helpers.ConvertInt64ToString(chatKeysLatestUpdateTime) + + profileMap["ChatKeysLatestUpdateTime"] = chatKeysLatestUpdateTimeString + } + + if (profileType == "Host"){ + + //TODO: Uncomment this once we add these attributes to the profileFormat package + /* + + hostingMateContent, err := helpers.GetRandomItemFromList([]string{"Yes", "No"}) + if (err != nil) { return nil, err } + + hostingHostContent, err := helpers.GetRandomItemFromList([]string{"Yes", "No"}) + if (err != nil) { return nil, err } + + hostingModeratorContent, err := helpers.GetRandomItemFromList([]string{"Yes", "No"}) + if (err != nil) { return nil, err } + + profileMap["HostingMateContent"] = hostingMateContent + + if (hostingMateContent == "Yes"){ + minimumBound, maximumBound := byteRange.GetMinimumMaximumIdentityHashBounds() + profileMap["MateIdentitiesRangeStart"] = minimumBound + profileMap["MateIdentitiesRangeEnd"] = maximumBound + } + + profileMap["HostingHostContent"] = hostingHostContent + profileMap["HostingModeratorContent"] = hostingModeratorContent + */ + } + + if (profileType == "Mate"){ + + includeAge, err := getIncludeAttributeBool() + if (err != nil) { return nil, err } + if (includeAge == true){ + + profileAge := getRandomNumberWithinRangeAsString(18, 150) + profileMap["Age"] = profileAge + } + + includeHeight, err := getIncludeAttributeBool() + if (err != nil) { return nil, err } + if (includeHeight == true){ + + profileHeight := getRandomNumberWithinRangeAsString(30, 400) + profileMap["Height"] = profileHeight + } + + includeSex, err := getIncludeAttributeBool() + if (err != nil) { return nil, err } + if (includeSex == true){ + + sexOptionsList := []string{"Male", "Female", "Intersex Male", "Intersex Female", "Intersex"} + + profileSex, err := helpers.GetRandomItemFromList(sexOptionsList) + if (err != nil) { return nil, err } + + profileMap["Sex"] = profileSex + } + + includePrimaryLocation, err := getIncludeAttributeBool() + if (err != nil) { return nil, err } + if (includePrimaryLocation == true){ + + primaryLocationLatitude := getRandomNumberWithinRangeAsString(-90, 90) + primaryLocationLongitude := getRandomNumberWithinRangeAsString(-180, 180) + + profileMap["PrimaryLocationLatitude"] = primaryLocationLatitude + profileMap["PrimaryLocationLongitude"] = primaryLocationLongitude + + includePrimaryLocationCountry, err := getIncludeAttributeBool() + if (err != nil) { return nil, err } + if (includePrimaryLocationCountry == true){ + + profileCountryIdentifier := getRandomNumberWithinRangeAsString(1, 218) + + profileMap["PrimaryLocationCountry"] = profileCountryIdentifier + } + + includeSecondaryLocation, err := getIncludeAttributeBool() + if (err != nil) { return nil, err } + if (includeSecondaryLocation == true){ + + secondaryLocationLatitude := getRandomNumberWithinRangeAsString(-90, 90) + secondaryLocationLongitude := getRandomNumberWithinRangeAsString(-180, 180) + + profileMap["SecondaryLocationLatitude"] = secondaryLocationLatitude + profileMap["SecondaryLocationLongitude"] = secondaryLocationLongitude + + includeSecondaryLocationCountry, err := getIncludeAttributeBool() + if (err != nil) { return nil, err } + if (includeSecondaryLocationCountry == true){ + + profileCountryIdentifier := getRandomNumberWithinRangeAsString(1, 218) + + profileMap["SecondaryLocationCountry"] = profileCountryIdentifier + } + } + } + + includeTags, err := getIncludeAttributeBool() + if (err != nil) { return nil, err } + if (includeTags == true){ + + numberOfTags := helpers.GetRandomIntWithinRange(1, 10) + + // We use this map to make sure we don't add any duplicate tags + tagsMap := make(map[string]struct{}) + + // We use this to create the Tags attribute + // It is a list of each tag, delimited by "+&" + var tagsAttributeBuilder strings.Builder + + numberOfAddedTags := 0 + + for { + + newTag, err := GetRandomText(40, false) + if (err != nil) { return nil, err } + + _, exists := tagsMap[newTag] + if (exists == true){ + continue + } + + tagsMap[newTag] = struct{}{} + + tagsAttributeBuilder.WriteString(newTag) + + numberOfAddedTags += 1 + + if (numberOfAddedTags == numberOfTags){ + break + } else { + tagsAttributeBuilder.WriteString("+&") + } + } + + tagsAttribute := tagsAttributeBuilder.String() + + profileMap["Tags"] = tagsAttribute + } + + includePhotos, err := getIncludeAttributeBool() + if (err != nil) { return nil, err } + if (includePhotos == true){ + + numberOfPhotos := helpers.GetRandomIntWithinRange(1, 4) + + // We use this to build the Photos attribute + // It is a list of base64-encoded photo bytes, delimited by "+" + var photosAttributeBuilder strings.Builder + + for i:=1; i <= numberOfPhotos; i++{ + + randomWebpImageBase64, err := getRandomWebpBase64Image() + if (err != nil) { return nil, err } + + photosAttributeBuilder.WriteString(randomWebpImageBase64) + + if (i != numberOfPhotos){ + photosAttributeBuilder.WriteString("+") + } + } + + photosAttribute := photosAttributeBuilder.String() + + profileMap["Photos"] = photosAttribute + } + + //TODO: Add Questionnaire + + includeAvatar, err := getIncludeAttributeBool() + if (err != nil) { return nil, err } + if (includeAvatar == true){ + + avatarIdentifier := getRandomNumberWithinRangeAsString(1, 3630) + + profileMap["Avatar"] = avatarIdentifier + } + + includeSexuality, err := getIncludeAttributeBool() + if (err != nil) { return nil, err } + if (includeSexuality == true){ + + sexualityOptionsList := []string{"Male", "Female", "Male And Female"} + + profileSexuality, err := helpers.GetRandomItemFromList(sexualityOptionsList) + if (err != nil) { return nil, err } + + profileMap["Sexuality"] = profileSexuality + } + + include23andMeAncestryComposition, err := getIncludeAttributeBool() + if (err != nil) { return nil, err } + if (include23andMeAncestryComposition == true){ + + // We first add all locations with no sublocations to the maps + // We then add random amounts to the locations + + // Map Structure: Location Name -> Percentage of total ancestry + continentsMap := make(map[string]float64) + regionsMap := make(map[string]float64) + subregionsMap := make(map[string]float64) + + continentsList := companyAnalysis.GetAncestryContinentsList_23andMe() + + for _, continentName := range continentsList{ + + continentRegionsList, err := companyAnalysis.GetAncestryContinentRegionsList_23andMe(continentName) + if (err != nil) { return nil, err } + + if (len(continentRegionsList) == 0){ + + continentsMap[continentName] = 0 + continue + } + + for _, regionName := range continentRegionsList{ + + subregionsList, err := companyAnalysis.GetAncestryRegionSubregionsList_23andMe(continentName, regionName) + if (err != nil) { return nil, err } + if (len(subregionsList) == 0){ + regionsMap[regionName] = 0 + continue + } + for _, subregionName := range subregionsList{ + + subregionsMap[subregionName] = 0 + } + } + } + + continentNamesList := helpers.GetListOfMapKeys(continentsMap) + regionNamesList := helpers.GetListOfMapKeys(regionsMap) + subregionNamesList := helpers.GetListOfMapKeys(subregionsMap) + + percentageRemaining := 100 + + for percentageRemaining > 0{ + + //Outputs: + // -map[string]float64: Location name -> Percentage of total ancestry + // -[]string: List of all location type location names (map keys) + // -error + getRandomLocationTypeMapAndLocationsList := func()(map[string]float64, []string){ + + randomInt := helpers.GetRandomIntWithinRange(1, 3) + + if (randomInt == 1){ + return continentsMap, continentNamesList + } else if (randomInt == 2){ + return regionsMap, regionNamesList + } + + return subregionsMap, subregionNamesList + } + + locationTypeMap, locationTypeNamesList := getRandomLocationTypeMapAndLocationsList() + + randomLocationName, err := helpers.GetRandomItemFromList(locationTypeNamesList) + if (err != nil) { return nil, err } + + percentageToAdd := helpers.GetRandomIntWithinRange(1, percentageRemaining) + + locationTypeMap[randomLocationName] += float64(percentageToAdd) + + percentageRemaining -= percentageToAdd + } + + inputsAreValid, ancestryAttribute, err := companyAnalysis.CreateAncestryCompositionAttribute_23andMe(continentsMap, regionsMap, subregionsMap) + if (err != nil) { return nil, err } + if (inputsAreValid == false){ + return nil, errors.New("Ancestry composition location maps are invalid when creating fake profile.") + } + + profileMap["23andMe_AncestryComposition"] = ancestryAttribute + } + + include23andMeNeanderthalVariants, err := getIncludeAttributeBool() + if (err != nil) { return nil, err } + if (include23andMeNeanderthalVariants == true){ + + neanderthalVariants := getRandomNumberWithinRangeAsString(0, 7462) + + profileMap["23andMe_NeanderthalVariants"] = neanderthalVariants + } + + include23andMeMaternalHaplogroup, err := getIncludeAttributeBool() + if (err != nil) { return nil, err } + if (include23andMeMaternalHaplogroup == true){ + + maternalHaplogroup, err := GetRandomText(25, false) + if (err != nil){ return nil, err } + + profileMap["23andMe_MaternalHaplogroup"] = maternalHaplogroup + } + + include23andMePaternalHaplogroup, err := getIncludeAttributeBool() + if (err != nil) { return nil, err } + if (include23andMePaternalHaplogroup == true){ + + paternalHaplogroup, err := GetRandomText(25, false) + if (err != nil){ return nil, err } + + profileMap["23andMe_PaternalHaplogroup"] = paternalHaplogroup + } + + includeBodyFat, err := getIncludeAttributeBool() + if (err != nil) { return nil, err } + if (includeBodyFat == true){ + + bodyFat := getRandomNumberWithinRangeAsString(1, 4) + + profileMap["BodyFat"] = bodyFat + } + includeBodyMuscle, err := getIncludeAttributeBool() + if (err != nil) { return nil, err } + if (includeBodyMuscle == true){ + + bodyMuscle := getRandomNumberWithinRangeAsString(1, 4) + + profileMap["BodyMuscle"] = bodyMuscle + } + + includeEyeColor, err := getIncludeAttributeBool() + if (err != nil) { return nil, err } + if (includeEyeColor == true){ + + //TODO: Combined colors + eyeColorOptionsList := []string{"Blue", "Green", "Amber", "Brown"} + + eyeColor, err := helpers.GetRandomItemFromList(eyeColorOptionsList) + if (err != nil) { return nil, err } + + profileMap["EyeColor"] = eyeColor + } + includeHairColor, err := getIncludeAttributeBool() + if (err != nil) { return nil, err } + if (includeHairColor == true){ + + //TODO: Combined colors + hairColorOptionsList := []string{"Black", "Brown", "Blonde", "Orange"} + + hairColor, err := helpers.GetRandomItemFromList(hairColorOptionsList) + if (err != nil) { return nil, err } + + profileMap["HairColor"] = hairColor + } + + includeHairTexture, err := getIncludeAttributeBool() + if (err != nil) { return nil, err } + if (includeHairTexture == true){ + + hairTexture := getRandomNumberWithinRangeAsString(1, 6) + + profileMap["HairTexture"] = hairTexture + } + + includeSkinColor, err := getIncludeAttributeBool() + if (err != nil) { return nil, err } + if (includeSkinColor == true){ + + skinColor := getRandomNumberWithinRangeAsString(1, 6) + + profileMap["SkinColor"] = skinColor + } + + includeHasHIV, err := getIncludeAttributeBool() + if (err != nil) { return nil, err } + if (includeHasHIV == true){ + + optionsList := []string{"Yes", "No"} + + hasHIV, err := helpers.GetRandomItemFromList(optionsList) + if (err != nil) { return nil, err } + + profileMap["HasHIV"] = hasHIV + } + + includeHasGenitalHerpes, err := getIncludeAttributeBool() + if (err != nil) { return nil, err } + if (includeHasGenitalHerpes == true){ + + optionsList := []string{"Yes", "No"} + + hasGenitalHerpes, err := helpers.GetRandomItemFromList(optionsList) + if (err != nil) { return nil, err } + + profileMap["HasGenitalHerpes"] = hasGenitalHerpes + } + + includeHobbies, err := getIncludeAttributeBool() + if (err != nil) { return nil, err } + if (includeHobbies == true){ + + profileHobbies, err := GetRandomText(1000, true) + if (err != nil) { return nil, err } + + profileMap["Hobbies"] = profileHobbies + } + + includeWealth, err := getIncludeAttributeBool() + if (err != nil) { return nil, err } + if (includeWealth == true){ + + allCurrencyObjectsMap, err := currencies.GetCurrencyObjectsMap() + if (err != nil) { return nil, err } + + allCurrencyCodesList := helpers.GetListOfMapKeys(allCurrencyObjectsMap) + + randomCurrencyCode, err := helpers.GetRandomItemFromList(allCurrencyCodesList) + if (err != nil) { return nil, err } + + profileWealth := helpers.GetRandomInt64WithinRange(0, 9223372036854775806) + profileWealthString := helpers.ConvertInt64ToString(profileWealth) + profileWealthIsLowerBound, err := helpers.GetRandomItemFromList([]string{"Yes", "No"}) + if (err != nil) { return nil, err } + + profileMap["Wealth"] = profileWealthString + profileMap["WealthCurrency"] = randomCurrencyCode + profileMap["WealthIsLowerBound"] = profileWealthIsLowerBound + } + + includeJob, err := getIncludeAttributeBool() + if (err != nil) { return nil, err } + if (includeJob == true){ + + profileJob, err := GetRandomText(100, true) + if (err != nil) { return nil, err } + + profileMap["Job"] = profileJob + } + + foodsList := []string{"Fruit", "Vegetables", "Nuts", "Grains", "Dairy", "Seafood", "Beef", "Pork", "Poultry", "Eggs", "Beans"} + + for _, foodName := range foodsList{ + + includeFood, err := getIncludeAttributeBool() + if (err != nil) { return nil, err } + if (includeFood == true){ + + foodRating := getRandomNumberWithinRangeAsString(1, 10) + profileMap[foodName + "Rating"] = foodRating + } + } + + includeFame, err := getIncludeAttributeBool() + if (err != nil) { return nil, err } + if (includeFame == true){ + + profileFame := getRandomNumberWithinRangeAsString(1, 10) + + profileMap["Fame"] = profileFame + } + + drugsList := []string{"Alcohol", "Tobacco", "Cannabis"} + + for _, drugName := range drugsList{ + + includeDrug, err := getIncludeAttributeBool() + if (err != nil) { return nil, err } + if (includeDrug == true){ + + drugFrequency := getRandomNumberWithinRangeAsString(1, 10) + + profileMap[drugName + "Frequency"] = drugFrequency + } + } + + //TODO: Language + + includeBeliefs, err := getIncludeAttributeBool() + if (err != nil) { return nil, err } + if (includeBeliefs == true){ + + profileBeliefs, err := GetRandomText(1000, true) + if (err != nil) { return nil, err } + + profileMap["Beliefs"] = profileBeliefs + } + + includeGenderIdentity, err := getIncludeAttributeBool() + if (err != nil) { return nil, err } + if (includeGenderIdentity == true){ + + includeCanonical, err := getIncludeAttributeBool() + if (err != nil) { return nil, err } + if (includeCanonical == true){ + + profileGender, err := helpers.GetRandomItemFromList([]string{"Man", "Woman"}) + if (err != nil) { return nil, err } + + profileMap["GenderIdentity"] = profileGender + } else { + + profileGender, err := GetRandomText(50, false) + if (err != nil) { return nil, err } + + profileMap["GenderIdentity"] = profileGender + } + } + + includePetsRating, err := getIncludeAttributeBool() + if (err != nil) { return nil, err } + if (includePetsRating == true){ + + profilePetsRating := getRandomNumberWithinRangeAsString(1, 10) + + profileMap["PetsRating"] = profilePetsRating + } + + includeDogsRating, err := getIncludeAttributeBool() + if (err != nil) { return nil, err } + if (includeDogsRating == true){ + + profileDogsRating := getRandomNumberWithinRangeAsString(1, 10) + + profileMap["DogsRating"] = profileDogsRating + } + + includeCatsRating, err := getIncludeAttributeBool() + if (err != nil) { return nil, err } + if (includeCatsRating == true){ + + profileCatsRating := getRandomNumberWithinRangeAsString(1, 10) + + profileMap["CatsRating"] = profileCatsRating + } + + includeMonogenicDiseaseProbabilities, err := getIncludeAttributeBool() + if (err != nil) { return nil, err } + if (includeMonogenicDiseaseProbabilities == true){ + + monogenicDiseaseObjectsList, err := monogenicDiseases.GetMonogenicDiseaseObjectsList() + if (err != nil) { return nil, err } + + for _, diseaseObject := range monogenicDiseaseObjectsList{ + + includeDisease, err := getIncludeAttributeBool() + if (err != nil) { return nil, err } + if (includeDisease == false){ + continue + } + + monogenicDiseaseName := diseaseObject.DiseaseName + diseaseVariantsList := diseaseObject.VariantsList + + diseaseNameWithUnderscores := strings.ReplaceAll(monogenicDiseaseName, " ", "_") + + probabilityOfPassingAVariantAttributeName := "MonogenicDisease_" + diseaseNameWithUnderscores + "_ProbabilityOfPassingAVariant" + numberOfVariantsTestedAttributeName := "MonogenicDisease_" + diseaseNameWithUnderscores + "_NumberOfVariantsTested" + + probabilityOfPassingAVariantString, err := helpers.GetRandomItemFromList([]string{"0", "50", "100"}) + if (err != nil) { return nil, err } + + numberOfDiseaseVariants := len(diseaseVariantsList) + + numberOfVariantsTestedString := getRandomNumberWithinRangeAsString(1, numberOfDiseaseVariants) + + profileMap[probabilityOfPassingAVariantAttributeName] = probabilityOfPassingAVariantString + profileMap[numberOfVariantsTestedAttributeName] = numberOfVariantsTestedString + } + } + + includeRSIDs, err := getIncludeAttributeBool() + if (err != nil) { return nil, err } + if (includeRSIDs == true){ + + locusBasesList := []string{"C", "A", "T", "G", "I", "D"} + + // This map will store all rsIDs for traits and polygenic diseases + shareableRSIDsMap := make(map[int64]struct{}) + + traitObjectsList, err := traits.GetTraitObjectsList() + if (err != nil){ return nil, err } + + for _, traitObject := range traitObjectsList{ + + traitLociList := traitObject.LociList + + for _, rsID := range traitLociList{ + + shareableRSIDsMap[rsID] = struct{}{} + } + } + + polygenicDiseaseObjectsList, err := polygenicDiseases.GetPolygenicDiseaseObjectsList() + if (err != nil) { return nil, err } + + for _, diseaseObject := range polygenicDiseaseObjectsList{ + + diseaseLociList := diseaseObject.LociList + + for _, locusObject := range diseaseLociList{ + + locusRSID := locusObject.LocusRSID + + shareableRSIDsMap[locusRSID] = struct{}{} + } + } + + for rsID, _ := range shareableRSIDsMap{ + + includeLocus, err := getIncludeAttributeBool() + if (err != nil) { return nil, err } + if (includeLocus == false){ + continue + } + + rsidString := helpers.ConvertInt64ToString(rsID) + + attributeName := "LocusValue_rs" + rsidString + + baseA, err := helpers.GetRandomItemFromList(locusBasesList) + if (err != nil) { return nil, err } + + baseB, err := helpers.GetRandomItemFromList(locusBasesList) + if (err != nil) { return nil, err } + + attributeValue := baseA + ";" + baseB + + profileMap[attributeName] = attributeValue + } + } + } + + fakeProfile, err := createProfiles.CreateProfile(identityPublicKey, identityPrivateKey, profileMap) + if (err != nil) { return nil, err } + + return fakeProfile, nil +} + +func GenerateFakeProfilesWithMessages(profileType string, networkType byte, numberOfProfilesToGenerate int)error{ + + if (profileType != "Mate" && profileType != "Moderator"){ + return errors.New("GenerateFakeProfilesWithMessages called with invalid profile type: " + profileType) + } + + isValid := helpers.VerifyNetworkType(networkType) + if (isValid == false){ + networkTypeString := helpers.ConvertByteToString(networkType) + return errors.New("GenerateFakeProfilesWithMessages called with invalid networkType: " + networkTypeString) + } + + for i := 0; i < numberOfProfilesToGenerate; i++ { + + newIdentityPublicKey, newIdentityPrivateKey, err := identity.GetNewRandomPublicPrivateIdentityKeys() + if (err != nil) { return err } + + fakeProfile, err := GetFakeProfile(profileType, newIdentityPublicKey, newIdentityPrivateKey, networkType) + if (err != nil) { return err } + + wellFormed, _, err := profileStorage.AddUserProfile(fakeProfile) + if (err != nil) { return err } + if (wellFormed == false){ + return errors.New("Profile to add is malformed.") + } + + textMessage, err := getFakeMessageSentToMe(profileType, newIdentityPublicKey, newIdentityPrivateKey, networkType, "Text") + if (err != nil) { return err } + + imageMessage, err := getFakeMessageSentToMe(profileType, newIdentityPublicKey, newIdentityPrivateKey, networkType, "Image") + if (err != nil) { return err } + + wellFormed, err = chatMessageStorage.AddMessage(textMessage) + if (err != nil) { return err } + if (wellFormed == false){ + return errors.New("Message to add is malformed.") + } + + wellFormed, err = chatMessageStorage.AddMessage(imageMessage) + if (err != nil) { return err } + if (wellFormed == false){ + return errors.New("Message to add is malformed.") + } + } + + //TODO: Approve profiles so they will be visible for the user + + return nil +} + +func GetFakeReview(reviewType string, networkType byte)([]byte, error){ + + if (reviewType != "Identity" && reviewType != "Profile" && reviewType != "Attribute" && reviewType != "Message"){ + return nil, errors.New("GetFakeReview called with invalid reviewType: " + reviewType) + } + + isValid := helpers.VerifyNetworkType(networkType) + if (isValid == false){ + networkTypeString := helpers.ConvertByteToString(networkType) + return nil, errors.New("GetFakeReview called with invalid networkType: " + networkTypeString) + } + + networkTypeString := helpers.ConvertByteToString(networkType) + + if (reviewType == "Identity"){ + + randomIdentityHash, err := identity.GetNewRandomIdentityHash(false, "") + if (err != nil) { return nil, err } + + randomIdentityHashString, _, err := identity.EncodeIdentityHashBytesToString(randomIdentityHash) + if (err != nil) { return nil, err } + + verdict, err := helpers.GetRandomItemFromList([]string{"Ban", "Ban", "Ban", "Ban", "Ban", "None"}) + if (err != nil) { return nil, err } + + newReviewMap := map[string]string{ + "NetworkType": networkTypeString, + "ReviewedHash": randomIdentityHashString, + "Verdict": verdict, + "Reason": "Unruleful conduct.", + } + + newIdentityPublicKey, newIdentityPrivateKey, err := identity.GetNewRandomPublicPrivateIdentityKeys() + if (err != nil) { return nil, err } + + reviewBytes, err := createReviews.CreateReview(newIdentityPublicKey, newIdentityPrivateKey, newReviewMap) + if (err != nil) { return nil, err } + + return reviewBytes, nil + } + if (reviewType == "Profile"){ + + profileType, err := helpers.GetRandomItemFromList([]string{"Mate", "Host", "Moderator"}) + if (err != nil) { return nil, err } + + profileHash, err := helpers.GetNewRandomProfileHash(true, profileType, true, false) + if (err != nil) { return nil, err } + + profileHashString := encoding.EncodeBytesToHexString(profileHash[:]) + + verdict, err := helpers.GetRandomItemFromList([]string{"Ban", "Approve", "Approve", "Approve", "Approve", "None"}) + if (err != nil) { return nil, err } + + newReviewMap := map[string]string{ + "NetworkType": networkTypeString, + "ReviewedHash": profileHashString, + "Verdict": verdict, + } + + if (verdict == "Ban"){ + newReviewMap["Reason"] = "Profile is unruleful." + } else { + newReviewMap["Reason"] = "Profile is good." + } + + newIdentityPublicKey, newIdentityPrivateKey, err := identity.GetNewRandomPublicPrivateIdentityKeys() + if (err != nil) { return nil, err } + + reviewBytes, err := createReviews.CreateReview(newIdentityPublicKey, newIdentityPrivateKey, newReviewMap) + if (err != nil) { return nil, err } + + return reviewBytes, nil + } + if (reviewType == "Attribute"){ + + attributeHash, err := helpers.GetNewRandomAttributeHash(false, "", false, false) + if (err != nil) { return nil, err } + + attributeHashHex := encoding.EncodeBytesToHexString(attributeHash[:]) + + verdict, err := helpers.GetRandomItemFromList([]string{"Ban", "Approve", "Approve", "Approve", "Approve", "None"}) + if (err != nil) { return nil, err } + + newReviewMap := map[string]string{ + "NetworkType": networkTypeString, + "ReviewedHash": attributeHashHex, + "Verdict": verdict, + } + + if (verdict == "Ban"){ + newReviewMap["Reason"] = "Attribute is unruleful." + } else { + newReviewMap["Reason"] = "Attribute is good." + } + + newIdentityPublicKey, newIdentityPrivateKey, err := identity.GetNewRandomPublicPrivateIdentityKeys() + if (err != nil) { return nil, err } + + reviewBytes, err := createReviews.CreateReview(newIdentityPublicKey, newIdentityPrivateKey, newReviewMap) + if (err != nil) { return nil, err } + + return reviewBytes, nil + } + if (reviewType == "Message"){ + + imageOrTextList := []string{"Image", "Text"} + imageOrText, err := helpers.GetRandomItemFromList(imageOrTextList) + if (err != nil) { return nil, err } + + _, messageHash, messageCipherKey, err := GetFakeMessage(networkType, imageOrText) + if (err != nil) { return nil, err } + + messageHashString := encoding.EncodeBytesToHexString(messageHash[:]) + + messageCipherKeyHex := encoding.EncodeBytesToHexString(messageCipherKey[:]) + + verdict, err := helpers.GetRandomItemFromList([]string{"Approve", "Ban", "Approve", "Ban", "None"}) + if (err != nil) { return nil, err } + + newReviewMap := map[string]string{ + "NetworkType": networkTypeString, + "ReviewedHash": messageHashString, + "MessageCipherKey": messageCipherKeyHex, + "Verdict": verdict, + } + + if (verdict == "Approve"){ + + newReviewMap["Reason"] = "Message is ruleful." + + } else if (verdict == "Ban"){ + + newReviewMap["Reason"] = "Message is unruleful." + } + + newIdentityPublicKey, newIdentityPrivateKey, err := identity.GetNewRandomPublicPrivateIdentityKeys() + if (err != nil) { return nil, err } + + reviewBytes, err := createReviews.CreateReview(newIdentityPublicKey, newIdentityPrivateKey, newReviewMap) + if (err != nil) { return nil, err } + + return reviewBytes, nil + } + + return nil, errors.New("GetFakeReview called with invalid reviewType: " + reviewType) +} + + +// This function will generate fake moderator reviews for content stored in the database +func GenerateFakeReviews(reviewType string, networkType byte, reviewsToGenerate int)error{ + + if (reviewType != "Identity" && reviewType != "Profile" && reviewType != "Attribute" && reviewType != "Message"){ + return errors.New("GenerateFakeReviews called with invalid reviewType: " + reviewType) + } + + isValid := helpers.VerifyNetworkType(networkType) + if (isValid == false){ + networkTypeString := helpers.ConvertByteToString(networkType) + return errors.New("GenerateFakeReviews called with invalid networkType: " + networkTypeString) + } + + networkTypeString := helpers.ConvertByteToString(networkType) + + if (reviewType == "Identity"){ + + storedIdentitiesList, err := profileStorage.GetAllStoredProfileIdentityHashes() + if (err != nil) { return err } + + if (len(storedIdentitiesList) == 0) { + return errors.New("No identities to create reviews for.") + } + + for i := 0; i < reviewsToGenerate; i++ { + + randomIdentityHash, err := helpers.GetRandomItemFromList(storedIdentitiesList) + if (err != nil) { return err } + + randomIdentityHashString, _, err := identity.EncodeIdentityHashBytesToString(randomIdentityHash) + if (err != nil) { return err } + + // Generates 3 reviews for each identity + for n := 0; n < 3; n++{ + + newIdentityPublicKey, newIdentityPrivateKey, err := identity.GetNewRandomPublicPrivateIdentityKeys() + if (err != nil) { return err } + + newProfile, err := GetFakeProfile("Moderator", newIdentityPublicKey, newIdentityPrivateKey, networkType) + if (err != nil) { return err } + + wellFormed, _, err := profileStorage.AddUserProfile(newProfile) + if (err != nil) { return err } + if (wellFormed == false){ + return errors.New("Profile to add is malformed.") + } + + verdict, err := helpers.GetRandomItemFromList([]string{"Ban", "Ban", "Ban", "Ban", "Ban", "None"}) + if (err != nil) { return err } + + newReviewMap := map[string]string{ + "NetworkType": networkTypeString, + "ReviewedHash": randomIdentityHashString, + "Verdict": verdict, + "Reason": "Unruleful content.", + } + + reviewBytes, err := createReviews.CreateReview(newIdentityPublicKey, newIdentityPrivateKey, newReviewMap) + if (err != nil) { return err } + + wellFormed, err = reviewStorage.AddReview(reviewBytes) + if (err != nil) { return err } + if (wellFormed == false){ + return errors.New("Review to add is malformed.") + } + } + } + + return nil + } + if (reviewType == "Profile" || reviewType == "Attribute"){ + + allStoredProfileHashes, err := profileStorage.GetAllStoredProfileHashes() + if (err != nil) { return err } + + if (len(allStoredProfileHashes) == 0) { + return errors.New("No profiles to generate reviews for.") + } + + helpers.RandomizeListOrder(allStoredProfileHashes) + + numberOfReviewsGenerated := 0 + + // This list will store eligible profiles to review + // Some profiles cannot be reviewed + // Examples include disabled profiles, or profiles that belong to a different network type + eligibleProfileHashesList := make([][28]byte, 0) + + // Map Structure: Profile hash -> Random profile attribute hash + // Map will not contain any entries if reviewType == "Profile" + eligibleProfileAttributeHashesMap := make(map[[28]byte][27]byte) + + for _, profileHash := range allStoredProfileHashes{ + + metadataExists, _, profileNetworkType, _, _, profileIsDisabled, _, profileAttributeHashesMap, err := contentMetadata.GetProfileMetadata(profileHash) + if (err != nil) { return err } + if (metadataExists == false){ + // Profile must have been deleted after list was generated. Skip the profile. + continue + } + if (profileNetworkType != networkType){ + // Profile was created for a different network type. + continue + } + if (profileIsDisabled == true){ + // We cannot review disabled profiles + continue + } + + // Profile is eligible for review + + eligibleProfileHashesList = append(eligibleProfileHashesList, profileHash) + + if (reviewType == "Attribute"){ + + // We get random profile attribute hash + + // This list stores all attributeHashes to review. + allProfileAttributeHashesList := helpers.GetListOfMapValues(profileAttributeHashesMap) + + attributeHashToReview, err := helpers.GetRandomItemFromList(allProfileAttributeHashesList) + if (err != nil) { return err } + + eligibleProfileAttributeHashesMap[profileHash] = attributeHashToReview + } + } + + if (len(eligibleProfileHashesList) == 0){ + return errors.New("No eligible profiles to generate reviews for.") + } + + for numberOfReviewsGenerated < reviewsToGenerate { + + randomProfileHash, err := helpers.GetRandomItemFromList(eligibleProfileHashesList) + if (err != nil) { return err } + + getRandomHashToReview := func()(string, error){ + + if (reviewType == "Profile"){ + + randomProfileHashHex := encoding.EncodeBytesToHexString(randomProfileHash[:]) + + return randomProfileHashHex, nil + } + + profileRandomAttributeHash, exists := eligibleProfileAttributeHashesMap[randomProfileHash] + if (exists == false){ + return "", errors.New("profileAttributeHashesMap missing random profile hash entry.") + } + + hashToReviewHex := encoding.EncodeBytesToHexString(profileRandomAttributeHash[:]) + + return hashToReviewHex, nil + } + + hashToReview, err := getRandomHashToReview() + if (err != nil) { return err } + + // We generate 3 reviews for each profile/attribute + for i:= 0; i < 3; i++{ + + newIdentityPublicKey, newIdentityPrivateKey, err := identity.GetNewRandomPublicPrivateIdentityKeys() + if (err != nil) { return err } + + newProfile, err := GetFakeProfile("Moderator", newIdentityPublicKey, newIdentityPrivateKey, networkType) + if (err != nil) { return err } + + wellFormed, _, err := profileStorage.AddUserProfile(newProfile) + if (err != nil) { return err } + if (wellFormed == false){ + return errors.New("Profile to add is malformed.") + } + + verdict, err := helpers.GetRandomItemFromList([]string{"Ban", "Approve", "Approve", "Approve", "Approve", "None"}) + if (err != nil) { return err } + + newReviewMap := map[string]string{ + "NetworkType": networkTypeString, + "ReviewedHash": hashToReview, + "Verdict": verdict, + } + + if (verdict == "Ban"){ + newReviewMap["Reason"] = reviewType + " is unruleful." + } else { + newReviewMap["Reason"] = reviewType + " is good." + } + + reviewBytes, err := createReviews.CreateReview(newIdentityPublicKey, newIdentityPrivateKey, newReviewMap) + if (err != nil) { return err } + + wellFormed, err = reviewStorage.AddReview(reviewBytes) + if (err != nil) { return err } + if (wellFormed == false){ + return errors.New("Review to add is malformed.") + } + } + + numberOfReviewsGenerated += 1 + } + + return nil + } + + if (reviewType == "Message"){ + + for i := 0; i < reviewsToGenerate; i++ { + + imageOrTextList := []string{"Image", "Text"} + imageOrText, err := helpers.GetRandomItemFromList(imageOrTextList) + if (err != nil) { return err } + + messageBytes, messageHash, messageCipherKey, err := GetFakeMessage(networkType, imageOrText) + if (err != nil) { return err } + + messageHashString := encoding.EncodeBytesToHexString(messageHash[:]) + + messageCipherKeyHex := encoding.EncodeBytesToHexString(messageCipherKey[:]) + + // Generates 3 reviews for each message + for n := 0; n < 3; n++{ + + newIdentityPublicKey, newIdentityPrivateKey, err := identity.GetNewRandomPublicPrivateIdentityKeys() + if (err != nil) { return err } + + newProfile, err := GetFakeProfile("Moderator", newIdentityPublicKey, newIdentityPrivateKey, networkType) + if (err != nil) { return err } + + wellFormed, _, err := profileStorage.AddUserProfile(newProfile) + if (err != nil) { return err } + if (wellFormed == false){ + return errors.New("Profile to add is malformed.") + } + + verdict, err := helpers.GetRandomItemFromList([]string{"Approve", "Ban", "Approve", "Ban", "None"}) + if (err != nil) { return err } + + newReviewMap := map[string]string{ + "NetworkType": networkTypeString, + "ReviewedHash": messageHashString, + "Verdict": verdict, + "Reason": "Message is unruleful", + "MessageCipherKey": messageCipherKeyHex, + } + + reviewBytes, err := createReviews.CreateReview(newIdentityPublicKey, newIdentityPrivateKey, newReviewMap) + if (err != nil) { return err } + + wellFormed, err = reviewStorage.AddReview(reviewBytes) + if (err != nil) { return err } + if (wellFormed == false){ + return errors.New("Review to add is malformed.") + } + } + + wellFormed, err := chatMessageStorage.AddMessage(messageBytes) + if (err != nil) { return err } + if (wellFormed == false){ + return errors.New("Message to add is malformed.") + } + } + + return nil + } + + return errors.New("GenerateFakeReviews called with invalid reviewType: " + reviewType) +} + + +// This function will create reports for existing identities, profiles, attributes, and messages, and add them to the database. +func GenerateFakeReports(reportType string, networkType byte, reportsToGenerate int)error{ + + if (reportType != "Identity" && reportType != "Profile" && reportType != "Attribute" && reportType != "Message"){ + return errors.New("GenerateFakeReports called with invalid reportType: " + reportType) + } + + isValid := helpers.VerifyNetworkType(networkType) + if (isValid == false){ + networkTypeString := helpers.ConvertByteToString(networkType) + return errors.New("GenerateFakeReports called with invalid networkType: " + networkTypeString) + } + + networkTypeString := helpers.ConvertByteToString(networkType) + + if (reportType == "Identity"){ + + storedIdentitiesList, err := profileStorage.GetAllStoredProfileIdentityHashes() + if (err != nil) { return err } + + if (len(storedIdentitiesList) == 0) { + return errors.New("No identities to create reports for.") + } + + for i := 0; i < reportsToGenerate; i++ { + + randomIdentityHash, err := helpers.GetRandomItemFromList(storedIdentitiesList) + if (err != nil) { return err } + + randomIdentityHashString, _, err := identity.EncodeIdentityHashBytesToString(randomIdentityHash) + if (err != nil) { return err } + + newReportMap := map[string]string{ + "NetworkType": networkTypeString, + "ReportedHash": randomIdentityHashString, + "Reason": "Bad Conduct.", + } + + reportBytes, err := createReports.CreateReport(newReportMap) + if (err != nil) { return err } + + wellFormed, err := reportStorage.AddReport(reportBytes) + if (err != nil) { return err } + if (wellFormed == false){ + return errors.New("Report to add is malformed.") + } + } + + return nil + } + if (reportType == "Profile" || reportType == "Attribute"){ + + allStoredProfileHashes, err := profileStorage.GetAllStoredProfileHashes() + if (err != nil) { return err } + + if (len(allStoredProfileHashes) == 0) { + return errors.New("No profiles to generate reports for.") + } + + // This list will store eligible profiles to report + // Some profiles cannot be reported + // Examples include disabled profiles, or profiles that belong to a different network type + eligibleProfileHashesList := make([][28]byte, 0) + + // Map Structure: Profile hash -> Random profile attribute hash + // Map will not contain any entries if reportType == "Profile" + eligibleProfileAttributeHashesMap := make(map[[28]byte][27]byte) + + for _, profileHash := range allStoredProfileHashes{ + + metadataExists, _, profileNetworkType, _, _, profileIsDisabled, _, profileAttributeHashesMap, err := contentMetadata.GetProfileMetadata(profileHash) + if (err != nil) { return err } + if (metadataExists == false){ + // Profile must have been deleted after list was generated. Skip the profile. + continue + } + if (profileNetworkType != networkType){ + // Profile was created for a different network type. + continue + } + if (profileIsDisabled == true){ + // We cannot review disabled profiles + continue + } + + // Profile is eligible to be reported + + eligibleProfileHashesList = append(eligibleProfileHashesList, profileHash) + + if (reportType == "Attribute"){ + + // We get random profile attribute hash + + // This list stores all attributeHashes to review. + allProfileAttributeHashesList := helpers.GetListOfMapValues(profileAttributeHashesMap) + + attributeHashToReport, err := helpers.GetRandomItemFromList(allProfileAttributeHashesList) + if (err != nil) { return err } + + eligibleProfileAttributeHashesMap[profileHash] = attributeHashToReport + } + } + + if (len(eligibleProfileHashesList) == 0){ + return errors.New("No eligible profiles to generate reports for.") + } + + numberOfReportsGenerated := 0 + + for numberOfReportsGenerated < reportsToGenerate { + + randomProfileHash, err := helpers.GetRandomItemFromList(eligibleProfileHashesList) + if (err != nil) { return err } + + getRandomHashToReport := func()(string, error){ + + if (reportType == "Profile"){ + + randomProfileHashHex := encoding.EncodeBytesToHexString(randomProfileHash[:]) + + return randomProfileHashHex, nil + } + + attributeHashToReport, exists := eligibleProfileAttributeHashesMap[randomProfileHash] + if (exists == false){ + return "", errors.New("eligibleProfileAttributeHashesMap missing random profile hash.") + } + + attributeHashToReportHex := encoding.EncodeBytesToHexString(attributeHashToReport[:]) + + return attributeHashToReportHex, nil + } + + hashToReport, err := getRandomHashToReport() + if (err != nil) { return err } + + newReportMap := map[string]string{ + "NetworkType": networkTypeString, + "ReportedHash": hashToReport, + "Reason": reportType + " is unruleful.", + } + + reportBytes, err := createReports.CreateReport(newReportMap) + if (err != nil) { return err } + + wellFormed, err := reportStorage.AddReport(reportBytes) + if (err != nil) { return err } + if (wellFormed == false){ + return errors.New("Report to add is malformed.") + } + + numberOfReportsGenerated += 1 + } + + return nil + } + + if (reportType == "Message"){ + + for i := 0; i < reportsToGenerate; i++ { + + textOrImage, err := helpers.GetRandomItemFromList([]string{"Text", "Image"}) + if (err != nil) { return err } + + messageBytes, messageHash, messageCipherKey, err := GetFakeMessage(networkType, textOrImage) + if (err != nil) { return err } + + messageHashString := encoding.EncodeBytesToHexString(messageHash[:]) + + messageCipherKeyHex := encoding.EncodeBytesToHexString(messageCipherKey[:]) + + newReportMap := map[string]string{ + "NetworkType": networkTypeString, + "ReportedHash": messageHashString, + "MessageCipherKey": messageCipherKeyHex, + "Reason": "Message is unruleful", + } + + reportBytes, err := createReports.CreateReport(newReportMap) + if (err != nil) { return err } + + wellFormed, err := reportStorage.AddReport(reportBytes) + if (err != nil) { return err } + if (wellFormed == false){ + return errors.New("Report to add is malformed.") + } + + wellFormed, err = chatMessageStorage.AddMessage(messageBytes) + if (err != nil) { return err } + if (wellFormed == false){ + return errors.New("Message to add is malformed.") + } + } + + return nil + } + + return errors.New("GenerateFakeReports called with invalid reportType: " + reportType) +} + + +func GetFakeReport(reportType string, networkType byte)([]byte, error){ + + if (reportType != "Identity" && reportType != "Profile" && reportType != "Attribute" && reportType != "Message"){ + return nil, errors.New("GetFakeReport called with invalid reportType: " + reportType) + } + + isValid := helpers.VerifyNetworkType(networkType) + if (isValid == false){ + networkTypeString := helpers.ConvertByteToString(networkType) + return nil, errors.New("GetFakeReport called with invalid networkType: " + networkTypeString) + } + + networkTypeString := helpers.ConvertByteToString(networkType) + + if (reportType == "Identity"){ + + randomIdentityHash, err := identity.GetNewRandomIdentityHash(false, "") + if (err != nil) { return nil, err } + + randomIdentityHashString, _, err := identity.EncodeIdentityHashBytesToString(randomIdentityHash) + if (err != nil) { return nil, err } + + newReportMap := map[string]string{ + "NetworkType": networkTypeString, + "ReportedHash": randomIdentityHashString, + "Reason": "Unruleful conduct.", + } + + reportBytes, err := createReports.CreateReport(newReportMap) + if (err != nil) { return nil, err } + + return reportBytes, nil + } + if (reportType == "Profile"){ + + profileHash, err := helpers.GetNewRandomProfileHash(false, "", true, false) + if (err != nil) { return nil, err } + + profileHashString := encoding.EncodeBytesToHexString(profileHash[:]) + + newReportMap := map[string]string{ + "NetworkType": networkTypeString, + "ReportedHash": profileHashString, + "Reason": "Profile is unruleful", + } + + reportBytes, err := createReports.CreateReport(newReportMap) + if (err != nil) { return nil, err } + + return reportBytes, nil + } + if (reportType == "Attribute"){ + + attributeHash, err := helpers.GetNewRandomAttributeHash(false, "", false, false) + if (err != nil) { return nil, err } + + attributeHashString := encoding.EncodeBytesToHexString(attributeHash[:]) + + newReportMap := map[string]string{ + "NetworkType": networkTypeString, + "ReportedHash": attributeHashString, + "Reason": "Attribute is unruleful", + } + + reportBytes, err := createReports.CreateReport(newReportMap) + if (err != nil) { return nil, err } + + return reportBytes, nil + } + + if (reportType == "Message"){ + + textOrImage, err := helpers.GetRandomItemFromList([]string{"Text", "Image"}) + if (err != nil) { return nil, err } + + _, messageHash, messageCipherKey, err := GetFakeMessage(networkType, textOrImage) + if (err != nil) { return nil, err } + + messageHashHex := encoding.EncodeBytesToHexString(messageHash[:]) + + messageCipherKeyHex := encoding.EncodeBytesToHexString(messageCipherKey[:]) + + newReportMap := map[string]string{ + "NetworkType": networkTypeString, + "ReportedHash": messageHashHex, + "MessageCipherKey": messageCipherKeyHex, + "Reason": "Message is unruleful", + } + + reportBytes, err := createReports.CreateReport(newReportMap) + if (err != nil) { return nil, err } + + return reportBytes, nil + } + + return nil, errors.New("GetFakeReport called with invalid reportType: " + reportType) +} + +// For below function, we read random base64 images into a list in memory +// Then they can be read without having to generate the Base64 webps again + +var webpBase64ImagesList []string + +func getRandomWebpBase64Image()(string, error){ + + if (webpBase64ImagesList == nil){ + + webpBase64ImagesList = make([]string, 0, 5) + + addImageToList := func(imageBytes []byte)error{ + + imageObject, err := imagery.ConvertPNGImageFileBytesToGolangImage(imageBytes) + if (err != nil) { return err } + + imageBase64, err := imagery.ConvertImageObjectToStandardWebpBase64String(imageObject) + if (err != nil) { return err } + + webpBase64ImagesList = append(webpBase64ImagesList, imageBase64) + + return nil + } + + image1Bytes := imageFiles.PNG_Pumpkin + image2Bytes := imageFiles.PNG_SmileyA + image3Bytes := imageFiles.PNG_SmileyB + image4Bytes := imageFiles.PNG_Ultrawide + image5Bytes := imageFiles.PNG_Ultrathin + + err := addImageToList(image1Bytes) + if (err != nil) { return "", err } + err = addImageToList(image2Bytes) + if (err != nil) { return "", err } + err = addImageToList(image3Bytes) + if (err != nil) { return "", err } + err = addImageToList(image4Bytes) + if (err != nil) { return "", err } + err = addImageToList(image5Bytes) + if (err != nil) { return "", err } + } + + randomImageString, err := helpers.GetRandomItemFromList(webpBase64ImagesList) + if (err != nil) { return "", err } + + return randomImageString, nil +} + +//Outputs: +// -[]byte: Message Bytes +// -[26]byte: Message Hash +// -[32]byte: Message Cipher Key +// -error +func GetFakeMessage(networkType byte, textOrImage string)([]byte, [26]byte, [32]byte, error){ + + isValid := helpers.VerifyNetworkType(networkType) + if (isValid == false){ + networkTypeString := helpers.ConvertByteToString(networkType) + return nil, [26]byte{}, [32]byte{}, errors.New("GetFakeMessage called with invalid networkType: " + networkTypeString) + } + + if (textOrImage != "Text" && textOrImage != "Image"){ + return nil, [26]byte{}, [32]byte{}, errors.New("GetFakeMessage called with invalid textOrImage: " + textOrImage) + } + + identityType, err := helpers.GetRandomItemFromList([]string{"Mate", "Moderator"}) + if (err != nil) { return nil, [26]byte{}, [32]byte{}, err } + + senderIdentityPublicKey, senderIdentityPrivateKey, err := identity.GetNewRandomPublicPrivateIdentityKeys() + if (err != nil) { return nil, [26]byte{}, [32]byte{}, err } + + senderIdentityHash, err := identity.ConvertIdentityKeyToIdentityHash(senderIdentityPublicKey, identityType) + if (err != nil) { return nil, [26]byte{}, [32]byte{}, err } + + messageSentTime := time.Now().Unix() + + senderCurrentSecretInboxSeed, err := inbox.GetNewRandomSecretInboxSeed() + if (err != nil) { return nil, [26]byte{}, [32]byte{}, err } + + senderNextSecretInboxSeed, err := inbox.GetNewRandomSecretInboxSeed() + if (err != nil) { return nil, [26]byte{}, [32]byte{}, err } + + senderDeviceIdentifier, err := helpers.GetNewRandomDeviceIdentifier() + if (err != nil) { return nil, [26]byte{}, [32]byte{}, err } + + senderChatKeysLatestUpdateTime := time.Now().Unix() + + getMessageCommunication := func()(string, error){ + + if (textOrImage == "Text"){ + + randomText, err := GetRandomText(200, true) + if (err != nil) { return "", err } + communication := "Hello! " + randomText + return communication, nil + } + + imageBase64, err := getRandomWebpBase64Image() + if (err != nil) { return "", err } + + communication := ">!>Photo=" + imageBase64 + + return communication, nil + } + + messageCommunication, err := getMessageCommunication() + if (err != nil) { return nil, [26]byte{}, [32]byte{}, err } + + recipientIdentityHash, err := identity.GetNewRandomIdentityHash(true, identityType) + if (err != nil) { return nil, [26]byte{}, [32]byte{}, err } + + doubleSealedKeysSealerKey, err := helpers.GetNewRandom32ByteArray() + if (err != nil) { return nil, [26]byte{}, [32]byte{}, err } + + recipientNaclPublicKey, recipientNaclPrivateKey, err := nacl.GetNewRandomPublicPrivateNaclKeys() + if (err != nil) { return nil, [26]byte{}, [32]byte{}, err } + recipientKyberPublicKey, recipientKyberPrivateKey, err := kyber.GetNewRandomPublicPrivateKyberKeys() + if (err != nil) { return nil, [26]byte{}, [32]byte{}, err } + + recipientDecryptionKeySetObject := readMessages.ChatKeySet{ + NaclPublicKey: recipientNaclPublicKey, + NaclPrivateKey: recipientNaclPrivateKey, + KyberPrivateKey: recipientKyberPrivateKey, + } + + recipientChatDecryptionKeySetsList := []readMessages.ChatKeySet{recipientDecryptionKeySetObject} + + recipientInbox, err := inbox.GetPublicInboxFromIdentityHash(recipientIdentityHash) + if (err != nil) { return nil, [26]byte{}, [32]byte{}, err } + + newMessage, _, err := createMessages.CreateChatMessage(networkType, senderIdentityHash, senderIdentityPublicKey, senderIdentityPrivateKey, messageSentTime, senderCurrentSecretInboxSeed, senderNextSecretInboxSeed, senderDeviceIdentifier, senderChatKeysLatestUpdateTime, messageCommunication, recipientIdentityHash, recipientInbox, recipientNaclPublicKey, recipientKyberPublicKey, doubleSealedKeysSealerKey) + if (err != nil) { return nil, [26]byte{}, [32]byte{}, err } + + ableToRead, messageHash, _, _, messageCipherKey, _, _, _, _, _, _, _, _, err := readMessages.ReadChatMessage(newMessage, doubleSealedKeysSealerKey, recipientChatDecryptionKeySetsList) + if (err != nil) { return nil, [26]byte{}, [32]byte{}, err } + if (ableToRead == false) { + return nil, [26]byte{}, [32]byte{}, errors.New("Unable to read chat message during generate.") + } + + return newMessage, messageHash, messageCipherKey, nil +} + +func getFakeMessageSentToMe(identityType string, senderIdentityPublicKey [32]byte, senderIdentityPrivateKey [64]byte, networkType byte, textOrImage string)([]byte, error){ + + isValid := helpers.VerifyNetworkType(networkType) + if (isValid == false){ + networkTypeString := helpers.ConvertByteToString(networkType) + return nil, errors.New("getFakeMessageSentToMe called with invalid networkType: " + networkTypeString) + } + + if (textOrImage != "Text" && textOrImage != "Image"){ + return nil, errors.New("getFakeMessageSentToMe called with invalid textOrImage: " + textOrImage) + } + + senderIdentityHash, err := identity.ConvertIdentityKeyToIdentityHash(senderIdentityPublicKey, identityType) + if (err != nil) { return nil, err } + + messageSentTime := time.Now().Unix() + + senderLatestChatKeysUpdateTime := time.Now().Unix() + + senderCurrentSecretInboxSeed, err := inbox.GetNewRandomSecretInboxSeed() + if (err != nil) { return nil, err } + + senderNextSecretInboxSeed, err := inbox.GetNewRandomSecretInboxSeed() + if (err != nil) { return nil, err } + + senderDeviceIdentifier, err := helpers.GetNewRandomDeviceIdentifier() + if (err != nil) { return nil, err } + + getMessageCommunication := func()(string, error){ + + if (textOrImage == "Text"){ + + //TODO: Greet/reject + + randomText, err := GetRandomText(300, true) + if (err != nil) { return "", err } + + communication := "Hello " + identityType + "! " + randomText + + return communication, nil + } + + imageBase64, err := getRandomWebpBase64Image() + if (err != nil) { return "", err } + + communication := ">!>Photo=" + imageBase64 + + return communication, nil + } + + messageCommunication, err := getMessageCommunication() + if (err != nil) { return nil, err } + + exists, myIdentityHash, err := myIdentity.GetMyIdentityHash(identityType) + if (err != nil) { return nil, err } + if (exists == false) { + return nil, errors.New("getFakeMessageSentToMe called when my " + identityType + " identity does not exist.") + } + + myInbox, err := inbox.GetPublicInboxFromIdentityHash(myIdentityHash) + if (err != nil) { return nil, err } + + myInboxDoubleSealedKeysSealerKey, err := inbox.GetPublicInboxSealerKeyFromIdentityHash(myIdentityHash) + if (err != nil) { return nil, err } + + myIdentityExists, myNaclKey, myKyberKey, err := myChatKeys.GetMyNewestPublicChatKeys(myIdentityHash, networkType) + if (err != nil) { return nil, err } + if (myIdentityExists == false) { + return nil, errors.New("My identity not found after being found already.") + } + + newMessage, _, err := createMessages.CreateChatMessage(networkType, senderIdentityHash, senderIdentityPublicKey, senderIdentityPrivateKey, messageSentTime, senderCurrentSecretInboxSeed, senderNextSecretInboxSeed, senderDeviceIdentifier, senderLatestChatKeysUpdateTime, messageCommunication, myIdentityHash, myInbox, myNaclKey, myKyberKey, myInboxDoubleSealedKeysSealerKey) + if (err != nil) { return nil, err } + + return newMessage, nil +} + + diff --git a/internal/generate/generate_test.go b/internal/generate/generate_test.go new file mode 100644 index 0000000..5143656 --- /dev/null +++ b/internal/generate/generate_test.go @@ -0,0 +1,179 @@ +package generate_test + +import "seekia/internal/generate" + +import "seekia/resources/geneticReferences/traits" +import "seekia/resources/geneticReferences/monogenicDiseases" +import "seekia/resources/geneticReferences/polygenicDiseases" + +import "seekia/internal/helpers" +import "seekia/internal/identity" +import "seekia/internal/messaging/readMessages" +import "seekia/internal/parameters/readParameters" +import "seekia/internal/profiles/profileFormat" + +import "testing" + + +func TestGenerateParameters(t *testing.T){ + + parametersTypesList := readParameters.GetAllParametersTypesList() + + for _, parametersType := range parametersTypesList{ + + networkType, err := helpers.GetRandomByteWithinRange(1, 2) + if (err != nil) { + t.Fatalf("GetRandomByteWithinRange failed: " + err.Error()) + } + + _, err = generate.GetFakeParameters(parametersType, networkType) + if (err != nil) { + t.Fatalf("Failed to get fake parameters: " + err.Error()) + } + } +} + + +func TestGenerateProfiles(t *testing.T){ + + monogenicDiseases.InitializeMonogenicDiseaseVariables() + polygenicDiseases.InitializePolygenicDiseaseVariables() + traits.InitializeTraitVariables() + + err := profileFormat.InitializeProfileFormatVariables() + if (err != nil) { + t.Fatalf("Failed to initialize profile format variables: " + err.Error()) + } + + newIdentityPublicKey, newIdentityPrivateKey, err := identity.GetNewRandomPublicPrivateIdentityKeys() + if (err != nil) { + t.Fatalf("Cannot get random identity keys: " + err.Error()) + } + + for i:=0; i < 20; i++{ + + networkType, err := helpers.GetRandomByteWithinRange(1, 2) + if (err != nil) { + t.Fatalf("GetRandomByteWithinRange failed: " + err.Error()) + } + + _, err = generate.GetFakeProfile("Mate", newIdentityPublicKey, newIdentityPrivateKey, networkType) + if (err != nil) { + t.Fatalf("Cannot create fake mate profile: " + err.Error()) + } + } + + networkType, err := helpers.GetRandomByteWithinRange(1, 2) + if (err != nil) { + t.Fatalf("GetRandomByteWithinRange failed: " + err.Error()) + } + + _, err = generate.GetFakeProfile("Host", newIdentityPublicKey, newIdentityPrivateKey, networkType) + if (err != nil) { + t.Fatalf("Cannot create fake host profile: " + err.Error()) + } + + _, err = generate.GetFakeProfile("Moderator", newIdentityPublicKey, newIdentityPrivateKey, networkType) + if (err != nil) { + t.Fatalf("Cannot create fake moderator profile: " + err.Error()) + } +} + +func TestGenerateReviews(t *testing.T){ + + networkType, err := helpers.GetRandomByteWithinRange(1, 2) + if (err != nil) { + t.Fatalf("GetRandomByteWithinRange failed: " + err.Error()) + } + + _, err = generate.GetFakeReview("Identity", networkType) + if (err != nil){ + t.Fatalf("Cannot create fake Identity review: " + err.Error()) + } + _, err = generate.GetFakeReview("Profile", networkType) + if (err != nil){ + t.Fatalf("Cannot create fake Profile review: " + err.Error()) + } + _, err = generate.GetFakeReview("Attribute", networkType) + if (err != nil){ + t.Fatalf("Cannot create fake Attribute review: " + err.Error()) + } + _, err = generate.GetFakeReview("Message", networkType) + if (err != nil){ + t.Fatalf("Cannot create fake Message review: " + err.Error()) + } + +} + +func TestGenerateReports(t *testing.T){ + + networkType, err := helpers.GetRandomByteWithinRange(1, 2) + if (err != nil) { + t.Fatalf("GetRandomByteWithinRange failed: " + err.Error()) + } + + _, err = generate.GetFakeReport("Identity", networkType) + if (err != nil){ + t.Fatalf("Cannot create fake Identity report: " + err.Error()) + } + _, err = generate.GetFakeReport("Profile", networkType) + if (err != nil){ + t.Fatalf("Cannot create fake Profile report: " + err.Error()) + } + _, err = generate.GetFakeReport("Attribute", networkType) + if (err != nil){ + t.Fatalf("Cannot create fake Attribute report: " + err.Error()) + } + _, err = generate.GetFakeReport("Message", networkType) + if (err != nil){ + t.Fatalf("Cannot create fake Message report: " + err.Error()) + } +} + +func TestGenerateMessages(t *testing.T){ + + networkType, err := helpers.GetRandomByteWithinRange(1, 2) + if (err != nil) { + t.Fatalf("GetRandomByteWithinRange failed: " + err.Error()) + } + + { + + messageBytes, _, messageCipherKey, err := generate.GetFakeMessage(networkType, "Text") + if (err != nil){ + t.Fatalf("Cannot create fake text message: " + err.Error()) + } + + ableToRead, _, _, _, _, _, err := readMessages.ReadChatMessageWithCipherKey(messageBytes, messageCipherKey) + if (err != nil){ + t.Fatalf("Failed to read generated text message: " + err.Error()) + } + if (ableToRead == false){ + t.Fatalf("Failed to read generated text message: Invalid result.") + } + } + + { + messageBytes, _, messageCipherKey, err := generate.GetFakeMessage(networkType, "Image") + if (err != nil){ + t.Fatalf("Cannot create fake image message: " + err.Error()) + } + + ableToRead, _, _, networkType_Received, _, _, err := readMessages.ReadChatMessageWithCipherKey(messageBytes, messageCipherKey) + if (err != nil){ + t.Fatalf("Failed to read generated image message: " + err.Error()) + } + if (ableToRead == false){ + t.Fatalf("Failed to read generated image message: Invalid result.") + } + if (networkType != networkType_Received){ + t.Fatalf("Generated image message network type does not match.") + } + } +} + + + + + + diff --git a/internal/genetics/companyAnalysis/23andMe.go b/internal/genetics/companyAnalysis/23andMe.go new file mode 100644 index 0000000..75023e0 --- /dev/null +++ b/internal/genetics/companyAnalysis/23andMe.go @@ -0,0 +1,1120 @@ +package companyAnalysis + +import "seekia/internal/allowedText" +import "seekia/internal/helpers" + +import "slices" +import "strings" +import "errors" +import "maps" + + +func VerifyNeanderthalVariants_23andMe(numberOfVariants int)bool{ + + if (numberOfVariants < 0 || numberOfVariants > 7462){ + return false + } + + return true +} + +// These are the maternal and paternal halpogroups we know +// There are more that should be documented +// If a user submits one of these, the attribute will be canonical +// Otherwise, moderators must approve it +// TODO: Someone needs to figure out all of 23andMe's haplogroups. + +func GetKnownMaternalHaplogroupsList_23andMe()[]string{ + + return knownMaternalHaplogroupsList +} + +func GetKnownPaternalHaplogroupsList_23andMe()[]string{ + + return knownPaternalHaplogroupsList +} + +var knownMaternalHaplogroupsList []string = []string{ + "H3", + "U4a1a", + "J1", + "U9a", + "H1a", + "U5b1b1a1", +} + +var knownPaternalHaplogroupsList []string = []string{ + "R-L1335", + "E-P147", + "E-L29", + "T-CTS8512", + "R-P311", +} + +//Outputs: +// -bool: Is Valid +// -bool: Is Canonical +func VerifyMaternalHaplogroup_23AndMe(inputValue string)(bool, bool){ + + isKnown := slices.Contains(knownMaternalHaplogroupsList, inputValue) + if (isKnown == true){ + return true, true + } + + if (len(inputValue) > 25){ + return false, false + } + + isAllowed := allowedText.VerifyStringIsAllowed(inputValue) + if (isAllowed == false){ + return false, false + } + + containsTabsOrNewlines := helpers.CheckIfStringContainsTabsOrNewlines(inputValue) + if (containsTabsOrNewlines == true){ + return false, false + } + + return true, true +} + +//Outputs: +// -bool: Is Valid +// -bool: Is Canonical +func VerifyPaternalHaplogroup_23AndMe(inputValue string)(bool, bool){ + + isKnown := slices.Contains(knownPaternalHaplogroupsList, inputValue) + if (isKnown == true){ + return true, true + } + + if (len(inputValue) > 25){ + return false, false + } + + isAllowed := allowedText.VerifyStringIsAllowed(inputValue) + if (isAllowed == false){ + return false, false + } + + containsTabsOrNewlines := helpers.CheckIfStringContainsTabsOrNewlines(inputValue) + if (containsTabsOrNewlines == true){ + return false, false + } + + return true, true +} + +//TODO: Replace float64 with float32 everywhere where ancestry composition percentage maps exist? +// We don't need more than 1 decimal of precision, and values only span 0-100 + +//Outputs: +// -bool: Inputs are valid (all locations are valid and sum to the correct amounts) +// -string: Composition attribute +// -error: There is a bug in the function +func CreateAncestryCompositionAttribute_23andMe(continentPercentagesMap map[string]float64, regionPercentagesMap map[string]float64, subregionPercentagesMap map[string]float64)(bool, string, error){ + + // We will round down all percentages to 1 decimal, because that is the highest precision that 23andMe offers + + getLocationSection := func(locationPercentagesMap map[string]float64)string{ + + if (len(locationPercentagesMap) == 0){ + return "None" + } + + locationItemsList := make([]string, 0) + + for locationName, locationPercentage := range locationPercentagesMap{ + + if (locationPercentage == 0){ + continue + } + + percentageString := helpers.ConvertFloat64ToStringRounded(locationPercentage, 1) + + locationItem := locationName + "$" + percentageString + + locationItemsList = append(locationItemsList, locationItem) + } + + if (len(locationItemsList) == 0){ + return "None" + } + + locationSection := strings.Join(locationItemsList, "#") + + return locationSection + } + + continentsSection := getLocationSection(continentPercentagesMap) + regionsSection := getLocationSection(regionPercentagesMap) + subregionsSection := getLocationSection(subregionPercentagesMap) + + compositionAttribute := continentsSection + "+" + regionsSection + "+" + subregionsSection + + attributeIsValid, _, _, _, err := ReadAncestryCompositionAttribute_23andMe(true, compositionAttribute) + if (err != nil){ return false, "", err } + if (attributeIsValid == false){ + return false, "", nil + } + + return true, compositionAttribute, nil +} + +// This function's maps only contains locations which have no sublocations +//Outputs: +// -bool: Attribute is valid +// -map[string]float64: Continent percentages map (continent name -> Percentage of total ancestry) +// -map[string]float64: Region percentages map (region name -> Percentage of total ancestry) +// -map[string]float64: Subregion percentages map (Subregion name -> Percentage of total ancestry) +// -error (if there is a bug in this function) +func ReadAncestryCompositionAttribute_23andMe(verifyData bool, attributeValue string)(bool, map[string]float64, map[string]float64, map[string]float64, error){ + + // Attribute is formatted as follows: + + // Continents section + "+" + Regions Section + "+" + Sub-Regions section + + // Continents section is made up of continent items, or "None" + // Region section is made up of region items, or "None" + // Subregion section is made up of subregion items, or "None" + + // Each section's items are separated by "#" + + // Continent Item: Continent name + "$" + Percentage of whole + // Region Item: Region name + "$" + Percentage of whole + // Subregion Item: Subregion name + "$" + Percentage of whole + + // Only locations with no sub-locations are included + // Example: Unassigned has no sub-locations, so it is a valid continent to include + // Thus, all percentage values for all continent, region and subregion locations must sum to 100 + + attributeList := strings.Split(attributeValue, "+") + + if (len(attributeList) != 3){ + return false, nil, nil, nil, nil + } + + //Map Structure: Continent name -> Percentage of total ancestry + continentPercentagesMap := make(map[string]float64) + + //Map Structure: Region name -> Percentage of total ancestry + regionPercentagesMap := make(map[string]float64) + + //Map Structure: Subregion name -> Percentage of total ancestry + subregionPercentagesMap := make(map[string]float64) + + // Outputs: + // -error: If attribute is invalid + addLocationItemsToMap := func(locationSection string, locationMap map[string]float64)error{ + + if (locationSection == "None"){ + return nil + } + + locationItemsList := strings.Split(locationSection, "#") + + for _, locationItem := range locationItemsList{ + + locationName, locationPercentage, delimiterFound := strings.Cut(locationItem, "$") + if (delimiterFound == false){ + return errors.New("Attribute is invalid: contains invalid locationItem") + } + + locationPercentageFloat64, err := helpers.ConvertStringToFloat64(locationPercentage) + if (err != nil){ + return errors.New("Attribute is invalid: location percentage is not float.") + } + + if (locationPercentageFloat64 <= 0 || locationPercentageFloat64 > 100){ + return errors.New("Attribute is invalid: location percentage is out of range.") + } + + _, exists := locationMap[locationName] + if (exists == true){ + return errors.New("Attribute is invalid: Contains duplicate locationName") + } + + locationMap[locationName] = locationPercentageFloat64 + } + + return nil + } + + continentsSection := attributeList[0] + regionsSection := attributeList[1] + subregionsSection := attributeList[2] + + err := addLocationItemsToMap(continentsSection, continentPercentagesMap) + if (err != nil){ + return false, nil, nil, nil, nil + } + + err = addLocationItemsToMap(regionsSection, regionPercentagesMap) + if (err != nil){ + return false, nil, nil, nil, nil + } + + err = addLocationItemsToMap(subregionsSection, subregionPercentagesMap) + if (err != nil){ + return false, nil, nil, nil, nil + } + + if (verifyData == false){ + return true, continentPercentagesMap, regionPercentagesMap, subregionPercentagesMap, nil + } + + // First we make sure all percentages sum to 100 + // These should all be locations which have no sublocations + + totalSum := float64(0) + + for _, continentPercentage := range continentPercentagesMap{ + totalSum += continentPercentage + } + for _, regionPercentage := range regionPercentagesMap{ + totalSum += regionPercentage + } + for _, subregionPercentage := range subregionPercentagesMap{ + totalSum += subregionPercentage + } + + if (totalSum != 100){ + return false, nil, nil, nil, nil + } + + // Now we add all parent locations + + mapsAreValid, filledContinentPercentagesMap, _, _, err := AddMissingParentsToAncestryCompositionMaps_23andMe(continentPercentagesMap, regionPercentagesMap, subregionPercentagesMap) + if (err != nil){ return false, nil, nil, nil, err } + if (mapsAreValid == false){ + return false, nil, nil, nil, nil + } + + // Now we make sure continents total is valid + // If continents total is valid, the region/subregion totals should also be valid + + continentsTotal := float64(0) + + for _, continentPercentage := range filledContinentPercentagesMap{ + continentsTotal += continentPercentage + } + + if (continentsTotal != 100){ + // Attribute is invalid: Continents do not sum to 100 + return false, nil, nil, nil, nil + } + + return true, continentPercentagesMap, regionPercentagesMap, subregionPercentagesMap, nil +} + +// This function will return new maps with the parent locations and their percentages added +// It takes as input location maps that only contain locations which have no sublocations +// It is used to read an ancestry composition attribute, and to show a user's progress when they are building their composition +// It does not verify that the percentages sum to 100, because it receives partially completed maps during the build process +// It verifies all location names are valid +//Outputs: +// -bool: Input maps are valid +// -map[string]float64: Continent percentages map (with parents added) +// -map[string]float64: Region percentages map (with parents added) +// -map[string]float64: Subregion percentages map (with parents added) +// -error (a bug exists in the function) +func AddMissingParentsToAncestryCompositionMaps_23andMe(inputContinentPercentagesMap map[string]float64, inputRegionPercentagesMap map[string]float64, inputSubregionPercentagesMap map[string]float64)(bool, map[string]float64, map[string]float64, map[string]float64, error){ + + // We copy the input maps because the we only need to add the missing parents for display, not for encoding + // We need to maintain the integrity of the input maps + // The missing parents are only added whenever the composition attribute is displayed in the GUI. + // This allows the attribute to be smaller in size, reducing the size of profiles. + + continentPercentagesMap := maps.Clone(inputContinentPercentagesMap) + regionPercentagesMap := maps.Clone(inputRegionPercentagesMap) + subregionPercentagesMap := maps.Clone(inputSubregionPercentagesMap) + + allContinentsList := GetAncestryContinentsList_23andMe() + + // We make sure all continent names are valid + + for continentName, _ := range continentPercentagesMap{ + + isValid := slices.Contains(allContinentsList, continentName) + if (isValid == false){ + // Continent name is unknown + return false, nil, nil, nil, nil + } + } + + // This list will store the number of regions we should have when complete + // This enables us to detect unknown region names + expectedRegionsCount := 0 + + // This list will store the number of subregions we should have when complete + // This enables us to detect unknown subregion names + expectedSubregionsCount := 0 + + for _, continentName := range allContinentsList{ + + continentRegionsList, err := GetAncestryContinentRegionsList_23andMe(continentName) + if (err != nil){ + return false, nil, nil, nil, errors.New("GetAncestryContinentRegionsList_23andMe missing continent regions: " + continentName) + } + + if (len(continentRegionsList) == 0){ + // This continent has no sublocations + // If it exists, it should be included + continue + } + + _, exists := continentPercentagesMap[continentName] + if (exists == true){ + // Ancestry composition is invalid: Contains continent with sublocations + // All provided locations should have no sublocations + return false, nil, nil, nil, nil + } + + continentPercentage := float64(0) + + for _, regionName := range continentRegionsList{ + + regionSubregionsList, err := GetAncestryRegionSubregionsList_23andMe(continentName, regionName) + if (err != nil){ + return false, nil, nil, nil, errors.New("GetAncestryRegionSubregionsList_23andMe missing region subregions: " + regionName) + } + + if (len(regionSubregionsList) == 0){ + + regionPercentage, exists := regionPercentagesMap[regionName] + if (exists == true){ + continentPercentage += regionPercentage + expectedRegionsCount += 1 + } + + continue + } + + _, exists := regionPercentagesMap[regionName] + if (exists == true){ + // Ancestry composition is invalid: Contains region with sublocations + // All provided locations should have no sublocations + return false, nil, nil, nil, nil + } + + regionPercentage := float64(0) + + for _, subregionName := range regionSubregionsList{ + + subregionPercentage, exists := subregionPercentagesMap[subregionName] + if (exists == true){ + regionPercentage += subregionPercentage + expectedSubregionsCount += 1 + } + } + + if (regionPercentage != 0){ + regionPercentagesMap[regionName] = regionPercentage + expectedRegionsCount += 1 + continentPercentage += regionPercentage + } + } + + if (continentPercentage != 0){ + continentPercentagesMap[continentName] = continentPercentage + } + } + + // We make sure no unknown region/subregion names exist + + if (len(regionPercentagesMap) != expectedRegionsCount){ + // An unknown region must exist + return false, nil, nil, nil, nil + } + if (len(subregionPercentagesMap) != expectedSubregionsCount){ + // An unknown subregion must exist + return false, nil, nil, nil, nil + } + + return true, continentPercentagesMap, regionPercentagesMap, subregionPercentagesMap, nil +} + +// This function does not add the missing parent locations +//Outputs: +// -map[string]float64: Continent percentages map +// -map[string]float64: Region percentages map +// -map[string]float64: Subregion percentages map +// -error +func GetOffspringAncestryComposition_23andMe(personAAncestryCompositionAttribute string, personBAncestryCompositionAttribute string)(map[string]float64, map[string]float64, map[string]float64, error){ + + personAAttributeIsValid, personAContinentPercentagesMap, personARegionPercentagesMap, personASubregionPercentagesMap, err := ReadAncestryCompositionAttribute_23andMe(true, personAAncestryCompositionAttribute) + if (err != nil) { return nil, nil, nil, err } + if (personAAttributeIsValid == false){ + return nil, nil, nil, errors.New("GetOffspringAncestryComposition_23andMe called with invalid person A ancestry composition attribute: " + personAAncestryCompositionAttribute) + } + + personBAttributeIsValid, personBContinentPercentagesMap, personBRegionPercentagesMap, personBSubregionPercentagesMap, err := ReadAncestryCompositionAttribute_23andMe(true, personBAncestryCompositionAttribute) + if (err != nil) { return nil, nil, nil, err } + if (personBAttributeIsValid == false){ + return nil, nil, nil, errors.New("GetOffspringAncestryComposition_23andMe called with invalid person B ancestry composition attribute: " + personBAncestryCompositionAttribute) + } + + offspringContinentPercentagesMap := make(map[string]float64) + + for continentName, continentPercentage := range personAContinentPercentagesMap{ + + personAPercentage := continentPercentage/2 + + offspringContinentPercentagesMap[continentName] = personAPercentage + } + + for continentName, continentPercentage := range personBContinentPercentagesMap{ + + personBPercentage := continentPercentage/2 + + offspringContinentPercentagesMap[continentName] += personBPercentage + } + + offspringRegionPercentagesMap := make(map[string]float64) + + for regionName, regionPercentage := range personARegionPercentagesMap{ + + personAPercentage := regionPercentage/2 + + offspringRegionPercentagesMap[regionName] = personAPercentage + } + + for regionName, regionPercentage := range personBRegionPercentagesMap{ + + personBPercentage := regionPercentage/2 + + offspringRegionPercentagesMap[regionName] += personBPercentage + } + + offspringSubregionPercentagesMap := make(map[string]float64) + + for subregionName, subregionPercentage := range personASubregionPercentagesMap{ + + personAPercentage := subregionPercentage/2 + + offspringSubregionPercentagesMap[subregionName] = personAPercentage + } + + for subregionName, subregionPercentage := range personBSubregionPercentagesMap{ + + personBPercentage := subregionPercentage/2 + + offspringSubregionPercentagesMap[subregionName] += personBPercentage + } + + return offspringContinentPercentagesMap, offspringRegionPercentagesMap, offspringSubregionPercentagesMap, nil +} + + +// Ancestral similarity aims to compare how closely related a 2 user's ancestors are. + +//Outputs: +// -int: A percentage between 0 and 100 +// -error +func GetAncestralSimilarity_23andMe(verifyAttributes bool, person1AncestryCompositionAttribute string, person2AncestryCompositionAttribute string)(int, error){ + + person1AttributeIsValid, person1ContinentPercentagesMap, person1RegionPercentagesMap, person1SubregionPercentagesMap, err := ReadAncestryCompositionAttribute_23andMe(verifyAttributes, person1AncestryCompositionAttribute) + if (err != nil) { return 0, err } + if (person1AttributeIsValid == false){ + return 0, errors.New("GetAncestralSimilarity_23andMe called with invalid person1 23andMe_AncestryComposition attribute: " + person1AncestryCompositionAttribute) + } + + person2AttributeIsValid, person2ContinentPercentagesMap, person2RegionPercentagesMap, person2SubregionPercentagesMap, err := ReadAncestryCompositionAttribute_23andMe(verifyAttributes, person2AncestryCompositionAttribute) + if (err != nil) { return 0, err } + if (person2AttributeIsValid == false){ + return 0, errors.New("GetAncestralSimilarity_23andMe called with invalid person2 23andMe_AncestryComposition attribute: " + person2AncestryCompositionAttribute) + } + + continentsAreEqual := maps.Equal(person1ContinentPercentagesMap, person2ContinentPercentagesMap) + if (continentsAreEqual == true){ + + regionsAreEqual := maps.Equal(person1RegionPercentagesMap, person2RegionPercentagesMap) + if (regionsAreEqual == true){ + + subregionsAreEqual := maps.Equal(person1SubregionPercentagesMap, person2SubregionPercentagesMap) + if (subregionsAreEqual == true){ + + return 100, nil + } + } + } + + totalSimilarity := float64(0) + + // Different continents are counted as having no similarity. + // Different regions within the same continent are counted as having 70% similarity + // Different subregions within the same region are counted as having 80% similarity + + for continentName, person1ContinentPercentage := range person1ContinentPercentagesMap{ + + person2ContinentPercentage, exists := person2ContinentPercentagesMap[continentName] + if (exists == true){ + + continentSimilarity := min(person1ContinentPercentage, person2ContinentPercentage) + totalSimilarity += continentSimilarity + } + } + + // We iterate through the region/subregion maps twice + // First we account for exact region/subregion similarity + // Next, we account for same-parent-location region/subregion similarity + + person1RegionsList := helpers.GetListOfMapKeys(person1RegionPercentagesMap) + + for _, regionName := range person1RegionsList{ + + person1RegionPercentage, exists := person1RegionPercentagesMap[regionName] + if (exists == false){ + return 0, errors.New("person1RegionPercentagesMap missing regionName: " + regionName) + } + + person2RegionPercentage, exists := person2RegionPercentagesMap[regionName] + if (exists == true){ + regionSimilarity := min(person1RegionPercentage, person2RegionPercentage) + totalSimilarity += regionSimilarity + + // We subtract the region similarity so we don't + // count it twice when we account for same-parent-location location similarity + person1RegionPercentagesMap[regionName] -= regionSimilarity + person2RegionPercentagesMap[regionName] -= regionSimilarity + } + } + + for person1RegionName, person1RegionPercentage := range person1RegionPercentagesMap{ + + if (person1RegionPercentage == 0){ + continue + } + + person1RegionContinent, err := GetAncestryRegionParentContinent_23andMe(person1RegionName) + if (err != nil){ return 0, err } + + // We iterate through person2 regions and find other regions belonging to the same continent + + for person2RegionName, person2RegionPercentage := range person2RegionPercentagesMap{ + + person2RegionContinent, err := GetAncestryRegionParentContinent_23andMe(person2RegionName) + if (err != nil){ return 0, err } + + if (person1RegionContinent == person2RegionContinent){ + + regionSimilarity := min(person1RegionPercentage, person2RegionPercentage) + + // We say regions from the same continent are 70% similar + totalSimilarity += (regionSimilarity * 0.7) + + person1RegionPercentage -= regionSimilarity + person2RegionPercentagesMap[person2RegionName] -= regionSimilarity + } + + if (person1RegionPercentage == 0){ + // We have accounted for any similar regions person2 has + // We will continue on to the next region in our composition + break + } + } + } + + person1SubregionsList := helpers.GetListOfMapKeys(person1SubregionPercentagesMap) + + for _, subregionName := range person1SubregionsList{ + + person1SubregionPercentage, exists := person1SubregionPercentagesMap[subregionName] + if (exists == false){ + return 0, errors.New("person1SubregionPercentagesMap missing subregionName: " + subregionName) + } + + person2SubregionPercentage, exists := person2SubregionPercentagesMap[subregionName] + if (exists == true){ + subregionSimilarity := min(person1SubregionPercentage, person2SubregionPercentage) + totalSimilarity += subregionSimilarity + + // We subtract the subregion similarity so we don't + // count it twice when we account for same-parent-location location similarity + person1SubregionPercentagesMap[subregionName] -= subregionSimilarity + person2SubregionPercentagesMap[subregionName] -= subregionSimilarity + } + } + + for person1SubregionName, person1SubregionPercentage := range person1SubregionPercentagesMap{ + + if (person1SubregionPercentage == 0){ + continue + } + + person1SubregionRegion, err := GetAncestrySubregionParentRegion_23andMe(person1SubregionName) + if (err != nil){ return 0, err } + + // We iterate through person2 subregions and find other subregions belonging to the same region + + for person2SubregionName, person2SubregionPercentage := range person2SubregionPercentagesMap{ + + person2SubregionRegion, err := GetAncestrySubregionParentRegion_23andMe(person2SubregionName) + if (err != nil){ return 0, err } + + if (person1SubregionRegion == person2SubregionRegion){ + + subregionSimilarity := min(person1SubregionPercentage, person2SubregionPercentage) + + // We say subregions from the same region are 80% similar + totalSimilarity += (subregionSimilarity * 0.8) + + person1SubregionPercentage -= subregionSimilarity + person2SubregionPercentagesMap[person2SubregionName] -= subregionSimilarity + } + + if (person1SubregionPercentage == 0){ + // We have accounted for any similar subregions person2 has + // We will continue on to the next subregion in our composition + break + } + } + } + + totalSimilarityInt, err := helpers.FloorFloat64ToInt(totalSimilarity) + if (err != nil) { return 0, err } + + if (totalSimilarityInt < 0 || totalSimilarityInt > 100){ + totalSimilarityString := helpers.ConvertIntToString(totalSimilarityInt) + return 0, errors.New("totalSimilarity is out of bounds after calculating ancestral similarity: " + totalSimilarityString) + } + + return totalSimilarityInt, nil +} + +// This is used to get all the names needed for translations +func GetAllAncestryLocationsList_23andMe()([]string, error){ + + continentsList := GetAncestryContinentsList_23andMe() + + allLocationsList := make([]string, 0, len(continentsList)) + + for _, continentName := range continentsList{ + + allLocationsList = append(allLocationsList, continentName) + + continentRegionsList, err := GetAncestryContinentRegionsList_23andMe(continentName) + if (err != nil) { return nil, err } + + for _, regionName := range continentRegionsList{ + + allLocationsList = append(allLocationsList, regionName) + + subregionsList, err := GetAncestryRegionSubregionsList_23andMe(continentName, regionName) + if (err != nil) { return nil, err } + + allLocationsList = append(allLocationsList, subregionsList...) + } + } + + return allLocationsList, nil +} + +// Categories are structured as follows: Continent -> Region -> Subregion +func GetAncestryContinentsList_23andMe()[]string{ + + continentsList := []string{ + "Central & South Asian", + "East Asian", + "European", + "Indigenous American", + "Melanesian", + "Sub-Saharan African", + "Western Asian & North African", + "Unassigned"} + + return continentsList +} + +func GetAncestryContinentRegionsList_23andMe(continentName string)([]string, error){ + + switch continentName{ + + case "Sub-Saharan African":{ + + regionsList := []string{"West African", + "Northern East African", + "Congolese & Southern East African", + "African Hunter-Gatherer", + "Broadly Sub-Saharan African"} + + return regionsList, nil + } + case "East Asian":{ + + regionsList := []string{"North Asian", + "Chinese", + "Vietnamese", + "Filipino & Austronesian", + "Indonesian, Khmer, Thai & Myanma", + "Chinese Dai", + "Japanese", + "Korean", + "Broadly East Asian"} + return regionsList, nil + } + case "European":{ + + regionsList := []string{"Northwestern European", + "Southern European", + "Eastern European", + "Ashkenazi Jewish", + "Broadly European"} + + return regionsList, nil + } + case "Western Asian & North African":{ + + regionsList := []string{"Northern West Asian", + "Arab, Egyptian & Levantine", + "North African", + "Broadly Western Asian & North African"} + return regionsList, nil + } + case "Central & South Asian":{ + + regionsList := []string{"Central Asian", + "Northern Indian & Pakistani", + "Bengali & Northeast Indian", + "Gujarati Patidar", + "Southern Indian Subgroup", + "Southern Indian & Sri Lankan", + "Malayali Subgroup", + "Broadly Central & South Asian"} + + return regionsList, nil + } + case "Melanesian", "Indigenous American", "Unassigned":{ + + regionsList := make([]string, 0) + + return regionsList, nil + } + } + + return nil, errors.New("GetAncestryContinentRegionsList_23andMe called with unknown continentName: " + continentName) +} + + +func GetAncestryRegionSubregionsList_23andMe(continentName string, regionName string)([]string, error){ + + switch continentName{ + + case "Sub-Saharan African":{ + + switch regionName{ + + case "West African":{ + + subregionsList := []string{"Senegambian & Guinean", + "Ghanaian, Liberian & Sierra Leonean", + "Nigerian", + "Broadly West African"} + + return subregionsList, nil + } + case "Northern East African":{ + + subregionsList := []string{"Sudanese", + "Ethiopian & Eritrean", + "Somali", + "Broadly Northern East African"} + + return subregionsList, nil + } + case "Congolese & Southern East African":{ + + subregionsList := []string{"Angolan & Congolese", + "Southern East African", + "Broadly Congolese & Southern East African"} + + return subregionsList, nil + } + + case "African Hunter-Gatherer", "Broadly Sub-Saharan African":{ + emptyList := make([]string, 0) + + return emptyList, nil + } + } + return nil, errors.New("GetAncestryRegionSubregionsList_23andMe called with unknown region for " + continentName + ": " + regionName) + } + case "East Asian":{ + + switch regionName{ + + case "North Asian":{ + + subregionsList := []string{"Siberian", + "Manchurian & Mongolian", + "Broadly North Asian"} + + return subregionsList, nil + } + case "Chinese":{ + + subregionsList := []string{"Northern Chinese & Tibetan", + "Southern Chinese & Taiwanese", + "South Chinese", + "Broadly Chinese"} + return subregionsList, nil + } + case "Vietnamese", + "Filipino & Austronesian", + "Indonesian, Khmer, Thai & Myanma", + "Chinese Dai", + "Japanese", + "Korean", + "Broadly East Asian":{ + + subregionsList := make([]string, 0) + + return subregionsList, nil + } + } + + return nil, errors.New("GetAncestryRegionSubregionsList_23andMe called with unknown region for " + continentName + ": " + regionName) + } + + case "European":{ + + switch regionName { + + case "Northwestern European":{ + + subregionsList := []string{"British & Irish", + "Finnish", + "French & German", + "Scandinavian", + "Broadly Northwestern European"} + + return subregionsList, nil + } + case "Southern European":{ + + subregionsList := []string{"Greek & Balkan", + "Spanish & Portuguese", + "Italian", + "Sardinian", + "Broadly Southern European"} + + return subregionsList, nil + } + + case "Eastern European", "Ashkenazi Jewish", "Broadly European":{ + + emptyList := make([]string, 0) + + return emptyList, nil + } + } + + return nil, errors.New("GetAncestryRegionSubregionsList_23andMe called with unknown region for " + continentName + ": " + regionName) + } + + case "Western Asian & North African":{ + + switch regionName{ + + case "Northern West Asian":{ + + subregionsList := []string{"Cypriot", + "Anatolian", + "Iranian, Caucasian & Mesopotamian", + "Broadly Northern West Asian"} + + return subregionsList, nil + } + case "Arab, Egyptian & Levantine":{ + + subregionsList := []string{"Peninsular Arab", + "Levantine", + "Egyptian", + "Coptic Egyptian", + "Broadly Arab, Egyptian, & Levantine"} + + return subregionsList, nil + } + case "North African", "Broadly Western Asian & North African":{ + + emptyList := make([]string, 0) + + return emptyList, nil + } + } + + return nil, errors.New("GetAncestryRegionSubregionsList_23andMe called with unknown region for " + continentName + ": " + regionName) + } + + case "Central & South Asian":{ + + switch regionName{ + + case "Central Asian", + "Northern Indian & Pakistani", + "Bengali & Northeast Indian", + "Gujarati Patidar", + "Southern Indian Subgroup", + "Southern Indian & Sri Lankan", + "Malayali Subgroup", + "Broadly Central & South Asian":{ + + emptyList := make([]string, 0) + + return emptyList, nil + } + } + + return nil, errors.New("GetAncestryRegionSubregionsList_23andMe called with unknown region for " + continentName + ": " + regionName) + } + } + + return nil, errors.New("GetAncestryRegionSubregionsList_23andMe called with unknown continent name: " + continentName) +} + + + +// This returns the continent that a region belongs to +func GetAncestryRegionParentContinent_23andMe(regionName string)(string, error){ + + switch regionName{ + + case "West African", + "Northern East African", + "Congolese & Southern East African", + "African Hunter-Gatherer", + "Broadly Sub-Saharan African":{ + + return "Sub-Saharan African", nil + } + + case "North Asian", + "Chinese", + "Vietnamese", + "Filipino & Austronesian", + "Indonesian, Khmer, Thai & Myanma", + "Chinese Dai", + "Japanese", + "Korean", + "Broadly East Asian":{ + + return "East Asian", nil + } + + case "Northwestern European", + "Southern European", + "Eastern European", + "Ashkenazi Jewish", + "Broadly European":{ + + return "European", nil + } + case "Northern West Asian", + "Arab, Egyptian & Levantine", + "North African", + "Broadly Western Asian & North African":{ + + return "Western Asian & North African", nil + } + case "Central Asian", + "Northern Indian & Pakistani", + "Bengali & Northeast Indian", + "Gujarati Patidar", + "Southern Indian Subgroup", + "Southern Indian & Sri Lankan", + "Malayali Subgroup", + "Broadly Central & South Asian":{ + + return "Central & South Asian", nil + } + } + + return "", errors.New("GetAncestryRegionContinent_23andMe called with unknown region: " + regionName) +} + +// This returns the region that a subregion belongs to +func GetAncestrySubregionParentRegion_23andMe(subregionName string)(string, error){ + + switch subregionName{ + + // Continent == "Sub-Saharan African" + + case "Senegambian & Guinean", + "Ghanaian, Liberian & Sierra Leonean", + "Nigerian", + "Broadly West African":{ + + return "West African", nil + } + case "Sudanese", + "Ethiopian & Eritrean", + "Somali", + "Broadly Northern East African":{ + + return "Northern East African", nil + } + case "Angolan & Congolese", + "Southern East African", + "Broadly Congolese & Southern East African":{ + + return "Congolese & Southern East African", nil + } + + // Continent == "East Asian" + + case "Siberian", + "Manchurian & Mongolian", + "Broadly North Asian":{ + + return "North Asian", nil + } + case "Northern Chinese & Tibetan", + "Southern Chinese & Taiwanese", + "South Chinese", + "Broadly Chinese":{ + + return "Chinese", nil + } + + // Continent == "European" + + case "British & Irish", + "Finnish", + "French & German", + "Scandinavian", + "Broadly Northwestern European":{ + + return "Northwestern European", nil + } + case "Greek & Balkan", + "Spanish & Portuguese", + "Italian", + "Sardinian", + "Broadly Southern European":{ + + return "Southern European", nil + } + + // Continent == "Western Asian & North African" + + case "Cypriot", + "Anatolian", + "Iranian, Caucasian & Mesopotamian", + "Broadly Northern West Asian":{ + + return "Northern West Asian", nil + } + case "Peninsular Arab", + "Levantine", + "Egyptian", + "Coptic Egyptian", + "Broadly Arab, Egyptian, & Levantine":{ + + return "Arab, Egyptian & Levantine", nil + } + } + + return "", errors.New("GetAncestrySubregionRegion_23andMe called with unknown subregion: " + subregionName) +} + + diff --git a/internal/genetics/companyAnalysis/companyAnalysis.go b/internal/genetics/companyAnalysis/companyAnalysis.go new file mode 100644 index 0000000..9936439 --- /dev/null +++ b/internal/genetics/companyAnalysis/companyAnalysis.go @@ -0,0 +1,13 @@ + +// companyAnalysis provides functions to read and format the information that genetic analysis companies provide +// This information can be shared on a Seekia profile. + +package companyAnalysis + +// The types of information for each company are described below: + +// 23andMe +// -Ancestry Composition +// -Neanderthal Variants +// -Maternal Haplogroup +// -Paternal Haplogroup diff --git a/internal/genetics/companyAnalysis/companyAnalysis_test.go b/internal/genetics/companyAnalysis/companyAnalysis_test.go new file mode 100644 index 0000000..15a6818 --- /dev/null +++ b/internal/genetics/companyAnalysis/companyAnalysis_test.go @@ -0,0 +1,242 @@ +package companyAnalysis_test + +import "seekia/internal/genetics/companyAnalysis" + +import "seekia/internal/helpers" + +import "testing" +import "strings" +import "errors" + +func TestAncestralLocations_23andMe(t *testing.T){ + + allAncestryLocationsList, err := companyAnalysis.GetAllAncestryLocationsList_23andMe() + if (err != nil){ + t.Fatalf("Failed to get all 23andMe ancestry locations list: " + err.Error()) + } + + containsDuplicates, duplicateLocationName := helpers.CheckIfListContainsDuplicates(allAncestryLocationsList) + if (containsDuplicates == true){ + t.Fatalf("allAncestryLocationsList contains duplicate locationName: " + duplicateLocationName) + } + + for _, locationName := range allAncestryLocationsList{ + + containsAny := strings.ContainsAny(locationName, "$+#") + if (containsAny == true){ + t.Fatalf("allAncestryLocationsList contains invalid location: " + locationName) + } + } + + ancestryContinentsList := companyAnalysis.GetAncestryContinentsList_23andMe() + + for _, continentName := range ancestryContinentsList{ + + regionsList, err := companyAnalysis.GetAncestryContinentRegionsList_23andMe(continentName) + if (err != nil){ + t.Fatalf("GetAncestryContinentRegionsList_23andMe failed: " + err.Error()) + } + + for _, regionName := range regionsList{ + + parentContinentName, err := companyAnalysis.GetAncestryRegionParentContinent_23andMe(regionName) + if (err != nil){ + t.Fatalf("GetAncestryRegionContinent_23andMe failed: " + err.Error()) + } + + if (parentContinentName != continentName){ + t.Fatalf("GetAncestryRegionParentContinent_23andMe returning invalid parent continent for region: " + regionName + ". Output: " + parentContinentName) + } + + subregionsList, err := companyAnalysis.GetAncestryRegionSubregionsList_23andMe(continentName, regionName) + if (err != nil){ + t.Fatalf("GetAncestryRegionSubregionsList_23andMe failed: " + err.Error()) + } + + for _, subregionName := range subregionsList{ + + parentRegionName, err := companyAnalysis.GetAncestrySubregionParentRegion_23andMe(subregionName) + if (err != nil){ + t.Fatalf("GetAncestryRegionContinent_23andMe failed: " + err.Error()) + } + if (parentRegionName != regionName){ + t.Fatalf("GetAncestrySubregionParentRegion_23andMe returning invalid parent region for subregion: " + subregionName + ". Output: " + parentRegionName) + } + + } + } + } + + // TODO: Create a composition using all locations to learn the maximum size of this attribute +} + + +func TestAncestralSimilarity_23andMe(t *testing.T){ + + testAncestralSimilarity := func(person1ContinentsMap map[string]float64, person1RegionsMap map[string]float64, person1SubregionsMap map[string]float64, person2ContinentsMap map[string]float64, person2RegionsMap map[string]float64, person2SubregionsMap map[string]float64, expectedAncestralSimilarity int)error{ + + inputsAreValid, person1AncestryCompositionAttribute, err := companyAnalysis.CreateAncestryCompositionAttribute_23andMe(person1ContinentsMap, person1RegionsMap, person1SubregionsMap) + if (err != nil){ return err } + if (inputsAreValid == false){ + return errors.New("testAncestralSimilarity called with invalid person1 location maps.") + } + + inputsAreValid, person2AncestryCompositionAttribute, err := companyAnalysis.CreateAncestryCompositionAttribute_23andMe(person2ContinentsMap, person2RegionsMap, person2SubregionsMap) + if (err != nil){ return err } + if (inputsAreValid == false){ + return errors.New("testAncestralSimilarity called with invalid person2 location maps.") + } + + ancestralSimilarity, err := companyAnalysis.GetAncestralSimilarity_23andMe(true, person1AncestryCompositionAttribute, person2AncestryCompositionAttribute) + if (err != nil) { return err } + + if (ancestralSimilarity != expectedAncestralSimilarity){ + ancestralSimilarityString := helpers.ConvertIntToString(ancestralSimilarity) + expectedAncestralSimilarityString := helpers.ConvertIntToString(expectedAncestralSimilarity) + return errors.New("testAncestralSimilarity returning unexpected ancestral similarity: " + ancestralSimilarityString + " != " + expectedAncestralSimilarityString) + } + + return nil + } + + { + person1ContinentsMap := map[string]float64{ + "Unassigned": 100, + } + person1RegionsMap := map[string]float64{} + person1SubregionsMap := map[string]float64{} + + person2ContinentsMap := map[string]float64{ + "Melanesian": 100, + } + person2RegionsMap := map[string]float64{} + person2SubregionsMap := map[string]float64{} + + err := testAncestralSimilarity(person1ContinentsMap, person1RegionsMap, person1SubregionsMap, person2ContinentsMap, person2RegionsMap, person2SubregionsMap, 0) + if (err != nil){ + t.Fatalf("testAncestralSimilarity failed test 1: " + err.Error()) + } + } + + { + person1ContinentsMap := map[string]float64{ + "Melanesian": 100, + } + person1RegionsMap := map[string]float64{} + person1SubregionsMap := map[string]float64{} + + person2ContinentsMap := map[string]float64{ + "Melanesian": 100, + } + person2RegionsMap := map[string]float64{} + person2SubregionsMap := map[string]float64{} + + err := testAncestralSimilarity(person1ContinentsMap, person1RegionsMap, person1SubregionsMap, person2ContinentsMap, person2RegionsMap, person2SubregionsMap, 100) + if (err != nil){ + t.Fatalf("testAncestralSimilarity failed test 2: " + err.Error()) + } + } + + { + person1ContinentsMap := map[string]float64{ + "Melanesian": 50, + "Unassigned": 50, + } + person1RegionsMap := map[string]float64{} + person1SubregionsMap := map[string]float64{} + + person2ContinentsMap := map[string]float64{ + "Melanesian": 50, + "Unassigned": 50, + } + person2RegionsMap := map[string]float64{} + person2SubregionsMap := map[string]float64{} + + err := testAncestralSimilarity(person1ContinentsMap, person1RegionsMap, person1SubregionsMap, person2ContinentsMap, person2RegionsMap, person2SubregionsMap, 100) + if (err != nil){ + t.Fatalf("testAncestralSimilarity failed test 3: " + err.Error()) + } + } + + { + person1ContinentsMap := map[string]float64{ + "Melanesian": 100, + } + person1RegionsMap := map[string]float64{} + person1SubregionsMap := map[string]float64{} + + person2ContinentsMap := map[string]float64{ + "Melanesian": 50, + "Unassigned": 50, + } + person2RegionsMap := map[string]float64{} + person2SubregionsMap := map[string]float64{} + + err := testAncestralSimilarity(person1ContinentsMap, person1RegionsMap, person1SubregionsMap, person2ContinentsMap, person2RegionsMap, person2SubregionsMap, 50) + if (err != nil){ + t.Fatalf("testAncestralSimilarity failed test 4: " + err.Error()) + } + } + + { + person1ContinentsMap := map[string]float64{} + person1RegionsMap := map[string]float64{} + person1SubregionsMap := map[string]float64{ + "Siberian": 100, + } + + person2ContinentsMap := map[string]float64{} + person2RegionsMap := map[string]float64{} + person2SubregionsMap := map[string]float64{ + "Siberian": 100, + } + + err := testAncestralSimilarity(person1ContinentsMap, person1RegionsMap, person1SubregionsMap, person2ContinentsMap, person2RegionsMap, person2SubregionsMap, 100) + if (err != nil){ + t.Fatalf("testAncestralSimilarity failed test 5: " + err.Error()) + } + } + + { + person1ContinentsMap := map[string]float64{} + person1RegionsMap := map[string]float64{} + person1SubregionsMap := map[string]float64{ + "Siberian": 100, + } + + person2ContinentsMap := map[string]float64{} + person2RegionsMap := map[string]float64{} + person2SubregionsMap := map[string]float64{ + "Siberian": 100, + } + + err := testAncestralSimilarity(person1ContinentsMap, person1RegionsMap, person1SubregionsMap, person2ContinentsMap, person2RegionsMap, person2SubregionsMap, 100) + if (err != nil){ + t.Fatalf("testAncestralSimilarity failed test 6: " + err.Error()) + } + } + + { + person1ContinentsMap := map[string]float64{} + person1RegionsMap := map[string]float64{} + person1SubregionsMap := map[string]float64{ + "Siberian": 100, + } + + person2ContinentsMap := map[string]float64{} + person2RegionsMap := map[string]float64{} + person2SubregionsMap := map[string]float64{ + "Manchurian & Mongolian": 100, + } + + err := testAncestralSimilarity(person1ContinentsMap, person1RegionsMap, person1SubregionsMap, person2ContinentsMap, person2RegionsMap, person2SubregionsMap, 80) + if (err != nil){ + t.Fatalf("testAncestralSimilarity failed test 7: " + err.Error()) + } + } + + //TODO: Add more tests. +} + + + diff --git a/internal/genetics/createGeneticAnalysis/createGeneticAnalysis.go b/internal/genetics/createGeneticAnalysis/createGeneticAnalysis.go new file mode 100644 index 0000000..d288cdb --- /dev/null +++ b/internal/genetics/createGeneticAnalysis/createGeneticAnalysis.go @@ -0,0 +1,2519 @@ + +// createGeneticAnalysis provides functions to create a genetic analysis +// These are performed on one or more genome files. +// They produce 3 kinds of results: Monogenic Diseases, Polygenic Diseases and Traits +// They can be performed on a Person or a Couple +// Couple analyses provide an analysis of the prospective offspring of the couple + +package createGeneticAnalysis + +// TODO: Some of the probabilities produced by this package are wrong +// In this package, we are assuming that genetic recombination (the formation of the genetic sequences for the sperm/eggs) happens randomly for each allele locus +// In reality, the recombination break points occur less often, and larger portions of each chromosome remain intact. +// This effects the estimates and probabilities for all of the generated analyses +// In particular, the probability of passing a defective gene does not increase as much as this package currently +// estimates that it does, in the case of multiple defects existing in the same gene. +// Also, based on my research, I believe that recombination break points are less likely to occur within genes, meaning they are more likely to occur at the gene boundaries (codons) +// We need to remedy this problem and fix this package +// Research gene linkage and recombination to understand more. +// +// Fixing this problem may require us to only allow will-pass-a-variant probabilities to be calculated from phased loci. +// The phase of a loci becomes much more important in determining the will-pass-a-variant probability +// Having multiple variants within a gene might not increase the probability of passing a variant, assuming all of those variants were on the same chromosome +// Thus, we need phased loci to determine an accurate will-pass-a-variant probability +// We will still be able to determine will-pass-a-variant probabilities for users who only have 1 variant on 1 base, +// regardless of if their loci phase is known or not. That probability is 50%. + + + +// TODO: We want to eventually use neural nets for both trait and polygenic disease analysis +// These will be trained on a set of genomes and will output a probability analysis for each trait/disease +// This is only possible once we get access to the necessary training data +// +// This is how offspring trait prediction could work with the neural net model: +// Both users will share all relevant SNPS base pairs that determine the trait on their profile. +// Each location has 4 possible outcomes, so for 1000 SNPs, there are 4^1000 possible offspring outcomes for a given couple. (this +// is actually too high because recombination break points do not occur at each locus, see genetic linkage) +// This is too many options for us to check all of them. +// Seekia will create 100 offspring that would be produced from both users, and run each offspring through the neural net. +// Each offspring would be different. The allele from each parent for each SNP would be randomly chosen. +// The user can choose how many prospective offspring to create in the settings. +// More offspring will take longer, but will yield a more accurate trait probability. +// Seekia will show the the average trait result and a chart showing the trait results for all created offspring. + + +// TODO: We should not add any passing-a-variant probabilities that are dependent on an unknown phase to user profiles +// For example, if the phase for any mutated bases are unknown, and this unknown phase effects the probability of +// the user passing a variant, that probability should not be shared on the user's profile. +// Seekia currently shares these probabilities on user profiles. +// Only probabilities which are fully known should be shared. Otherwise, the user is sharing +// information that is not fully accurate but is instead speculative. +// +// We must add this warning into the GUI for a user and couple genetic analyses too. +// Basically, we need to make it clear that some probabilities are based completely on random genetic chance, and others +// are based on our limited understanding of the user's genome (due to unphased data). + + +// TODO: Add the ability to weight different genome files based on their reliability. +// Some files are much more accurate because they record each location many times. + + +import "seekia/resources/geneticReferences/locusMetadata" +import "seekia/resources/geneticReferences/monogenicDiseases" +import "seekia/resources/geneticReferences/polygenicDiseases" +import "seekia/resources/geneticReferences/traits" + +import "seekia/internal/genetics/locusValue" +import "seekia/internal/genetics/prepareRawGenomes" +import "seekia/internal/helpers" + +import "errors" +import "math" +import "encoding/json" +import "strings" +import "slices" + + +func verifyBasePair(inputBasePair string)bool{ + + base1, base2, delimiterFound := strings.Cut(inputBasePair, ";") + if (delimiterFound == false){ + return false + } + + // I = Insertion + // D = Deletion + + validBasesList := []string{"C", "A", "T", "G", "I", "D"} + + baseIsValid := slices.Contains(validBasesList, base1) + if (baseIsValid == false){ + return false + } + + baseIsValid = slices.Contains(validBasesList, base2) + if (baseIsValid == false){ + return false + } + + return true +} + +//Inputs: +// -map[string]string: Genome Identifier -> Genome Raw data string +//Outputs: +// -bool: Process completed (it was not stopped manually before completion) +// -string: New Genetic analysis string +// -error +func CreatePersonGeneticAnalysis(genomesList []prepareRawGenomes.RawGenomeWithMetadata, updatePercentageCompleteFunction func(int)error, checkIfProcessIsStopped func()bool)(bool, string, error){ + + prepareRawGenomesUpdatePercentageCompleteFunction := func(newPercentage int)error{ + + newPercentageCompletion, err := helpers.ScaleNumberProportionally(true, newPercentage, 0, 100, 0, 50) + if (err != nil){ return err } + + err = updatePercentageCompleteFunction(newPercentageCompletion) + if (err != nil) { return err } + + return nil + } + + genomesWithMetadataList, allRawGenomeIdentifiersList, multipleGenomesExist, onlyExcludeConflictsGenomeIdentifier, onlyIncludeSharedGenomeIdentifier, err := prepareRawGenomes.GetGenomesWithMetadataListFromRawGenomesList(genomesList, prepareRawGenomesUpdatePercentageCompleteFunction) + if (err != nil) { return false, "", err } + + combinedGenomesExistString := helpers.ConvertBoolToYesOrNoString(multipleGenomesExist) + + metadataItem := map[string]string{ + "ItemType": "Metadata", + "AnalysisVersion": "1", + "AnalysisType": "Person", + "CombinedGenomesExist": combinedGenomesExistString, + } + + if (multipleGenomesExist == true){ + + metadataItem["OnlyExcludeConflictsGenomeIdentifier"] = onlyExcludeConflictsGenomeIdentifier + metadataItem["OnlyIncludeSharedGenomeIdentifier"] = onlyIncludeSharedGenomeIdentifier + + allRawGenomeIdentifiersListString := strings.Join(allRawGenomeIdentifiersList, "+") + metadataItem["AllRawGenomeIdentifiersList"] = allRawGenomeIdentifiersListString + + } else { + + genomeIdentifier := allRawGenomeIdentifiersList[0] + + metadataItem["GenomeIdentifier"] = genomeIdentifier + } + + processIsStopped := checkIfProcessIsStopped() + if (processIsStopped == true){ + return false, "", nil + } + + geneticAnalysisMapList := []map[string]string{metadataItem} + + monogenicDiseasesList, err := monogenicDiseases.GetMonogenicDiseaseObjectsList() + if (err != nil) { return false, "", err } + + for _, monogenicDiseaseObject := range monogenicDiseasesList{ + + diseaseName := monogenicDiseaseObject.DiseaseName + + diseaseAnalysisMap, variantsMapList, err := getMonogenicDiseaseAnalysis(genomesWithMetadataList, monogenicDiseaseObject) + if (err != nil) { return false, "", err } + + diseaseAnalysisMap["ItemType"] = "MonogenicDisease" + diseaseAnalysisMap["DiseaseName"] = diseaseName + + geneticAnalysisMapList = append(geneticAnalysisMapList, diseaseAnalysisMap) + geneticAnalysisMapList = append(geneticAnalysisMapList, variantsMapList...) + } + + polygenicDiseaseObjectsList, err := polygenicDiseases.GetPolygenicDiseaseObjectsList() + if (err != nil) { return false, "", err } + + for _, diseaseObject := range polygenicDiseaseObjectsList{ + + diseaseAnalysisMap, diseaseLociMapList, err := getPolygenicDiseaseAnalysis(genomesWithMetadataList, diseaseObject) + if (err != nil) { return false, "", err } + + diseaseName := diseaseObject.DiseaseName + + diseaseAnalysisMap["ItemType"] = "PolygenicDisease" + diseaseAnalysisMap["DiseaseName"] = diseaseName + + geneticAnalysisMapList = append(geneticAnalysisMapList, diseaseAnalysisMap) + geneticAnalysisMapList = append(geneticAnalysisMapList, diseaseLociMapList...) + } + + traitObjectsList, err := traits.GetTraitObjectsList() + if (err != nil) { return false, "", err } + + for _, traitObject := range traitObjectsList{ + + traitAnalysisMap, traitRulesMapList, err := getTraitAnalysis(genomesWithMetadataList, traitObject) + if (err != nil) { return false, "", err } + + traitName := traitObject.TraitName + + traitAnalysisMap["ItemType"] = "Trait" + traitAnalysisMap["TraitName"] = traitName + + geneticAnalysisMapList = append(geneticAnalysisMapList, traitAnalysisMap) + geneticAnalysisMapList = append(geneticAnalysisMapList, traitRulesMapList...) + } + + analysisBytes, err := json.MarshalIndent(geneticAnalysisMapList, "", "\t") + if (err != nil) { return false, "", err } + + analysisString := string(analysisBytes) + + return true, analysisString, nil +} + +//Outputs: +// -bool: Process completed (was not manually stopped mid-way) +// -string: Couple genetic analysis string +// -error +func CreateCoupleGeneticAnalysis(personAGenomesList []prepareRawGenomes.RawGenomeWithMetadata, personBGenomesList []prepareRawGenomes.RawGenomeWithMetadata, updatePercentageCompleteFunction func(int)error, checkIfProcessIsStopped func()bool)(bool, string, error){ + + personAPrepareRawGenomesUpdatePercentageCompleteFunction := func(newPercentage int)error{ + + newPercentageCompletion, err := helpers.ScaleNumberProportionally(true, newPercentage, 0, 100, 0, 25) + if (err != nil){ return err } + + err = updatePercentageCompleteFunction(newPercentageCompletion) + if (err != nil) { return err } + + return nil + } + + personAGenomesWithMetadataList, allPersonARawGenomeIdentifiersList, personAHasMultipleGenomes, personAOnlyExcludeConflictsGenomeIdentifier, personAOnlyIncludeSharedGenomeIdentifier, err := prepareRawGenomes.GetGenomesWithMetadataListFromRawGenomesList(personAGenomesList, personAPrepareRawGenomesUpdatePercentageCompleteFunction) + if (err != nil) { return false, "", err } + + processIsStopped := checkIfProcessIsStopped() + if (processIsStopped == true){ + return false, "", nil + } + + personBPrepareRawGenomesUpdatePercentageCompleteFunction := func(newPercentage int)error{ + + newPercentageCompletion, err := helpers.ScaleNumberProportionally(true, newPercentage, 0, 100, 25, 50) + if (err != nil){ return err } + + err = updatePercentageCompleteFunction(newPercentageCompletion) + if (err != nil) { return err } + + return nil + } + + personBGenomesWithMetadataList, allPersonBRawGenomeIdentifiersList, personBHasMultipleGenomes, personBOnlyExcludeConflictsGenomeIdentifier, personBOnlyIncludeSharedGenomeIdentifier, err := prepareRawGenomes.GetGenomesWithMetadataListFromRawGenomesList(personBGenomesList, personBPrepareRawGenomesUpdatePercentageCompleteFunction) + if (err != nil) { return false, "", err } + + processIsStopped = checkIfProcessIsStopped() + if (processIsStopped == true){ + return false, "", nil + } + + // The analysis will analyze either 1 or 2 genome pairs + // The gui will display the results from each pair + //Outputs: + // -string: Pair 1 Person A Genome Identifier + // -string: Pair 1 Person B Genome Identifier + // -bool: Second pair exists + // -string: Pair 2 Person A Genome Identifier + // -string: Pair 2 Person B Genome Identifier + // -error + getGenomePairsToAnalyze := func()(string, string, bool, string, string, error){ + + if (personAHasMultipleGenomes == true && personBHasMultipleGenomes == true){ + + return personAOnlyExcludeConflictsGenomeIdentifier, personBOnlyExcludeConflictsGenomeIdentifier, true, personAOnlyIncludeSharedGenomeIdentifier, personBOnlyIncludeSharedGenomeIdentifier, nil + } + if (personAHasMultipleGenomes == true && personBHasMultipleGenomes == false){ + + personBGenomeIdentifier := allPersonBRawGenomeIdentifiersList[0] + + return personAOnlyExcludeConflictsGenomeIdentifier, personBGenomeIdentifier, true, personAOnlyIncludeSharedGenomeIdentifier, personBGenomeIdentifier, nil + } + if (personAHasMultipleGenomes == false && personBHasMultipleGenomes == true){ + + personAGenomeIdentifier := allPersonARawGenomeIdentifiersList[0] + + return personAGenomeIdentifier, personBOnlyExcludeConflictsGenomeIdentifier, true, personAGenomeIdentifier, personBOnlyIncludeSharedGenomeIdentifier, nil + } + + //personAHasMultipleGenomes == false && personBHasMultipleGenomes == false + + personAGenomeIdentifier := allPersonARawGenomeIdentifiersList[0] + personBGenomeIdentifier := allPersonBRawGenomeIdentifiersList[0] + + return personAGenomeIdentifier, personBGenomeIdentifier, false, "", "", nil + } + + pair1PersonAGenomeIdentifier, pair1PersonBGenomeIdentifier, genomePair2Exists, pair2PersonAGenomeIdentifier, pair2PersonBGenomeIdentifier, err := getGenomePairsToAnalyze() + if (err != nil){ return false, "", err } + + personAHasMultipleGenomesString := helpers.ConvertBoolToYesOrNoString(personAHasMultipleGenomes) + personBHasMultipleGenomesString := helpers.ConvertBoolToYesOrNoString(personBHasMultipleGenomes) + + secondPairExistsString := helpers.ConvertBoolToYesOrNoString(genomePair2Exists) + + metadataItem := map[string]string{ + "ItemType": "Metadata", + "AnalysisVersion": "1", + "AnalysisType": "Couple", + "PersonAHasMultipleGenomes": personAHasMultipleGenomesString, + "PersonBHasMultipleGenomes": personBHasMultipleGenomesString, + "Pair1PersonAGenomeIdentifier": pair1PersonAGenomeIdentifier, + "Pair1PersonBGenomeIdentifier": pair1PersonBGenomeIdentifier, + "SecondPairExists": secondPairExistsString, + } + + if (genomePair2Exists == true){ + + // At least 1 of the people have multiple genomes + + metadataItem["Pair2PersonAGenomeIdentifier"] = pair2PersonAGenomeIdentifier + metadataItem["Pair2PersonBGenomeIdentifier"] = pair2PersonBGenomeIdentifier + + if (personAHasMultipleGenomes == true){ + metadataItem["PersonAOnlyExcludeConflictsGenomeIdentifier"] = personAOnlyExcludeConflictsGenomeIdentifier + metadataItem["PersonAOnlyIncludeSharedGenomeIdentifier"] = personAOnlyIncludeSharedGenomeIdentifier + } + + if (personBHasMultipleGenomes == true){ + metadataItem["PersonBOnlyExcludeConflictsGenomeIdentifier"] = personBOnlyExcludeConflictsGenomeIdentifier + metadataItem["PersonBOnlyIncludeSharedGenomeIdentifier"] = personBOnlyIncludeSharedGenomeIdentifier + } + } + + newAnalysisMapList := []map[string]string{metadataItem} + + //First we add monogenic disease probabilities + + monogenicDiseasesList, err := monogenicDiseases.GetMonogenicDiseaseObjectsList() + if (err != nil) { return false, "", err } + + for _, diseaseObject := range monogenicDiseasesList{ + + diseaseName := diseaseObject.DiseaseName + variantsList := diseaseObject.VariantsList + diseaseIsDominantOrRecessive := diseaseObject.DominantOrRecessive + + offspringDiseaseMap := map[string]string{ + "ItemType": "MonogenicDisease", + "DiseaseName": diseaseName, + } + + personADiseaseAnalysisMap, personAVariantsMapList, err := getMonogenicDiseaseAnalysis(personAGenomesWithMetadataList, diseaseObject) + if (err != nil) { return false, "", err } + + personBDiseaseAnalysisMap, personBVariantsMapList, err := getMonogenicDiseaseAnalysis(personBGenomesWithMetadataList, diseaseObject) + if (err != nil) { return false, "", err } + + // This will calculate the probability of monogenic disease for the offspring from the two specified genomes + // It then adds the pair entry to the offspringDiseaseMap + addPairProbabilitiesToDiseaseMap := func(personAGenomeIdentifier string, personBGenomeIdentifier string)error{ + + //Outputs: + // -bool: Probability is known + // -int: Probability of passing a disease variant (value between 0 and 100) + // -int: Number of variants tested + // -error + getPersonWillPassDiseaseVariantProbability := func(personDiseaseAnalysisMap map[string]string, genomeIdentifier string)(bool, int, int, error){ + + probabilityMapKey := genomeIdentifier + "_ProbabilityOfPassingADiseaseVariant" + + genomeDiseasePercentageProbability, exists := personDiseaseAnalysisMap[probabilityMapKey] + if (exists == false) { + return false, 0, 0, errors.New("Malformed person analysis map list: Missing probability of passing a variant value") + } + if (genomeDiseasePercentageProbability == "Unknown"){ + return false, 0, 0, nil + } + + genomeDiseaseProbabilityInt, err := helpers.ConvertStringToInt(genomeDiseasePercentageProbability) + if (err != nil) { + return false, 0, 0, errors.New("Malformed person analysis map list: Invalid probability of passing a variant value: " + genomeDiseasePercentageProbability) + } + + numberOfTestedVariantsString, exists := personDiseaseAnalysisMap[genomeIdentifier + "_NumberOfVariantsTested"] + if (exists == false){ + return false, 0, 0, errors.New("Malformed person analysis map list: Probabilities map missing _NumberOfVariantsTested") + } + + numberOfTestedVariants, err := helpers.ConvertStringToInt(numberOfTestedVariantsString) + if (err != nil){ + return false, 0, 0, errors.New("Malformed person analysis map list: Contains invalid _NumberOfVariantsTested") + } + + return true, genomeDiseaseProbabilityInt, numberOfTestedVariants, nil + } + + personAProbabilityIsKnown, personAWillPassVariantProbability, personANumberOfVariantsTested, err := getPersonWillPassDiseaseVariantProbability(personADiseaseAnalysisMap, personAGenomeIdentifier) + if (err != nil) { return err } + + personBProbabilityIsKnown, personBWillPassVariantProbability, personBNumberOfVariantsTested, err := getPersonWillPassDiseaseVariantProbability(personBDiseaseAnalysisMap, personBGenomeIdentifier) + if (err != nil) { return err } + + personANumberOfVariantsTestedString := helpers.ConvertIntToString(personANumberOfVariantsTested) + personBNumberOfVariantsTestedString := helpers.ConvertIntToString(personBNumberOfVariantsTested) + + offspringDiseaseMap[personAGenomeIdentifier + "_NumberOfVariantsTested"] = personANumberOfVariantsTestedString + offspringDiseaseMap[personBGenomeIdentifier + "_NumberOfVariantsTested"] = personBNumberOfVariantsTestedString + + genomePairIdentifier := personAGenomeIdentifier + "+" + personBGenomeIdentifier + + if (personAProbabilityIsKnown == false || personBProbabilityIsKnown == false){ + + offspringDiseaseMap[genomePairIdentifier + "_ProbabilityOffspringHasDisease"] = "Unknown" + offspringDiseaseMap[genomePairIdentifier + "_ProbabilityOffspringHasVariant"] = "Unknown" + + return nil + } + + percentageProbabilityOffspringHasDisease, percentageProbabilityOffspringHasVariant, err := GetOffspringMonogenicDiseaseProbabilities(diseaseIsDominantOrRecessive, personAWillPassVariantProbability, personBWillPassVariantProbability) + if (err != nil) { return err } + + probabilityOffspringHasDiseaseString := helpers.ConvertIntToString(percentageProbabilityOffspringHasDisease) + probabilityOffspringHasVariantString := helpers.ConvertIntToString(percentageProbabilityOffspringHasVariant) + + offspringDiseaseMap[genomePairIdentifier + "_ProbabilityOffspringHasDisease"] = probabilityOffspringHasDiseaseString + offspringDiseaseMap[genomePairIdentifier + "_ProbabilityOffspringHasVariant"] = probabilityOffspringHasVariantString + + return nil + } + + err = addPairProbabilitiesToDiseaseMap(pair1PersonAGenomeIdentifier, pair1PersonBGenomeIdentifier) + if (err != nil) { return false, "", err } + + if (genomePair2Exists == true){ + + err := addPairProbabilitiesToDiseaseMap(pair2PersonAGenomeIdentifier, pair2PersonBGenomeIdentifier) + if (err != nil) { return false, "", err } + } + + // Now we calculate the probabilities for individual variants + // We add a map for each variant + + offspringVariantsMapList := make([]map[string]string, 0, len(variantsList)) + + for _, variantObject := range variantsList{ + + variantIdentifier := variantObject.VariantIdentifier + + offspringVariantMap := map[string]string{ + "ItemType": "MonogenicDiseaseVariant", + "VariantIdentifier": variantIdentifier, + "DiseaseName": diseaseName, + } + + addPairProbabilitiesToVariantMap := func(personAGenomeIdentifier string, personBGenomeIdentifier string)error{ + + genomePairIdentifier := personAGenomeIdentifier + "+" + personBGenomeIdentifier + + //Outputs: + // -bool: Probability is known + // -float64: Probability that person will pass variant to offspring (between 0 and 1) + // -error + getPersonWillPassVariantProbability := func(personVariantsMapList []map[string]string, personGenomeIdentifier string)(bool, float64, error){ + + for _, personVariantMap := range personVariantsMapList{ + + itemType, exists := personVariantMap["ItemType"] + if (exists == false){ + return false, 0, errors.New("PersonVariantsMapList missing ItemType") + } + if (itemType != "MonogenicDiseaseVariant"){ + return false, 0, errors.New("Person variant map is malformed: Contains non-MonogenicDiseaseVariant itemType: " + itemType) + } + + currentVariantIdentifier, exists := personVariantMap["VariantIdentifier"] + if (exists == false){ + return false, 0, errors.New("Person variant map is malformed: Missing VariantIdentifier") + } + if (currentVariantIdentifier != variantIdentifier){ + continue + } + + personHasVariantInfo, exists := personVariantMap[personGenomeIdentifier + "_HasVariant"] + if (exists == false){ + return false, 0, errors.New("Person variant map is malformed: missing person genome _HasVariant value.") + } + + if (personHasVariantInfo == "Unknown"){ + return false, 0, nil + } + if (personHasVariantInfo == "Yes;Yes"){ + return true, 1, nil + } + if (personHasVariantInfo == "Yes;No" || personHasVariantInfo == "No;Yes"){ + return true, 0.5, nil + } + if (personHasVariantInfo == "No;No"){ + return true, 0, nil + } + return false, 0, errors.New("Person variant map is malformed: Contains invalid variant info: " + personHasVariantInfo) + } + + return false, 0, errors.New("Cannot find disease variant in personVariantsMapList: " + variantIdentifier) + } + + // Outputs: + // -bool: Probabilities are known + // -int: Lower bound Percentage Probability that offspring will have 0 mutations + // -int: Upper bound Percentage Probability that offspring will have 0 mutations + // -int: Lower bound Percentage Probability that offspring will have 1 mutation + // -int: Upper bound Percentage Probability that offspring will have 1 mutation + // -int: Lower bound Percentage Probability that offspring will have 2 mutations + // -int: Upper bound Percentage Probability that offspring will have 2 mutations + // -error + getOffspringVariantProbabilities := func()(bool, int, int, int, int, int, int, error){ + + //Outputs: + // -int: Percentage Probability of 0 mutations + // -int: Percentage Probability of 1 mutation + // -int: Percentage Probability of 2 mutations + // -error + getOffspringProbabilities := func(personAWillPassVariantProbability float64, personBWillPassVariantProbability float64)(int, int, int, error){ + // This is the probability that neither will pass variant + // P = P(!A) * P(!B) + probabilityOf0Mutations := (1 - personAWillPassVariantProbability) * (1 - personBWillPassVariantProbability) + + // This is the probability that either will pass variant, but not both + // P(A XOR B) = P(A) + P(B) - (2 * P(A and B)) + probabilityOf1Mutation := personAWillPassVariantProbability + personBWillPassVariantProbability - (2 * personAWillPassVariantProbability * personBWillPassVariantProbability) + + // This is the probability that both will pass variant + // P(A and B) = P(A) * P(B) + probabilityOf2Mutations := personAWillPassVariantProbability * personBWillPassVariantProbability + + percentageProbabilityOf0Mutations, err := helpers.FloorFloat64ToInt(probabilityOf0Mutations * 100) + if (err != nil) { return 0, 0, 0, err } + percentageProbabilityOf1Mutation, err := helpers.FloorFloat64ToInt(probabilityOf1Mutation * 100) + if (err != nil) { return 0, 0, 0, err } + percentageProbabilityOf2Mutations, err := helpers.FloorFloat64ToInt(probabilityOf2Mutations * 100) + if (err != nil) { return 0, 0, 0, err } + + return percentageProbabilityOf0Mutations, percentageProbabilityOf1Mutation, percentageProbabilityOf2Mutations, nil + } + + personAProbabilityIsKnown, personAWillPassVariantProbability, err := getPersonWillPassVariantProbability(personAVariantsMapList, personAGenomeIdentifier) + if (err != nil) { return false, 0, 0, 0, 0, 0, 0, err } + + personBProbabilityIsKnown, personBWillPassVariantProbability, err := getPersonWillPassVariantProbability(personBVariantsMapList, personBGenomeIdentifier) + if (err != nil) { return false, 0, 0, 0, 0, 0, 0, err } + + if (personAProbabilityIsKnown == false && personBProbabilityIsKnown == false){ + return false, 0, 0, 0, 0, 0, 0, nil + } + + if (personAProbabilityIsKnown == true && personBProbabilityIsKnown == false){ + + bestCasePersonBWillPassVariantProbability := float64(0) + worstCasePersonBWillPassVariantProbability := float64(1) + + bestCase0MutationsProbability, bestCase1MutationProbability, bestCase2MutationsProbability, err := getOffspringProbabilities(personAWillPassVariantProbability, bestCasePersonBWillPassVariantProbability) + if (err != nil) { return false, 0, 0, 0, 0, 0, 0, err } + + worstCase0MutationsProbability, worstCase1MutationProbability, worstCase2MutationsProbability, err := getOffspringProbabilities(personAWillPassVariantProbability, worstCasePersonBWillPassVariantProbability) + if (err != nil) { return false, 0, 0, 0, 0, 0, 0, err } + + lowerBound1MutationProbability := min(bestCase1MutationProbability, worstCase1MutationProbability) + upperBound1MutationProbability := max(bestCase1MutationProbability, worstCase1MutationProbability) + + return true, worstCase0MutationsProbability, bestCase0MutationsProbability, lowerBound1MutationProbability, upperBound1MutationProbability, bestCase2MutationsProbability, worstCase2MutationsProbability, nil + } + + if (personAProbabilityIsKnown == false && personBProbabilityIsKnown == true){ + + bestCasePersonAWillPassVariantProbability := float64(0) + worstCasePersonAWillPassVariantProbability := float64(1) + + bestCase0MutationsProbability, bestCase1MutationProbability, bestCase2MutationsProbability, err := getOffspringProbabilities(bestCasePersonAWillPassVariantProbability, personBWillPassVariantProbability) + if (err != nil) { return false, 0, 0, 0, 0, 0, 0, err } + + worstCase0MutationsProbability, worstCase1MutationProbability, worstCase2MutationsProbability, err := getOffspringProbabilities(worstCasePersonAWillPassVariantProbability, personBWillPassVariantProbability) + if (err != nil) { return false, 0, 0, 0, 0, 0, 0, err } + + lowerBound1MutationProbability := min(bestCase1MutationProbability, worstCase1MutationProbability) + upperBound1MutationProbability := max(bestCase1MutationProbability, worstCase1MutationProbability) + + return true, worstCase0MutationsProbability, bestCase0MutationsProbability, lowerBound1MutationProbability, upperBound1MutationProbability, bestCase2MutationsProbability, worstCase2MutationsProbability, nil + } + + offspring0MutationsProbability, offspring1MutationProbability, offspring2MutationsProbability, err := getOffspringProbabilities(personAWillPassVariantProbability, personBWillPassVariantProbability) + if (err != nil) { return false, 0, 0, 0, 0, 0, 0, err } + + return true, offspring0MutationsProbability, offspring0MutationsProbability, offspring1MutationProbability, offspring1MutationProbability, offspring2MutationsProbability, offspring2MutationsProbability, nil + } + + probabilitiesKnown, probabilityOf0MutationsLowerBound, probabilityOf0MutationsUpperBound, probabilityOf1MutationLowerBound, probabilityOf1MutationUpperBound, probabilityOf2MutationsLowerBound, probabilityOf2MutationsUpperBound, err := getOffspringVariantProbabilities() + if (err != nil) { return err } + + if (probabilitiesKnown == false){ + offspringVariantMap[genomePairIdentifier + "_ProbabilityOf0MutationsLowerBound"] = "Unknown" + offspringVariantMap[genomePairIdentifier + "_ProbabilityOf0MutationsUpperBound"] = "Unknown" + offspringVariantMap[genomePairIdentifier + "_ProbabilityOf1MutationLowerBound"] = "Unknown" + offspringVariantMap[genomePairIdentifier + "_ProbabilityOf1MutationUpperBound"] = "Unknown" + offspringVariantMap[genomePairIdentifier + "_ProbabilityOf2MutationsLowerBound"] = "Unknown" + offspringVariantMap[genomePairIdentifier + "_ProbabilityOf2MutationsUpperBound"] = "Unknown" + + return nil + } + + probabilityOf0MutationsLowerBoundString := helpers.ConvertIntToString(probabilityOf0MutationsLowerBound) + probabilityOf0MutationsUpperBoundString := helpers.ConvertIntToString(probabilityOf0MutationsUpperBound) + probabilityOf1MutationLowerBoundString := helpers.ConvertIntToString(probabilityOf1MutationLowerBound) + probabilityOf1MutationUpperBoundString := helpers.ConvertIntToString(probabilityOf1MutationUpperBound) + probabilityOf2MutationsLowerBoundString := helpers.ConvertIntToString(probabilityOf2MutationsLowerBound) + probabilityOf2MutationsUpperBoundString := helpers.ConvertIntToString(probabilityOf2MutationsUpperBound) + + offspringVariantMap[genomePairIdentifier + "_ProbabilityOf0MutationsLowerBound"] = probabilityOf0MutationsLowerBoundString + offspringVariantMap[genomePairIdentifier + "_ProbabilityOf0MutationsUpperBound"] = probabilityOf0MutationsUpperBoundString + offspringVariantMap[genomePairIdentifier + "_ProbabilityOf1MutationLowerBound"] = probabilityOf1MutationLowerBoundString + offspringVariantMap[genomePairIdentifier + "_ProbabilityOf1MutationUpperBound"] = probabilityOf1MutationUpperBoundString + offspringVariantMap[genomePairIdentifier + "_ProbabilityOf2MutationsLowerBound"] = probabilityOf2MutationsLowerBoundString + offspringVariantMap[genomePairIdentifier + "_ProbabilityOf2MutationsUpperBound"] = probabilityOf2MutationsUpperBoundString + + return nil + } + + err = addPairProbabilitiesToVariantMap(pair1PersonAGenomeIdentifier, pair1PersonBGenomeIdentifier) + if (err != nil) { return false, "", err } + + if (genomePair2Exists == true){ + + err := addPairProbabilitiesToVariantMap(pair2PersonAGenomeIdentifier, pair2PersonBGenomeIdentifier) + if (err != nil) { return false, "", err } + } + + offspringVariantsMapList = append(offspringVariantsMapList, offspringVariantMap) + } + + // Now we check for conflicts in monogenic disease results + + // For couples, a conflict is when either genome pair has differing results for any disease probability/variant probability + // This is different from a person having conflicts within their different genomes + // Each genome pair only uses 1 genome from each person. + + if (genomePair2Exists == true){ + + // Conflicts are only possible if two genome pairs exist + + checkIfConflictExists := func()(bool, error){ + + genomePair1Identifier := pair1PersonAGenomeIdentifier + "+" + pair1PersonBGenomeIdentifier + + genomePair2Identifier := pair2PersonAGenomeIdentifier + "+" + pair2PersonBGenomeIdentifier + + allGenomePairIdentifiersList := []string{genomePair1Identifier, genomePair2Identifier} + + probabilityOffspringHasDisease := "" + probabilityOffspringHasVariant := "" + + for index, genomePairIdentifier := range allGenomePairIdentifiersList{ + + currentProbabilityOffspringHasDisease, exists := offspringDiseaseMap[genomePairIdentifier + "_ProbabilityOffspringHasDisease"] + if (exists == false) { + return false, errors.New("Cannot find _ProbabilityOffspringHasDisease key when searching for conflicts.") + } + currentProbabilityOffspringHasVariant, exists := offspringDiseaseMap[genomePairIdentifier + "_ProbabilityOffspringHasVariant"] + if (exists == false) { + return false, errors.New("Cannot find _ProbabilityOffspringHasVariant key when searching for conflicts.") + } + + if (index == 0){ + probabilityOffspringHasDisease = currentProbabilityOffspringHasDisease + probabilityOffspringHasVariant = currentProbabilityOffspringHasVariant + continue + } + if (currentProbabilityOffspringHasDisease != probabilityOffspringHasDisease){ + return true, nil + } + if (currentProbabilityOffspringHasVariant != probabilityOffspringHasVariant){ + return true, nil + } + } + + for _, variantMap := range offspringVariantsMapList{ + + probabilityOf0MutationsLowerBound := "" + probabilityOf0MutationsUpperBound := "" + probabilityOf1MutationLowerBound := "" + probabilityOf1MutationUpperBound := "" + probabilityOf2MutationsLowerBound := "" + probabilityOf2MutationsUpperBound := "" + + for index, genomePairIdentifier := range allGenomePairIdentifiersList{ + + currentProbabilityOf0MutationsLowerBound, exists := variantMap[genomePairIdentifier + "_ProbabilityOf0MutationsLowerBound"] + if (exists == false){ + return false, errors.New("Cannot find _ProbabilityOf0MutationsLowerBound key when searching for conflicts.") + } + currentProbabilityOf0MutationsUpperBound, exists := variantMap[genomePairIdentifier + "_ProbabilityOf0MutationsUpperBound"] + if (exists == false){ + return false, errors.New("Cannot find _ProbabilityOf0MutationsUpperBound key when searching for conflicts.") + } + currentProbabilityOf1MutationLowerBound, exists := variantMap[genomePairIdentifier + "_ProbabilityOf1MutationLowerBound"] + if (exists == false){ + return false, errors.New("Cannot find _ProbabilityOf1MutationLowerBound key when searching for conflicts.") + } + currentProbabilityOf1MutationUpperBound, exists := variantMap[genomePairIdentifier + "_ProbabilityOf1MutationUpperBound"] + if (exists == false){ + return false, errors.New("Cannot find _ProbabilityOf1MutationUpperBound key when searching for conflicts.") + } + currentProbabilityOf2MutationsLowerBound, exists := variantMap[genomePairIdentifier + "_ProbabilityOf2MutationsLowerBound"] + if (exists == false){ + return false, errors.New("Cannot find _ProbabilityOf2MutationsLowerBound key when searching for conflicts.") + } + currentProbabilityOf2MutationsUpperBound, exists := variantMap[genomePairIdentifier + "_ProbabilityOf2MutationsUpperBound"] + if (exists == false){ + return false, errors.New("Cannot find _ProbabilityOf2MutationsUpperBound key when searching for conflicts.") + } + + if (index == 0){ + probabilityOf0MutationsLowerBound = currentProbabilityOf0MutationsLowerBound + probabilityOf0MutationsUpperBound = currentProbabilityOf0MutationsUpperBound + + probabilityOf1MutationLowerBound = currentProbabilityOf1MutationLowerBound + probabilityOf1MutationUpperBound = currentProbabilityOf1MutationUpperBound + + probabilityOf2MutationsLowerBound = currentProbabilityOf2MutationsLowerBound + probabilityOf2MutationsUpperBound = currentProbabilityOf2MutationsUpperBound + continue + } + + if (currentProbabilityOf0MutationsLowerBound != probabilityOf0MutationsLowerBound){ + return true, nil + } + if (currentProbabilityOf0MutationsUpperBound != probabilityOf0MutationsUpperBound){ + return true, nil + } + if (currentProbabilityOf1MutationLowerBound != probabilityOf1MutationLowerBound){ + return true, nil + } + if (currentProbabilityOf1MutationUpperBound != probabilityOf1MutationUpperBound){ + return true, nil + } + if (currentProbabilityOf2MutationsLowerBound != probabilityOf2MutationsLowerBound){ + return true, nil + } + if (currentProbabilityOf2MutationsUpperBound != probabilityOf2MutationsUpperBound){ + return true, nil + } + } + } + + return false, nil + } + + conflictExists, err := checkIfConflictExists() + if (err != nil) { return false, "", err } + + conflictExistsString := helpers.ConvertBoolToYesOrNoString(conflictExists) + + offspringDiseaseMap["ConflictExists"] = conflictExistsString + } + + newAnalysisMapList = append(newAnalysisMapList, offspringDiseaseMap) + newAnalysisMapList = append(newAnalysisMapList, offspringVariantsMapList...) + } + + // Step 2: Polygenic Diseases + + polygenicDiseaseObjectsList, err := polygenicDiseases.GetPolygenicDiseaseObjectsList() + if (err != nil){ return false, "", err } + + for _, diseaseObject := range polygenicDiseaseObjectsList{ + + diseaseName := diseaseObject.DiseaseName + diseaseLociList := diseaseObject.LociList + + _, personALociMapList, err := getPolygenicDiseaseAnalysis(personAGenomesWithMetadataList, diseaseObject) + if (err != nil) { return false, "", err } + + _, personBLociMapList, err := getPolygenicDiseaseAnalysis(personBGenomesWithMetadataList, diseaseObject) + if (err != nil) { return false, "", err } + + offspringLociMapList := make([]map[string]string, 0, len(diseaseLociList)) + + for _, locusObject := range diseaseLociList{ + + locusIdentifier := locusObject.LocusIdentifier + + locusRiskWeightsMap := locusObject.RiskWeightsMap + locusOddsRatiosMap := locusObject.OddsRatiosMap + + offspringLocusMap := map[string]string{ + "ItemType": "PolygenicDiseaseLocus", + "LocusIdentifier": locusIdentifier, + "DiseaseName": diseaseName, + } + + // This will calculate the average risk weight and probability change for the offspring from the two specified genomes + // It then adds the pair entry to the diseaseMap + addPairDiseaseLocusInfoToLocusMap := func(personAGenomeIdentifier string, personBGenomeIdentifier string)error{ + + getPersonGenomeLocusBasePair := func(personGenomeIdentifier string, personLociMapList []map[string]string)(bool, string, error){ + + for _, locusMap := range personLociMapList{ + + itemType, exists := locusMap["ItemType"] + if (exists == false){ + return false, "", errors.New("getPolygenicDiseaseAnalysis returning lociMapList with item missing ItemType") + } + if (itemType != "PolygenicDiseaseLocus"){ + return false, "", errors.New("getPolygenicDiseaseAnalysis returning lociMapList with non-PolygenicDiseaseLocus item: " + itemType) + } + currentLocusIdentifier, exists := locusMap["LocusIdentifier"] + if (exists == false){ + return false, "", errors.New("getPolygenicDiseaseAnalysis returning lociMapList with item missing LocusIdentifier") + } + + if (currentLocusIdentifier != locusIdentifier){ + continue + } + + locusBasePair, exists := locusMap[personGenomeIdentifier + "_LocusBasePair"] + if (exists == false){ + return false, "", errors.New("getPolygenicDiseaseAnalysis returning lociMapList with item missing _LocusBasePair") + } + + if (locusBasePair == "Unknown"){ + return false, "", nil + } + + return true, locusBasePair, nil + } + + return false, "", errors.New("getPolygenicDiseaseAnalysis returning lociMapList missing a locus: " + locusIdentifier) + } + + genomePairIdentifier := personAGenomeIdentifier + "+" + personBGenomeIdentifier + + personALocusBasePairKnown, personALocusBasePair, err := getPersonGenomeLocusBasePair(personAGenomeIdentifier, personALociMapList) + if (err != nil) { return err } + + personBLocusBasePairKnown, personBLocusBasePair, err := getPersonGenomeLocusBasePair(personBGenomeIdentifier, personBLociMapList) + if (err != nil) { return err } + + if (personALocusBasePairKnown == false || personBLocusBasePairKnown == false){ + + offspringLocusMap[genomePairIdentifier + "_OffspringRiskWeight"] = "Unknown" + offspringLocusMap[genomePairIdentifier + "_OffspringOddsRatio"] = "Unknown" + offspringLocusMap[genomePairIdentifier + "_OffspringUnknownOddsRatiosWeightSum"] = "Unknown" + + return nil + } + + offspringAverageRiskWeight, offspringOddsRatioKnown, offspringAverageOddsRatio, averageUnknownOddsRatiosWeightSum, err := GetOffspringPolygenicDiseaseLocusInfo(locusRiskWeightsMap, locusOddsRatiosMap, personALocusBasePair, personBLocusBasePair) + if (err != nil) { return err } + + averageRiskWeightString := helpers.ConvertIntToString(offspringAverageRiskWeight) + + offspringLocusMap[genomePairIdentifier + "_OffspringRiskWeight"] = averageRiskWeightString + + if (offspringOddsRatioKnown == false){ + + offspringLocusMap[genomePairIdentifier + "_OffspringOddsRatio"] = "Unknown" + + } else { + + averageOddsRatioString := helpers.ConvertFloat64ToString(offspringAverageOddsRatio) + + offspringLocusMap[genomePairIdentifier + "_OffspringOddsRatio"] = averageOddsRatioString + } + + averageUnknownOddsRatiosWeightSumString := helpers.ConvertIntToString(averageUnknownOddsRatiosWeightSum) + + offspringLocusMap[genomePairIdentifier + "_OffspringUnknownOddsRatiosWeightSum"] = averageUnknownOddsRatiosWeightSumString + + return nil + } + + err = addPairDiseaseLocusInfoToLocusMap(pair1PersonAGenomeIdentifier, pair1PersonBGenomeIdentifier) + if (err != nil) { return false, "", err } + + if (genomePair2Exists == true){ + + err := addPairDiseaseLocusInfoToLocusMap(pair2PersonAGenomeIdentifier, pair2PersonBGenomeIdentifier) + if (err != nil) { return false, "", err } + } + + offspringLociMapList = append(offspringLociMapList, offspringLocusMap) + } + + // Now we iterate through loci to determine genome pair disease info + + offspringPolygenicDiseaseMap := map[string]string{ + "ItemType": "PolygenicDisease", + "DiseaseName": diseaseName, + } + + polygenicDiseaseLociMap, err := polygenicDiseases.GetPolygenicDiseaseLociMap(diseaseName) + if (err != nil) { return false, "", err } + + addPairPolygenicDiseaseInfoToDiseaseMap := func(personAGenomeIdentifier string, personBGenomeIdentifier string)error{ + + genomePairIdentifier := personAGenomeIdentifier + "+" + personBGenomeIdentifier + + numberOfLociTested := 0 + summedRiskWeights := 0 + minimumPossibleRiskWeightSum := 0 + maximumPossibleRiskWeightSum := 0 + + for _, locusMap := range offspringLociMapList{ + + locusIdentifier, exists := locusMap["LocusIdentifier"] + if (exists == false){ + return errors.New("offspringLociMapList item missing LocusIdentifier") + } + + locusObject, exists := polygenicDiseaseLociMap[locusIdentifier] + if (exists == false){ + return errors.New("offspringLociMapList contains locus not found in allDiseaseLociObjectsMap") + } + + locusMinimumRiskWeight := locusObject.MinimumRiskWeight + locusMaximumRiskWeight := locusObject.MaximumRiskWeight + + minimumPossibleRiskWeightSum += locusMinimumRiskWeight + maximumPossibleRiskWeightSum += locusMaximumRiskWeight + + locusRiskWeight, exists := locusMap[genomePairIdentifier + "_OffspringRiskWeight"] + if (exists == false){ + return errors.New("offspringLociMapList contains locusMap missing _OffspringRiskWeight") + } + + if (locusRiskWeight == "Unknown"){ + continue + } + numberOfLociTested += 1 + + locusRiskWeightInt, err := helpers.ConvertStringToInt(locusRiskWeight) + if (err != nil){ + return errors.New("offspringLociMapList contains locusMap with invalid _OffspringRiskWeight: " + locusRiskWeight) + } + + summedRiskWeights += locusRiskWeightInt + } + + if (numberOfLociTested == 0){ + + // No loci were tested + offspringPolygenicDiseaseMap[genomePairIdentifier + "_NumberOfLociTested"] = "0" + offspringPolygenicDiseaseMap[genomePairIdentifier + "_OffspringRiskScore"] = "Unknown" + + return nil + } + + numberOfLociTestedString := helpers.ConvertIntToString(numberOfLociTested) + offspringPolygenicDiseaseMap[genomePairIdentifier + "_NumberOfLociTested"] = numberOfLociTestedString + + pairDiseaseRiskScore, err := helpers.ScaleNumberProportionally(true, summedRiskWeights, minimumPossibleRiskWeightSum, maximumPossibleRiskWeightSum, 0, 10) + if (err != nil) { return err } + + pairDiseaseRiskScoreString := helpers.ConvertIntToString(pairDiseaseRiskScore) + + offspringPolygenicDiseaseMap[genomePairIdentifier + "_OffspringRiskScore"] = pairDiseaseRiskScoreString + + return nil + } + + addPairPolygenicDiseaseInfoToDiseaseMap(pair1PersonAGenomeIdentifier, pair1PersonBGenomeIdentifier) + if (err != nil) { return false, "", err } + + if (genomePair2Exists == true){ + + err := addPairPolygenicDiseaseInfoToDiseaseMap(pair2PersonAGenomeIdentifier, pair2PersonBGenomeIdentifier) + if (err != nil) { return false, "", err } + } + + if (genomePair2Exists == true){ + + // We check for conflicts + // Conflicts are only possible if two genome pairs exist + + checkIfConflictExists := func()(bool, error){ + + genomePair1Identifier := pair1PersonAGenomeIdentifier + "+" + pair1PersonBGenomeIdentifier + + genomePair2Identifier := pair2PersonAGenomeIdentifier + "+" + pair2PersonBGenomeIdentifier + + allGenomePairIdentifiersList := []string{genomePair1Identifier, genomePair2Identifier} + + for _, locusMap := range offspringLociMapList{ + + offspringRiskWeight := "" + offspringOddsRatio := "" + offspringUnknownOddsRatiosWeightSum := "" + + for index, genomePairIdentifier := range allGenomePairIdentifiersList{ + + currentOffspringRiskWeight, exists := locusMap[genomePairIdentifier + "_OffspringRiskWeight"] + if (exists == false){ + return false, errors.New("Cannot find _OffspringRiskWeight key when searching for conflicts.") + } + currentOffspringOddsRatio, exists := locusMap[genomePairIdentifier + "_OffspringOddsRatio"] + if (exists == false){ + return false, errors.New("Cannot find _OffspringOddsRatio key when searching for conflicts.") + } + currentOffspringUnknownOddsRatiosWeightSum, exists := locusMap[genomePairIdentifier + "_OffspringUnknownOddsRatiosWeightSum"] + if (exists == false){ + return false, errors.New("Cannot find _OffspringUnknownOddsRatiosWeightSum key when searching for conflicts.") + } + + if (index == 0){ + offspringRiskWeight = currentOffspringRiskWeight + offspringOddsRatio = currentOffspringOddsRatio + offspringUnknownOddsRatiosWeightSum = currentOffspringUnknownOddsRatiosWeightSum + continue + } + if (currentOffspringRiskWeight != offspringRiskWeight){ + return true, nil + } + if (currentOffspringOddsRatio != offspringOddsRatio){ + return true, nil + } + if (currentOffspringUnknownOddsRatiosWeightSum != offspringUnknownOddsRatiosWeightSum){ + return true, nil + } + } + } + + return false, nil + } + + conflictExists, err := checkIfConflictExists() + if (err != nil) { return false, "", err } + + conflictExistsString := helpers.ConvertBoolToYesOrNoString(conflictExists) + + offspringPolygenicDiseaseMap["ConflictExists"] = conflictExistsString + } + + newAnalysisMapList = append(newAnalysisMapList, offspringPolygenicDiseaseMap) + newAnalysisMapList = append(newAnalysisMapList, offspringLociMapList...) + } + + // Step 3: Traits + + traitObjectsList, err := traits.GetTraitObjectsList() + if (err != nil) { return false, "", err } + + for _, traitObject := range traitObjectsList{ + + traitName := traitObject.TraitName + traitRulesList := traitObject.RulesList + + _, personATraitRulesMapList, err := getTraitAnalysis(personAGenomesWithMetadataList, traitObject) + if (err != nil) { return false, "", err } + + _, personBTraitRulesMapList, err := getTraitAnalysis(personBGenomesWithMetadataList, traitObject) + if (err != nil) { return false, "", err } + + offspringRulesMapList := make([]map[string]string, 0, len(traitRulesList)) + + for _, ruleObject := range traitRulesList{ + + ruleIdentifier := ruleObject.RuleIdentifier + + // This is a list that describes the locus rsids and their values that must be fulfilled to pass the rule + ruleLocusObjectsList := ruleObject.LociList + + offspringRuleMap := map[string]string{ + "ItemType": "TraitRule", + "RuleIdentifier": ruleIdentifier, + "TraitName": traitName, + } + + // This will calculate the number of offspring that will pass the rule for the offspring from the two specified genomes + // It then adds the pair entry to the ruleMap + addPairTraitRuleInfoToRuleMap := func(personAGenomeIdentifier string, personBGenomeIdentifier string)error{ + + //Outputs: + // -bool: All rule loci are known + // -map[string]string: RSID -> Locus base pair value + // -error + getPersonGenomeRuleLociValuesMap := func(personGenomeIdentifier string, personTraitRulesMapList []map[string]string)(bool, map[string]string, error){ + + for _, ruleMap := range personTraitRulesMapList{ + + itemType, exists := ruleMap["ItemType"] + if (exists == false){ + return false, nil, errors.New("getTraitAnalysis returning rulesMapList with item missing ItemType") + } + if (itemType != "TraitRule"){ + return false, nil, errors.New("getTraitAnalysis returning rulesMapList with non-TraitRule item: " + itemType) + } + currentRuleIdentifier, exists := ruleMap["RuleIdentifier"] + if (exists == false){ + return false, nil, errors.New("getTraitAnalysis returning rulesMapList with item missing RuleIdentifier") + } + + if (currentRuleIdentifier != ruleIdentifier){ + continue + } + + // Map structure: Locus identifier -> Locus base pair value + personTraitLociBasePairsMap := make(map[string]string) + + for _, locusObject := range ruleLocusObjectsList{ + + locusIdentifier := locusObject.LocusIdentifier + + personLocusBasePair, exists := ruleMap[personGenomeIdentifier + "_RuleLocusBasePair_" + locusIdentifier] + if (exists == false){ + return false, nil, errors.New("_LocusBasePair_ value not found in person trait analysis ruleMap") + } + + if (personLocusBasePair == "Unknown"){ + // Not all locus values are known, thus, we cannot determine if the genome passes the rule + return false, nil, nil + } + + personTraitLociBasePairsMap[locusIdentifier] = personLocusBasePair + } + + return true, personTraitLociBasePairsMap, nil + } + + return false, nil, errors.New("getTraitAnalysis returning rulesMapList missing a rule: " + ruleIdentifier) + } + + allPersonALociKnown, personAGenomeRuleLociValuesMap, err := getPersonGenomeRuleLociValuesMap(personAGenomeIdentifier, personATraitRulesMapList) + if (err != nil) { return err } + + allPersonBLociKnown, personBGenomeRuleLociValuesMap, err := getPersonGenomeRuleLociValuesMap(personBGenomeIdentifier, personBTraitRulesMapList) + if (err != nil) { return err } + + genomePairIdentifier := personAGenomeIdentifier + "+" + personBGenomeIdentifier + + if (allPersonALociKnown == false || allPersonBLociKnown == false){ + + // We only know how many of the 4 prospective offspring pass the rule if all loci are known for both people's genomes + + offspringRuleMap[genomePairIdentifier + "_OffspringProbabilityOfPassingRule"] = "Unknown" + return nil + } + + // This is a probability between 0 and 1 + offspringProbabilityOfPassingRule := float64(1) + + for _, ruleLocusObject := range ruleLocusObjectsList{ + + locusIdentifier := ruleLocusObject.LocusIdentifier + + personALocusBasePair, exists := personAGenomeRuleLociValuesMap[locusIdentifier] + if (exists == false){ + return errors.New("personAGenomeRuleLociValuesMap missing locusIdentifier") + } + + personBLocusBasePair, exists := personBGenomeRuleLociValuesMap[locusIdentifier] + if (exists == false){ + return errors.New("personBGenomeRuleLociValuesMap missing locusIdentifier") + } + + locusRequiredBasePairsList := ruleLocusObject.BasePairsList + + offspringProbabilityOfPassingRuleLocus, err := GetOffspringTraitRuleLocusInfo(locusRequiredBasePairsList, personALocusBasePair, personBLocusBasePair) + if (err != nil) { return err } + + offspringProbabilityOfPassingRule *= offspringProbabilityOfPassingRuleLocus + } + + offspringPercentageProbabilityOfPassingRule := offspringProbabilityOfPassingRule * 100 + + offspringPercentageProbabilityOfPassingRuleString := helpers.ConvertFloat64ToStringRounded(offspringPercentageProbabilityOfPassingRule, 0) + + offspringRuleMap[genomePairIdentifier + "_OffspringProbabilityOfPassingRule"] = offspringPercentageProbabilityOfPassingRuleString + + return nil + } + + err = addPairTraitRuleInfoToRuleMap(pair1PersonAGenomeIdentifier, pair1PersonBGenomeIdentifier) + if (err != nil) { return false, "", err } + + if (genomePair2Exists == true){ + + err := addPairTraitRuleInfoToRuleMap(pair2PersonAGenomeIdentifier, pair2PersonBGenomeIdentifier) + if (err != nil) { return false, "", err } + } + + offspringRulesMapList = append(offspringRulesMapList, offspringRuleMap) + } + + // Now we iterate through rules to determine genome pair trait info + + offspringTraitMap := map[string]string{ + "ItemType": "Trait", + "TraitName": traitName, + } + + traitRulesMap, err := traits.GetTraitRulesMap(traitName) + if (err != nil) { return false, "", err } + + addPairTraitInfoToTraitMap := func(personAGenomeIdentifier string, personBGenomeIdentifier string)error{ + + genomePairIdentifier := personAGenomeIdentifier + "+" + personBGenomeIdentifier + + if (len(traitRulesList) == 0){ + // This trait is not yet able to be analyzed. + // This feature will be added soon + // These traits will use neural networks instead of rules + offspringTraitMap[genomePairIdentifier + "_NumberOfRulesTested"] = "0" + return nil + } + + numberOfRulesTested := 0 + + // Map Structure: Outcome -> Average score from all rules + averageOutcomeScoresMap := make(map[string]float64) + + for _, ruleMap := range offspringRulesMapList{ + + ruleIdentifier, exists := ruleMap["RuleIdentifier"] + if (exists == false){ + return errors.New("offspringRulesMapList item missing RuleIdentifier") + } + + offspringPercentageProbabilityOfPassingRule, exists := ruleMap[genomePairIdentifier + "_OffspringProbabilityOfPassingRule"] + if (exists == false){ + return errors.New("offspringRulesMapList contains ruleMap missing _OffspringProbabilityOfPassingRule") + } + + if (offspringPercentageProbabilityOfPassingRule == "Unknown"){ + continue + } + + numberOfRulesTested += 1 + + offspringPercentageProbabilityOfPassingRuleFloat64, err := helpers.ConvertStringToFloat64(offspringPercentageProbabilityOfPassingRule) + if (err != nil){ + return errors.New("offspringRulesMapList contains ruleMap with invalid _OffspringProbabilityOfPassingRule: " + offspringPercentageProbabilityOfPassingRule) + } + + // This is the 0 - 1 probability value + offspringProbabilityOfPassingRule := offspringPercentageProbabilityOfPassingRuleFloat64/100 + + ruleObject, exists := traitRulesMap[ruleIdentifier] + if (exists == false){ + return errors.New("offspringRulesMapList contains rule not found in traitRulesMap: " + ruleIdentifier) + } + + ruleOutcomePointsMap := ruleObject.OutcomePointsMap + + for outcomeName, outcomePointsEffect := range ruleOutcomePointsMap{ + + pointsToAdd := float64(outcomePointsEffect) * offspringProbabilityOfPassingRule + + averageOutcomeScoresMap[outcomeName] += pointsToAdd + } + } + + numberOfRulesTestedString := helpers.ConvertIntToString(numberOfRulesTested) + offspringTraitMap[genomePairIdentifier + "_NumberOfRulesTested"] = numberOfRulesTestedString + + if (numberOfRulesTested == 0){ + + return nil + } + + traitOutcomesList := traitObject.OutcomesList + + for _, outcomeName := range traitOutcomesList{ + + getOffspringAverageOutcomeScore := func()float64{ + + averageOutcomeScore, exists := averageOutcomeScoresMap[outcomeName] + if (exists == false){ + // No rules effected this outcome. + return 0 + } + return averageOutcomeScore + } + + offspringAverageOutcomeScore := getOffspringAverageOutcomeScore() + + offspringAverageOutcomeScoreString := helpers.ConvertFloat64ToStringRounded(offspringAverageOutcomeScore, 2) + + offspringTraitMap[genomePairIdentifier + "_OffspringAverageOutcomeScore_" + outcomeName] = offspringAverageOutcomeScoreString + } + + return nil + } + + addPairTraitInfoToTraitMap(pair1PersonAGenomeIdentifier, pair1PersonBGenomeIdentifier) + if (err != nil) { return false, "", err } + + if (genomePair2Exists == true){ + + err := addPairTraitInfoToTraitMap(pair2PersonAGenomeIdentifier, pair2PersonBGenomeIdentifier) + if (err != nil) { return false, "", err } + } + + if (genomePair2Exists == true){ + + // We check for conflicts + // Conflicts are only possible if two genome pairs exist + + checkIfConflictExists := func()(bool, error){ + + genomePair1Identifier := pair1PersonAGenomeIdentifier + "+" + pair1PersonBGenomeIdentifier + + genomePair2Identifier := pair2PersonAGenomeIdentifier + "+" + pair2PersonBGenomeIdentifier + + allGenomePairIdentifiersList := []string{genomePair1Identifier, genomePair2Identifier} + + for _, ruleMap := range offspringRulesMapList{ + + offspringProbabilityOfPassingRule := "" + + for index, genomePairIdentifier := range allGenomePairIdentifiersList{ + + currentOffspringProbabilityOfPassingRule, exists := ruleMap[genomePairIdentifier + "_OffspringProbabilityOfPassingRule"] + if (exists == false){ + return false, errors.New("Cannot find _OffspringProbabilityOfPassingRule key when searching for conflicts.") + } + + if (index == 0){ + + offspringProbabilityOfPassingRule = currentOffspringProbabilityOfPassingRule + continue + } + if (currentOffspringProbabilityOfPassingRule != offspringProbabilityOfPassingRule){ + return true, nil + } + } + } + + return false, nil + } + + conflictExists, err := checkIfConflictExists() + if (err != nil) { return false, "", err } + + conflictExistsString := helpers.ConvertBoolToYesOrNoString(conflictExists) + + offspringTraitMap["ConflictExists"] = conflictExistsString + } + + newAnalysisMapList = append(newAnalysisMapList, offspringTraitMap) + newAnalysisMapList = append(newAnalysisMapList, offspringRulesMapList...) + } + + analysisBytes, err := json.MarshalIndent(newAnalysisMapList, "", "\t") + if (err != nil) { return false, "", err } + + analysisString := string(analysisBytes) + + return true, analysisString, nil +} + +// We also use this function when calculating offspring probabilities between users in viewProfileGui.go +//Outputs: +// -int: Percentage probability offspring has disease (0-100) +// -int: Percentage probability offspring has variant (0-100) +// -error +func GetOffspringMonogenicDiseaseProbabilities(dominantOrRecessive string, personAWillPassVariantPercentageProbability int, personBWillPassVariantPercentageProbability int)(int, int, error){ + + if (dominantOrRecessive != "Dominant" && dominantOrRecessive != "Recessive"){ + return 0, 0, errors.New("GetOffspringMonogenicDiseaseProbabilities called with invalid dominantOrRecessive: " + dominantOrRecessive) + } + + personAWillPassVariantProbability := float64(personAWillPassVariantPercentageProbability)/100 + personBWillPassVariantProbability := float64(personBWillPassVariantPercentageProbability)/100 + + if (personAWillPassVariantProbability < 0 || personAWillPassVariantProbability > 1){ + return 0, 0, errors.New("GetOffspringMonogenicDiseaseProbabilities called with invalid personAWillPassVariantProbability") + } + + if (personBWillPassVariantProbability < 0 || personBWillPassVariantProbability > 1){ + return 0, 0, errors.New("GetOffspringMonogenicDiseaseProbabilities called with invalid personBWillPassVariantProbability") + } + + // The probability offspring has a variant = the probability that either parent passes a variant (inclusive or) + // We find the probability of the offspring having a monogenic disease variant as follows: + // P(A U B) = P(A) + P(B) - P(A ∩ B) + // (Probability of person A passing a variant) + (Probability of person B passing a variant) - (Probability of offspring having disease) + // A person with a variant may have the disease, or just be a carrier. + probabilityOffspringHasVariant := personAWillPassVariantProbability + personBWillPassVariantProbability - (personAWillPassVariantProbability * personBWillPassVariantProbability) + + if (dominantOrRecessive == "Dominant"){ + + // The probability of having the monogenic disease is the same as the probability of having a variant + + percentageProbabilityOffspringHasVariant := int(probabilityOffspringHasVariant * 100) + + return percentageProbabilityOffspringHasVariant, percentageProbabilityOffspringHasVariant, nil + } + + // We find the probability of the offspring having the mongenic disease as follows: + // P(A and B) = P(A) * P(B) + // (Probability of person A Passing a variant) * (Probability of person B passing a variant) + probabilityOffspringHasDisease := personAWillPassVariantProbability * personBWillPassVariantProbability + + percentageProbabilityOffspringHasDisease := probabilityOffspringHasDisease * 100 + percentageProbabilityOffspringHasVariant := probabilityOffspringHasVariant * 100 + + // This conversion remove any digits after the radix point + // This will not result in any false 0% values, an example being 0.9% becoming 0% + // This is because the lowest non-zero probability a person can have for passing a variant is 50% + // Thus, the lowest non-zero probability of an offspring having a disease is 25% + percentageProbabilityOffspringHasDiseaseInt := int(percentageProbabilityOffspringHasDisease) + percentageProbabilityOffspringHasVariantInt := int(percentageProbabilityOffspringHasVariant) + + return percentageProbabilityOffspringHasDiseaseInt, percentageProbabilityOffspringHasVariantInt, nil +} + +//Outputs: +// -int: Offspring average risk weight +// -bool: Odds ratio is known +// -float64: Offspring average odds ratio +// -int: Offspring unknown odds ratios weight sum +// -error +func GetOffspringPolygenicDiseaseLocusInfo(locusRiskWeightsMap map[string]int, locusOddsRatiosMap map[string]float64, personALocusBasePair string, personBLocusBasePair string)(int, bool, float64, int, error){ + + personABase1, personABase2, delimiterFound := strings.Cut(personALocusBasePair, ";") + if (delimiterFound == false){ + return 0, false, 0, 0, errors.New("GetOffspringPolygenicDiseaseLocusInfo called with invalid person A locus base pair: " + personALocusBasePair) + } + personBBase1, personBBase2, delimiterFound := strings.Cut(personBLocusBasePair, ";") + if (delimiterFound == false){ + return 0, false, 0, 0, errors.New("GetOffspringPolygenicDiseaseLocusInfo called with invalid person B locus base pair: " + personBLocusBasePair) + } + + // We create the 4 options for the offspring's bases at this locus + + offspringBasePairOutcomeA := personABase1 + ";" + personBBase1 + offspringBasePairOutcomeB := personABase2 + ";" + personBBase2 + offspringBasePairOutcomeC := personABase1 + ";" + personBBase2 + offspringBasePairOutcomeD := personABase2 + ";" + personBBase1 + + baseOutcomesList := []string{offspringBasePairOutcomeA, offspringBasePairOutcomeB, offspringBasePairOutcomeC, offspringBasePairOutcomeD} + + summedRiskWeight := 0 + + numberOfSummedOddsRatios := 0 + summedOddsRatios := float64(0) + + numberOfSummedUnknownOddsRatioWeights := 0 + summedUnknownOddsRatioWeights := 0 + + for _, outcomeBasePair := range baseOutcomesList{ + + isValid := verifyBasePair(outcomeBasePair) + if (isValid == false){ + return 0, false, 0, 0, errors.New("GetOffspringPolygenicDiseaseLocusInfo called with invalid locus base pair: " + outcomeBasePair) + } + + offspringOutcomeRiskWeight, exists := locusRiskWeightsMap[outcomeBasePair] + if (exists == false){ + // We do not know the risk weight for this base pair + // We treat this as a 0 risk for both weight and odds ratio + + summedOddsRatios += 1 + numberOfSummedOddsRatios += 1 + continue + } + summedRiskWeight += offspringOutcomeRiskWeight + + offspringOutcomeOddsRatio, exists := locusOddsRatiosMap[outcomeBasePair] + if (exists == false){ + // This particular outcome has no known odds ratio + // We add it to the unknown odds ratio weights sum + summedUnknownOddsRatioWeights += offspringOutcomeRiskWeight + numberOfSummedUnknownOddsRatioWeights += 1 + } else { + summedOddsRatios += offspringOutcomeOddsRatio + numberOfSummedOddsRatios += 1 + } + } + + averageRiskWeight := summedRiskWeight/4 + + getAverageUnknownOddsRatiosWeightSum := func()int{ + + if (numberOfSummedUnknownOddsRatioWeights == 0){ + return 0 + } + averageUnknownOddsRatiosWeightSum := summedUnknownOddsRatioWeights/numberOfSummedUnknownOddsRatioWeights + return averageUnknownOddsRatiosWeightSum + } + + averageUnknownOddsRatiosWeightSum := getAverageUnknownOddsRatiosWeightSum() + + if (numberOfSummedOddsRatios == 0){ + + return averageRiskWeight, false, 0, averageUnknownOddsRatiosWeightSum, nil + } + + averageOddsRatio := summedOddsRatios/float64(numberOfSummedOddsRatios) + + return averageRiskWeight, true, averageOddsRatio, averageUnknownOddsRatiosWeightSum, nil +} + + +//Outputs: +// -float64: Probability of offspring passing rule (0-1) +// -error +func GetOffspringTraitRuleLocusInfo(locusRequiredBasePairsList []string, personALocusBasePair string, personBLocusBasePair string)(float64, error){ + + personABase1, personABase2, delimiterFound := strings.Cut(personALocusBasePair, ";") + if (delimiterFound == false){ + return 0, errors.New("GetOffspringTraitRuleLocusInfo called with invalid personA locus base pair: " + personALocusBasePair) + } + personBBase1, personBBase2, delimiterFound := strings.Cut(personBLocusBasePair, ";") + if (delimiterFound == false){ + return 0, errors.New("GetOffspringTraitRuleLocusInfo called with invalid personB locus base pair: " + personBLocusBasePair) + } + + // We create the 4 options for the offspring's bases at this locus + + offspringBasePairOutcomeA := personABase1 + ";" + personBBase1 + offspringBasePairOutcomeB := personABase2 + ";" + personBBase2 + offspringBasePairOutcomeC := personABase1 + ";" + personBBase2 + offspringBasePairOutcomeD := personABase2 + ";" + personBBase1 + + baseOutcomesList := []string{offspringBasePairOutcomeA, offspringBasePairOutcomeB, offspringBasePairOutcomeC, offspringBasePairOutcomeD} + + numberOfOffspringOutcomesWhomPassRuleLocus := 0 + + for _, outcomeBasePair := range baseOutcomesList{ + + isValid := verifyBasePair(outcomeBasePair) + if (isValid == false){ + return 0, errors.New("GetOffspringTraitRuleLocusInfo called with invalid locus base pair: " + outcomeBasePair) + } + + outcomePassesRuleLocus := slices.Contains(locusRequiredBasePairsList, outcomeBasePair) + if (outcomePassesRuleLocus == true){ + numberOfOffspringOutcomesWhomPassRuleLocus += 1 + } + } + + offspringProbabilityOfPassingRuleLocus := float64(numberOfOffspringOutcomesWhomPassRuleLocus)/float64(4) + + return offspringProbabilityOfPassingRuleLocus, nil +} + + +// This function will retrieve the base pair of the locus from the input genome map +// We need this because each rsID has aliases, so we must sometimes check those aliases to find locus values +// +// Outputs: +// -bool: Valid base pair value found +// -string: Base 1 Value (Nucleotide base for the SNP) +// -string: Base 2 Value (Nucleotide base for the SNP) +// -bool: Base pairs are phased +// -error +func getGenomeLocusBasePair(inputGenomeMap map[int64]locusValue.LocusValue, locusRSID int64)(bool, string, string, bool, error){ + + // Outputs: + // -bool: Locus value found + // -locusValue.LocusValue + // -error + getLocusValue := func()(bool, locusValue.LocusValue, error){ + + currentLocusValue, exists := inputGenomeMap[locusRSID] + if (exists == true){ + return true, currentLocusValue, nil + } + + // We check for aliases + + anyAliasesExist, rsidAliasesList, err := locusMetadata.GetRSIDAliases(locusRSID) + if (err != nil) { return false, locusValue.LocusValue{}, err } + if (anyAliasesExist == false){ + return false, locusValue.LocusValue{}, nil + } + + for _, rsidAlias := range rsidAliasesList{ + + currentLocusValue, exists := inputGenomeMap[rsidAlias] + if (exists == true){ + return true, currentLocusValue, nil + } + } + + return false, locusValue.LocusValue{}, nil + } + + locusValueFound, locusValueObject, err := getLocusValue() + if (err != nil) { return false, "", "", false, err } + if (locusValueFound == false){ + return false, "", "", false, nil + } + + base1Value := locusValueObject.Base1Value + base2Value := locusValueObject.Base2Value + locusIsPhased := locusValueObject.LocusIsPhased + + return true, base1Value, base2Value, locusIsPhased, nil +} + + + +//Outputs: +// -map[string]string: Monogenic disease analysis map (contains probabilities for each genomeIdentifier and number of tested variants) +// -[]map[string]string: Variants map list (contains variant info for each genomeIdentifier) +// -error +func getMonogenicDiseaseAnalysis(inputGenomesWithMetadataList []prepareRawGenomes.GenomeWithMetadata, diseaseObject monogenicDiseases.MonogenicDisease)(map[string]string, []map[string]string, error){ + + diseaseName := diseaseObject.DiseaseName + dominantOrRecessive := diseaseObject.DominantOrRecessive + variantsList := diseaseObject.VariantsList + + // We use this map to keep track of which RSIDs corresponds to each variant + // Map Structure: Variant Identifier -> []rsID + variantRSIDsMap := make(map[string][]int64) + + variantsMapList := make([]map[string]string, 0, len(variantsList)) + + for _, variantObject := range variantsList{ + + variantIdentifier := variantObject.VariantIdentifier + + // Map Structure: + // -GenomeIdentifier -> ("Yes"/"No" + ";" + "Yes"/"No") or "Unknown" + variantMap := map[string]string{ + "ItemType": "MonogenicDiseaseVariant", + "VariantIdentifier": variantIdentifier, + "DiseaseName": diseaseName, + } + + variantRSID := variantObject.VariantRSID + variantDefectiveBase := variantObject.DefectiveBase + + variantRSIDsList := []int64{variantRSID} + + // We add aliases to variantRSIDsList + + anyAliasesExist, rsidAliasesList, err := locusMetadata.GetRSIDAliases(variantRSID) + if (err != nil) { return nil, nil, err } + if (anyAliasesExist == true){ + variantRSIDsList = append(variantRSIDsList, rsidAliasesList...) + } + + variantRSIDsMap[variantIdentifier] = variantRSIDsList + + for _, genomeWithMetadataObject := range inputGenomesWithMetadataList{ + + genomeIdentifier := genomeWithMetadataObject.GenomeIdentifier + genomeMap := genomeWithMetadataObject.GenomeMap + + basePairValueFound, base1Value, base2Value, locusIsPhased, err := getGenomeLocusBasePair(genomeMap, variantRSID) + if (err != nil) { return nil, nil, err } + if (basePairValueFound == false){ + + variantMap[genomeIdentifier + "_HasVariant"] = "Unknown" + continue + } + + getBaseIsVariantMutationBool := func(inputBase string)bool{ + + if (inputBase == variantDefectiveBase){ + return true + } + // Base could be mutated to a different unhealthy base + // That mutation could be a neutral/healthier change + // We only care about this specific variant + return false + } + + base1IsDefective := getBaseIsVariantMutationBool(base1Value) + base2IsDefective := getBaseIsVariantMutationBool(base2Value) + + base1IsDefectiveString := helpers.ConvertBoolToYesOrNoString(base1IsDefective) + base2IsDefectiveString := helpers.ConvertBoolToYesOrNoString(base2IsDefective) + + genomeHasVariantMapValue := base1IsDefectiveString + ";" + base2IsDefectiveString + locusIsPhasedString := helpers.ConvertBoolToYesOrNoString(locusIsPhased) + + variantMap[genomeIdentifier + "_HasVariant"] = genomeHasVariantMapValue + variantMap[genomeIdentifier + "_LocusIsPhased"] = locusIsPhasedString + + //TODO: Add LocusIsPhased to readGeneticAnalysis package + } + + variantsMapList = append(variantsMapList, variantMap) + } + + // Now we determine probability that user will pass disease to offspring, and probability that user has disease + // We compute this for each genome + + diseaseAnalysisMap := make(map[string]string) + + // We will use this list when checking for conflicts + allGenomeIdentifiersList := make([]string, 0, len(inputGenomesWithMetadataList)) + + for _, genomeWithMetadataObject := range inputGenomesWithMetadataList{ + + genomeIdentifier := genomeWithMetadataObject.GenomeIdentifier + + allGenomeIdentifiersList = append(allGenomeIdentifiersList, genomeIdentifier) + + numberOfVariantsTested := 0 + + for _, variantMap := range variantsMapList{ + + genomeHasVariantValue, exists := variantMap[genomeIdentifier + "_HasVariant"] + if (exists == false){ + return nil, nil, errors.New("variantMap malformed: Map missing a _HasVariant for genome") + } + + if (genomeHasVariantValue != "Unknown"){ + numberOfVariantsTested += 1 + } + } + + numberOfVariantsTestedString := helpers.ConvertIntToString(numberOfVariantsTested) + + diseaseAnalysisMap[genomeIdentifier + "_NumberOfVariantsTested"] = numberOfVariantsTestedString + + // Outputs: + // -bool: Probability is known (will be false if the genome has no tested variants) + // -float64: Probability Person has disease + // -float64: Probability Person will pass a defect (variant) to offspring + // -error + getPersonDiseaseInfo := func()(bool, float64, float64, error){ + + anyProbabilityKnown := false + + // These variables are used to count the number of defective variants that exist on each chromosome + numberOfVariants_Chromosome1 := 0 + numberOfVariants_Chromosome2 := 0 + numberOfVariants_UnknownChromosome := 0 + + // We use this map to keep track of how many mutations exist for each rsid + // This allows us to know if 2 different variant mutations exist for a single RSID + // For example, base1 is a different deleterious mutation than base2 + // If this ever happens, we know that the user has the disease, + // because both copies of the gene locus are defective. + rsidMutationsMap := make(map[int64]int) + + for _, variantMap := range variantsMapList{ + + personHasVariantStatus, exists := variantMap[genomeIdentifier + "_HasVariant"] + if (exists == false){ + return false, 0, 0, errors.New("variantMap malformed: Map missing a _HasVariant for genome.") + } + + if (personHasVariantStatus == "Unknown"){ + // We don't know if the genome has the variant. Skip to next variant + continue + } + + anyProbabilityKnown = true + + locusIsPhasedStatus, exists := variantMap[genomeIdentifier + "_LocusIsPhased"] + if (exists == false){ + return false, 0, 0, errors.New("variantMap malformed: Map missing _LocusIsPhased for genome.") + } + + base1HasVariant, base2HasVariant, foundSemicolon := strings.Cut(personHasVariantStatus, ";") + if (foundSemicolon == false){ + return false, 0, 0, errors.New("variantMap is malformed: Contains invalid personHasVariantValue: " + personHasVariantStatus) + } + + if (base1HasVariant == "No" && base2HasVariant == "No"){ + // Neither chromosome contains the variant mutation. + continue + } + + if (base1HasVariant == "Yes" && base2HasVariant == "Yes"){ + // Both chromosomes contain the same variant mutation. + // Person has the disease. + // Person will definitely pass disease variant to offspring. + return true, 1, 1, nil + } + + // We know that this variant exists on 1 of the bases, but not both. + + variantIdentifier, exists := variantMap["VariantIdentifier"] + if (exists == false){ + return false, 0, 0, errors.New("VariantMap missing VariantIdentifier.") + } + + variantRSIDsList, exists := variantRSIDsMap[variantIdentifier] + if (exists == false){ + return false, 0, 0, errors.New("variantRSIDMap missing variantIdentifier.") + } + + for _, rsid := range variantRSIDsList{ + rsidMutationsMap[rsid] += 1 + } + + if (locusIsPhasedStatus == "Yes"){ + + if (base1HasVariant == "Yes"){ + numberOfVariants_Chromosome1 += 1 + } + if (base2HasVariant == "Yes"){ + numberOfVariants_Chromosome2 += 1 + } + } else { + + if (base1HasVariant == "Yes" || base2HasVariant == "Yes"){ + numberOfVariants_UnknownChromosome += 1 + } + } + } + + if (anyProbabilityKnown == false){ + return false, 0, 0, nil + } + + totalNumberOfVariants := numberOfVariants_Chromosome1 + numberOfVariants_Chromosome2 + numberOfVariants_UnknownChromosome + + if (totalNumberOfVariants == 0){ + // Person does not have any disease variants. + // They do not have the disease, and have no chance of passing a disease variant + return true, 0, 0, nil + } + + // Now we check to see if there are any loci which have 2 different variants, one for each base + + for _, numberOfMutations := range rsidMutationsMap{ + + if (numberOfMutations >= 2){ + // Person has 2 mutations on the same location + // They must have the disease, and will definitely pass a variant to their offspring + return true, 1, 1, nil + } + } + + // At this point, we know that there are no homozygous variant mutations + // All variant mutations are heterozygous, meaning the other chromosome base is healthy + + // Probability is expressed as a float between 0 - 1 + getProbabilityPersonHasDisease := func()float64{ + + if (dominantOrRecessive == "Dominant"){ + // Only 1 variant is needed for the person to have the disease + // We know they have at least 1 variant + return 1 + } + + // dominantOrRecessive == "Recessive" + + if (totalNumberOfVariants == 1){ + // There is only 1 variant in total. + // This single variant cannot exist on both chromosomes. + // The person does not have the disease + return 0 + } + + if (numberOfVariants_Chromosome1 >= 1 && numberOfVariants_Chromosome2 >= 1){ + + // We know there is at least 1 variant mutation on each chromosome + // Therefore, the person has the disease + return 1 + } + + if (numberOfVariants_UnknownChromosome == 0){ + + // We know that variants do not exist on both chromosomes, only on 1. + // Thus, the person does not have the disease + return 0 + } + + if (numberOfVariants_Chromosome1 == 0 && numberOfVariants_Chromosome2 == 0){ + + // All variants have an unknown phase, and we know there are multiple of them. + // The probability the person does not have the disease is the probability that all mutations are on the same chromosome + // We calculate the probability that all of the mutations are on the same chromosome + // If they are all on the same chromosome, the person does not have the disease + // If at least 1 variant exists on both chromosomes, the person has the disease + + // We calculate the probability that all variants are all on the same chromosome + // Probability of n variants existing on the same chromosome = 1/(2^n) + // P(X) = Probability all variants existing on chromosome 1 = 1/(2^n) + // P(Y) = Probability all variants existing on chromosome 2 = 1/(2^n) + // P(A U B) = P(A) + P(B) (If A and B are mutually exclusive) + // P(X) and P(Y) are mutually exclusive + // Probability of n variants existing on either chromosome 1 or 2 = P(X) + P(Y) + // Probability person has disease = !P(X U Y) + + probabilityAllVariantsAreOnOneChromosome := 1/(math.Pow(2, float64(numberOfVariants_UnknownChromosome))) + + probabilityPersonHasDisease := 1 - (probabilityAllVariantsAreOnOneChromosome * 2) + + return probabilityPersonHasDisease + } + + // We know that there is at least 1 variant whose phase is known + // We know that there are no variants whose phase is known which exist on both chromosomes + // We know there are at least some variants whose phase is unknown + + // The probability that the person has the disease is + // the probability that the unknown-phase variants exist on the same chromosome as the ones which we know exist do + // This probability is 50% for each unknown-phase variant + + probabilityAllVariantsAreOnOneChromosome := 1/(math.Pow(2, float64(numberOfVariants_UnknownChromosome))) + + probabilityPersonHasDisease := 1 - probabilityAllVariantsAreOnOneChromosome + + return probabilityPersonHasDisease + } + + probabilityPersonHasDisease := getProbabilityPersonHasDisease() + + // We know all variants are heterozygous + + // Probability person will not pass any of n variants to their offspring: 1/(2^n) + // Probability person will pass at least 1 of n variants to their offspring: 1 - (1/(2^n)) + + probabilityPersonWillPassAnyVariant := 1 - (1/(math.Pow(2, float64(totalNumberOfVariants)))) + + return true, probabilityPersonHasDisease, probabilityPersonWillPassAnyVariant, nil + } + + probabilityKnown, probabilityPersonHasDisease, probabilityPersonWillPassAnyVariant, err := getPersonDiseaseInfo() + if (err != nil) { return nil, nil, err } + if (probabilityKnown == false){ + diseaseAnalysisMap[genomeIdentifier + "_ProbabilityOfHavingDisease"] = "Unknown" + diseaseAnalysisMap[genomeIdentifier + "_ProbabilityOfPassingADiseaseVariant"] = "Unknown" + continue + } + + percentageProbabilityPersonHasDisease := probabilityPersonHasDisease * 100 + percentageProbabilityPersonWillPassADiseaseVariant := probabilityPersonWillPassAnyVariant * 100 + + probabilityOfHavingDiseaseString := helpers.ConvertFloat64ToStringRounded(percentageProbabilityPersonHasDisease, 0) + probabilityOfPassingADiseaseVariantString := helpers.ConvertFloat64ToStringRounded(percentageProbabilityPersonWillPassADiseaseVariant, 0) + + diseaseAnalysisMap[genomeIdentifier + "_ProbabilityOfHavingDisease"] = probabilityOfHavingDiseaseString + diseaseAnalysisMap[genomeIdentifier + "_ProbabilityOfPassingADiseaseVariant"] = probabilityOfPassingADiseaseVariantString + } + + if (len(allGenomeIdentifiersList) == 1){ + // We do not need to check for conflicts + // Nothing left to do. Analysis is complete. + return diseaseAnalysisMap, variantsMapList, nil + } + + // We check for conflicts + + getConflictExistsBool := func()(bool, error){ + + // We start with disease analysis map + + probabilityOfHavingDisease := "" + probabilityOfPassingAVariant := "" + + for index, genomeIdentifier := range allGenomeIdentifiersList{ + + currentProbabilityOfHavingDisease, exists := diseaseAnalysisMap[genomeIdentifier + "_ProbabilityOfHavingDisease"] + if (exists == false){ + return false, errors.New("Cannot create analysis: diseaseAnalysisMap missing _ProbabilityOfHavingDisease") + } + + currentProbabilityOfPassingAVariant, exists := diseaseAnalysisMap[genomeIdentifier + "_ProbabilityOfPassingADiseaseVariant"] + if (exists == false){ + return false, errors.New("Cannot create analysis: diseaseAnalysisMap missing _ProbabilityOfPassingADiseaseVariant") + } + + if (index == 0){ + probabilityOfHavingDisease = currentProbabilityOfHavingDisease + probabilityOfPassingAVariant = currentProbabilityOfPassingAVariant + continue + } + + if (currentProbabilityOfHavingDisease != probabilityOfHavingDisease){ + return true, nil + } + if (currentProbabilityOfPassingAVariant != probabilityOfPassingAVariant){ + return true, nil + } + } + + // Now we test variants for conflicts + + for _, variantMap := range variantsMapList{ + + // Each variant value is either "Yes;No", "No;Yes", "No;No", or "Yes;Yes" + + // Two different genomes have "Yes;No" and "No;Yes", it will not count as a conflict + // If the locus is unphased, then there is no difference between "Yes;No" and "No;Yes" + // If the locus is phased, then this flip is only meaningful if it effects the probability of disease/passing a variant + // We already checked those probabilities for conflicts earlier + // Therefore, any flip is not considered a conflict + + genomeHasVariantStatus := "" + + for index, genomeIdentifier := range allGenomeIdentifiersList{ + + currentGenomeHasVariantStatus, exists := variantMap[genomeIdentifier + "_HasVariant"] + if (exists == false){ + return false, errors.New("variantMap missing genomeIdentifier key when checking for conflicts") + } + + if (index == 0){ + genomeHasVariantStatus = currentGenomeHasVariantStatus + continue + } + + if (currentGenomeHasVariantStatus != genomeHasVariantStatus){ + + if (currentGenomeHasVariantStatus == "Yes;No" && genomeHasVariantStatus == "No;Yes"){ + continue + } + if (currentGenomeHasVariantStatus == "No;Yes" && genomeHasVariantStatus == "Yes;No"){ + continue + } + return true, nil + } + } + } + + return false, nil + } + + conflictExists, err := getConflictExistsBool() + if (err != nil) { return nil, nil, err } + + conflictExistsString := helpers.ConvertBoolToYesOrNoString(conflictExists) + + diseaseAnalysisMap["ConflictExists"] = conflictExistsString + + return diseaseAnalysisMap, variantsMapList, nil +} + + + +//Outputs: +// -map[string]string: Polygenic Disease analysis map (contains weight/probability for each genomeIdentifier and number of loci tested) +// -[]map[string]string: Loci map list (contains locus info for each genomeIdentifier) +// -error +func getPolygenicDiseaseAnalysis(inputGenomesWithMetadataList []prepareRawGenomes.GenomeWithMetadata, diseaseObject polygenicDiseases.PolygenicDisease)(map[string]string, []map[string]string, error){ + + diseaseName := diseaseObject.DiseaseName + diseaseLociList := diseaseObject.LociList + + lociMapList := make([]map[string]string, 0, len(diseaseLociList)) + + // Map Structure: Locus identifier -> Disease Locus object + locusObjectsMap := make(map[string]polygenicDiseases.DiseaseLocus) + + for _, locusObject := range diseaseLociList{ + + locusIdentifier := locusObject.LocusIdentifier + + locusObjectsMap[locusIdentifier] = locusObject + + locusMap := map[string]string{ + "ItemType": "PolygenicDiseaseLocus", + "LocusIdentifier": locusIdentifier, + "DiseaseName": diseaseName, + } + + locusRSID := locusObject.LocusRSID + locusRiskWeightsMap := locusObject.RiskWeightsMap + locusOddsRatiosMap := locusObject.OddsRatiosMap + + for _, genomeWithMetadataObject := range inputGenomesWithMetadataList{ + + genomeIdentifier := genomeWithMetadataObject.GenomeIdentifier + genomeMap := genomeWithMetadataObject.GenomeMap + + //Outputs: + // -bool: Disease locus info is known + // -string: Locus Bases + // -int: Genome disease locus risk weight + // -bool: Genome disease locus odds ratio known + // -float64: Genome disease locus odds ratio + // -error + getGenomeDiseaseLocusInfo := func()(bool, string, int, bool, float64, error){ + + basePairValueFound, base1Value, base2Value, _, err := getGenomeLocusBasePair(genomeMap, locusRSID) + if (err != nil) { return false, "", 0, false, 0, err } + if (basePairValueFound == false){ + return false, "", 0, false, 0, nil + } + + locusBasePair := base1Value + ";" + base2Value + + riskWeight, exists := locusRiskWeightsMap[locusBasePair] + if (exists == false){ + // This is an unknown base combination + // We will treat it as a 0 risk weight + return true, locusBasePair, 0, true, 1, nil + } + + if (riskWeight == 0){ + return true, locusBasePair, 0, true, 1, nil + } + + oddsRatio, exists := locusOddsRatiosMap[locusBasePair] + if (exists == false){ + return true, locusBasePair, 0, false, 0, nil + } + + return true, locusBasePair, riskWeight, true, oddsRatio, nil + } + + diseaseLocusInfoKnown, locusBasePair, locusRiskWeight, locusOddsRatioKnown, locusOddsRatio, err := getGenomeDiseaseLocusInfo() + if (err != nil) { return nil, nil, err } + + if (diseaseLocusInfoKnown == false){ + + locusMap[genomeIdentifier + "_LocusBasePair"] = "Unknown" + locusMap[genomeIdentifier + "_RiskWeight"] = "Unknown" + locusMap[genomeIdentifier + "_OddsRatio"] = "Unknown" + } else { + + locusMap[genomeIdentifier + "_LocusBasePair"] = locusBasePair + + riskWeightString := helpers.ConvertIntToString(locusRiskWeight) + + locusMap[genomeIdentifier + "_RiskWeight"] = riskWeightString + + if (locusOddsRatioKnown == false){ + locusMap[genomeIdentifier + "_OddsRatio"] = "Unknown" + } else { + locusOddsRatioString := helpers.ConvertFloat64ToString(locusOddsRatio) + locusMap[genomeIdentifier + "_OddsRatio"] = locusOddsRatioString + } + } + } + + lociMapList = append(lociMapList, locusMap) + } + + // Now we construct polygenic disease probability map for each genome + + diseaseAnalysisMap := make(map[string]string) + + // We use this list to check for conflicts later + allGenomeIdentifiersList := make([]string, 0, len(inputGenomesWithMetadataList)) + + for _, genomeWithMetadataObject := range inputGenomesWithMetadataList{ + + genomeIdentifier := genomeWithMetadataObject.GenomeIdentifier + + allGenomeIdentifiersList = append(allGenomeIdentifiersList, genomeIdentifier) + + numberOfLociTested := 0 + + minimumPossibleRiskWeightSum := 0 + maximumPossibleRiskWeightSum := 0 + + summedDiseaseRiskWeight := 0 + + for _, locusMap := range lociMapList{ + + genomeRiskWeight, exists := locusMap[genomeIdentifier + "_RiskWeight"] + if (exists == false){ + return nil, nil, errors.New("locusMap malformed: Map missing a genomeIdentifier riskWeight") + } + + if (genomeRiskWeight == "Unknown"){ + // The genome does not have this locus + // We cannot test for risk weight + continue + } + + numberOfLociTested += 1 + + locusIdentifier, exists := locusMap["LocusIdentifier"] + if (exists == false) { + return nil, nil, errors.New("locusMap malformed: Map missing LocusIdentifier") + } + + locusObject, exists := locusObjectsMap[locusIdentifier] + if (exists == false){ + return nil, nil, errors.New("LocusObjectsMap missing locus: " + locusIdentifier) + } + + locusMinimumWeight := locusObject.MinimumRiskWeight + locusMaximumWeight := locusObject.MaximumRiskWeight + + minimumPossibleRiskWeightSum += locusMinimumWeight + maximumPossibleRiskWeightSum += locusMaximumWeight + + genomeRiskWeightInt, err := helpers.ConvertStringToInt(genomeRiskWeight) + if (err != nil) { + return nil, nil, errors.New("Locus map malformed: Contains invalid _RiskWeight: " + genomeRiskWeight) + } + + summedDiseaseRiskWeight += genomeRiskWeightInt + } + + numberOfLociTestedString := helpers.ConvertIntToString(numberOfLociTested) + + diseaseAnalysisMap[genomeIdentifier + "_NumberOfLociTested"] = numberOfLociTestedString + + if (numberOfLociTested == 0){ + diseaseAnalysisMap[genomeIdentifier + "_RiskScore"] = "Unknown" + + continue + } + + diseaseRiskScore, err := helpers.ScaleNumberProportionally(true, summedDiseaseRiskWeight, minimumPossibleRiskWeightSum, maximumPossibleRiskWeightSum, 0, 10) + if (err != nil) { return nil, nil, err } + + diseaseRiskScoreString := helpers.ConvertIntToString(diseaseRiskScore) + + diseaseAnalysisMap[genomeIdentifier + "_RiskScore"] = diseaseRiskScoreString + } + + if (len(allGenomeIdentifiersList) == 1){ + // We do not need to check for conflicts + // Nothing left to do. Analysis is complete. + return diseaseAnalysisMap, lociMapList, nil + } + + // We check for conflicts + + getConflictExistsBool := func()(bool, error){ + + for _, locusMap := range lociMapList{ + + locusBasePair := "" + + for index, genomeIdentifier := range allGenomeIdentifiersList{ + + currentLocusBasePair, exists := locusMap[genomeIdentifier + "_LocusBasePair"] + if (exists == false){ + return false, errors.New("Cannot find _LocusBasePair key when searching for conflicts") + } + + if (index == 0){ + + locusBasePair = currentLocusBasePair + continue + } + if (currentLocusBasePair != locusBasePair){ + return true, nil + } + } + } + + return false, nil + } + + conflictExists, err := getConflictExistsBool() + if (err != nil) { return nil, nil, err } + + conflictExistsString := helpers.ConvertBoolToYesOrNoString(conflictExists) + + diseaseAnalysisMap["ConflictExists"] = conflictExistsString + + return diseaseAnalysisMap, lociMapList, nil +} + + + +//Outputs: +// -map[string]string: Trait analysis map (contains weight/probability for each genomeIdentifier and number of rules tested) +// -[]map[string]string: Loci map list (contains locus info for each genomeIdentifier) +// -error +func getTraitAnalysis(inputGenomesWithMetadataList []prepareRawGenomes.GenomeWithMetadata, traitObject traits.Trait)(map[string]string, []map[string]string, error){ + + traitName := traitObject.TraitName + traitLociList := traitObject.LociList + traitRulesList := traitObject.RulesList + + // We first add loci values to trait analysis map + + traitAnalysisMap := make(map[string]string) + + // We use this list to check for conflicts later + allGenomeIdentifiersList := make([]string, 0, len(inputGenomesWithMetadataList)) + + for _, genomeWithMetadataObject := range inputGenomesWithMetadataList{ + + genomeIdentifier := genomeWithMetadataObject.GenomeIdentifier + + allGenomeIdentifiersList = append(allGenomeIdentifiersList, genomeIdentifier) + + genomeMap := genomeWithMetadataObject.GenomeMap + + for _, locusRSID := range traitLociList{ + + locusRSIDString := helpers.ConvertInt64ToString(locusRSID) + + locusBasePairKnown, locusBase1, locusBase2, locusIsPhased, err := getGenomeLocusBasePair(genomeMap, locusRSID) + if (err != nil) { return nil, nil, err } + if (locusBasePairKnown == false){ + + traitAnalysisMap[genomeIdentifier + "_LocusValue_rs" + locusRSIDString] = "Unknown" + } else { + + locusBasePair := locusBase1 + ";" + locusBase2 + + locusIsPhasedString := helpers.ConvertBoolToYesOrNoString(locusIsPhased) + + traitAnalysisMap[genomeIdentifier + "_LocusValue_rs" + locusRSIDString] = locusBasePair + traitAnalysisMap[genomeIdentifier + "_LocusIsPhased_rs" + locusRSIDString] = locusIsPhasedString + } + } + } + + // This map stores the TraitRule maps + rulesMapList := make([]map[string]string, 0, len(traitRulesList)) + + // Map Structure: Rule identifier -> Rule object + ruleObjectsMap := make(map[string]traits.TraitRule) + + if (len(traitRulesList) != 0){ + + // This trait contains at least 1 rule + + for _, ruleObject := range traitRulesList{ + + ruleIdentifier := ruleObject.RuleIdentifier + + ruleObjectsMap[ruleIdentifier] = ruleObject + + ruleMap := map[string]string{ + "ItemType": "TraitRule", + "RuleIdentifier": ruleIdentifier, + "TraitName": traitName, + } + + ruleLociList := ruleObject.LociList + + for _, genomeWithMetadataObject := range inputGenomesWithMetadataList{ + + genomeIdentifier := genomeWithMetadataObject.GenomeIdentifier + genomeMap := genomeWithMetadataObject.GenomeMap + + // We add locus base pairs to ruleMap + // We also check to see if genome passes all rule loci + // We only consider a rule Known if the genome either passes all loci, or fails to pass 1 locus + + allRuleLociKnown := true + anyLocusFailureKnown := false //This is true if at least 1 locus is known to not pass + + for _, locusObject := range ruleLociList{ + + locusIdentifier := locusObject.LocusIdentifier + locusRSID := locusObject.LocusRSID + + locusBasePairKnown, locusBase1, locusBase2, _, err := getGenomeLocusBasePair(genomeMap, locusRSID) + if (err != nil) { return nil, nil, err } + if (locusBasePairKnown == false){ + + ruleMap[genomeIdentifier + "_RuleLocusBasePair_" + locusIdentifier] = "Unknown" + + // The genome has failed to pass a single rule locus, thus, the rule is not passed + // We still continue so we can add all locus base pairs to the ruleMap + allRuleLociKnown = false + continue + } + + locusBasePair := locusBase1 + ";" + locusBase2 + + ruleMap[genomeIdentifier + "_RuleLocusBasePair_" + locusIdentifier] = locusBasePair + + if (anyLocusFailureKnown == true){ + // We don't have to check if the rule locus is passed, because we already know the rule is not passed + continue + } + + locusBasePairsList := locusObject.BasePairsList + + genomePassesRuleLocus := slices.Contains(locusBasePairsList, locusBasePair) + if (genomePassesRuleLocus == false){ + // We know the rule is not passed + // We still continue so we can add all locus base pairs to the ruleMap + anyLocusFailureKnown = true + continue + } + } + + if (anyLocusFailureKnown == true){ + + ruleMap[genomeIdentifier + "_PassesRule"] = "No" + + } else if (allRuleLociKnown == false){ + + ruleMap[genomeIdentifier + "_PassesRule"] = "Unknown" + } else { + + ruleMap[genomeIdentifier + "_PassesRule"] = "Yes" + } + } + + rulesMapList = append(rulesMapList, ruleMap) + } + + // Now we construct trait outcome scores map for each genome + + traitOutcomesList := traitObject.OutcomesList + + for _, genomeWithMetadataObject := range inputGenomesWithMetadataList{ + + genomeIdentifier := genomeWithMetadataObject.GenomeIdentifier + + // A rule is considered tested if at least 1 locus within the rule is known to fail, or all loci within the rule pass + numberOfRulesTested := 0 + + // Map Structure: Trait outcome -> Number of points + traitOutcomeScoresMap := make(map[string]int) + + for _, ruleMap := range rulesMapList{ + + genomePassesRule, exists := ruleMap[genomeIdentifier + "_PassesRule"] + if (exists == false){ + return nil, nil, errors.New("ruleMap malformed: Map missing a genomeIdentifier _PassesRule") + } + + if (genomePassesRule == "Unknown"){ + continue + } + + numberOfRulesTested += 1 + + if (genomePassesRule == "No"){ + continue + } + + ruleIdentifier, exists := ruleMap["RuleIdentifier"] + if (exists == false) { + return nil, nil, errors.New("ruleMap malformed: Map missing RuleIdentifier") + } + + ruleObject, exists := ruleObjectsMap[ruleIdentifier] + if (exists == false){ + return nil, nil, errors.New("RuleObjectsMap missing rule: " + ruleIdentifier) + } + + ruleOutcomePointsMap := ruleObject.OutcomePointsMap + + for traitOutcome, pointsChange := range ruleOutcomePointsMap{ + + traitOutcomeScoresMap[traitOutcome] += pointsChange + } + } + + numberOfRulesTestedString := helpers.ConvertIntToString(numberOfRulesTested) + + traitAnalysisMap[genomeIdentifier + "_NumberOfRulesTested"] = numberOfRulesTestedString + + if (numberOfRulesTested == 0){ + // No rules were tested. The number of points in each outcome are all unknown. + continue + } + + // We add all outcomes for which there were no points + + for _, traitOutcome := range traitOutcomesList{ + + _, exists := traitOutcomeScoresMap[traitOutcome] + if (exists == false){ + traitOutcomeScoresMap[traitOutcome] = 0 + } + } + + for traitOutcome, outcomeScore := range traitOutcomeScoresMap{ + + outcomeScoreString := helpers.ConvertIntToString(outcomeScore) + + traitAnalysisMap[genomeIdentifier + "_OutcomeScore_" + traitOutcome] = outcomeScoreString + } + } + } else { + + // No rules exist for this trait + + for _, genomeIdentifier := range allGenomeIdentifiersList{ + + traitAnalysisMap[genomeIdentifier + "_NumberOfRulesTested"] = "0" + } + } + + if (len(allGenomeIdentifiersList) == 1){ + // We do not need to check for conflicts + // Nothing left to do. Analysis is complete. + return traitAnalysisMap, rulesMapList, nil + } + + // We check for conflicts + + getConflictExistsBool := func()(bool, error){ + + //TODO: Check for locus value conflicts once locus values are used in neural network prediction. + + // We only have to check each rule result, because they will determine the overall person results + + for _, ruleMap := range rulesMapList{ + + passesRule := "" + + for index, genomeIdentifier := range allGenomeIdentifiersList{ + + currentPassesRule, exists := ruleMap[genomeIdentifier + "_PassesRule"] + if (exists == false){ + return false, errors.New("Cannot find _PassesRule key when searching for conflicts") + } + + if (index == 0){ + + passesRule = currentPassesRule + continue + } + if (currentPassesRule != passesRule){ + return true, nil + } + } + } + + return false, nil + } + + conflictExists, err := getConflictExistsBool() + if (err != nil) { return nil, nil, err } + + conflictExistsString := helpers.ConvertBoolToYesOrNoString(conflictExists) + + traitAnalysisMap["ConflictExists"] = conflictExistsString + + return traitAnalysisMap, rulesMapList, nil +} + + diff --git a/internal/genetics/createGeneticAnalysis/createGeneticAnalysis_test.go b/internal/genetics/createGeneticAnalysis/createGeneticAnalysis_test.go new file mode 100644 index 0000000..c68e7a2 --- /dev/null +++ b/internal/genetics/createGeneticAnalysis/createGeneticAnalysis_test.go @@ -0,0 +1,427 @@ +package createGeneticAnalysis_test + +import "seekia/internal/genetics/createGeneticAnalysis" + +import "seekia/internal/genetics/readGeneticAnalysis" + +import "seekia/resources/geneticReferences/locusMetadata" +import "seekia/resources/geneticReferences/monogenicDiseases" +import "seekia/resources/geneticReferences/polygenicDiseases" +import "seekia/resources/geneticReferences/traits" + +import "seekia/internal/genetics/createRawGenomes" +import "seekia/internal/genetics/prepareRawGenomes" +import "seekia/internal/helpers" + +import "testing" +import "errors" + +func TestCreatePersonGeneticAnalysis_SingleGenome(t *testing.T){ + + err := locusMetadata.InitializeLocusMetadataVariables() + if (err != nil) { + t.Fatalf("InitializeLocusMetadataVariables failed: " + err.Error()) + } + + monogenicDiseases.InitializeMonogenicDiseaseVariables() + polygenicDiseases.InitializePolygenicDiseaseVariables() + traits.InitializeTraitVariables() + + genomeIdentifier, err := helpers.GetNewRandomHexString(16) + if (err != nil) { + t.Fatalf("Failed to get random hex string: " + err.Error()) + } + + fakeRawGenome, _, _, _, err := createRawGenomes.CreateFakeRawGenome_AncestryDNA() + if (err != nil) { + t.Fatalf("Failed to create fake raw AncestryDNA genome: " + err.Error()) + } + + genomeIsValid, rawGenomeWithMetadata, err := prepareRawGenomes.CreateRawGenomeWithMetadataObject(genomeIdentifier, fakeRawGenome) + if (err != nil){ + t.Fatalf("CreateRawGenomeWithMetadataObject failed: " + err.Error()) + } + if (genomeIsValid == false){ + t.Fatalf("CreateRawGenomeWithMetadataObject failed: Genome is not valid.") + } + + genomesList := []prepareRawGenomes.RawGenomeWithMetadata{rawGenomeWithMetadata} + + updateProgressFunction := func(_ int)error{ + return nil + } + + checkIfProcessIsStoppedFunction := func()bool{ + return false + } + + processCompleted, personGeneticAnalysis, err := createGeneticAnalysis.CreatePersonGeneticAnalysis(genomesList, updateProgressFunction, checkIfProcessIsStoppedFunction) + if (err != nil){ + t.Fatalf("Failed to create person genetic analysis: " + err.Error()) + } + if (processCompleted == false){ + t.Fatalf("Failed to create person genetic analysis: Process did not complete.") + } + + personGeneticAnalysisMapList, err := readGeneticAnalysis.ReadGeneticAnalysisString(personGeneticAnalysis) + if (err != nil){ + t.Fatalf("Failed to read person genetic analysis string: " + err.Error()) + } + + err = readGeneticAnalysis.ReadPersonGeneticAnalysisForTests(personGeneticAnalysisMapList) + if (err != nil){ + t.Fatalf("Failed to read person genetic analysis: " + err.Error()) + } +} + + +func TestCreatePersonGeneticAnalysis_MultipleGenomes(t *testing.T){ + + err := locusMetadata.InitializeLocusMetadataVariables() + if (err != nil) { + t.Fatalf("InitializeLocusMetadataVariables failed: " + err.Error()) + } + + monogenicDiseases.InitializeMonogenicDiseaseVariables() + polygenicDiseases.InitializePolygenicDiseaseVariables() + traits.InitializeTraitVariables() + + numberOfGenomesToAdd := helpers.GetRandomIntWithinRange(2, 5) + + genomesList := make([]prepareRawGenomes.RawGenomeWithMetadata, 0, numberOfGenomesToAdd) + + for i:=0; i < numberOfGenomesToAdd; i++{ + + genomeIdentifier, err := helpers.GetNewRandomHexString(16) + if (err != nil) { + t.Fatalf("Failed to get random hex string: " + err.Error()) + } + + getFakeRawGenome := func()(string, error){ + + is23andMe := helpers.GetRandomBool() + if (is23andMe == true){ + fakeRawGenome, _, _, _, err := createRawGenomes.CreateFakeRawGenome_23andMe() + if (err != nil) { + return "", errors.New("Failed to create fake raw 23andMe genome: " + err.Error()) + } + + return fakeRawGenome, nil + } + + fakeRawGenome, _, _, _, err := createRawGenomes.CreateFakeRawGenome_AncestryDNA() + if (err != nil) { + return "", errors.New("Failed to create fake raw AncestryDNA genome: " + err.Error()) + } + + return fakeRawGenome, nil + } + + fakeRawGenome, err := getFakeRawGenome() + if (err != nil){ + t.Fatalf("Failed to get fake raw genome: " + err.Error()) + } + + genomeIsValid, rawGenomeWithMetadata, err := prepareRawGenomes.CreateRawGenomeWithMetadataObject(genomeIdentifier, fakeRawGenome) + if (err != nil){ + t.Fatalf("CreateRawGenomeWithMetadataObject failed: " + err.Error()) + } + if (genomeIsValid == false){ + t.Fatalf("CreateRawGenomeWithMetadataObject failed: Genome is not valid.") + } + + genomesList = append(genomesList, rawGenomeWithMetadata) + } + + updateProgressFunction := func(_ int)error{ + return nil + } + + checkIfProcessIsStoppedFunction := func()bool{ + return false + } + + processCompleted, personGeneticAnalysis, err := createGeneticAnalysis.CreatePersonGeneticAnalysis(genomesList, updateProgressFunction, checkIfProcessIsStoppedFunction) + if (err != nil){ + t.Fatalf("Failed to create person genetic analysis: " + err.Error()) + } + if (processCompleted == false){ + t.Fatalf("Failed to create person genetic analysis: Process did not complete.") + } + + personGeneticAnalysisMapList, err := readGeneticAnalysis.ReadGeneticAnalysisString(personGeneticAnalysis) + if (err != nil){ + t.Fatalf("Failed to read person genetic analysis string: " + err.Error()) + } + + err = readGeneticAnalysis.ReadPersonGeneticAnalysisForTests(personGeneticAnalysisMapList) + if (err != nil){ + t.Fatalf("Failed to read person genetic analysis: " + err.Error()) + } +} + + +func TestCreateCoupleGeneticAnalysis_SingleGenomes(t *testing.T){ + + err := locusMetadata.InitializeLocusMetadataVariables() + if (err != nil) { + t.Fatalf("InitializeLocusMetadataVariables failed: " + err.Error()) + } + + monogenicDiseases.InitializeMonogenicDiseaseVariables() + polygenicDiseases.InitializePolygenicDiseaseVariables() + traits.InitializeTraitVariables() + + getPersonGenomesList := func()([]prepareRawGenomes.RawGenomeWithMetadata, error){ + + genomeIdentifier, err := helpers.GetNewRandomHexString(16) + if (err != nil) { + return nil, errors.New("Failed to get random hex string: " + err.Error()) + } + + fakeRawGenome, _, _, _, err := createRawGenomes.CreateFakeRawGenome_AncestryDNA() + if (err != nil) { + return nil, errors.New("Failed to create fake raw AncestryDNA genome: " + err.Error()) + } + + genomeIsValid, rawGenomeWithMetadata, err := prepareRawGenomes.CreateRawGenomeWithMetadataObject(genomeIdentifier, fakeRawGenome) + if (err != nil){ + return nil, errors.New("CreateRawGenomeWithMetadataObject failed: " + err.Error()) + } + if (genomeIsValid == false){ + return nil, errors.New("CreateRawGenomeWithMetadataObject failed: Genome is not valid.") + } + + personGenomesList := []prepareRawGenomes.RawGenomeWithMetadata{rawGenomeWithMetadata} + + return personGenomesList, nil + } + + personAGenomesList, err := getPersonGenomesList() + if (err != nil){ + t.Fatalf("getPersonGenomesList failed: " + err.Error()) + } + + personBGenomesList, err := getPersonGenomesList() + if (err != nil){ + t.Fatalf("getPersonGenomesList failed: " + err.Error()) + } + + updateProgressFunction := func(_ int)error{ + return nil + } + + checkIfProcessIsStoppedFunction := func()bool{ + return false + } + + processCompleted, coupleGeneticAnalysis, err := createGeneticAnalysis.CreateCoupleGeneticAnalysis(personAGenomesList, personBGenomesList, updateProgressFunction, checkIfProcessIsStoppedFunction) + if (err != nil){ + t.Fatalf("Failed to create couple genetic analysis: " + err.Error()) + } + if (processCompleted == false){ + t.Fatalf("Failed to create couple genetic analysis: Process did not complete.") + } + + coupleGeneticAnalysisMapList, err := readGeneticAnalysis.ReadGeneticAnalysisString(coupleGeneticAnalysis) + if (err != nil){ + t.Fatalf("Failed to read couple genetic analysis string: " + err.Error()) + } + + err = readGeneticAnalysis.ReadCoupleGeneticAnalysisForTests(coupleGeneticAnalysisMapList) + if (err != nil){ + t.Fatalf("Failed to read couple genetic analysis: " + err.Error()) + } +} + + + +func TestCreateCoupleGeneticAnalysis_SingleAndMultipleGenomes(t *testing.T){ + + err := locusMetadata.InitializeLocusMetadataVariables() + if (err != nil) { + t.Fatalf("InitializeLocusMetadataVariables failed: " + err.Error()) + } + + monogenicDiseases.InitializeMonogenicDiseaseVariables() + polygenicDiseases.InitializePolygenicDiseaseVariables() + traits.InitializeTraitVariables() + + getPersonGenomesList := func(addSecondGenome bool)([]prepareRawGenomes.RawGenomeWithMetadata, error){ + + genomeIdentifier1, err := helpers.GetNewRandomHexString(16) + if (err != nil) { + return nil, errors.New("Failed to get random hex string: " + err.Error()) + } + + fakeRawGenome1, _, _, _, err := createRawGenomes.CreateFakeRawGenome_AncestryDNA() + if (err != nil) { + return nil, errors.New("Failed to create fake raw AncestryDNA genome: " + err.Error()) + } + + genomeIsValid, rawGenomeWithMetadata1, err := prepareRawGenomes.CreateRawGenomeWithMetadataObject(genomeIdentifier1, fakeRawGenome1) + if (err != nil){ + return nil, errors.New("CreateRawGenomeWithMetadataObject failed: " + err.Error()) + } + if (genomeIsValid == false){ + return nil, errors.New("CreateRawGenomeWithMetadataObject failed: Genome is not valid.") + } + + genomesList := []prepareRawGenomes.RawGenomeWithMetadata{rawGenomeWithMetadata1} + + if (addSecondGenome == true){ + + genomeIdentifier2, err := helpers.GetNewRandomHexString(16) + if (err != nil) { + return nil, errors.New("Failed to get random hex string: " + err.Error()) + } + + fakeRawGenome2, _, _, _, err := createRawGenomes.CreateFakeRawGenome_23andMe() + if (err != nil) { + return nil, errors.New("Failed to create fake raw 23andMe genome: " + err.Error()) + } + + genomeIsValid, rawGenomeWithMetadata2, err := prepareRawGenomes.CreateRawGenomeWithMetadataObject(genomeIdentifier2, fakeRawGenome2) + if (err != nil){ + return nil, errors.New("CreateRawGenomeWithMetadataObject failed: " + err.Error()) + } + if (genomeIsValid == false){ + return nil, errors.New("CreateRawGenomeWithMetadataObject failed: Genome is not valid.") + } + + genomesList = append(genomesList, rawGenomeWithMetadata2) + } + + return genomesList, nil + } + + personAGenomesList, err := getPersonGenomesList(false) + if (err != nil){ + t.Fatalf("getPersonGenomesList failed: " + err.Error()) + } + + personBGenomesList, err := getPersonGenomesList(true) + if (err != nil){ + t.Fatalf("getPersonGenomesList failed: " + err.Error()) + } + + updateProgressFunction := func(_ int)error{ + return nil + } + + checkIfProcessIsStoppedFunction := func()bool{ + return false + } + + processCompleted, coupleGeneticAnalysis, err := createGeneticAnalysis.CreateCoupleGeneticAnalysis(personAGenomesList, personBGenomesList, updateProgressFunction, checkIfProcessIsStoppedFunction) + if (err != nil){ + t.Fatalf("Failed to create couple genetic analysis: " + err.Error()) + } + if (processCompleted == false){ + t.Fatalf("Failed to create couple genetic analysis: Process did not complete.") + } + + coupleGeneticAnalysisMapList, err := readGeneticAnalysis.ReadGeneticAnalysisString(coupleGeneticAnalysis) + if (err != nil){ + t.Fatalf("Failed to read couple genetic analysis string: " + err.Error()) + } + + err = readGeneticAnalysis.ReadCoupleGeneticAnalysisForTests(coupleGeneticAnalysisMapList) + if (err != nil){ + t.Fatalf("Failed to read couple genetic analysis: " + err.Error()) + } +} + + + +func TestCreateCoupleGeneticAnalysis_MultipleGenomes(t *testing.T){ + + err := locusMetadata.InitializeLocusMetadataVariables() + if (err != nil) { + t.Fatalf("InitializeLocusMetadataVariables failed: " + err.Error()) + } + + monogenicDiseases.InitializeMonogenicDiseaseVariables() + polygenicDiseases.InitializePolygenicDiseaseVariables() + traits.InitializeTraitVariables() + + getPersonGenomesList := func()([]prepareRawGenomes.RawGenomeWithMetadata, error){ + + genomeIdentifier1, err := helpers.GetNewRandomHexString(16) + if (err != nil) { + return nil, errors.New("Failed to get random hex string: " + err.Error()) + } + + fakeRawGenome1, _, _, _, err := createRawGenomes.CreateFakeRawGenome_AncestryDNA() + if (err != nil) { + return nil, errors.New("Failed to create fake raw AncestryDNA genome: " + err.Error()) + } + + genomeIsValid, rawGenomeWithMetadata1, err := prepareRawGenomes.CreateRawGenomeWithMetadataObject(genomeIdentifier1, fakeRawGenome1) + if (err != nil){ + return nil, errors.New("CreateRawGenomeWithMetadataObject failed: " + err.Error()) + } + if (genomeIsValid == false){ + return nil, errors.New("CreateRawGenomeWithMetadataObject failed: Genome is not valid.") + } + + genomeIdentifier2, err := helpers.GetNewRandomHexString(16) + if (err != nil) { + return nil, errors.New("Failed to get random hex string: " + err.Error()) + } + + fakeRawGenome2, _, _, _, err := createRawGenomes.CreateFakeRawGenome_23andMe() + if (err != nil) { + return nil, errors.New("Failed to create fake raw 23andMe genome: " + err.Error()) + } + + genomeIsValid, rawGenomeWithMetadata2, err := prepareRawGenomes.CreateRawGenomeWithMetadataObject(genomeIdentifier2, fakeRawGenome2) + if (err != nil){ + return nil, errors.New("CreateRawGenomeWithMetadataObject failed: " + err.Error()) + } + if (genomeIsValid == false){ + return nil, errors.New("CreateRawGenomeWithMetadataObject failed: Genome is not valid.") + } + + genomesList := []prepareRawGenomes.RawGenomeWithMetadata{rawGenomeWithMetadata1, rawGenomeWithMetadata2} + + return genomesList, nil + } + + personAGenomesList, err := getPersonGenomesList() + if (err != nil){ + t.Fatalf("getPersonGenomesList failed: " + err.Error()) + } + + personBGenomesList, err := getPersonGenomesList() + if (err != nil){ + t.Fatalf("getPersonGenomesList failed: " + err.Error()) + } + + updateProgressFunction := func(_ int)error{ + return nil + } + + checkIfProcessIsStoppedFunction := func()bool{ + return false + } + + processCompleted, coupleGeneticAnalysis, err := createGeneticAnalysis.CreateCoupleGeneticAnalysis(personAGenomesList, personBGenomesList, updateProgressFunction, checkIfProcessIsStoppedFunction) + if (err != nil){ + t.Fatalf("Failed to create couple genetic analysis: " + err.Error()) + } + if (processCompleted == false){ + t.Fatalf("Failed to create couple genetic analysis: Process did not complete.") + } + + coupleGeneticAnalysisMapList, err := readGeneticAnalysis.ReadGeneticAnalysisString(coupleGeneticAnalysis) + if (err != nil){ + t.Fatalf("Failed to read couple genetic analysis string: " + err.Error()) + } + + err = readGeneticAnalysis.ReadCoupleGeneticAnalysisForTests(coupleGeneticAnalysisMapList) + if (err != nil){ + t.Fatalf("Failed to read couple genetic analysis: " + err.Error()) + } +} + diff --git a/internal/genetics/createRawGenomes/createRawGenomes.go b/internal/genetics/createRawGenomes/createRawGenomes.go new file mode 100644 index 0000000..35cd430 --- /dev/null +++ b/internal/genetics/createRawGenomes/createRawGenomes.go @@ -0,0 +1,360 @@ + +// createRawGenomes provides functions to create fake raw genome files +// This package's functions are only used to test the readRawGenomes and createGeneticAnalysis packages. + +package createRawGenomes + +import "seekia/resources/geneticReferences/locusMetadata" + +import "seekia/internal/genetics/readRawGenomes" +import "seekia/internal/helpers" +import "seekia/internal/unixTime" + +import "time" +import "errors" +import "strings" + + +// Only use this function for tests +// Outputs: +// -string: Fake raw genome file string +// -int64: Time of fake file generation +// -int64: Number of loci +// -This does not include loci which have no base pair value in the file (Denoted by: "--") +// -map[int64]readRawGenomes.RawGenomeLocusValue: Raw genome map (rsID -> Locus base pair value) +// -error +func CreateFakeRawGenome_23andMe()(string, int64, int64, map[int64]readRawGenomes.RawGenomeLocusValue, error){ + + err := locusMetadata.InitializeLocusMetadataVariables() + if (err != nil){ + return "", 0, 0, nil, errors.New("InitializeLocusMetadataVariables failed: " + err.Error()) + } + + yearUnix := unixTime.GetYearUnix() + + maximumTime := time.Now().Unix() + minimumTime := maximumTime - (yearUnix*20) + + randomUnixTime := helpers.GetRandomInt64WithinRange(minimumTime, maximumTime) + + randomTimeObject := time.Unix(randomUnixTime, 0) + + timeMonthObject := randomTimeObject.Month() + timeDayInt := randomTimeObject.Day() + timeYearInt := randomTimeObject.Year() + + fileCreationTimeObject := time.Date(timeYearInt, timeMonthObject, timeDayInt, 0, 0, 0, 0, time.UTC) + fileCreationTimeUnix := fileCreationTimeObject.Unix() + + timeWeekdayString := randomTimeObject.Weekday().String() + + timeWeekdayTrimmed := timeWeekdayString[:3] + + timeMonthString := timeMonthObject.String() + timeMonthTrimmed := timeMonthString[:3] + + timeYearString := helpers.ConvertIntToString(timeYearInt) + + getTimeDayFormatted := func()string{ + + timeDayString := helpers.ConvertIntToString(timeDayInt) + + if (len(timeDayString) == 2){ + return timeDayString + } + + // We have to add 0 prefix + + result := "0" + timeDayString + + return result + } + + timeDayFormatted := getTimeDayFormatted() + + fileTimeString := timeWeekdayTrimmed + " " + timeMonthTrimmed + " " + timeDayFormatted + " 12:34:56 " + timeYearString + + // We use this builder to create the file string + var fileContentsBuilder strings.Builder + + fileHeader := `# This data file generated by 23andMe at: ` + fileTimeString + ` +# +# This file contains raw genotype data, including data that is not used in 23andMe reports. +# This data has undergone a general quality review however only a subset of markers have been +# individually validated for accuracy. As such, this data is suitable only for research, +# educational, and informational use and not for medical or other use. +# +# Below is a text version of your data. Fields are TAB-separated +# Each line corresponds to a single SNP. For each SNP, we provide its identifier +# (an rsid or an internal id), its location on the reference human genome, and the +# genotype call oriented with respect to the plus strand on the human reference sequence. +# We are using reference human assembly build 37 (also known as Annotation Release 104). +# Note that it is possible that data downloaded at different times may be different due to ongoing +# improvements in our ability to call genotypes. More information about these changes can be found at: +# https://you.23andme.com/p//tools/data/download/ +# +# More information on reference human assembly builds: +# https://www.ncbi.nlm.nih.gov/assembly/GCF_000001405.13/ +# +# rsid chromosome position genotype +` + + _, err = fileContentsBuilder.WriteString(fileHeader) + if (err != nil){ + return "", 0, 0, nil, errors.New("Failed to WriteString to string builder: " + err.Error()) + } + + numberOfLociToAdd := helpers.GetRandomInt64WithinRange(500000, 600000) + + numberOfAddedLoci := int64(0) + + // We use this map to avoid adding duplicate rsIDs + // Map Structure: rsID -> Nothing + addedRSIDsMap := make(map[int64]struct{}) + + // We use this map to return the contents of the map so we can verify reading it correctly + // Map Structure: rsID -> Locus value object (Example: G,G, I,D) + fileRSIDsMap := make(map[int64]readRawGenomes.RawGenomeLocusValue) + + // We use this map to avoid adding duplicate positions + addedPositionsMap := make(map[int]struct{}) + + allelePossibilities := []string{"G", "C", "A", "T", "I", "D"} + + for numberOfAddedLoci < numberOfLociToAdd{ + + locusRSID := helpers.GetRandomInt64WithinRange(1, 10000000) + + _, exists := addedRSIDsMap[locusRSID] + if (exists == true){ + // We try again to get a unique rsid + continue + } + + locusChromosome := helpers.GetRandomIntWithinRange(1, 26) + + locusPosition := helpers.GetRandomIntWithinRange(1, 10000000) + + _, exists = addedPositionsMap[locusPosition] + if (exists == true){ + // We try again to get a unique position + continue + } + + locusRSIDString := helpers.ConvertInt64ToString(locusRSID) + locusChromosomeString := helpers.ConvertIntToString(locusChromosome) + locusPositionString := helpers.ConvertIntToString(locusPosition) + + // Outputs: + // -string: Base pair for file + // -bool: Base pair exists + // -string: Allele A for rsidsMap + // -string: Allele B for rsidsMap + // -error + getBasePair := func()(string, bool, string, string, error){ + + randomInt := helpers.GetRandomIntWithinRange(1, 1000) + if (randomInt == 1){ + // ~1/1000 loci will be unknown + return "--", false, "", "", nil + } + + alleleA, err := helpers.GetRandomItemFromList(allelePossibilities) + if (err != nil){ return "", false, "", "", err } + alleleB, err := helpers.GetRandomItemFromList(allelePossibilities) + if (err != nil){ return "", false, "", "", err } + + basePairForFile := alleleA + alleleB + + return basePairForFile, true, alleleA, alleleB, nil + } + + basePairForFile, basePairExists, alleleA, alleleB, err := getBasePair() + if (err != nil){ + return "", 0, 0, nil, errors.New("getBasePair failed: " + err.Error()) + } + + newLine := "rs" + locusRSIDString + "\t" + locusChromosomeString + "\t" + locusPositionString + "\t" + basePairForFile + string(byte(13)) + "\n" + + _, err = fileContentsBuilder.WriteString(newLine) + if (err != nil){ + return "", 0, 0, nil, errors.New("Failed to WriteString to string builder: " + err.Error()) + } + + addedRSIDsMap[locusRSID] = struct{}{} + addedPositionsMap[locusPosition] = struct{}{} + + if (basePairExists == false){ + continue + } + + numberOfAddedLoci += 1 + + locusValueObject := readRawGenomes.RawGenomeLocusValue{ + Allele1: alleleA, + Allele2Exists: true, + Allele2: alleleB, + } + + fileRSIDsMap[locusRSID] = locusValueObject + } + + fileString := fileContentsBuilder.String() + + return fileString, fileCreationTimeUnix, numberOfAddedLoci, fileRSIDsMap, nil +} + + + +// Only use this function for tests +// Outputs: +// -string: Fake raw genome file string +// -int64: File creation time +// -int64: Number of loci in file +// -map[int64]readRawGenomes.RawGenomeLocusValue: Raw genome map (rsID -> Locus base pair value) +// -error +func CreateFakeRawGenome_AncestryDNA()(string, int64, int64, map[int64]readRawGenomes.RawGenomeLocusValue, error){ + + err := locusMetadata.InitializeLocusMetadataVariables() + if (err != nil){ + return "", 0, 0, nil, errors.New("InitializeLocusMetadataVariables failed: " + err.Error()) + } + + yearUnix := unixTime.GetYearUnix() + + maximumTime := time.Now().Unix() + minimumTime := maximumTime - (yearUnix*20) + + randomUnixTime := helpers.GetRandomInt64WithinRange(minimumTime, maximumTime) + + randomTimeObject := time.Unix(randomUnixTime, 0) + + timeMonthInt := randomTimeObject.Month() + timeDayInt := randomTimeObject.Day() + timeYearInt := randomTimeObject.Year() + + fileCreationTimeObject := time.Date(timeYearInt, timeMonthInt, timeDayInt, 0, 0, 0, 0, time.UTC) + fileCreationTimeUnix := fileCreationTimeObject.Unix() + + timeDayString := helpers.ConvertIntToString(timeDayInt) + timeYearString := helpers.ConvertIntToString(timeYearInt) + + getTimeMonthFormatted := func()string{ + + timeMonthString := helpers.ConvertIntToString(int(timeMonthInt)) + + if (len(timeMonthString) == 2){ + return timeMonthString + } + + // We have to add 0 prefix + + result := "0" + timeMonthString + + return result + } + + timeMonthFormatted := getTimeMonthFormatted() + + fileTimeString := timeMonthFormatted + "/" + timeDayString + "/" + timeYearString + + // We use this builder to create the file string + var fileContentsBuilder strings.Builder + + fileHeader := `#AncestryDNA raw data download +#This file was generated by AncestryDNA at: ` + fileTimeString + ` 10:00:00 UTC +#Data was collected using AncestryDNA array version: V2.0 +#Data is formatted using AncestryDNA converter version: V1.0 +#Below is a text version of your DNA file from Ancestry.com DNA, LLC. THIS +#INFORMATION IS FOR YOUR PERSONAL USE AND IS INTENDED FOR GENEALOGICAL RESEARCH +#ONLY. IT IS NOT INTENDED FOR MEDICAL, DIAGNOSTIC, OR HEALTH PURPOSES. THE EXPORTED DATA IS +#SUBJECT TO THE AncestryDNA TERMS AND CONDITIONS, BUT PLEASE BE AWARE THAT THE +#DOWNLOADED DATA WILL NO LONGER BE PROTECTED BY OUR SECURITY MEASURES. +#WHEN YOU DOWNLOAD YOUR RAW DNA DATA, YOU ASSUME ALL RISK OF STORING, +#SECURING AND PROTECTING YOUR DATA. FOR MORE INFORMATION, SEE ANCESTRYDNA FAQS. +# +#Genetic data is provided below as five TAB delimited columns. Each line +#corresponds to a SNP. Column one provides the SNP identifier (rsID where +#possible). Columns two and three contain the chromosome and basepair position +#of the SNP using human reference build 37.1 coordinates. Columns four and five +#contain the two alleles observed at this SNP (genotype). The genotype is reported +#on the forward (+) strand with respect to the human reference. +rsid chromosome position allele1 allele2 +` + + _, err = fileContentsBuilder.WriteString(fileHeader) + if (err != nil){ + return "", 0, 0, nil, errors.New("Failed to WriteString to string builder: " + err.Error()) + } + + numberOfLociToAdd := helpers.GetRandomInt64WithinRange(500000, 600000) + + numberOfAddedLoci := int64(0) + + // We use this map to avoid adding duplicate rsIDs and to verify results of file read + // Map Structure: rsID -> Base pair (Example: "G,G", "I,D") + fileRSIDsMap := make(map[int64]readRawGenomes.RawGenomeLocusValue) + + // We use this map to avoid adding duplicate positions + addedPositionsMap := make(map[int]struct{}) + + allelePossibilities := []string{"0", "G", "C", "A", "T", "I", "D"} + + for numberOfAddedLoci < numberOfLociToAdd{ + + locusRSID := helpers.GetRandomInt64WithinRange(1, 10000000) + + _, exists := fileRSIDsMap[locusRSID] + if (exists == true){ + // We try again to get a unique rsid + continue + } + + locusChromosome := helpers.GetRandomIntWithinRange(1, 26) + + locusPosition := helpers.GetRandomIntWithinRange(1, 10000000) + + _, exists = addedPositionsMap[locusPosition] + if (exists == true){ + // We try again to get a unique position + continue + } + + locusRSIDString := helpers.ConvertInt64ToString(locusRSID) + locusChromosomeString := helpers.ConvertIntToString(locusChromosome) + locusPositionString := helpers.ConvertIntToString(locusPosition) + + alleleA, err := helpers.GetRandomItemFromList(allelePossibilities) + if (err != nil){ + return "", 0, 0, nil, errors.New("GetRandomItemFromList failed: " + err.Error()) + } + alleleB, err := helpers.GetRandomItemFromList(allelePossibilities) + if (err != nil){ + return "", 0, 0, nil, errors.New("GetRandomItemFromList failed: " + err.Error()) + } + + newLine := "rs" + locusRSIDString + "\t" + locusChromosomeString + "\t" + locusPositionString + "\t" + alleleA + "\t" + alleleB + "\n" + + _, err = fileContentsBuilder.WriteString(newLine) + if (err != nil){ + return "", 0, 0, nil, errors.New("Failed to WriteString to string builder: " + err.Error()) + } + + locusValueObject := readRawGenomes.RawGenomeLocusValue{ + Allele1: alleleA, + Allele2Exists: true, + Allele2: alleleB, + } + + fileRSIDsMap[locusRSID] = locusValueObject + addedPositionsMap[locusPosition] = struct{}{} + + numberOfAddedLoci += 1 + } + + fileString := fileContentsBuilder.String() + + return fileString, fileCreationTimeUnix, numberOfAddedLoci, fileRSIDsMap, nil +} + + diff --git a/internal/genetics/geneticPrediction/geneticPrediction.go b/internal/genetics/geneticPrediction/geneticPrediction.go new file mode 100644 index 0000000..d909f8c --- /dev/null +++ b/internal/genetics/geneticPrediction/geneticPrediction.go @@ -0,0 +1,836 @@ + +// geneticPrediction provides functions to train and query neural network models +// These models are currently used to predict traits such as eye color from user genome files + +package geneticPrediction + +// I am a neophyte in the ways of neural networks. +// Machine learning experts should chime in and offer improvements. +// We have to make sure that model inference remains very fast +// Sorting matches by offspring total polygenic disease score will require inference on dozens of models for each match +// We could create slower models that provide more accurate predictions + +import "seekia/resources/geneticReferences/traits" + +import "seekia/internal/genetics/locusValue" +import "seekia/internal/genetics/readBiobankData" +import "seekia/internal/helpers" + +import "gorgonia.org/gorgonia" +import "gorgonia.org/tensor" + +import mathRand "math/rand" +import "bytes" +import "encoding/gob" +import "slices" +import "errors" + + +type NeuralNetwork struct{ + + // ExprGraph is a data structure for a directed acyclic graph (of expressions). + graph *gorgonia.ExprGraph + + // These are the weights for each layer of neurons + weights1 *gorgonia.Node + weights2 *gorgonia.Node + weights3 *gorgonia.Node + weights4 *gorgonia.Node + + // This is the computed prediction + prediction *gorgonia.Node +} + +// This struct stores a user's training data +// Each TrainingData represents a single data example +// For example, the InputLayer is a column of neurons representing a user's genetics, +// and the OutputLayer is a column representing their phenotype, such as eye color +type TrainingData struct{ + + // InputLayer stores relevant rsID values for each trait from the user's genomes + // It also stores if each rsID is phased and if each rsID exists + InputLayer []float32 + + // OutputLayer stores user phenotype data as neurons + // Each neuron represents an outcome + // For example, for Eye Color, each neuron represents an eye color + OutputLayer []float32 +} + + +func EncodeTrainingDataObjectToBytes(inputTrainingData TrainingData)([]byte, error){ + + buffer := new(bytes.Buffer) + + encoder := gob.NewEncoder(buffer) + + err := encoder.Encode(inputTrainingData) + if (err != nil) { return nil, err } + + trainingDataBytes := buffer.Bytes() + + return trainingDataBytes, nil +} + +func DecodeBytesToTrainingDataObject(inputTrainingData []byte)(TrainingData, error){ + + if (inputTrainingData == nil){ + return TrainingData{}, errors.New("DecodeBytesToTrainingDataObject called with nil inputTrainingData.") + } + + buffer := bytes.NewBuffer(inputTrainingData) + + decoder := gob.NewDecoder(buffer) + + var newTrainingData TrainingData + + err := decoder.Decode(&newTrainingData) + if (err != nil){ return TrainingData{}, err } + + return newTrainingData, nil +} + +// We use this to store a neural network's weights as a .gob file +type neuralNetworkForEncoding struct{ + + // These are the weights for each layer of neurons + Weights1 []float32 + Weights2 []float32 + Weights3 []float32 + Weights4 []float32 + + Weights1Rows int + Weights1Columns int + Weights2Rows int + Weights2Columns int + Weights3Rows int + Weights3Columns int + Weights4Rows int + Weights4Columns int +} + +func EncodeNeuralNetworkObjectToBytes(inputNeuralNetwork NeuralNetwork)([]byte, error){ + + weights1 := inputNeuralNetwork.weights1 + weights2 := inputNeuralNetwork.weights2 + weights3 := inputNeuralNetwork.weights3 + weights4 := inputNeuralNetwork.weights4 + + weights1Slice := weights1.Value().Data().([]float32) + weights2Slice := weights2.Value().Data().([]float32) + weights3Slice := weights3.Value().Data().([]float32) + weights4Slice := weights4.Value().Data().([]float32) + + weights1Rows := weights1.Shape()[0] + weights1Columns := weights1.Shape()[1] + weights2Rows := weights2.Shape()[0] + weights2Columns := weights2.Shape()[1] + weights3Rows := weights3.Shape()[0] + weights3Columns := weights3.Shape()[1] + weights4Rows := weights4.Shape()[0] + weights4Columns := weights4.Shape()[1] + + newNeuralNetworkForEncoding := neuralNetworkForEncoding{ + Weights1: weights1Slice, + Weights2: weights2Slice, + Weights3: weights3Slice, + Weights4: weights4Slice, + + Weights1Rows: weights1Rows, + Weights1Columns: weights1Columns, + Weights2Rows: weights2Rows, + Weights2Columns: weights2Columns, + Weights3Rows: weights3Rows, + Weights3Columns: weights3Columns, + Weights4Rows: weights4Rows, + Weights4Columns: weights4Columns, + } + + buffer := new(bytes.Buffer) + + encoder := gob.NewEncoder(buffer) + + err := encoder.Encode(newNeuralNetworkForEncoding) + if (err != nil) { return nil, err } + + neuralNetworkBytes := buffer.Bytes() + + return neuralNetworkBytes, nil +} + +func DecodeBytesToNeuralNetworkObject(inputNeuralNetwork []byte)(NeuralNetwork, error){ + + if (inputNeuralNetwork == nil){ + return NeuralNetwork{}, errors.New("DecodeBytesToNeuralNetworkObject called with nil inputNeuralNetwork.") + } + + buffer := bytes.NewBuffer(inputNeuralNetwork) + + decoder := gob.NewDecoder(buffer) + + var newNeuralNetworkForEncoding neuralNetworkForEncoding + + err := decoder.Decode(&newNeuralNetworkForEncoding) + if (err != nil){ return NeuralNetwork{}, err } + + weights1 := newNeuralNetworkForEncoding.Weights1 + weights2 := newNeuralNetworkForEncoding.Weights2 + weights3 := newNeuralNetworkForEncoding.Weights3 + weights4 := newNeuralNetworkForEncoding.Weights4 + + weights1Rows := newNeuralNetworkForEncoding.Weights1Rows + weights1Columns := newNeuralNetworkForEncoding.Weights1Columns + weights2Rows := newNeuralNetworkForEncoding.Weights2Rows + weights2Columns := newNeuralNetworkForEncoding.Weights2Columns + weights3Rows := newNeuralNetworkForEncoding.Weights3Rows + weights3Columns := newNeuralNetworkForEncoding.Weights3Columns + weights4Rows := newNeuralNetworkForEncoding.Weights4Rows + weights4Columns := newNeuralNetworkForEncoding.Weights4Columns + + // This is the graph object we add each layer to + newGraph := gorgonia.NewGraph() + + // A layer is a column of neurons + // Each neuron has an initial value between 0 and 1 + getNewNeuralNetworkLayerWeights := func(layerName string, layerNeuronRows int, layerNeuronColumns int, layerWeightsList []float32)*gorgonia.Node{ + + layerNameObject := gorgonia.WithName(layerName) + + layerBacking := tensor.WithBacking(layerWeightsList) + layerShape := tensor.WithShape(layerNeuronRows, layerNeuronColumns) + layerTensor := tensor.New(layerBacking, layerShape) + + layerValueObject := gorgonia.WithValue(layerTensor) + + layerObject := gorgonia.NewMatrix(newGraph, tensor.Float32, layerNameObject, layerValueObject) + + return layerObject + } + + layer1 := getNewNeuralNetworkLayerWeights("Weights1", weights1Rows, weights1Columns, weights1) + layer2 := getNewNeuralNetworkLayerWeights("Weights2", weights2Rows, weights2Columns, weights2) + layer3 := getNewNeuralNetworkLayerWeights("Weights3", weights3Rows, weights3Columns, weights3) + layer4 := getNewNeuralNetworkLayerWeights("Weights4", weights4Rows, weights4Columns, weights4) + + newNeuralNetworkObject := NeuralNetwork{ + + graph: newGraph, + + weights1: layer1, + weights2: layer2, + weights3: layer3, + weights4: layer4, + } + + return newNeuralNetworkObject, nil +} + + +//Outputs: +// -int: Layer 1 neuron count (input layer) +// -int: Layer 2 neuron count +// -int: Layer 3 neuron count +// -int: Layer 4 neuron count +// -int: Layer 5 neuron count (output layer) +// -error +func getNeuralNetworkLayerSizes(traitName string)(int, int, int, int, int, error){ + + switch traitName{ + + case "Eye Color":{ + + // There are 376 input neurons + // There are 4 output neurons, each representing a color + // There are 4 colors: Blue, Green, Brown, Hazel + + return 376, 200, 100, 50, 4, nil + } + } + + return 0, 0, 0, 0, 0, errors.New("getNeuralNetworkLayerSizes called with unknown traitName: " + traitName) +} + +//This function converts a genome allele to a neuron to use in a tensor +func convertAlleleToNeuron(allele string)(float32, error){ + + switch allele{ + + case "C":{ + + return 0, nil + } + case "A":{ + + return 0.2, nil + } + case "T":{ + + return 0.4, nil + } + case "G":{ + + return 0.6, nil + } + case "I":{ + + return 0.8, nil + } + case "D":{ + + return 1, nil + } + } + + return 0, errors.New("convertAlleleToNeuron called with invalid allele: " + allele) +} + + +// This function returns training data to use to train each neural network prediction model +// Outputs: +// -bool: User has phenotype data and enough loci to train model +// -[]TrainingData: List of TrainingData for the user which we will use to train the model +// -error +func CreateGeneticPredictionTrainingData_OpenSNP( + traitName string, + userPhenotypeDataObject readBiobankData.PhenotypeData_OpenSNP, + userLocusValuesMap map[int64]locusValue.LocusValue)(bool, []TrainingData, error){ + + if (traitName != "Eye Color"){ + return false, nil, errors.New("CreateGeneticPredictionTrainingData_OpenSNP called with unknown traitName: " + traitName) + } + + traitObject, err := traits.GetTraitObject(traitName) + if (err != nil) { return false, nil, err } + + // This is a list of rsIDs which influence this trait + traitRSIDs := traitObject.LociList + + if (len(traitRSIDs) == 0){ + return false, nil, errors.New("traitObject contains no rsIDs.") + } + + // Each layer is represented as a []float32 + // Each float is a value between 0 and 1 + // + // Each TrainingData holds a variation of the user's genome rsID values + // We add many rows with withheld data to improve training data + + numberOfInputLayerRows, _, _, _, numberOfOutputLayerRows, err := getNeuralNetworkLayerSizes(traitName) + if (err != nil) { return false, nil, err } + + // Each rsID is represented by 4 neurons: LocusExists, LocusIsPhased, Allele1 Value, Allele2 Value + expectedNumberOfInputLayerRows := len(traitRSIDs) * 4 + + if (numberOfInputLayerRows != expectedNumberOfInputLayerRows){ + + expectedNumberOfInputLayerRowsString := helpers.ConvertIntToString(expectedNumberOfInputLayerRows) + + return false, nil, errors.New("numberOfInputLayerRows is not expected: " + expectedNumberOfInputLayerRowsString) + } + + checkIfAnyTraitLocusValuesExist := func()bool{ + + for _, rsID := range traitRSIDs{ + + _, exists := userLocusValuesMap[rsID] + if (exists == true){ + return true + } + } + + return false + } + + anyTraitLocusValuesExist := checkIfAnyTraitLocusValuesExist() + if (anyTraitLocusValuesExist == false){ + // The user's genome does not contain any of this trait's locus values + // We will not train on their data + return false, nil, nil + } + + // We sort rsIDs in ascending order + // We copy list so we don't change the original + + traitRSIDsList := slices.Clone(traitRSIDs) + + slices.Sort(traitRSIDsList) + + // This function returns the outputLayer for all trainingDatas for this user + // Each outputLayer represents the user's trait value (Example: "Blue" for Eye Color) + // Each outputLayer is identical, because each TrainingData example belongs to the same user + // + // Outputs: + // -bool: User trait value is known + // -[]float32: Neuron values for layer + // -error + getUserTraitValueNeurons := func()(bool, []float32, error){ + + if (traitName == "Eye Color"){ + + userEyeColorIsKnown := userPhenotypeDataObject.EyeColorIsKnown + if (userEyeColorIsKnown == false){ + return false, nil, nil + } + + userEyeColor := userPhenotypeDataObject.EyeColor + + if (userEyeColor == "Blue"){ + + return true, []float32{1, 0, 0, 0}, nil + + } else if (userEyeColor == "Green"){ + + return true, []float32{0, 1, 0, 0}, nil + + } else if (userEyeColor == "Hazel"){ + + return true, []float32{0, 0, 1, 0}, nil + + } else if (userEyeColor == "Brown"){ + + return true, []float32{0, 0, 0, 1}, nil + } + + return false, nil, errors.New("Malformed userPhenotypeDataObject: Invalid eyeColor: " + userEyeColor) + } + + return false, nil, errors.New("Unknown traitName: " + traitName) + } + + userTraitValueExists, userTraitValueNeurons, err := getUserTraitValueNeurons() + if (err != nil) { return false, nil, err } + if (userTraitValueExists == false){ + // User cannot be used to train the model. + // They do not have a value for this trait. + return false, nil, nil + } + + if (len(userTraitValueNeurons) != numberOfOutputLayerRows){ + return false, nil, errors.New("getUserTraitValueNeurons returning invalid length layer slice.") + } + + // We create 110 examples per user. + // We randomize allele order whenever phase for the locus is unknown + // 50% of the time we randomize allele order even when phase is known to train the model on unphased data + // Unphased data is data where the order each allele (Example: G;A) has no meaning, because phase data was not captured + // We randomize allele order to simulate unphased data + // For example, if a user's genome is phased, we will randomize the base pair order and set the LocusIsPhased neuron to false + // + // Examples 0-10: 100% of the user's loci are used + // Examples 11-30: 90% of the user's loci are used + // Examples 31-50: 70% of the user's loci are used + // Examples 51-70: 50% of the user's loci are used + // Examples 71-90: 30% of the user's loci are used + // Examples 91-110: 10% of the user's loci are used + + // We now add this user's data to the trainingDataList + + trainingDataList := make([]TrainingData, 0, 110) + + for i:=0; i < 110; i++{ + + getRandomizePhaseBool := func()bool{ + + if (i%2 == 0){ + return true + } + return false + } + + randomizePhaseBool := getRandomizePhaseBool() + + getProbabilityOfUsingLoci := func()float64{ + + if (i <= 10){ + return 1 + + } else if (i <= 30){ + return 0.9 + + } else if (i <= 50){ + return 0.7 + + } else if (i <= 70){ + return 0.5 + + } else if (i <= 90){ + return 0.3 + } + + return 0.1 + } + + probabilityOfUsingLoci := getProbabilityOfUsingLoci() + + // In the inputLayer, each locus value is represented by 4 neurons: + // 1. LocusExists (Either 0 or 1) + // 2. LocusIsPhased (Either 0 or 1) + // 3. Allele1 Locus Value (Value between 0-1) + // 4. Allele2 Locus Value (Value between 0-1) + + inputLayerLength := len(traitRSIDsList) * 4 + + inputLayer := make([]float32, 0, inputLayerLength) + + for _, rsID := range traitRSIDsList{ + + useLocusBool, err := helpers.GetRandomBoolWithProbability(probabilityOfUsingLoci) + if (err != nil) { return false, nil, err } + if (useLocusBool == false){ + // We are skipping this locus + inputLayer = append(inputLayer, 0, 0, 0, 0) + continue + } + + userLocusValue, exists := userLocusValuesMap[rsID] + if (exists == false){ + // This user's locus value is unknown + inputLayer = append(inputLayer, 0, 0, 0, 0) + continue + } + + getLocusAlleles := func()(string, string){ + + locusAllele1 := userLocusValue.Base1Value + locusAllele2 := userLocusValue.Base2Value + + if (randomizePhaseBool == false){ + return locusAllele1, locusAllele2 + } + + randomBool := helpers.GetRandomBool() + + if (randomBool == false){ + return locusAllele1, locusAllele2 + } + + return locusAllele2, locusAllele1 + } + + locusAllele1, locusAllele2 := getLocusAlleles() + + locusAllele1NeuronValue, err := convertAlleleToNeuron(locusAllele1) + if (err != nil){ return false, nil, err } + locusAllele2NeuronValue, err := convertAlleleToNeuron(locusAllele2) + if (err != nil) { return false, nil, err } + + getLocusIsPhasedNeuronValue := func()float32{ + + if (randomizePhaseBool == true){ + return 0 + } + + locusIsPhased := userLocusValue.LocusIsPhased + if (locusIsPhased == true){ + return 1 + } + + return 0 + } + + locusIsPhasedNeuronValue := getLocusIsPhasedNeuronValue() + + inputLayer = append(inputLayer, 1, locusIsPhasedNeuronValue, locusAllele1NeuronValue, locusAllele2NeuronValue) + } + + userTraitValueNeuronsCopy := slices.Clone(userTraitValueNeurons) + + newTrainingData := TrainingData{ + InputLayer: inputLayer, + OutputLayer: userTraitValueNeuronsCopy, + } + + trainingDataList = append(trainingDataList, newTrainingData) + } + + return true, trainingDataList, nil +} + +func GetNewUntrainedNeuralNetworkObject(traitName string)(*NeuralNetwork, error){ + + layer1NeuronCount, layer2NeuronCount, layer3NeuronCount, layer4NeuronCount, layer5NeuronCount, err := getNeuralNetworkLayerSizes(traitName) + if (err != nil) { return nil, err } + + // This is the graph object we add each layer to + newGraph := gorgonia.NewGraph() + + // We want the initial weights to be the same for each call of this function that has the same input parameters + // This is a necessary step so our neural network models will be reproducable + // Reproducable means that other people can run the code and produce the same models, byte-for-byte + + pseudorandomNumberGenerator := mathRand.New(mathRand.NewSource(1)) + + // A layer is a column of neurons + // Each neuron has an initial value between 0 and 1 + getNewNeuralNetworkLayerWeights := func(layerName string, layerNeuronRows int, layerNeuronColumns int)*gorgonia.Node{ + + layerNameObject := gorgonia.WithName(layerName) + + totalNumberOfNeurons := layerNeuronRows * layerNeuronColumns + + layerInitialWeightsList := make([]float32, 0, totalNumberOfNeurons) + + for i:=0; i < totalNumberOfNeurons; i++{ + + // This returns a pseudo-random number between 0 and 1 + newWeight := pseudorandomNumberGenerator.Float32() + + layerInitialWeightsList = append(layerInitialWeightsList, newWeight) + } + + layerBacking := tensor.WithBacking(layerInitialWeightsList) + + layerShape := tensor.WithShape(layerNeuronRows, layerNeuronColumns) + + layerTensor := tensor.New(layerBacking, layerShape) + + layerValueObject := gorgonia.WithValue(layerTensor) + + layerObject := gorgonia.NewMatrix(newGraph, tensor.Float32, layerNameObject, layerValueObject) + + return layerObject + } + + layer1 := getNewNeuralNetworkLayerWeights("Weights1", layer1NeuronCount, layer2NeuronCount) + layer2 := getNewNeuralNetworkLayerWeights("Weights2", layer2NeuronCount, layer3NeuronCount) + layer3 := getNewNeuralNetworkLayerWeights("Weights3", layer3NeuronCount, layer4NeuronCount) + layer4 := getNewNeuralNetworkLayerWeights("Weights4", layer4NeuronCount, layer5NeuronCount) + + newNeuralNetworkObject := NeuralNetwork{ + + graph: newGraph, + + weights1: layer1, + weights2: layer2, + weights3: layer3, + weights4: layer4, + } + + return &newNeuralNetworkObject, nil +} + +// This function returns the weights of the neural network +// We need this for training +func (inputNetwork *NeuralNetwork)getLearnables()gorgonia.Nodes{ + + weights1 := inputNetwork.weights1 + weights2 := inputNetwork.weights2 + weights3 := inputNetwork.weights3 + weights4 := inputNetwork.weights4 + + result := gorgonia.Nodes{weights1, weights2, weights3, weights4} + + return result +} + + +// This function will train the neural network +// The function is passed a single TrainingData example to train on +// +// TODO: This function doesn't work +// The weights do not change during training +// I think the layer dimensions are wrong? +// +func TrainNeuralNetwork(traitName string, neuralNetworkObject *NeuralNetwork, trainingData TrainingData)error{ + + layer1NeuronCount, _, _, _, layer5NeuronCount, err := getNeuralNetworkLayerSizes(traitName) + if (err != nil) { return err } + + neuralNetworkGraph := neuralNetworkObject.graph + + // This inputLayer contains the allele values for this training example + trainingDataInputLayer := trainingData.InputLayer + + // This outputLayer contains the phenotype for this training example (example: Eye color of Blue) + trainingDataOutputLayer := trainingData.OutputLayer + + // We convert our inputTensor and outputTensor to the type *Node + + inputTensorShapeObject := tensor.WithShape(1, layer1NeuronCount) + outputTensorShapeObject := tensor.WithShape(1, layer5NeuronCount) + + inputTensorBacking := tensor.WithBacking(trainingDataInputLayer) + outputTensorBacking := tensor.WithBacking(trainingDataOutputLayer) + + inputTensor := tensor.New(inputTensorShapeObject, inputTensorBacking) + outputTensor := tensor.New(outputTensorShapeObject, outputTensorBacking) + + trainingDataInputNode := gorgonia.NewMatrix(neuralNetworkGraph, + tensor.Float32, + gorgonia.WithName("input"), + gorgonia.WithShape(1, layer1NeuronCount), + gorgonia.WithValue(inputTensor), + ) + + trainingDataOutputNode := gorgonia.NewMatrix(neuralNetworkGraph, + tensor.Float32, + gorgonia.WithName("expectedOutput"), + gorgonia.WithShape(1, layer5NeuronCount), + gorgonia.WithValue(outputTensor), + ) + + err = neuralNetworkObject.prepareToComputePrediction(trainingDataInputNode) + if (err != nil) { return err } + + // This computes the loss (how accurate was our prediction) + losses, err := gorgonia.Sub(trainingDataOutputNode, neuralNetworkObject.prediction) + if (err != nil) { return err } + + // Cost is an average of the losses + cost, err := gorgonia.Mean(losses) + if (err != nil) { return err } + + neuralNetworkLearnables := neuralNetworkObject.getLearnables() + + // Grad takes a scalar cost node and a list of with-regards-to, and returns the gradient + _, err = gorgonia.Grad(cost, neuralNetworkLearnables...) + if (err != nil) { return err } + + bindDualValues := gorgonia.BindDualValues(neuralNetworkLearnables...) + + // NewTapeMachine creates a Virtual Machine that compiles a graph into a prog. + virtualMachine := gorgonia.NewTapeMachine(neuralNetworkGraph, bindDualValues) + + // This is the learn rate or step size for the solver. + learningRate := gorgonia.WithLearnRate(.001) + + // This clips the gradient if it gets too crazy + //gradientClip := gorgonia.WithClip(5) + + solver := gorgonia.NewVanillaSolver(learningRate) + //solver := gorgonia.NewVanillaSolver(learningRate, gradientClip) + + for i:=0; i < 10; i++{ + + err = virtualMachine.RunAll() + if (err != nil) { return err } + + // NodesToValueGrads is a utility function that converts a Nodes to a slice of ValueGrad for the solver + valueGrads := gorgonia.NodesToValueGrads(neuralNetworkLearnables) + + err := solver.Step(valueGrads) + if (err != nil) { return err } + + virtualMachine.Reset() + } + + return nil +} + + +// This function computes a raw prediction from the neural network +// Outputs: +// -[]float32: Output neurons +// -error +func GetNeuralNetworkRawPrediction(inputNeuralNetwork *NeuralNetwork, inputLayer []float32)([]float32, error){ + + neuralNetworkGraph := inputNeuralNetwork.graph + + // We convert the inputLayer []float32 to a node object + + numberOfInputNeurons := len(inputLayer) + + inputTensorShapeObject := tensor.WithShape(1, numberOfInputNeurons) + + inputTensorBacking := tensor.WithBacking(inputLayer) + + inputTensor := tensor.New(inputTensorShapeObject, inputTensorBacking) + + inputNode := gorgonia.NewMatrix(neuralNetworkGraph, + tensor.Float32, + gorgonia.WithName("input"), + gorgonia.WithShape(1, numberOfInputNeurons), + gorgonia.WithValue(inputTensor), + ) + + err := inputNeuralNetwork.prepareToComputePrediction(inputNode) + if (err != nil){ return nil, err } + + prediction := inputNeuralNetwork.prediction + + // Now we create a virtual machine to compute the prediction + + neuralNetworkLearnables := inputNeuralNetwork.getLearnables() + + bindDualValues := gorgonia.BindDualValues(neuralNetworkLearnables...) + + virtualMachine := gorgonia.NewTapeMachine(neuralNetworkGraph, bindDualValues) + + err = virtualMachine.RunAll() + if (err != nil) { return nil, err } + + predictionValues := prediction.Value().Data().([]float32) + + return predictionValues, nil +} + + +// This function will take a neural network and input layer and prepare the network to compute a prediction +// We still need to run a virtual machine after calling this function in order for the prediction to be generated +func (inputNetwork *NeuralNetwork)prepareToComputePrediction(inputLayer *gorgonia.Node)error{ + + // We copy pointer (says to do this in a resource i'm reading) + + inputLayerCopy := inputLayer + + // We multiply weights at each layer and perform rectification (ReLU) after each multiplication + + weights1 := inputNetwork.weights1 + weights2 := inputNetwork.weights2 + weights3 := inputNetwork.weights3 + weights4 := inputNetwork.weights4 + + layer1Product, err := gorgonia.Mul(inputLayerCopy, weights1) + if (err != nil) { + return errors.New("Layer 1 multiplication failed: " + err.Error()) + } + + layer1ProductRectified, err := gorgonia.Rectify(layer1Product) + if (err != nil){ + return errors.New("Layer 1 rectification failed: " + err.Error()) + } + + layer2Product, err := gorgonia.Mul(layer1ProductRectified, weights2) + if (err != nil) { + return errors.New("Layer 2 multiplication failed: " + err.Error()) + } + + layer2ProductRectified, err := gorgonia.Rectify(layer2Product) + if (err != nil){ + return errors.New("Layer 2 rectification failed: " + err.Error()) + } + + layer3Product, err := gorgonia.Mul(layer2ProductRectified, weights3) + if (err != nil) { + return errors.New("Layer 3 multiplication failed: " + err.Error()) + } + + layer3ProductRectified, err := gorgonia.Rectify(layer3Product) + if (err != nil){ + return errors.New("Layer 3 rectification failed: " + err.Error()) + } + + layer4Product, err := gorgonia.Mul(layer3ProductRectified, weights4) + if (err != nil) { + return errors.New("Layer 4 multiplication failed: " + err.Error()) + } + + layer4ProductRectified, err := gorgonia.Rectify(layer4Product) + if (err != nil){ + return errors.New("Layer 4 rectification failed: " + err.Error()) + } + + // We sigmoid the output to get the prediction + //TODO: Use SoftMax instead? + + prediction, err := gorgonia.Sigmoid(layer4ProductRectified) + if (err != nil) { + return errors.New("Sigmoid failed: " + err.Error()) + } + + inputNetwork.prediction = prediction + + return nil +} + + diff --git a/internal/genetics/geneticPrediction/geneticPrediction_test.go b/internal/genetics/geneticPrediction/geneticPrediction_test.go new file mode 100644 index 0000000..faf7757 --- /dev/null +++ b/internal/genetics/geneticPrediction/geneticPrediction_test.go @@ -0,0 +1,28 @@ +package geneticPrediction_test + + +import "seekia/internal/genetics/geneticPrediction" + +import "testing" + + +// We test the encoding/decoding of a neural network object +func TestEncodeNeuralNetwork(t *testing.T){ + + neuralNetworkObject, err := geneticPrediction.GetNewUntrainedNeuralNetworkObject("Eye Color") + if (err != nil) { + t.Fatalf("GetNewUntrainedNeuralNetworkObject failed: " + err.Error()) + } + + neuralNetworkBytes, err := geneticPrediction.EncodeNeuralNetworkObjectToBytes(*neuralNetworkObject) + if (err != nil){ + t.Fatalf("EncodeNeuralNetworkObjectToBytes failed: " + err.Error()) + } + + _, err = geneticPrediction.DecodeBytesToNeuralNetworkObject(neuralNetworkBytes) + if (err != nil){ + t.Fatalf("DecodeBytesToNeuralNetworkObject failed: " + err.Error()) + } + + //TODO: Verify values are the same +} \ No newline at end of file diff --git a/internal/genetics/locusValue/locusValue.go b/internal/genetics/locusValue/locusValue.go new file mode 100644 index 0000000..a4867ba --- /dev/null +++ b/internal/genetics/locusValue/locusValue.go @@ -0,0 +1,22 @@ + +// locusValue provides the LocusValue type +// This represents the value at a particular locus in a genome + +package locusValue + + +//TODO: Add the ability for only 1 base to exist (Example: Y chromosome loci) +//TODO: Rename Base1/2 to Allele1/2 + + +// This type represents a locus base pair value +type LocusValue struct{ + + // If true, then Base 1 and 2 have significance + // If false, Base 1 and 2 could be swapped and it makes no difference to accuracy + LocusIsPhased bool + + // Potential options: "C"/"A"/"T"/"G"/"I"/"D" + Base1Value string + Base2Value string +} \ No newline at end of file diff --git a/internal/genetics/myAnalyses/myAnalyses.go b/internal/genetics/myAnalyses/myAnalyses.go new file mode 100644 index 0000000..af0176c --- /dev/null +++ b/internal/genetics/myAnalyses/myAnalyses.go @@ -0,0 +1,989 @@ + +// myAnalyses provides functions to manage genome analyses for People and Couples +// Analyses are created using the createGeneticAnalysis package + +package myAnalyses + +import "seekia/internal/helpers" +import "seekia/internal/appMemory" +import "seekia/internal/appValues" +import "seekia/internal/localFilesystem" +import "seekia/internal/myDatastores/myMapList" +import "seekia/internal/genetics/createGeneticAnalysis" +import "seekia/internal/genetics/prepareRawGenomes" +import "seekia/internal/genetics/readGeneticAnalysis" +import "seekia/internal/genetics/myGenomes" + +import "path/filepath" +import "strings" +import "time" +import "sync" +import "errors" + +// This will be locked anytime analyses are being added/deleted to the myMapList datastores +var updatingMyAnalysesMutex sync.Mutex + +var myPersonAnalysesMapListDatastore *myMapList.MyMapList +var myCoupleAnalysesMapListDatastore *myMapList.MyMapList + +// This function must be called whenever an app user signs in +func InitializeMyAnalysesDatastores()error{ + + updatingMyAnalysesMutex.Lock() + defer updatingMyAnalysesMutex.Unlock() + + newMyPersonAnalysesMapListDatastore, err := myMapList.CreateNewMapList("MyPersonAnalyses") + if (err != nil) { return err } + + newMyCoupleAnalysesMapListDatastore, err := myMapList.CreateNewMapList("MyCoupleAnalyses") + if (err != nil) { return err } + + myPersonAnalysesMapListDatastore = newMyPersonAnalysesMapListDatastore + myCoupleAnalysesMapListDatastore = newMyCoupleAnalysesMapListDatastore + + return nil +} + +// This function must be called whenever an app user signs in +func CreateMyAnalysesFolder() error{ + + userDirectory, err := localFilesystem.GetAppUserFolderPath() + if (err != nil) { return err } + + myAnalysesFolderPath := filepath.Join(userDirectory, "MyAnalyses") + + _, err = localFilesystem.CreateFolder(myAnalysesFolderPath) + if (err != nil) { return err } + + return nil +} + +// This function should be called whenever an app user signs in +func PruneOldAnalyses()error{ + + updatingMyAnalysesMutex.Lock() + defer updatingMyAnalysesMutex.Unlock() + + //TODO: This function should delete analyses we don't need anymore + // These include analyses for people who dont exist anymore, and analyses for people/couples whom have newer analyses + + return nil +} + + +func DeleteAllAnalysesForPerson(personIdentifier string)error{ + + updatingMyAnalysesMutex.Lock() + defer updatingMyAnalysesMutex.Unlock() + + mapToDelete := map[string]string{ + "PersonIdentifer": personIdentifier, + } + + err := myPersonAnalysesMapListDatastore.DeleteMapListItems(mapToDelete) + if (err != nil) { return err } + + mapToDelete = map[string]string{ + "PersonAIdentifier": personIdentifier, + } + + err = myCoupleAnalysesMapListDatastore.DeleteMapListItems(mapToDelete) + if (err != nil) { return err } + + mapToDelete = map[string]string{ + "PersonBIdentifier": personIdentifier, + } + + err = myCoupleAnalysesMapListDatastore.DeleteMapListItems(mapToDelete) + if (err != nil) { return err } + + return nil +} + +func DeleteAllAnalysesForCouple(inputPersonAIdentifier string, inputPersonBIdentifier string)error{ + + updatingMyAnalysesMutex.Lock() + defer updatingMyAnalysesMutex.Unlock() + + personAIdentifier, personBIdentifier, err := GetPeopleIdentifiersSortedForCouple(inputPersonAIdentifier, inputPersonBIdentifier) + if (err != nil) { return err } + + mapToDelete := map[string]string{ + "PersonAIdentifier": personAIdentifier, + "PersonBIdentifier": personBIdentifier, + } + + err = myCoupleAnalysesMapListDatastore.DeleteMapListItems(mapToDelete) + if (err != nil) { return err } + + return nil +} + +// This function is used to sort the person identifiers, so that each pair of identifiers will only map to a single couple +// PersonA and PersonB will always follow this order within a couple analysis +// The sorting method has no significance +func GetPeopleIdentifiersSortedForCouple(personAIdentifier string, personBIdentifier string)(string, string, error){ + + if (personAIdentifier == personBIdentifier){ + return "", "", errors.New("GetPeopleIdentifiersSortedForCouple called with identical person identifiers: " + personAIdentifier) + } + + if (personAIdentifier < personBIdentifier){ + return personAIdentifier, personBIdentifier, nil + } + return personBIdentifier, personAIdentifier, nil +} + + +//Outputs: +// -bool: Any analysis exists +// -string: Newest analysis identifier +// -int64: Time newest analysis was performed +// -[]string: List of genomes analyzed +// -bool: Newer analysis version available +// -This is not the same as new genomes being available. New version can be for the same genomes. +// -To fully determine if the analysis is up to date, we must check if new (or less) genomes are available to analyse for the person +// -error +func GetPersonNewestGeneticAnalysisInfo(personIdentifier string)(bool, string, int64, []string, bool, error){ + + lookupMap := map[string]string{ + "PersonIdentifier": personIdentifier, + } + + anyItemsFound, analysisItemsMapList, err := myPersonAnalysesMapListDatastore.GetMapListItems(lookupMap) + if (err != nil) { return false, "", 0, nil, false, err } + if (anyItemsFound == false){ + return false, "", 0, nil, false, nil + } + + // Below is the newest analysis version, which would update if the user had updated their Seekia client + appAnalysisVersion := appValues.GetGeneticAnalysisVersion() + + newestAnalysisFound := false + newestAnalysisCreatedTime := int64(0) + newestAnalysisIdentifier := "" + newestAnalysisAnalyzedGenomesList := []string{} + newerAnalysisVersionAvailable := false + + for _, analysisMap := range analysisItemsMapList{ + + currentPersonIdentifier, exists := analysisMap["PersonIdentifier"] + if (exists == false) { + return false, "", 0, nil, false, errors.New("Malformed myAnalysesMapList: Item missing PersonIdentifier") + } + + if (personIdentifier != currentPersonIdentifier){ + return false, "", 0, nil, false, errors.New("GetMapListItems returning map with different personIdentifier.") + } + + timeCreated, exists := analysisMap["TimeCreated"] + if (exists == false) { + return false, "", 0, nil, false, errors.New("Malformed myAnalysesMapList: Item missing TimeCreated") + } + + timeCreatedInt64, err := helpers.ConvertStringToInt64(timeCreated) + if (err != nil) { + return false, "", 0, nil, false, errors.New("Malformed myAnalysesMapList: Item contains invalid timeCreated: " + timeCreated) + } + + if (newestAnalysisFound == true && newestAnalysisCreatedTime > timeCreatedInt64){ + continue + } + + newestAnalysisFound = true + + analysisIdentifier, exists := analysisMap["AnalysisIdentifier"] + if (exists == false) { + return false, "", 0, nil, false, errors.New("Malformed myAnalysesMapList: Item missing AnalysisIdentifier") + } + + analyzedGenomes, exists := analysisMap["AnalyzedGenomes"] + if (exists == false) { + return false, "", 0, nil, false, errors.New("Malformed myAnalysesMapList: Item missing AnalyzedGenomes") + } + + analyzedGenomesList := strings.Split(analyzedGenomes, "+") + + analysisVersion, exists := analysisMap["AnalysisVersion"] + if (exists == false) { + return false, "", 0, nil, false, errors.New("Malformed myAnalysesMapList: Item missing AnalysisVersion") + } + + analysisVersionInt, err := helpers.ConvertStringToInt(analysisVersion) + if (err != nil) { + return false, "", 0, nil, false, errors.New("Malformed myAnalysesMapList: Item contains invalid AnalysisVersion: " + analysisVersion) + } + + if (appAnalysisVersion > analysisVersionInt){ + // This analysis is not of the newest analysis version + // The user should run a new analysis on the same genomes to get the newest, most up to date and informative results + newerAnalysisVersionAvailable = true + } else { + newerAnalysisVersionAvailable = false + } + + newestAnalysisCreatedTime = timeCreatedInt64 + newestAnalysisIdentifier = analysisIdentifier + newestAnalysisAnalyzedGenomesList = analyzedGenomesList + } + + if (newestAnalysisFound == false){ + return false, "", 0, nil, false, errors.New("GetMapListItems not returning any items when anyItemsFound == true.") + } + + return true, newestAnalysisIdentifier, newestAnalysisCreatedTime, newestAnalysisAnalyzedGenomesList, newerAnalysisVersionAvailable, nil +} + +//Outputs: +// -bool: Analysis exists +// -string: Newest analysis identifier +// -int64: Time newest analysis was performed +// -[]string: Person A list of genomes analyzed +// -[]string: Person B list of genomes analyzed +// -bool: Newer analysis version available +// -This is not the same as new genomes being available. New version can be for the same genomes. +// -To fully determine if the analysis is up to date, we must check if new (or less) genomes are available to analyse for the person +// -error +func GetCoupleNewestGeneticAnalysisInfo(inputPersonAIdentifier string, inputPersonBIdentifier string)(bool, string, int64, []string, []string, bool, error){ + + personAIdentifier, personBIdentifier, err := GetPeopleIdentifiersSortedForCouple(inputPersonAIdentifier, inputPersonBIdentifier) + if (err != nil) { return false, "", 0, nil, nil, false, err } + + lookupMap := map[string]string{ + "PersonAIdentifier": personAIdentifier, + "PersonBIdentifier": personBIdentifier, + } + + anyItemsFound, analysisItemsMapList, err := myCoupleAnalysesMapListDatastore.GetMapListItems(lookupMap) + if (err != nil) { return false, "", 0, nil, nil, false, err } + if (anyItemsFound == false){ + return false, "", 0, nil, nil, false, nil + } + + // Below is the newest analysis version, which would update if the user had updated their Seekia client + appAnalysisVersion := appValues.GetGeneticAnalysisVersion() + + newestAnalysisFound := false + newestAnalysisCreatedTime := int64(0) + newestAnalysisIdentifier := "" + newestAnalysisPersonAAnalyzedGenomesList := []string{} + newestAnalysisPersonBAnalyzedGenomesList := []string{} + newerAnalysisVersionAvailable := false + + for _, analysisMap := range analysisItemsMapList{ + + currentPersonAIdentifier, exists := analysisMap["PersonAIdentifier"] + if (exists == false) { + return false, "", 0, nil, nil, false, errors.New("Malformed myAnalysesMapList: Item missing PersonAIdentifier") + } + + if (personAIdentifier != currentPersonAIdentifier){ + return false, "", 0, nil, nil, false, errors.New("GetMapListItems returning map with different personAIdentifier.") + } + + currentPersonBIdentifier, exists := analysisMap["PersonBIdentifier"] + if (exists == false) { + return false, "", 0, nil, nil, false, errors.New("Malformed myAnalysesMapList: Item missing PersonBIdentifier") + } + + if (personBIdentifier != currentPersonBIdentifier){ + return false, "", 0, nil, nil, false, errors.New("GetMapListItems returning map with different personBIdentifier.") + } + + timeCreated, exists := analysisMap["TimeCreated"] + if (exists == false) { + return false, "", 0, nil, nil, false, errors.New("Malformed myAnalysesMapList: Item missing TimeCreated") + } + + timeCreatedInt64, err := helpers.ConvertStringToInt64(timeCreated) + if (err != nil) { + return false, "", 0, nil, nil, false, errors.New("Malformed myAnalysesMapList: Item contains invalid timeCreated: " + timeCreated) + } + + if (newestAnalysisFound == true && newestAnalysisCreatedTime > timeCreatedInt64){ + continue + } + newestAnalysisFound = true + + analysisIdentifier, exists := analysisMap["AnalysisIdentifier"] + if (exists == false) { + return false, "", 0, nil, nil, false, errors.New("Malformed myAnalysesMapList: Item missing AnalysisIdentifier") + } + + personAAnalyzedGenomes, exists := analysisMap["PersonAAnalyzedGenomes"] + if (exists == false) { + return false, "", 0, nil, nil, false, errors.New("Malformed myAnalysesMapList: Item missing PersonAAnalyzedGenomes") + } + + personAAnalyzedGenomesList := strings.Split(personAAnalyzedGenomes, "+") + + personBAnalyzedGenomes, exists := analysisMap["PersonBAnalyzedGenomes"] + if (exists == false) { + return false, "", 0, nil, nil, false, errors.New("Malformed myAnalysesMapList: Item missing PersonBAnalyzedGenomes") + } + + personBAnalyzedGenomesList := strings.Split(personBAnalyzedGenomes, "+") + + analysisVersion, exists := analysisMap["AnalysisVersion"] + if (exists == false) { + return false, "", 0, nil, nil, false, errors.New("Malformed myAnalysesMapList: Item missing AnalysisVersion") + } + + analysisVersionInt, err := helpers.ConvertStringToInt(analysisVersion) + if (err != nil) { + return false, "", 0, nil, nil, false, errors.New("Malformed myAnalysesMapList: Item contains invalid AnalysisVersion: " + analysisVersion) + } + + if (appAnalysisVersion > analysisVersionInt){ + // This analysis is not of the newest analysis version + // The user should run a new analysis on the same genomes to get the newest, most up to date and informative results + newerAnalysisVersionAvailable = true + } else { + newerAnalysisVersionAvailable = false + } + + newestAnalysisCreatedTime = timeCreatedInt64 + newestAnalysisIdentifier = analysisIdentifier + newestAnalysisPersonAAnalyzedGenomesList = personAAnalyzedGenomesList + newestAnalysisPersonBAnalyzedGenomesList = personBAnalyzedGenomesList + } + + if (newestAnalysisFound == false){ + return false, "", 0, nil, nil, false, errors.New("GetMapListItems not returning any items when anyItemsFound == true.") + } + + // We have to return the personA/PersonB analyzed genomes list in the same order that they came in + + if (inputPersonAIdentifier == personAIdentifier){ + // No swapping happened. + return true, newestAnalysisIdentifier, newestAnalysisCreatedTime, newestAnalysisPersonAAnalyzedGenomesList, newestAnalysisPersonBAnalyzedGenomesList, newerAnalysisVersionAvailable, nil + } + + // We swap the personA/PersonB analyzed genomes lists + + return true, newestAnalysisIdentifier, newestAnalysisCreatedTime, newestAnalysisPersonBAnalyzedGenomesList, newestAnalysisPersonAAnalyzedGenomesList, newerAnalysisVersionAvailable, nil +} + + +// This function should only be used to retrieve an existing analysis +// It can be used for person and couple genetic analyses +//Outputs: +// -bool: Analysis found +// -[]map[string]string: Analysis map list +// -error +func GetGeneticAnalysis(analysisIdentifier string)(bool, []map[string]string, error){ + + userDirectory, err := localFilesystem.GetAppUserFolderPath() + if (err != nil) { return false, nil, err } + + analysisFileName := analysisIdentifier + ".json" + + analysisFilePath := filepath.Join(userDirectory, "MyAnalyses", analysisFileName) + + fileExists, fileBytes, err := localFilesystem.GetFileContents(analysisFilePath) + if (err != nil) { return false, nil, err } + if (fileExists == false){ + return false, nil, nil + } + + fileString := string(fileBytes) + + analysisMapList, err := readGeneticAnalysis.ReadGeneticAnalysisString(fileString) + if (err != nil) { return false, nil, err } + + return true, analysisMapList, nil +} + +// This map keeps track of current person analyses being generated +// Map structure: Person identifier -> Process identifier +var personAnalysisProcessesMap map[string]string = make(map[string]string) + +var personAnalysisProcessesMapMutex sync.RWMutex + +// This map keeps track of current couple analyses being generated +// Map structure: PersonAIdentifier + "+" + PersonBIdentifier -> Process identifier +var coupleAnalysisProcessesMap map[string]string = make(map[string]string) + +var coupleAnalysisProcessesMapMutex sync.RWMutex + + +// Returns the process identifier for the current running genetic analyses for the person +// Outputs: +// -bool: Any process found (The process may be complete) +// -string: Process identifier +// -error +func GetPersonGeneticAnalysisProcessIdentifier(personIdentifier string)(bool, string, error){ + + personAnalysisProcessesMapMutex.RLock() + processIdentifier, exists := personAnalysisProcessesMap[personIdentifier] + personAnalysisProcessesMapMutex.RUnlock() + if (exists == false){ + return false, "", nil + } + + return true, processIdentifier, nil +} + +// Returns the process identifier for the current running genetic analyses for the couple +// Outputs: +// -bool: Any process found (The process may be complete) +// -string: Process identifier +// -error +func GetCoupleGeneticAnalysisProcessIdentifier(inputPersonAIdentifier string, inputPersonBIdentifier string)(bool, string, error){ + + personAIdentifier, personBIdentifier, err := GetPeopleIdentifiersSortedForCouple(inputPersonAIdentifier, inputPersonBIdentifier) + if (err != nil) { return false, "", err } + + coupleIdentifier := personAIdentifier + "+" + personBIdentifier + + coupleAnalysisProcessesMapMutex.RLock() + processIdentifier, exists := coupleAnalysisProcessesMap[coupleIdentifier] + coupleAnalysisProcessesMapMutex.RUnlock() + if (exists == false){ + return false, "", nil + } + + return true, processIdentifier, nil +} + +//Outputs: +// -bool: Process found +// -bool: Process is complete +// -bool: Process encountered error +// -error: Error encountered by process +// -int: Process percentage complete +// -error +func GetAnalysisProcessInfo(processIdentifier string)(bool, bool, bool, error, int, error){ + + processExists, processPercentageComplete := appMemory.GetMemoryEntry(processIdentifier + "_ProgressPercentageComplete") + if (processExists == false){ + // This should not happen... + return false, false, false, nil, 0, nil + } + + exists, errorEncountered := appMemory.GetMemoryEntry(processIdentifier + "_ErrorEncountered") + if (exists == true){ + + errorEncounteredError := errors.New(errorEncountered) + + return true, true, true, errorEncounteredError, 100, nil + } + + if (processPercentageComplete == "100"){ + return true, true, false, nil, 100, nil + } + + processPercentageCompleteInt, err := helpers.ConvertStringToInt(processPercentageComplete) + if (err != nil) { + return false, false, false, nil, 0, errors.New("Invalid processPercentageComplete for genetic analysis process: " + processPercentageComplete) + } + if (processPercentageCompleteInt < 0 || processPercentageCompleteInt > 100){ + return false, false, false, nil, 0, errors.New("Invalid processPercentageComplete for genetic analysis process: " + processPercentageComplete) + } + + return true, false, false, nil, processPercentageCompleteInt, nil +} + + +func StopProcess(processIdentifier string){ + + appMemory.SetMemoryEntry(processIdentifier + "_StopProcess", "Yes") + appMemory.SetMemoryEntry(processIdentifier + "_ProgressPercentageComplete", "100") +} + +// This funciton will stop any currently running processes for this person and start generating a new genetic analysis +//Outputs: +// -string: New process identifier +// -error +func StartCreateNewPersonGeneticAnalysis(personIdentifier string)(string, error){ + + personAnalysisProcessesMapMutex.Lock() + existingProcessIdentifier, exists := personAnalysisProcessesMap[personIdentifier] + if (exists == true){ + progressExists, processPercentageComplete := appMemory.GetMemoryEntry(existingProcessIdentifier + "_ProgressPercentageComplete") + if (progressExists == false){ + personAnalysisProcessesMapMutex.Unlock() + return "", errors.New("personAnalysisProcessesMap process percentageComplete value not found.") + } + if (processPercentageComplete != "100"){ + // There is another process running. + // We will stop the current process for this person + StopProcess(existingProcessIdentifier) + } + } + + newProcessIdentifier, err := helpers.GetNewRandomHexString(18) + if (err != nil) { return "", err } + + personAnalysisProcessesMap[personIdentifier] = newProcessIdentifier + personAnalysisProcessesMapMutex.Unlock() + + updatePercentageCompleteFunction := func(newPercentage int)error{ + + if (newPercentage < 0 || newPercentage > 100){ + return errors.New("Invalid person analysis generation progress percentage.") + } + + newPercentageString := helpers.ConvertIntToString(newPercentage) + + appMemory.SetMemoryEntry(newProcessIdentifier + "_ProgressPercentageComplete", newPercentageString) + + return nil + } + + err = updatePercentageCompleteFunction(0) + if (err != nil) { return "", err } + + createNewPersonGeneticAnalysis := func()error{ + + checkIfProcessIsStopped := func()bool{ + + exists, _ := appMemory.GetMemoryEntry(newProcessIdentifier + "_StopProcess") + if (exists == true){ + return true + } + + return false + } + + personGenomesMapList, err := myGenomes.GetAllPersonGenomesMapList(personIdentifier) + if (err != nil){ return err } + + if (len(personGenomesMapList) == 0){ + return errors.New("Cannot create person genetic analysis: No genomes found for Person.") + } + + genomeIdentifiersList := make([]string, 0, len(personGenomesMapList)) + + genomesList := make([]prepareRawGenomes.RawGenomeWithMetadata, 0, len(personGenomesMapList)) + + finalIndex := len(personGenomesMapList) - 1 + + for index, genomeMap := range personGenomesMapList{ + + newPercentageProgress, err := helpers.ScaleNumberProportionally(true, index, 0, finalIndex, 0, 10) + if (err != nil) { return err } + + err = updatePercentageCompleteFunction(newPercentageProgress) + if (err != nil) { return err } + + currentPersonIdentifier, exists := genomeMap["PersonIdentifier"] + if (exists == false) { + return errors.New("PersonGenomesMapList malformed: Item missing PersonIdentifier") + } + + if (personIdentifier != currentPersonIdentifier){ + return errors.New("GetAllPersonGenomesMapList returning genome for different person.") + } + + genomeIdentifier, exists := genomeMap["GenomeIdentifier"] + if (exists == false) { + return errors.New("PersonGenomesMapList malformed: Item missing GenomeIdentifier") + } + + genomeRawDataString, err := myGenomes.GetGenomeRawDataString(genomeIdentifier) + if (err != nil) { return err } + + rawGenomeIsValid, rawGenomeWithMetadata, err := prepareRawGenomes.CreateRawGenomeWithMetadataObject(genomeIdentifier, genomeRawDataString) + if (err != nil) { return err } + if (rawGenomeIsValid == false){ + return errors.New("myGenomes contains invalid rawGenomeDataString: " + genomeIdentifier) + } + + genomesList = append(genomesList, rawGenomeWithMetadata) + genomeIdentifiersList = append(genomeIdentifiersList, genomeIdentifier) + } + + analysisUpdatePercentageCompleteFunction := func(inputProgress int)error{ + + newPercentageProgress, err := helpers.ScaleNumberProportionally(true, inputProgress, 0, 100, 10, 10) + if (err != nil) { return err } + + err = updatePercentageCompleteFunction(newPercentageProgress) + if (err != nil) { return err } + + return nil + } + + processCompleted, newGeneticAnalysisString, err := createGeneticAnalysis.CreatePersonGeneticAnalysis(genomesList, analysisUpdatePercentageCompleteFunction, checkIfProcessIsStopped) + if (err != nil) { return err } + if (processCompleted == false){ + // User stopped the analysis mid-way + return nil + } + + analyzedGenomesListString := strings.Join(genomeIdentifiersList, "+") + + analysisIdentifier, err := helpers.GetNewRandomHexString(17) + if (err != nil) { return err } + + currentTime := time.Now().Unix() + currentTimeString := helpers.ConvertInt64ToString(currentTime) + + currentAnalysisVersion := appValues.GetGeneticAnalysisVersion() + currentAnalysisVersionString := helpers.ConvertIntToString(currentAnalysisVersion) + + newAnalysisMap := map[string]string{ + "PersonIdentifier": personIdentifier, + "AnalysisIdentifier": analysisIdentifier, + "TimeCreated": currentTimeString, + "AnalyzedGenomes": analyzedGenomesListString, + "AnalysisVersion": currentAnalysisVersionString, + } + + updatingMyAnalysesMutex.Lock() + defer updatingMyAnalysesMutex.Unlock() + + err = myPersonAnalysesMapListDatastore.AddMapListItem(newAnalysisMap) + if (err != nil) { return err } + + userDirectory, err := localFilesystem.GetAppUserFolderPath() + if (err != nil) { return err } + + myAnalysesFolderPath := filepath.Join(userDirectory, "MyAnalyses") + + analysisFileName := analysisIdentifier + ".json" + + err = localFilesystem.CreateOrOverwriteFile([]byte(newGeneticAnalysisString), myAnalysesFolderPath, analysisFileName) + if (err != nil) { return err } + + err = updatePercentageCompleteFunction(100) + if (err != nil) { return err } + + personAnalysisProcessesMapMutex.Lock() + delete(personAnalysisProcessesMap, personIdentifier) + personAnalysisProcessesMapMutex.Unlock() + + return nil + } + + runAnalysis := func(){ + + err := createNewPersonGeneticAnalysis() + if (err != nil){ + appMemory.SetMemoryEntry(newProcessIdentifier + "_ProgressPercentageComplete", "100") + appMemory.SetMemoryEntry(newProcessIdentifier + "_ErrorEncountered", err.Error()) + return + } + } + + go runAnalysis() + + return newProcessIdentifier, nil +} + + +// This will stop any currently running processes for this couple and start generating a new genetic analysis +//Outputs: +// -string: New process identifier +// -error +func StartCreateNewCoupleGeneticAnalysis(inputPersonAIdentifier string, inputPersonBIdentifier string)(string, error){ + + personAIdentifier, personBIdentifier, err := GetPeopleIdentifiersSortedForCouple(inputPersonAIdentifier, inputPersonBIdentifier) + if (err != nil) { return "", err } + + coupleIdentifier := personAIdentifier + "+" + personBIdentifier + + coupleAnalysisProcessesMapMutex.Lock() + existingProcessIdentifier, exists := coupleAnalysisProcessesMap[coupleIdentifier] + if (exists == true){ + progressExists, processPercentageComplete := appMemory.GetMemoryEntry(existingProcessIdentifier + "_ProgressPercentageComplete") + if (progressExists == false){ + coupleAnalysisProcessesMapMutex.Unlock() + return "", errors.New("coupleAnalysisProcessesMap process percentageComplete value not found.") + } + if (processPercentageComplete != "100"){ + // There is another process running. + // We will stop the current process for this person + StopProcess(existingProcessIdentifier) + } + } + + newCoupleProcessIdentifier, err := helpers.GetNewRandomHexString(18) + if (err != nil) { return "", err } + + coupleAnalysisProcessesMap[coupleIdentifier] = newCoupleProcessIdentifier + coupleAnalysisProcessesMapMutex.Unlock() + + updatePercentageCompleteFunction := func(newPercentage int)error{ + + if (newPercentage < 0 || newPercentage > 100){ + return errors.New("Invalid couple analysis generation progress percentage.") + } + + newPercentageString := helpers.ConvertIntToString(newPercentage) + + appMemory.SetMemoryEntry(newCoupleProcessIdentifier + "_ProgressPercentageComplete", newPercentageString) + + return nil + } + + err = updatePercentageCompleteFunction(0) + if (err != nil) { return "", err } + + createNewCoupleGeneticAnalysis := func()error{ + + checkIfProcessIsStopped := func()bool{ + + exists, _ := appMemory.GetMemoryEntry(newCoupleProcessIdentifier + "_StopProcess") + if (exists == true){ + return true + } + return false + } + + // We need both personA and personB to have an analysis before performing the couple analysis + // We will see if an analysis is already running for each person + // We will either start a new analysis for each person or monitor the existing one until it is done + + personIdentifiersList := []string{personAIdentifier, personBIdentifier} + + for index, personIdentifier := range personIdentifiersList{ + + // This function will return the percentage progress that this person's analysis will use of the entire couple analysis + getPercentageRangeForPersonAnalysis := func()(int, int){ + if (index == 0){ + return 0, 33 + } + return 33, 66 + } + + personPercentageRangeStart, personPercentageRangeEnd := getPercentageRangeForPersonAnalysis() + + // Outputs: + // -bool: Person needs update + // -string: Person process identifier + // -error + getPersonProcessIdentifier := func()(bool, string, error){ + + runningProcessFound, runningProcessIdentifier, err := GetPersonGeneticAnalysisProcessIdentifier(personIdentifier) + if (err != nil) { return false, "", err } + if (runningProcessFound == true){ + // This person has a running analysis. We will show the progress in our current process + return true, runningProcessIdentifier, nil + } + + // We check if the person needs a new analysis. + getPersonNewAnalysisNeededBool := func()(bool, error){ + + anyPersonAnalysisFound, _, _, newestPersonAnalysisListOfGenomesAnalyzed, newerAnalysisVersionAvailable, err := GetPersonNewestGeneticAnalysisInfo(personIdentifier) + if (err != nil) { return false, err } + if (anyPersonAnalysisFound == false){ + // No analysis exists. We need to create a new analysis + return true, nil + } + if (newerAnalysisVersionAvailable == true){ + // New analysis version is available. + return true, nil + } + + allPersonRawGenomeIdentifiersList, err := myGenomes.GetAllPersonRawGenomeIdentifiersList(personAIdentifier) + if (err != nil) { return false, err } + + genomesAreIdentical := helpers.CheckIfTwoListsContainIdenticalItems(allPersonRawGenomeIdentifiersList, newestPersonAnalysisListOfGenomesAnalyzed) + if (genomesAreIdentical == false){ + // New genome exists/a genome was deleted. A new analysis is needed + return true, nil + } + // Analysis is up to date. Nothing to do. + return false, nil + } + + personNewAnalysisIsNeeded, err := getPersonNewAnalysisNeededBool() + if (err != nil) { return false, "", err } + if (personNewAnalysisIsNeeded == false){ + return false, "", nil + } + + // We start a new analysis + + newProcessIdentifier, err := StartCreateNewPersonGeneticAnalysis(personIdentifier) + if (err != nil) { return false, "", err } + + return true, newProcessIdentifier, nil + } + + personNeedsUpdate, personProcessIdentifier, err := getPersonProcessIdentifier() + if (err != nil) { return err } + if (personNeedsUpdate == false){ + // No analysis needed + err := updatePercentageCompleteFunction(personPercentageRangeEnd) + if (err != nil) { return err } + + continue + } + + for{ + + processFound, processIsComplete, processEncounteredError, errorEncounteredByProcess, processPercentageComplete, err := GetAnalysisProcessInfo(personProcessIdentifier) + if (err != nil){ return err } + if (processFound == false){ + return errors.New("Person process not found after being found already.") + } + if (processIsComplete == true){ + if (processEncounteredError == true){ + return errorEncounteredByProcess + } + err := updatePercentageCompleteFunction(personPercentageRangeEnd) + if (err != nil) { return err } + + break + } + + personPercentageComplete, err := helpers.ScaleNumberProportionally(true, processPercentageComplete, 0, 100, personPercentageRangeStart, personPercentageRangeEnd) + if (err != nil) { return err } + + err = updatePercentageCompleteFunction(personPercentageComplete) + if (err != nil) { return err } + + time.Sleep(100 * time.Millisecond) + } + } + + // Both people's analyses are complete. + // The percentage progress is at 66% + // Now we perform the couple analysis + + //Outputs: + // -[]prepareRawGenomes.RawGenomeWithMetadata: Genome Identifier -> Genome raw data string + // -[]string: Person raw genome identifiers list + // -error + getPersonGenomesList := func(personIdentifier string)([]prepareRawGenomes.RawGenomeWithMetadata, []string, error){ + + personGenomesMapList, err := myGenomes.GetAllPersonGenomesMapList(personIdentifier) + if (err != nil){ return nil, nil, err } + + if (len(personGenomesMapList) == 0){ + return nil, nil, errors.New("Cannot create person genetic analysis: No genomes found for Person.") + } + + genomeIdentifiersList := make([]string, 0, len(personGenomesMapList)) + + // Map structure: Genome identifier -> Genome raw data string + genomesList := make([]prepareRawGenomes.RawGenomeWithMetadata, 0, len(personGenomesMapList)) + + for _, genomeMap := range personGenomesMapList{ + + currentPersonIdentifier, exists := genomeMap["PersonIdentifier"] + if (exists == false) { + return nil, nil, errors.New("PersonGenomesMapList malformed: Item missing PersonIdentifier") + } + + if (personIdentifier != currentPersonIdentifier){ + return nil, nil, errors.New("GetAllPersonGenomesMapList returning genome for different person.") + } + + genomeIdentifier, exists := genomeMap["GenomeIdentifier"] + if (exists == false) { + return nil, nil, errors.New("PersonGenomesMapList malformed: Item missing GenomeIdentifier") + } + + genomeRawDataString, err := myGenomes.GetGenomeRawDataString(genomeIdentifier) + if (err != nil) { return nil, nil, err } + + rawGenomeIsValid, rawGenomeWithMetadata, err := prepareRawGenomes.CreateRawGenomeWithMetadataObject(genomeIdentifier, genomeRawDataString) + if (err != nil) { return nil, nil, err } + if (rawGenomeIsValid == false){ + return nil, nil, errors.New("myGenomes contains invalid rawGenomeDataString: " + genomeIdentifier) + } + + genomesList = append(genomesList, rawGenomeWithMetadata) + genomeIdentifiersList = append(genomeIdentifiersList, genomeIdentifier) + } + + return genomesList, genomeIdentifiersList, nil + } + + personAGenomesList, personARawGenomeIdentifiersList, err := getPersonGenomesList(personAIdentifier) + if (err != nil) { return err } + + updatePercentageCompleteFunction(70) + + personBGenomesList, personBRawGenomeIdentifiersList, err := getPersonGenomesList(personBIdentifier) + if (err != nil) { return err } + + updatePercentageCompleteFunction(74) + + updateCoupleAnalysisPercentageCompleteFunction := func(newPercentage int)error{ + + personPercentageComplete, err := helpers.ScaleNumberProportionally(true, newPercentage, 0, 100, 74, 100) + if (err != nil) { return err } + + err = updatePercentageCompleteFunction(personPercentageComplete) + if (err != nil) { return err } + + return nil + } + + processCompleted, newGeneticAnalysisString, err := createGeneticAnalysis.CreateCoupleGeneticAnalysis(personAGenomesList, personBGenomesList, updateCoupleAnalysisPercentageCompleteFunction, checkIfProcessIsStopped) + if (err != nil) { return err } + if (processCompleted == false){ + // User stopped the analysis mid-way + return nil + } + + personAAnalyzedGenomesListString := strings.Join(personARawGenomeIdentifiersList, "+") + personBAnalyzedGenomesListString := strings.Join(personBRawGenomeIdentifiersList, "+") + + analysisIdentifier, err := helpers.GetNewRandomHexString(17) + if (err != nil) { return err } + + currentTime := time.Now().Unix() + currentTimeString := helpers.ConvertInt64ToString(currentTime) + + currentAnalysisVersion := appValues.GetGeneticAnalysisVersion() + currentAnalysisVersionString := helpers.ConvertIntToString(currentAnalysisVersion) + + newAnalysisMap := map[string]string{ + "PersonAIdentifier": personAIdentifier, + "PersonBIdentifier": personBIdentifier, + "AnalysisIdentifier": analysisIdentifier, + "TimeCreated": currentTimeString, + "PersonAAnalyzedGenomes": personAAnalyzedGenomesListString, + "PersonBAnalyzedGenomes": personBAnalyzedGenomesListString, + "AnalysisVersion": currentAnalysisVersionString, + } + + updatingMyAnalysesMutex.Lock() + defer updatingMyAnalysesMutex.Unlock() + + err = myCoupleAnalysesMapListDatastore.AddMapListItem(newAnalysisMap) + if (err != nil) { return err } + + userDirectory, err := localFilesystem.GetAppUserFolderPath() + if (err != nil) { return err } + + myAnalysesFolderPath := filepath.Join(userDirectory, "MyAnalyses") + + analysisFileName := analysisIdentifier + ".json" + + err = localFilesystem.CreateOrOverwriteFile([]byte(newGeneticAnalysisString), myAnalysesFolderPath, analysisFileName) + if (err != nil) { return err } + + err = updatePercentageCompleteFunction(100) + if (err != nil) { return err } + + coupleAnalysisProcessesMapMutex.Lock() + delete(coupleAnalysisProcessesMap, coupleIdentifier) + coupleAnalysisProcessesMapMutex.Unlock() + + return nil + } + + runAnalysis := func(){ + + err := createNewCoupleGeneticAnalysis() + if (err != nil){ + appMemory.SetMemoryEntry(newCoupleProcessIdentifier + "_ProgressPercentageComplete", "100") + appMemory.SetMemoryEntry(newCoupleProcessIdentifier + "_ErrorEncountered", err.Error()) + return + } + } + + go runAnalysis() + + return newCoupleProcessIdentifier, nil +} + + + diff --git a/internal/genetics/myChosenAnalysis/myChosenAnalysis.go b/internal/genetics/myChosenAnalysis/myChosenAnalysis.go new file mode 100644 index 0000000..5e97e9c --- /dev/null +++ b/internal/genetics/myChosenAnalysis/myChosenAnalysis.go @@ -0,0 +1,183 @@ + +// myChosenAnalysis provides a function to get a mate user's chosen genetic analysis +// Mate users choose a genome person and genome in the Build Profile gui. +// The genome person's analysis is used for calculating genetic attributes with other users and for sharing on their profile +// The user can choose to not share any of the genetic analysis information on their profile, in which case +// the analysis is only used when calculating attributes with other users +// An example of one of these calculated attributes is "OffspringProbabilityOfAnyMonogenicDisease" + +package myChosenAnalysis + +import "seekia/internal/genetics/myAnalyses" +import "seekia/internal/genetics/myPeople" +import "seekia/internal/genetics/readGeneticAnalysis" +import "seekia/internal/helpers" + +import "seekia/internal/profiles/myLocalProfiles" + +import "errors" +import "sync" + + +// We use this mutex whenever we update the cacheChosenGeneticAnalysis global variables +var myCacheChosenGeneticAnalysisMutex sync.RWMutex + +// We use this identifier to keep track of which analysis we have stored in the cache +// Whenever we update our genome person's analysis, we will update our cache analysis +var myCacheChosenGeneticAnalysisIdentifier string + +// We use this variable to store the analysis in memory +// This prevents us from having to read and unmarshal the json file each time we want to retrieve the analysis +// TODO: Read attributes from the analysis into maps for faster retrieval +var myCacheChosenGeneticAnalysis []map[string]string + +// These variables store metadata about the cache genetic analysis +var myCacheChosenGeneticAnalysis_GenomeIdentifierToUse string + +// This variable tells us if the current cache chosen genetic analysis contains multiple genomes +var myCacheChosenGeneticAnalysis_MultipleGenomesExist bool + +// This function is used to retrieve the user's chosen person genetic analysis +// The user must choose a person to link to their mate profile/identity +// This genetic analysis is not shared on the person's profile publicly. +// Portions of the analysis may be shared if the user opts in. +//Outputs: +// -bool: Person identifier chosen +// -bool: Any genomes exist +// -bool: Person analysis is ready +// -[]map[string]string: Genetic analysis map list +// -string: Genome identifier to use +// -bool: Multiple genomes exist +// -error +func GetMyChosenMateGeneticAnalysis()(bool, bool, bool, []map[string]string, string, bool, error){ + + // We check if the user has added a genome person, and add their genome info if necessary + + myGenomePersonIdentifierExists, myGenomePersonIdentifier, err := myLocalProfiles.GetProfileData("Mate", "GenomePersonIdentifier") + if (err != nil){ return false, false, false, nil, "", false, err } + if (myGenomePersonIdentifierExists == false){ + return false, false, false, nil, "", false, nil + } + + anyGenomesExist, personAnalysisIsReady, personAnalysisIdentifier, err := myPeople.CheckIfPersonAnalysisIsReady(myGenomePersonIdentifier) + if (err != nil){ return false, false, false, nil, "", false, err } + if (anyGenomesExist == false){ + return true, false, false, nil, "", false, nil + } + if (personAnalysisIsReady == false){ + return true, true, false, nil, "", false, nil + } + + myCacheChosenGeneticAnalysisMutex.RLock() + myCacheChosenGeneticAnalysisIdentifierCopy := myCacheChosenGeneticAnalysisIdentifier + myCacheChosenGeneticAnalysisMutex.RUnlock() + + if (myCacheChosenGeneticAnalysisIdentifierCopy == personAnalysisIdentifier){ + // The analysis exists in cache + // We copy it and return it + + myCacheChosenGeneticAnalysisMutex.RLock() + + myCacheChosenGeneticAnalysisCopy := helpers.DeepCopyStringToStringMapList(myCacheChosenGeneticAnalysis) + + myCacheChosenGeneticAnalysis_GenomeIdentifierToUseCopy := myCacheChosenGeneticAnalysis_GenomeIdentifierToUse + myCacheChosenGeneticAnalysis_MultipleGenomesExistCopy := myCacheChosenGeneticAnalysis_MultipleGenomesExist + + myCacheChosenGeneticAnalysisMutex.RUnlock() + + return true, true, true, myCacheChosenGeneticAnalysisCopy, myCacheChosenGeneticAnalysis_GenomeIdentifierToUseCopy, myCacheChosenGeneticAnalysis_MultipleGenomesExistCopy, nil + } + + // The analysis and its metadata have not been read into the cache yet + // We will retrieve it from its .json file + + myAnalysisFound, myGeneticAnalysisMapList, err := myAnalyses.GetGeneticAnalysis(personAnalysisIdentifier) + if (err != nil){ return false, false, false, nil, "", false, err } + if (myAnalysisFound == false){ + return false, false, false, nil, "", false, errors.New("CheckIfPersonAnalysisIsReady returning missing genetic analysis.") + } + + allRawGenomeIdentifiersList, multipleGenomesExist, onlyExcludeConflictsGenomeIdentifier, onlyIncludeSharedGenomeIdentifier, err := readGeneticAnalysis.GetMetadataFromPersonGeneticAnalysis(myGeneticAnalysisMapList) + if (err != nil) { return false, false, false, nil, "", false, err } + + getGenomeIdentifierToUse := func()(string, error){ + + if (multipleGenomesExist == false){ + + // The analysis only has 1 genome identifier. + analysisOnlyRawGenomeIdentifier := allRawGenomeIdentifiersList[0] + + return analysisOnlyRawGenomeIdentifier, nil + } + + currentCombinedGenomeToUse, err := GetMyCombinedGenomeToUse() + if (err != nil){ return "", err } + + if (currentCombinedGenomeToUse == "Only Exclude Conflicts"){ + return onlyExcludeConflictsGenomeIdentifier, nil + } + // currentCombinedGenomeToUse == "Only Include Shared" + return onlyIncludeSharedGenomeIdentifier, nil + } + + genomeIdentifierToUse, err := getGenomeIdentifierToUse() + if (err != nil) { return false, false, false, nil, "", false, err } + + myCacheChosenGeneticAnalysisMutex.Lock() + + myCacheChosenGeneticAnalysisIdentifier = personAnalysisIdentifier + myCacheChosenGeneticAnalysis = myGeneticAnalysisMapList + myCacheChosenGeneticAnalysis_GenomeIdentifierToUse = genomeIdentifierToUse + myCacheChosenGeneticAnalysis_MultipleGenomesExist = multipleGenomesExist + + myChosenGeneticAnalysisCopy := helpers.DeepCopyStringToStringMapList(myGeneticAnalysisMapList) + + myCacheChosenGeneticAnalysisMutex.Unlock() + + return true, true, true, myChosenGeneticAnalysisCopy, genomeIdentifierToUse, multipleGenomesExist, nil +} + + +//Outputs: +// -string: "Only Include Shared", "Only Exclude Conflicts" +// -error +func GetMyCombinedGenomeToUse()(string, error){ + + combinedGenomeToUseExists, combinedGenomeToUse, err := myLocalProfiles.GetProfileData("Mate", "CombinedGenomeToUse") + if (err != nil){ return "", err } + if (combinedGenomeToUseExists == false){ + return "Only Exclude Conflicts", nil + } + + if (combinedGenomeToUse != "Only Include Shared" && combinedGenomeToUse != "Only Exclude Conflicts"){ + return "", errors.New("myLocalProfiles is malformed: Contains invalid combinedGenomeToUse: " + combinedGenomeToUse) + } + + return combinedGenomeToUse, nil +} + + +func SetMyCombinedGenomeToUse(combinedGenomeToUse string)error{ + + if (combinedGenomeToUse != "Only Include Shared" && combinedGenomeToUse != "Only Exclude Conflicts"){ + return errors.New("SetCombinedGenomeToUse called with invalid combinedGenomeToUse: " + combinedGenomeToUse) + } + + err := myLocalProfiles.SetProfileData("Mate", "CombinedGenomeToUse", combinedGenomeToUse) + if (err != nil) { return err } + + // We reset the global variables so the new analysis GenomeIdentifierToUse global variable will be updated + myCacheChosenGeneticAnalysisMutex.Lock() + + myCacheChosenGeneticAnalysisIdentifier = "" + myCacheChosenGeneticAnalysis = nil + myCacheChosenGeneticAnalysis_GenomeIdentifierToUse = "" + myCacheChosenGeneticAnalysis_MultipleGenomesExist = false + + myCacheChosenGeneticAnalysisMutex.Unlock() + + return nil +} + + + diff --git a/internal/genetics/myCouples/myCouples.go b/internal/genetics/myCouples/myCouples.go new file mode 100644 index 0000000..ad2d981 --- /dev/null +++ b/internal/genetics/myCouples/myCouples.go @@ -0,0 +1,178 @@ + +// myCouples provides functions to manage a user's genome couples +// A couple is recorded as 2 person identifiers. + +package myCouples + +import "seekia/internal/myDatastores/myMapList" +import "seekia/internal/genetics/myAnalyses" + +import "sync" +import "errors" + +// This will be locked anytime Couples are being added/deleted +var updatingMyCouplesMutex sync.Mutex + +var myGenomeCouplesMapListDatastore *myMapList.MyMapList + +// This function must be called whenever an app user signs in +func InitializeMyGenomeCouplesDatastore()error{ + + updatingMyCouplesMutex.Lock() + defer updatingMyCouplesMutex.Unlock() + + newMyGenomeCouplesMapListDatastore, err := myMapList.CreateNewMapList("MyGenomeCouples") + if (err != nil) { return err } + + myGenomeCouplesMapListDatastore = newMyGenomeCouplesMapListDatastore + + return nil +} + +//Outputs: +// -[]map[string]string +// -PersonAIdentifier -> Person A Identifier +// -PersonBIdentifier -> Person B Identifier +// -error +func GetMyGenomeCouplesMapList()([]map[string]string, error){ + + couplesMapList, err := myGenomeCouplesMapListDatastore.GetMapList() + if (err != nil) { return nil, err } + + return couplesMapList, nil +} + +//Outputs: +// -bool: Couple already exists +// -error +func AddCouple(inputPersonAIdentifier string, inputPersonBIdentifier string)(bool, error){ + + personAIdentifier, personBIdentifier, err := getPeopleIdentifiersSortedForCouple(inputPersonAIdentifier, inputPersonBIdentifier) + if (err != nil) { return false, err } + + updatingMyCouplesMutex.Lock() + defer updatingMyCouplesMutex.Unlock() + + couplesMapList, err := myGenomeCouplesMapListDatastore.GetMapList() + if (err != nil) { return false, err } + + for _, coupleMap := range couplesMapList{ + + currentPersonAIdentifier, exists := coupleMap["PersonAIdentifier"] + if (exists == false){ + return false, errors.New("MyGenomeCouplesMapList malformed: Contains entry missing PersonAIdentifier") + } + + currentPersonBIdentifier, exists := coupleMap["PersonBIdentifier"] + if (exists == false){ + return false, errors.New("MyGenomeCouplesMapList malformed: Contains entry missing PersonBIdentifier") + } + + if (currentPersonAIdentifier == personAIdentifier && currentPersonBIdentifier == personBIdentifier){ + return true, nil + } + } + + // Couple does not exist. We add it. + + newPersonMap := map[string]string{ + "PersonAIdentifier": personAIdentifier, + "PersonBIdentifier": personBIdentifier, + } + + newCouplesMapList := append(couplesMapList, newPersonMap) + + err = myGenomeCouplesMapListDatastore.OverwriteMapList(newCouplesMapList) + if (err != nil) { return false, err } + + return false, nil +} + +func DeleteCouple(inputPersonAIdentifier string, inputPersonBIdentifier string)error{ + + personAIdentifier, personBIdentifier, err := getPeopleIdentifiersSortedForCouple(inputPersonAIdentifier, inputPersonBIdentifier) + if (err != nil) { return err } + + updatingMyCouplesMutex.Lock() + defer updatingMyCouplesMutex.Unlock() + + mapToDelete := map[string]string{ + "PersonAIdentifier": personAIdentifier, + "PersonBIdentifier": personBIdentifier, + } + + err = myGenomeCouplesMapListDatastore.DeleteMapListItems(mapToDelete) + if (err != nil) { return err } + + err = myAnalyses.DeleteAllAnalysesForCouple(personAIdentifier, personBIdentifier) + if (err != nil) { return err } + + return nil +} + +func DeleteAllCouplesForPerson(personIdentifier string)error{ + + updatingMyCouplesMutex.Lock() + defer updatingMyCouplesMutex.Unlock() + + mapToDeleteA := map[string]string{ + "PersonAIdentifier": personIdentifier, + } + + err := myGenomeCouplesMapListDatastore.DeleteMapListItems(mapToDeleteA) + if (err != nil) { return err } + + mapToDeleteB := map[string]string{ + "PersonBIdentifier": personIdentifier, + } + + err = myGenomeCouplesMapListDatastore.DeleteMapListItems(mapToDeleteB) + if (err != nil) { return err } + + return nil +} + +// This is used to show how many couples will be deleted if a user is deleting a person. +func GetNumberOfCouplesForPerson(personIdentifier string)(int, error){ + + totalCouples := 0 + + lookupMapA := map[string]string{ + "PersonAIdentifier": personIdentifier, + } + + anyItemsFoundA, matchingItemsListA, err := myGenomeCouplesMapListDatastore.GetMapListItems(lookupMapA) + if (err != nil){ return 0, err } + if (anyItemsFoundA == true){ + totalCouples += len(matchingItemsListA) + } + + lookupMapB := map[string]string{ + "PersonBIdentifier": personIdentifier, + } + + anyItemsFoundB, matchingItemsListB, err := myGenomeCouplesMapListDatastore.GetMapListItems(lookupMapB) + if (err != nil){ return 0, err } + if (anyItemsFoundB == true){ + totalCouples += len(matchingItemsListB) + } + + return totalCouples, nil +} + + +// This function is used to sort the person identifiers, so that each pair of identifiers will always map to the same couple when added +// The sorting method has no significance +func getPeopleIdentifiersSortedForCouple(personAIdentifier string, personBIdentifier string)(string, string, error){ + + if (personAIdentifier == personBIdentifier){ + return "", "", errors.New("getPeopleIdentifiersSortedForCouple called with identical person identifiers: " + personAIdentifier) + } + + if (personAIdentifier < personBIdentifier){ + return personAIdentifier, personBIdentifier, nil + } + return personBIdentifier, personAIdentifier, nil +} + + diff --git a/internal/genetics/myGenomes/myGenomes.go b/internal/genetics/myGenomes/myGenomes.go new file mode 100644 index 0000000..2684684 --- /dev/null +++ b/internal/genetics/myGenomes/myGenomes.go @@ -0,0 +1,431 @@ + +// myGenomes provides functions to store a user's raw genome files +// These are exported from sequencing companies like 23andMe and AncestryDNA + +package myGenomes + +import "seekia/internal/myDatastores/myMapList" +import "seekia/internal/genetics/readRawGenomes" +import "seekia/internal/localFilesystem" +import "seekia/internal/helpers" +import "seekia/internal/cryptography/blake3" + +import "path/filepath" +import "time" +import "sync" +import "errors" +import "strings" + +//TODO: Delete unused raw genome files + +// This will be locked anytime Genomes are being added/deleted +var updatingMyGenomesMutex sync.Mutex + +var myGenomesMapListDatastore *myMapList.MyMapList + +// This function must be called whenever an app user signs in +func CreateUserGenomesFolder() error{ + + userDirectory, err := localFilesystem.GetAppUserFolderPath() + if (err != nil) { return err } + + myGenomesFolderPath := filepath.Join(userDirectory, "MyGenomes") + + _, err = localFilesystem.CreateFolder(myGenomesFolderPath) + if (err != nil) { return err } + + return nil +} + +// This function must be called whenever an app user signs in +func InitializeMyGenomeDatastore()error{ + + updatingMyGenomesMutex.Lock() + defer updatingMyGenomesMutex.Unlock() + + newMyGenomesMapListDatastore, err := myMapList.CreateNewMapList("MyGenomes") + if (err != nil) { return err } + + myGenomesMapListDatastore = newMyGenomesMapListDatastore + + return nil +} + +//Outputs: +// -[]map[string]string +// -PersonIdentifier -> Identifier of Genome Person +// -GenomeIdentifier -> Genome identifier (this is the name of the .txt file stored on disk) +// -TimeExported -> Time the genome file was exported from company +// -TimeImported -> Time the genome was imported into Seekia +// -IsPhased -> "Yes"/"No" +// -SNPCount -> Number of readable SNPs in file +// -CompanyName -> Company name ("23andMe", "AncestryDNA") +// -ImportVersion -> Import version for the company from which the metadata was retrieved +// -FileHash -> 256 bits Blake3 hash of the genome file +// -error +func GetMyRawGenomesMetadataMapList()([]map[string]string, error){ + + myRawGenomesMapList, err := myGenomesMapListDatastore.GetMapList() + if (err != nil) { return nil, err } + + return myRawGenomesMapList, nil +} + +//Outputs: +// -bool: File is valid +// -bool: File already exists +// -error +func AddRawGenome(personIdentifier string, rawGenomeString string)(bool, bool, error){ + + isValid := helpers.VerifyHexString(15, personIdentifier) + if (isValid == false) { + return false, false, errors.New("AddRawGenome called with invalid personIdentifier: " + personIdentifier) + } + + updatingMyGenomesMutex.Lock() + defer updatingMyGenomesMutex.Unlock() + + currentFileHash, err := blake3.GetBlake3HashAsHexString(32, []byte(rawGenomeString)) + if (err != nil) { return false, false, err } + + // We check to see if this file has already been imported for this Person + lookupMap := map[string]string{ + "PersonIdentifier": personIdentifier, + "FileHash": currentFileHash, + } + + anyItemFound, _, err := myGenomesMapListDatastore.GetMapListItems(lookupMap) + if (err != nil) { return false, false, err } + if (anyItemFound == true){ + // Genome already exists + return true, true, nil + } + + // Genome is new. We will add it to the map list and copy the file to Seekia local storage + + rawGenomeReader := strings.NewReader(rawGenomeString) + + companyName, importVersion, timeFileWasGenerated, snpCount, genomeIsPhased, _, err := readRawGenomes.ReadRawGenomeFile(rawGenomeReader) + if (err != nil){ + return false, false, nil + } + + genomeIdentifier, err := helpers.GetNewRandomHexString(16) + if (err != nil) { return false, false, err } + + importVersionString := helpers.ConvertIntToString(importVersion) + + timeExported := helpers.ConvertInt64ToString(timeFileWasGenerated) + + timeImported := time.Now().Unix() + timeImportedString := helpers.ConvertInt64ToString(timeImported) + + isPhasedString := helpers.ConvertBoolToYesOrNoString(genomeIsPhased) + + snpCountString := helpers.ConvertInt64ToString(snpCount) + + newGenomeMap := map[string]string{ + "PersonIdentifier": personIdentifier, + "GenomeIdentifier": genomeIdentifier, + "TimeExported": timeExported, + "TimeImported": timeImportedString, + "IsPhased": isPhasedString, + "SNPCount": snpCountString, + "CompanyName": companyName, + "ImportVersion": importVersionString, + "FileHash": currentFileHash, + } + + err = myGenomesMapListDatastore.AddMapListItem(newGenomeMap) + if (err != nil) { return false, false, err } + + userDirectory, err := localFilesystem.GetAppUserFolderPath() + if (err != nil) { return false, false, err } + + myGenomesFolderPath := filepath.Join(userDirectory, "MyGenomes") + + genomeFileName := genomeIdentifier + ".txt" + + err = localFilesystem.CreateOrOverwriteFile([]byte(rawGenomeString), myGenomesFolderPath, genomeFileName) + if (err != nil) { return false, false, err } + + return true, false, nil +} + +func DeleteMyRawGenome(genomeIdentifier string)error{ + + isValid := helpers.VerifyHexString(16, genomeIdentifier) + if (isValid == false){ + return errors.New("DeleteMyRawGenome called with invalid genomeIdentifier: " + genomeIdentifier) + } + + updatingMyGenomesMutex.Lock() + defer updatingMyGenomesMutex.Unlock() + + mapToDelete := map[string]string{ + "GenomeIdentifier": genomeIdentifier, + } + + err := myGenomesMapListDatastore.DeleteMapListItems(mapToDelete) + if (err != nil) { return err } + + userDirectory, err := localFilesystem.GetAppUserFolderPath() + if (err != nil) { return err } + + genomeFileName := genomeIdentifier + ".txt" + + genomeFilePath := filepath.Join(userDirectory, "MyGenomes", genomeFileName) + + _, err = localFilesystem.DeleteFileOrFolder(genomeFilePath) + if (err != nil) { return err } + + return nil +} + +//Outputs: +// -bool: Genome found +// -string: Person identifier +// -int64: Time Genome was exported from company +// -int64: Time genome was imported into Seekia +// -bool: Is Phased +// -int64: SNP Count +// -string: CompanyName +// -int: Import version +// -string: FileHash +// -error +func GetMyRawGenomeMetadata(genomeIdentifier string)(bool, string, int64, int64, bool, int64, string, int, string, error){ + + if (genomeIdentifier == "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" || genomeIdentifier == "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"){ + // These are the genome identifiers we use for example reports + // These are used to show the user what a genetic analysis would look like + + getPersonIdentifier := func()string{ + + if (genomeIdentifier == "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"){ + return "111111111111111111111111111111" + } + + return "222222222222222222222222222222" + } + + personIdentifier := getPersonIdentifier() + + return true, personIdentifier, 0, 0, false, 676720, "AncestryDNA", 1, "", nil + } + + lookupMap := map[string]string{ + "GenomeIdentifier": genomeIdentifier, + } + + anyItemFound, foundItemsMapList, err := myGenomesMapListDatastore.GetMapListItems(lookupMap) + if (anyItemFound == false){ + return false, "", 0, 0, false, 0, "", 0, "", nil + } + if (len(foundItemsMapList) != 1){ + return false, "", 0, 0, false, 0, "", 0, "", errors.New("Malformed myGenomesMapList: Contains multiple entries for same GenomeIdentifier") + } + genomeMap := foundItemsMapList[0] + + personIdentifier, exists := genomeMap["PersonIdentifier"] + if (exists == false){ + return false, "", 0, 0, false, 0, "", 0, "", errors.New("Malformed myGenomesMapList: Item missing PersonIdentifier") + } + + timeExported, exists := genomeMap["TimeExported"] + if (exists == false){ + return false, "", 0, 0, false, 0, "", 0, "", errors.New("Malformed myGenomesMapList: Item missing TimeExported") + } + + timeImported, exists := genomeMap["TimeImported"] + if (exists == false){ + return false, "", 0, 0, false, 0, "", 0, "", errors.New("Malformed myGenomesMapList: Item missing TimeImported") + } + + isPhased, exists := genomeMap["IsPhased"] + if (exists == false){ + return false, "", 0, 0, false, 0, "", 0, "", errors.New("Malformed myGenomesMapList: Item missing IsPhased") + } + snpCount, exists := genomeMap["SNPCount"] + if (exists == false){ + return false, "", 0, 0, false, 0, "", 0, "", errors.New("Malformed myGenomesMapList: Item missing SNPCount") + } + companyName, exists := genomeMap["CompanyName"] + if (exists == false){ + return false, "", 0, 0, false, 0, "", 0, "", errors.New("Malformed myGenomesMapList: Item missing CompanyName") + } + importVersion, exists := genomeMap["ImportVersion"] + if (exists == false){ + return false, "", 0, 0, false, 0, "", 0, "", errors.New("Malformed myGenomesMapList: Item missing ImportVersion") + } + fileHash, exists := genomeMap["FileHash"] + if (exists == false){ + return false, "", 0, 0, false, 0, "", 0, "", errors.New("Malformed myGenomesMapList: Item missing FileHash") + } + + timeExportedInt64, err := helpers.ConvertStringToInt64(timeExported) + if (err != nil){ + return false, "", 0, 0, false, 0, "", 0, "", errors.New("Malformed myGenomesMapList: Item contains invalid TimeExported: " + timeExported) + } + timeImportedInt64, err := helpers.ConvertStringToInt64(timeImported) + if (err != nil){ + return false, "", 0, 0, false, 0, "", 0, "", errors.New("Malformed myGenomesMapList: Item contains invalid TimeImported: " + timeImported) + } + + isPhasedBool, err := helpers.ConvertYesOrNoStringToBool(isPhased) + if (err != nil) { + return false, "", 0, 0, false, 0, "", 0, "", errors.New("Malformed myGenomesMapList: Item contains invalid isPhased: " + isPhased) + } + + snpCountInt64, err := helpers.ConvertStringToInt64(snpCount) + if (err != nil){ + return false, "", 0, 0, false, 0, "", 0, "", errors.New("Malformed myGenomesMapList: Item contains invalid snpCount: " + snpCount) + } + + importVersionInt, err := helpers.ConvertStringToInt(importVersion) + if (err != nil){ + return false, "", 0, 0, false, 0, "", 0, "", errors.New("Malformed myGenomesMapList: Item contains invalid ImportVersion: " + importVersion) + } + + return true, personIdentifier, timeExportedInt64, timeImportedInt64, isPhasedBool, snpCountInt64, companyName, importVersionInt, fileHash, nil +} + +// This function is used to refresh the genome metadata when a new import version is available +func RefreshRawGenomeMetadata(genomeIdentifier string)error{ + + updatingMyGenomesMutex.Lock() + defer updatingMyGenomesMutex.Unlock() + + myGenomesMapList, err := myGenomesMapListDatastore.GetMapList() + if (err != nil) { return err } + + foundGenome := false + + for _, genomeMap := range myGenomesMapList{ + + currentGenomeIdentifier, exists := genomeMap["GenomeIdentifier"] + if (exists == false){ + return errors.New("myGenomesMapList item is malformed: item missing GenomeIdentifier.") + } + + if (currentGenomeIdentifier != genomeIdentifier){ + continue + } + if (foundGenome == true){ + return errors.New("myGenomesMapList is malformed: Multiple entries for the same GenomeIdentifier exist.") + } + foundGenome = true + + rawGenomeString, err := GetGenomeRawDataString(genomeIdentifier) + if (err != nil){ return err } + + rawGenomeReader := strings.NewReader(rawGenomeString) + + companyName, importVersion, timeFileWasGenerated, snpCount, genomeIsPhased, _, err := readRawGenomes.ReadRawGenomeFile(rawGenomeReader) + if (err != nil){ + // Could be that file was importable via old import version, but new import version rejects it. + // That would still be bad undesireable behavior. + return errors.New("Unable to import raw genome during RefreshRawGenomeMetadata: " + err.Error()) + } + + importVersionString := helpers.ConvertIntToString(importVersion) + + timeExported := helpers.ConvertInt64ToString(timeFileWasGenerated) + + isPhasedString := helpers.ConvertBoolToYesOrNoString(genomeIsPhased) + + snpCountString := helpers.ConvertInt64ToString(snpCount) + + genomeMap["TimeExported"] = timeExported + genomeMap["IsPhased"] = isPhasedString + genomeMap["SNPCount"] = snpCountString + genomeMap["CompanyName"] = companyName + genomeMap["ImportVersion"] = importVersionString + } + + if (foundGenome == false){ + return errors.New("Genome not found during RefreshRawGenomeMetadata") + } + + err = myGenomesMapListDatastore.OverwriteMapList(myGenomesMapList) + if (err != nil) { return err } + + return nil +} + +//Outputs: +// -string: Genome raw data string +// -error +func GetGenomeRawDataString(genomeIdentifier string)(string, error){ + + userDirectory, err := localFilesystem.GetAppUserFolderPath() + if (err != nil) { return "", err } + + genomeFileName := genomeIdentifier + ".txt" + + genomeFilePath := filepath.Join(userDirectory, "MyGenomes", genomeFileName) + + fileExists, fileBytes, err := localFilesystem.GetFileContents(genomeFilePath) + if (err != nil) { return "", err } + if (fileExists == false){ + return "", errors.New("GetGenomeRawDataString called with genome whose file we cannot find.") + } + fileString := string(fileBytes) + + return fileString, nil +} + +// Returns all genomes for a person +func GetAllPersonGenomesMapList(personIdentifier string)([]map[string]string, error){ + + lookupMap := map[string]string{ + "PersonIdentifier": personIdentifier, + } + + anyItemsFound, matchingItemsMapList, err := myGenomesMapListDatastore.GetMapListItems(lookupMap) + if (err != nil) { return nil, err } + if (anyItemsFound == false){ + emptyMapList := make([]map[string]string, 0) + return emptyMapList, nil + } + + return matchingItemsMapList, nil +} + +// This will not include any calculated genome identifiers, which only exist within analyses +func GetAllPersonRawGenomeIdentifiersList(personIdentifier string)([]string, error){ + + allPersonGenomesMapList, err := GetAllPersonGenomesMapList(personIdentifier) + if (err != nil) { return nil, err } + + personGenomeIdentifiersList := make([]string, 0, len(allPersonGenomesMapList)) + + for _, genomeMap := range allPersonGenomesMapList{ + + genomeIdentifier, exists := genomeMap["GenomeIdentifier"] + if (exists == false){ + return nil, errors.New("Malformed myGenomesMapList: Item missing GenomeIdentifier") + } + + personGenomeIdentifiersList = append(personGenomeIdentifiersList, genomeIdentifier) + } + + return personGenomeIdentifiersList, nil +} + + +func DeleteAllPersonGenomes(personIdentifier string)error{ + + updatingMyGenomesMutex.Lock() + defer updatingMyGenomesMutex.Unlock() + + mapToDelete := map[string]string{ + "PersonIdentifier": personIdentifier, + } + + err := myGenomesMapListDatastore.DeleteMapListItems(mapToDelete) + if (err != nil) { return err } + + return nil +} + + diff --git a/internal/genetics/myPeople/myPeople.go b/internal/genetics/myPeople/myPeople.go new file mode 100644 index 0000000..985828e --- /dev/null +++ b/internal/genetics/myPeople/myPeople.go @@ -0,0 +1,283 @@ + +// myPeople provides functions to manage a user's genome people +// Each person can have many genomes and a genetic analysis +// Each person's genomes are stored in myGenomes +// This package stores each person's identifier, name, sex, and createdTime + +package myPeople + +import "seekia/internal/myDatastores/myMapList" +import "seekia/internal/helpers" +import "seekia/internal/genetics/myGenomes" +import "seekia/internal/genetics/myCouples" +import "seekia/internal/genetics/myAnalyses" + +import "time" +import "sync" +import "errors" + +// This will be locked anytime People are being added/deleted +var updatingMyPeopleMutex sync.Mutex + +var myGenomePeopleMapListDatastore *myMapList.MyMapList + +// This function must be called whenever an app user signs in +func InitializeMyGenomePeopleDatastore()error{ + + updatingMyPeopleMutex.Lock() + defer updatingMyPeopleMutex.Unlock() + + newMyGenomePeopleMapListDatastore, err := myMapList.CreateNewMapList("MyGenomePeople") + if (err != nil) { return err } + + myGenomePeopleMapListDatastore = newMyGenomePeopleMapListDatastore + + return nil +} + +//Outputs: +// -[]map[string]string +// -PersonIdentifier -> Person Identifier +// -PersonName -> Person Name +// -CreatedTime -> Time the person was created +// -error +func GetMyGenomePeopleMapList()([]map[string]string, error){ + + peopleMapList, err := myGenomePeopleMapListDatastore.GetMapList() + if (err != nil) { return nil, err } + + return peopleMapList, nil +} + +//Outputs: +// -bool: Person with this name already exists +// -error +func AddPerson(personName string, personSex string)(bool, error){ + + if (personName == ""){ + return false, errors.New("AddPerson called with empty personName") + } + if (personSex != "Male" && personSex != "Female" && personSex != "Intersex"){ + return false, errors.New("AddPerson called with invalid personSex: " + personSex) + } + + personIdentifier, err := helpers.GetNewRandomHexString(15) + if (err != nil) { return false, err } + + createdTime := time.Now().Unix() + createdTimeString := helpers.ConvertInt64ToString(createdTime) + + updatingMyPeopleMutex.Lock() + defer updatingMyPeopleMutex.Unlock() + + lookupMap := map[string]string{ + "PersonName": personName, + } + + anyExist, _, err := myGenomePeopleMapListDatastore.GetMapListItems(lookupMap) + if (err != nil) { return false, err } + if (anyExist == true){ + return true, nil + } + + peopleMapList, err := myGenomePeopleMapListDatastore.GetMapList() + if (err != nil) { return false, err } + + newPersonMap := map[string]string{ + "PersonIdentifier": personIdentifier, + "PersonName": personName, + "CreatedTime": createdTimeString, + "PersonSex": personSex, + } + + newPeopleMapList := append(peopleMapList, newPersonMap) + + err = myGenomePeopleMapListDatastore.OverwriteMapList(newPeopleMapList) + if (err != nil) { return false, err } + + return false, nil +} + +//Outputs: +// -bool: Person with this name already exists +// -error +func EditPerson(personIdentifier string, newPersonName string, newPersonSex string)(bool, error){ + + if (newPersonName == ""){ + return false, errors.New("EditPerson called with empty newPersonName") + } + if (newPersonSex != "Male" && newPersonSex != "Female" && newPersonSex != "Intersex"){ + return false, errors.New("EditPerson called with invalid newPersonSex: " + newPersonSex) + } + + updatingMyPeopleMutex.Lock() + defer updatingMyPeopleMutex.Unlock() + + lookupMap := map[string]string{ + "PersonName": newPersonName, + } + + anyExist, _, err := myGenomePeopleMapListDatastore.GetMapListItems(lookupMap) + if (err != nil) { return false, err } + if (anyExist == true){ + // Someone else already has this name + return true, nil + } + + peopleMapList, err := myGenomePeopleMapListDatastore.GetMapList() + if (err != nil) { return false, err } + + for _, personMap := range peopleMapList{ + + currentPersonIdentifier, exists := personMap["PersonIdentifier"] + if (exists == false){ + return false, errors.New("MyGenomePeopleMapList malformed: contains entry missing PersonIdentifier") + } + + if (currentPersonIdentifier == personIdentifier){ + + personMap["PersonName"] = newPersonName + personMap["PersonSex"] = newPersonSex + } + } + + err = myGenomePeopleMapListDatastore.OverwriteMapList(peopleMapList) + if (err != nil) { return false, err } + + return false, nil +} + +// This function will delete the person, any couples with the person, and all of the person's genomes +func DeletePerson(personIdentifier string)error{ + + updatingMyPeopleMutex.Lock() + defer updatingMyPeopleMutex.Unlock() + + err := myGenomes.DeleteAllPersonGenomes(personIdentifier) + if (err != nil) { return err } + + err = myCouples.DeleteAllCouplesForPerson(personIdentifier) + if (err != nil) { return err } + + mapToDelete := map[string]string{ + "PersonIdentifier": personIdentifier, + } + + err = myGenomePeopleMapListDatastore.DeleteMapListItems(mapToDelete) + if (err != nil) { return err } + + err = myAnalyses.DeleteAllAnalysesForPerson(personIdentifier) + if (err != nil) { return err } + + return nil +} + +//Outputs: +// -bool: Person found +// -string: Person name +// -int64: Created time +// -string: Sex ("Male"/"Female"/"Intersex") +// -error +func GetPersonInfo(personIdentifier string)(bool, string, int64, string, error){ + + if (personIdentifier == "111111111111111111111111111111" || personIdentifier == "222222222222222222222222222222"){ + // These are the identifiers we use for sample analyses + // These are analyses the user can view if they want to see what an analysis will look like + + getPersonName := func()string{ + if (personIdentifier == "111111111111111111111111111111"){ + return "Person A" + } + return "Person B" + } + + personName := getPersonName() + + currentTime := time.Now().Unix() + + getPersonSex := func()string{ + if (personIdentifier == "111111111111111111111111111111"){ + return "Male" + } + return "Female" + } + + personSex := getPersonSex() + + return true, personName, currentTime, personSex, nil + } + + lookupMap := map[string]string{ + "PersonIdentifier": personIdentifier, + } + + anyItemFound, retrievedItemsList, err := myGenomePeopleMapListDatastore.GetMapListItems(lookupMap) + if (err != nil) { return false, "", 0, "", err } + if (anyItemFound == false){ + return false, "", 0, "", nil + } + if (len(retrievedItemsList) != 1){ + return false, "", 0, "", errors.New("myGenomePeopleMapList malformed: Multiple entries exist for 1 person") + } + + personMap := retrievedItemsList[0] + + createdTime, exists := personMap["CreatedTime"] + if (exists == false) { + return false, "", 0, "", errors.New("myGenomePeopleMapList malformed: Entry missing CreatedTime") + } + + createdTimeInt64, err := helpers.ConvertStringToInt64(createdTime) + if (err != nil) { + return false, "", 0, "", errors.New("myGenomePeopleMapList malformed: Invalid CreatedTime: " + createdTime) + } + + personName, exists := personMap["PersonName"] + if (exists == false) { + return false, "", 0, "", errors.New("myGenomePeopleMapList malformed: Entry missing PersonName") + } + + personSex, exists := personMap["PersonSex"] + if (exists == false) { + return false, "", 0, "", errors.New("myGenomePeopleMapList malformed: Entry missing PersonSex") + } + + return true, personName, createdTimeInt64, personSex, nil +} + + +// This will return true if a person's analysis is ready +// This means it was created with all of the person's genomes using the newest available analysis version +// Outputs: +// -bool: Any Genomes exist +// -bool: Person analysis is ready +// -string: Newest ready person genetic analysis +// -error +func CheckIfPersonAnalysisIsReady(personIdentifier string)(bool, bool, string, error){ + + allPersonRawGenomeIdentifiersList, err := myGenomes.GetAllPersonRawGenomeIdentifiersList(personIdentifier) + if (err != nil){ return false, false, "", err } + if (len(allPersonRawGenomeIdentifiersList) == 0){ + return false, false, "", nil + } + + anyAnalysisFound, newestAnalysisIdentifier, _, newestAnalysisListOfGenomesAnalyzed, newerAnalysisVersionAvailable, err := myAnalyses.GetPersonNewestGeneticAnalysisInfo(personIdentifier) + if (err != nil){ return false, false, "", err } + if (anyAnalysisFound == false){ + return true, false, "", nil + } + if (newerAnalysisVersionAvailable == true){ + return true, false, "", nil + } + + genomesAreIdentical := helpers.CheckIfTwoListsContainIdenticalItems(allPersonRawGenomeIdentifiersList, newestAnalysisListOfGenomesAnalyzed) + if (genomesAreIdentical == false){ + // A newer genome has been added + return true, false, "", nil + } + + return true, true, newestAnalysisIdentifier, nil +} + + + diff --git a/internal/genetics/prepareRawGenomes/prepareRawGenomes.go b/internal/genetics/prepareRawGenomes/prepareRawGenomes.go new file mode 100644 index 0000000..f612131 --- /dev/null +++ b/internal/genetics/prepareRawGenomes/prepareRawGenomes.go @@ -0,0 +1,524 @@ + +// prepareRawGenomes provides a function to read and merge raw genomes into formatted genome maps which are ready to analyze. +// Each raw genome comes from a genome file from a company such as 23andMe, AncestryDNA, etc... +// Two combined genomes are created: OnlyExcludeConflicts and OnlyIncludeShared + +package prepareRawGenomes + + +import "seekia/resources/geneticReferences/locusMetadata" + +import "seekia/internal/genetics/locusValue" +import "seekia/internal/genetics/readRawGenomes" +import "seekia/internal/helpers" + +import "errors" +import "strings" + + +type RawGenomeWithMetadata struct{ + + GenomeIdentifier string + + GenomeIsPhased bool + + RawGenomeMap map[int64]readRawGenomes.RawGenomeLocusValue +} + + +type GenomeWithMetadata struct{ + + // "Single"/"OnlyExcludeConflicts"/"OnlyIncludeShared" + GenomeType string + + // A 16 byte hex-encoded identifier + GenomeIdentifier string + + // Map Structure: RSID -> Locus base pair value object + GenomeMap map[int64]locusValue.LocusValue +} + + +//Outputs: +// -bool: Raw genome string is valid +// -RawGenomeWithMetadata +// -error (returns non-nil if called with invalid genomeIdentifier) +func CreateRawGenomeWithMetadataObject(genomeIdentifier string, rawGenomeString string)(bool, RawGenomeWithMetadata, error){ + + isValid := helpers.VerifyHexString(16, genomeIdentifier) + if (isValid == false){ + return false, RawGenomeWithMetadata{}, errors.New("CreateRawGenomeWithMetadataObject called with invalid genomeIdentifier: " + genomeIdentifier) + } + + rawDataReader := strings.NewReader(rawGenomeString) + + _, _, _, _, genomeIsPhased, rawGenomeMap, err := readRawGenomes.ReadRawGenomeFile(rawDataReader) + if (err != nil) { + // The raw genome is not valid + return false, RawGenomeWithMetadata{}, nil + } + + newRawGenomeWithMetadataObject := RawGenomeWithMetadata{ + GenomeIdentifier: genomeIdentifier, + GenomeIsPhased: genomeIsPhased, + RawGenomeMap: rawGenomeMap, + } + + return true, newRawGenomeWithMetadataObject, nil +} + + +// Function takes a map of raw genome strings and converts it into a map list containing a map for each genome +// If there exists more than 1 genome, it also creates the two combined genomes: Only Include Shared/Only Exclude Conflicts +// Inputs: +// -[]RawGenomeWithMetadata +// -func(int)error: Update Percentage Complete Function +//Outputs: +// -[]GenomeWithMetadata: Genomes with metadata list +// -[]string: All raw genome identifiers list (not including combined genomes) +// -bool: Combined genomes exist +// -string: Only exclude conflicts genome identifier +// -string: Only include shared genome identifier +// -error +func GetGenomesWithMetadataListFromRawGenomesList(inputGenomesList []RawGenomeWithMetadata, updatePercentageCompleteFunction func(int)error)([]GenomeWithMetadata, []string, bool, string, string, error){ + + // The reading of genomes will take up the first 20% of the percentage range + // The creation of multiple genomes will take up the last 80% of the percentage range + + // Structure: A list of genomes. + // Each map stores a genome from a company or a combined genome. + genomesWithMetadataList := make([]GenomeWithMetadata, 0) + + numberOfGenomesRead := 0 + totalNumberOfGenomesToRead := len(inputGenomesList) + + allRawGenomeIdentifiersList := make([]string, 0) + + for _, rawGenomeWithMetadataObject := range inputGenomesList{ + + newPercentageCompletion, err := helpers.ScaleNumberProportionally(true, numberOfGenomesRead, 0, totalNumberOfGenomesToRead, 0, 20) + if (err != nil) { return nil, nil, false, "", "", err } + + err = updatePercentageCompleteFunction(newPercentageCompletion) + if (err != nil) { return nil, nil, false, "", "", err } + + genomeIdentifier := rawGenomeWithMetadataObject.GenomeIdentifier + genomeIsPhased := rawGenomeWithMetadataObject.GenomeIsPhased + rawGenomeMap := rawGenomeWithMetadataObject.RawGenomeMap + + // Now we convert rawGenomeMap to a genomeMap + + // Map Structure: RSID -> Locus Value + genomeMap := make(map[int64]locusValue.LocusValue) + + // We use this list to check for alias collisions later + allRSIDsList := make([]int64, 0) + + for rsID, locusBasePairValue := range rawGenomeMap{ + + locusAllele2Exists := locusBasePairValue.Allele2Exists + if (locusAllele2Exists == false){ + // This SNP contains less than 2 bases + // We don't support reading these kinds of SNP values yet + continue + } + + locusAllele1 := locusBasePairValue.Allele1 + locusAllele2 := locusBasePairValue.Allele2 + + locusValueObject := locusValue.LocusValue{ + LocusIsPhased: genomeIsPhased, + Base1Value: locusAllele1, + Base2Value: locusAllele2, + } + + genomeMap[rsID] = locusValueObject + + allRSIDsList = append(allRSIDsList, rsID) + } + + // Now we check for rsID aliases + // rsID aliases are multiple rsids that refer to the same location + // If a single genome file contained two identical rsids whose value conflicted, the company must have made an error in reporting + + for _, rsID := range allRSIDsList{ + + rsidLocusValue, exists := genomeMap[rsID] + if (exists == false){ + // This must have been an alias that was deleted + continue + } + + aliasExists, rsidAliasesList, err := locusMetadata.GetRSIDAliases(rsID) + if (err != nil){ return nil, nil, false, "", "", err } + if (aliasExists == false){ + continue + } + + checkIfRSIDCollisionExists := func()bool{ + + for _, rsidAlias := range rsidAliasesList{ + + aliasLocusValue, exists := genomeMap[rsidAlias] + if (exists == false){ + continue + } + + if (aliasLocusValue != rsidLocusValue){ + // A collision exists with an alias rsID + // The company must be creating invalid results, or our alias list is invalid + return true + } + } + + return false + } + + rsidCollisionExists := checkIfRSIDCollisionExists() + if (rsidCollisionExists == true){ + // We will delete this rsID + // We cannot trust any of the results + + delete(genomeMap, rsID) + } + + // We delete all aliases from the genome map + // We do this to save space + + for _, rsidAlias := range rsidAliasesList{ + delete(genomeMap, rsidAlias) + } + } + + if (len(genomeMap) == 0){ + // No valid locations exist + continue + } + + genomeWithMetadataObject := GenomeWithMetadata{ + GenomeType: "Single", + GenomeIdentifier: genomeIdentifier, + GenomeMap: genomeMap, + } + + genomesWithMetadataList = append(genomesWithMetadataList, genomeWithMetadataObject) + allRawGenomeIdentifiersList = append(allRawGenomeIdentifiersList, genomeIdentifier) + + numberOfGenomesRead += 1 + } + + containsDuplicates, _ := helpers.CheckIfListContainsDuplicates(allRawGenomeIdentifiersList) + if (containsDuplicates == true){ + return nil, nil, false, "", "", errors.New("GetGenomesWithMetadataListFromRawGenomesList called with inputGenomesList containing duplicate genomeIdentifiers.") + } + + err := updatePercentageCompleteFunction(20) + if (err != nil){ return nil, nil, false, "", "", err } + + if (len(genomesWithMetadataList) == 1){ + + // Only 1 genome exists. + // No genome combining is needed. + + err = updatePercentageCompleteFunction(100) + if (err != nil){ return nil, nil, false, "", "", err } + + return genomesWithMetadataList, allRawGenomeIdentifiersList, false, "", "", nil + } + + // Now we create the shared genomes + // The "OnlyExcludeConflicts" genome only excludes an SNP when there is disagreement about its value + // The "OnlyIncludeShared" genome only includes SNPs that at least 2 genome files share. This will be the most reliable genome. + // For both of the genomes, if more than 2 base pairs exist for the same rsid, the most reported SNP value will be used + // For instance, if 3 genome files are imported, and 1 disagrees with the other two, the other two's snp value will be used + // If there is a tie, the rsid will be omitted + + // This map stores all RSIDs across all genomes + allRSIDsMap := make(map[int64]struct{}) + + finalIndex := len(genomesWithMetadataList) - 1 + + for index, genomeWithMetadataObject := range genomesWithMetadataList{ + + newPercentageCompletion, err := helpers.ScaleNumberProportionally(true, index, 0, finalIndex, 20, 50) + if (err != nil){ return nil, nil, false, "", "", err } + + err = updatePercentageCompleteFunction(newPercentageCompletion) + if (err != nil){ return nil, nil, false, "", "", err } + + genomeMap := genomeWithMetadataObject.GenomeMap + + for rsID, _ := range genomeMap{ + allRSIDsMap[rsID] = struct{}{} + } + } + + onlyExcludeConflictsGenomeMap := make(map[int64]locusValue.LocusValue) + onlyIncludeSharedGenomeMap := make(map[int64]locusValue.LocusValue) + + index := 0 + finalIndex = len(allRSIDsMap) - 1 + + // This map stores the rsid aliases that we have already added + // We will skip these, because we will have already dealt with them + addedRSIDAliasesMap := make(map[int64]struct{}) + + for rsID, _ := range allRSIDsMap{ + + newPercentageCompletion, err := helpers.ScaleNumberProportionally(true, index, 0, finalIndex, 50, 100) + if (err != nil){ return nil, nil, false, "", "", err } + + err = updatePercentageCompleteFunction(newPercentageCompletion) + if (err != nil){ return nil, nil, false, "", "", err } + + index += 1 + + _, exists := addedRSIDAliasesMap[rsID] + if (exists == true){ + // This rsID is an alias for an rsid we already added + continue + } + + anyAliasesExist, rsidAliasesList, err := locusMetadata.GetRSIDAliases(rsID) + if (err != nil){ return nil, nil, false, "", "", err } + if (anyAliasesExist == true){ + + for _, rsidAlias := range rsidAliasesList{ + + addedRSIDAliasesMap[rsidAlias] = struct{}{} + } + } + + // We first check to see which base pairs are most commonly reported, without any regard for the order (is phased status) + // After doing this, we will see if we have enough information to determine if the phase is known + + locusValuesList := make([]locusValue.LocusValue, 0) + + for _, genomeWithMetadataObject := range genomesWithMetadataList{ + + genomeMap := genomeWithMetadataObject.GenomeMap + + locusValueObject, exists := genomeMap[rsID] + if (exists == true){ + locusValuesList = append(locusValuesList, locusValueObject) + continue + } + + // We did not find the rsid + // We see if its alias exists + + if (anyAliasesExist == false){ + continue + } + + for _, rsidAlias := range rsidAliasesList{ + + aliasLocusValueObject, exists := genomeMap[rsidAlias] + if (exists == true){ + locusValuesList = append(locusValuesList, aliasLocusValueObject) + + // We only want to add 1 alias value to the locusValuesList + // We already pruned the genomeMap of any multiple alias values, so searching further is unnecessary + break + } + } + } + + //TODO: Our current methods could be improved here. + // Currently, we are sorting base pairs for genomes which report the phase as being known, and ones which do not + // In a better system, we should have 3 categories we keep track of for each letter pair: + // 1. AB (unknown phase) + // 2. BA (known phase) + // 3. AB (known phase) + // We should develop a way to weigh these against each other to return the most accurate results + // The way we do this should depend on how common phase-flips are compared to invalid base recordings + // Basically, we want base-pair-order disagreements between genomes who are confident about phase to be accounted for + + // We use this map to store the sorted base pair counts + // We sort the base pairs in unicode order + // Map Structure: Sorted RSID Base pair value -> Number of genomes that contain the value + sortedLocusBasePairCountsMap := make(map[string]int) + + for _, locusValueObject := range locusValuesList{ + + base1Value := locusValueObject.Base1Value + base2Value := locusValueObject.Base2Value + + getSortedBasePairValue := func()string{ + + if (base1Value < base2Value){ + result := base1Value + ";" + base2Value + return result + } + result := base2Value + ";" + base1Value + return result + } + + sortedBasePairValue := getSortedBasePairValue() + + sortedLocusBasePairCountsMap[sortedBasePairValue] += 1 + } + + // Now we figure out which sorted base pair value has the highest count + + mostRecordedSortedBasePair := "" + mostRecordedSortedBasePairCount := 0 + tieExists := false + + for basePair, basePairCount := range sortedLocusBasePairCountsMap{ + + if (basePairCount > mostRecordedSortedBasePairCount){ + mostRecordedSortedBasePair = basePair + mostRecordedSortedBasePairCount = basePairCount + tieExists = false + continue + } + if (basePairCount == mostRecordedSortedBasePairCount){ + // There is a tie between differing basePair values + tieExists = true + continue + } + } + + if (tieExists == true){ + // The most recorded sorted base pair value has a tie with a different base pair value + // We will not add this locus to either combined map + continue + } + + // Now we determine the phase for the most recorded sorted base pair + // The phase will be either known/unknown for each combined genome + + //Outputs: + // -string: Locus Base 1 + // -string: Locus Base 2 + // -bool: Phase is known (Only exclude conflicts) + // -bool: Phase is known (Only include shared) + // -error + getLocusBasePair := func()(string, string, bool, bool, error){ + + sortedBase1, sortedBase2, delimiterFound := strings.Cut(mostRecordedSortedBasePair, ";") + if (delimiterFound == false){ + return "", "", false, false, errors.New("mostRecordedSortedBasePair missing ; delimeter") + } + + // We cycle through our locus objects to see how many report that the phase for this base pair is known + + // basePair1 == sortedBase1, sortedBase2 + // basePair2 == sortedBase2, sortedBase1 + + // We count how many genomes report each base pair order as being the true phased order + basePair1Count := 0 + basePair2Count := 0 + + for _, locusObject := range locusValuesList{ + + locusIsPhased := locusObject.LocusIsPhased + if (locusIsPhased == false){ + // This genome does not claim to know the phase of this locus base pair. + // We skip it. + continue + } + + locusBase1 := locusObject.Base1Value + locusBase2 := locusObject.Base2Value + + if (locusBase1 == sortedBase1 && locusBase2 == sortedBase2){ + basePair1Count += 1 + + } else if (locusBase1 == sortedBase2 && locusBase2 == sortedBase1){ + basePair2Count += 1 + } + } + + if (basePair1Count == 0 && basePair2Count == 0){ + // No phase is known for this locus for any of the genomes + return sortedBase1, sortedBase2, false, false, nil + } + + if (basePair1Count == basePair2Count){ + // There is a tie. Thus, phase is unknown. + return sortedBase1, sortedBase2, false, false, nil + } + + if (basePair1Count > basePair2Count){ + + if (basePair1Count < 2){ + // There are not enough phaseIsKnown advocate genomes to be able to say this phase + // is known for the OnlyIncludeShared genome. + + return sortedBase1, sortedBase2, true, false, nil + } + + return sortedBase1, sortedBase2, true, true, nil + } + + // basePair1Count < basePair2Count + + if (basePair2Count < 2){ + // There are not enough phaseIsKnown advocate genomes to be able to say this phase + // is known for the OnlyIncludeShared genome. + + return sortedBase2, sortedBase1, true, false, nil + } + + return sortedBase2, sortedBase1, true, true, nil + } + + locusBase1, locusBase2, phaseIsKnown_OnlyExcludeConflicts, phaseIsKnown_OnlyIncludeShared, err := getLocusBasePair() + if (err != nil){ return nil, nil, false, "", "", err } + + // Now we add to the combined genome maps + // The OnlyExcludeConflicts will only omit when there is a tie + // The OnlyIncludeShared requires at least 2 to agree + + onlyExcludeConflictsLocusValue := locusValue.LocusValue{ + + LocusIsPhased: phaseIsKnown_OnlyExcludeConflicts, + Base1Value: locusBase1, + Base2Value: locusBase2, + } + + onlyExcludeConflictsGenomeMap[rsID] = onlyExcludeConflictsLocusValue + + if (mostRecordedSortedBasePairCount >= 2){ + + onlyIncludeSharedLocusValue := locusValue.LocusValue{ + LocusIsPhased: phaseIsKnown_OnlyIncludeShared, + Base1Value: locusBase1, + Base2Value: locusBase2, + } + + onlyIncludeSharedGenomeMap[rsID] = onlyIncludeSharedLocusValue + } + } + + onlyExcludeConflictsGenomeIdentifier, err := helpers.GetNewRandomHexString(16) + if (err != nil) { return nil, nil, false, "", "", err } + + onlyExcludeConflictsGenomeWithMetadataObject := GenomeWithMetadata{ + + GenomeType: "OnlyExcludeConflicts", + GenomeIdentifier: onlyExcludeConflictsGenomeIdentifier, + GenomeMap: onlyExcludeConflictsGenomeMap, + } + + onlyIncludeSharedGenomeIdentifier, err := helpers.GetNewRandomHexString(16) + if (err != nil) { return nil, nil, false, "", "", err } + + onlyIncludeSharedGenomeWithMetadataObject := GenomeWithMetadata{ + + GenomeType: "OnlyIncludeShared", + GenomeIdentifier: onlyIncludeSharedGenomeIdentifier, + GenomeMap: onlyIncludeSharedGenomeMap, + } + + genomesWithMetadataList = append(genomesWithMetadataList, onlyExcludeConflictsGenomeWithMetadataObject, onlyIncludeSharedGenomeWithMetadataObject) + + err = updatePercentageCompleteFunction(100) + if (err != nil){ return nil, nil, false, "", "", err } + + return genomesWithMetadataList, allRawGenomeIdentifiersList, true, onlyExcludeConflictsGenomeIdentifier, onlyIncludeSharedGenomeIdentifier, nil +} + + diff --git a/internal/genetics/readBiobankData/openSNP.go b/internal/genetics/readBiobankData/openSNP.go new file mode 100644 index 0000000..0f8f3ee --- /dev/null +++ b/internal/genetics/readBiobankData/openSNP.go @@ -0,0 +1,400 @@ +package readBiobankData + +// file openSNP.go provides a datastructure and function to read data from the OpenSNP.org biobank. + +import "seekia/internal/helpers" + +import "encoding/csv" +import "os" +import "io" + + +type PhenotypeData_OpenSNP struct{ + + UserID int + + + EyeColorIsKnown bool + + // Either "Green", "Blue", "Hazel", or "Brown" + EyeColor string + + + LactoseToleranceIsKnown bool + + // true == Is lactose Tolerant + LactoseTolerance bool + + + HairColorIsKnown bool + + HairColor string + + + HeightIsKnown bool + + // Height is expressed in centimeters + Height float64 +} + +// This function reads the phenotypes_202308230100.csv file in the openSNP biobank data. +// +//Outputs: +// -bool: Able to read file (file is well formed and not corrupt) +// -[]PhenotypeData_OpenSNP +func ReadOpenSNPPhenotypesFile(fileObject *os.File)(bool, []PhenotypeData_OpenSNP){ + + csvFileReader := csv.NewReader(fileObject) + csvFileReader.LazyQuotes = true + csvFileReader.Comma = ';' + + // First we read the first line (header line) + + _, err := csvFileReader.Read() + if (err != nil){ + + // File is corrupt + return false, nil + } + + // Now we iterate through each user's phenotype data + + //Map Structure: User ID -> Phenotype data object + userPhenotypeDataMap := make(map[int]PhenotypeData_OpenSNP) + + for { + + userDataLineSlice, err := csvFileReader.Read() + if (err != nil) { + + if (err == io.EOF){ + // We have reached the end of the file + break + } + // File is corrupt + return false, nil + } + + userIDString := userDataLineSlice[0] + + userID, err := helpers.ConvertStringToInt(userIDString) + if (err != nil){ + // File is corrupt + return false, nil + } + + _, exists := userPhenotypeDataMap[userID] + if (exists == true){ + + // This user has multiple entries + // Each entry is identical except for the raw genome filename + // We will continue + + continue + } + + //Outputs: + // -bool: User eye color is known + // -string: User eye color + getUserEyeColor := func()(bool, string){ + + userEyeColorRaw := userDataLineSlice[5] + + switch userEyeColorRaw{ + + case "-":{ + return false, "" + } + case "Brown", + "brown", + "Dark brown", + "Brown/black", + "Grey brown":{ + + return true, "Brown" + } + case "Hazel", + "hazel", + "Brown-green", + "brown-green", + "Hazel/Light Brown", + "Hazel (light brown, dark green, dark blue)", + "Brown-amber", + "Indeterminate brown-green with a subtle grey caste", + "Hazel (brown/green)", + "Green-hazel", + "Amber - (yellow/ocre brown)", + "Hazel/light brown", + "Green-brown", + "green-brown", + "Brown - brown and green in bright sunlight", + "Hazel/yellow", + "Brown-(green when external temperature rises)", + "Ambar-green", + "Olive-brown ringing burnt umber-brown", + "Green with brown freckles", + "Green-Hazel", + "Ambar-Green", + "Brown-Amber", + "Hazel/Yellow", + "Brown center starburst, amber and olive green, with dark gray outer ring":{ + + return true, "Hazel" + } + case "Blue", + "Blue-grey", + "Blue grey", + "Gray-blue", + "Blue-grey with central heterochromia", + "Dark blue", + "blue", + "Light blue-green", + "blue-grey", + "Dark Grayish-Blue Eyes (like a stone)":{ + + return true, "Blue" + } + case "Green", + "Green ", + "green", + "Green-gray", + "Blue-green ", + "Blue-green", + "blue-green ", + "Light-mixed green", + "blue-green", + "Blue with a yellow ring of flecks that make my eyes look green depending on the light or my mood", + "Light-mixed Green", + "Blue-green heterochromia", + "Blue-green-grey":{ + + return true, "Green" + } + + //TODO: Add grey as its own seperate color? + } + + return false, "" + } + + userEyeColorIsKnown, userEyeColor := getUserEyeColor() + + //Outputs: + // -bool: User lactose Tolerance is known + // -bool: User lactose Tolerance + getUserLactoseTolerance := func()(bool, bool){ + + userLactoseToleranceRaw := userDataLineSlice[6] + + switch userLactoseToleranceRaw{ + + case "-":{ + return false, false + } + case "Yes", + "Lactose-intolerant", + "Lactose intolerant", + "lactose-intolerant", + " allergic to all forms of dairy ", + " Allergic to all forms of dairy ", + "Severe gi pain ", + "severe GI pain ":{ + + return true, false + } + case "No", + "Lactose-tolerant", + "Lactose tolerant", + "lactose-tolerant", + "lactose tolerant", + "False":{ + return true, true + } + } + + return false, false + } + + userLactoseToleranceIsKnown, userLactoseTolerance := getUserLactoseTolerance() + + //Outputs: + // -bool: User hair color is known + // -string: Hair Color + getUserHairColor := func()(bool, string){ + + //userHairColorRaw := userDataLineSlice[11] + + //TODO + + return false, "" + } + + userHairColorIsKnown, userHairColor := getUserHairColor() + + + // Outputs: + // -bool: User height is known + // -int: User height (in centimeters) + getUserHeight := func()(bool, float64){ + + userHeightRaw := userDataLineSlice[13] + + switch userHeightRaw{ + case "-":{ + return false, 0 + } + case `4'0"`:{ + return true, 121.92 + } + case `4'1"`:{ + return true, 124.46 + } + case `4'2"`:{ + return true, 127 + } + case `4'3"`:{ + return true, 129.54 + } + case `4'4"`:{ + return true, 132.08 + } + case `4'5"`:{ + return true, 134.62 + } + case `4'6"`:{ + return true, 137.16 + } + case `4'7"`:{ + return true, 139.7 + } + case `4'8"`:{ + return true, 142.24 + } + case `4'9"`:{ + return true, 144.78 + } + case `4'10"`:{ + return true, 147.32 + } + case `4'11"`:{ + return true, 149.86 + } + case `5'`:{ + return true, 152.4 + } + case `5'1"`:{ + return true, 154.94 + } + case `5'2"`:{ + return true, 157.48 + } + case `5'3"`, `5'3''`, `160 cm`:{ + return true, 160 + } + case `5'4"`:{ + return true, 162.56 + } + case `5'5"`:{ + return true, 165.1 + } + case `5'6"`:{ + return true, 167.64 + } + case `168 cm`:{ + return true, 168 + } + case `5'7"`:{ + return true, 170.18 + } + case `5'8"`:{ + return true, 172.72 + } + case `5'9"`:{ + return true, 175.26 + } + case `5'10"`, `5'10''`:{ + return true, 177.8 + } + case `179 cm`:{ + return true, 179 + } + case `180cm`:{ + return true, 180 + } + case `5'11"`:{ + return true, 180.34 + } + case `6'`:{ + return true, 182.88 + } + case `183 cm`:{ + return true, 183 + } + case `6'1"`:{ + return true, 185.42 + } + case `6'2"`:{ + return true, 187.96 + } + case `6'3"`:{ + return true, 190.5 + } + case `6'4"`:{ + return true, 193.04 + } + case `6'5"`:{ + return true, 195.58 + } + case `6'6"`:{ + return true, 198.12 + } + case `6'7"`:{ + return true, 200.66 + } + case `6'8"`:{ + return true, 203.2 + } + case `6'9"`:{ + return true, 205.74 + } + case `6'10"`:{ + return true, 208.28 + } + case `6'11"`:{ + return true, 210.82 + } + case `7'`:{ + return true, 213.36 + } + + //TODO: Add more responses + } + + return false, 0 + } + + userHeightIsKnown, userHeight := getUserHeight() + + userPhenotypeDataObject := PhenotypeData_OpenSNP{ + UserID: userID, + EyeColorIsKnown: userEyeColorIsKnown, + EyeColor: userEyeColor, + LactoseToleranceIsKnown: userLactoseToleranceIsKnown, + LactoseTolerance: userLactoseTolerance, + HairColorIsKnown: userHairColorIsKnown, + HairColor: userHairColor, + HeightIsKnown: userHeightIsKnown, + Height: userHeight, + } + + userPhenotypeDataMap[userID] = userPhenotypeDataObject + } + + userPhenotypeDataList := helpers.GetListOfMapValues(userPhenotypeDataMap) + + return true, userPhenotypeDataList +} + + + + diff --git a/internal/genetics/readBiobankData/readBiobankData.go b/internal/genetics/readBiobankData/readBiobankData.go new file mode 100644 index 0000000..7865f06 --- /dev/null +++ b/internal/genetics/readBiobankData/readBiobankData.go @@ -0,0 +1,7 @@ + +// readBiobankData provides functions to read data from biobanks such as OpenSNP.org. + +package readBiobankData + + +// file openSNP.go provides a datastructure and function to read data from the OpenSNP.org biobank. \ No newline at end of file diff --git a/internal/genetics/readGeneticAnalysis/readGeneticAnalysis.go b/internal/genetics/readGeneticAnalysis/readGeneticAnalysis.go new file mode 100644 index 0000000..747b919 --- /dev/null +++ b/internal/genetics/readGeneticAnalysis/readGeneticAnalysis.go @@ -0,0 +1,1633 @@ + +// readGeneticAnalysis provides function to read genetic analyses + +package readGeneticAnalysis + +import "seekia/resources/geneticReferences/monogenicDiseases" +import "seekia/resources/geneticReferences/polygenicDiseases" +import "seekia/resources/geneticReferences/traits" + + +import "seekia/internal/helpers" + +import "encoding/json" +import "strings" +import "errors" + +// This works for person and couple analyses +func ReadGeneticAnalysisString(inputAnalysisString string)([]map[string]string, error){ + + inputAnalysisBytes := []byte(inputAnalysisString) + + var newAnalysisMapList []map[string]string + + err := json.Unmarshal(inputAnalysisBytes, &newAnalysisMapList) + if (err != nil) { return nil, err } + + return newAnalysisMapList, nil +} + +//Outputs: +// -[]string: All raw genome identifiers list (excluding combined genomes) +// -bool: Multiple genomes exist +// -string: OnlyExcludeConflicts GenomeIdentifier +// -string: OnlyIncludeShared GenomeIdentifier +// -error +func GetMetadataFromPersonGeneticAnalysis(inputGeneticAnalysisMapList []map[string]string)([]string, bool, string, string, error){ + + for _, element := range inputGeneticAnalysisMapList{ + + itemType, exists := element["ItemType"] + if (exists == false){ + return nil, false, "", "", errors.New("Malformed genetic analysis map list: Item missing itemType") + } + if (itemType != "Metadata"){ + continue + } + + analysisVersion, exists := element["AnalysisVersion"] + if (exists == false){ + return nil, false, "", "", errors.New("Cannot read analysis: Metadata missing analysisVersion") + } + + if (analysisVersion != "1"){ + // This analysis must have been created by a newer version of Seekia + // We cannot read it + return nil, false, "", "", errors.New("Cannot read analysis: Is a newer analysis version.") + } + + analysisType, exists := element["AnalysisType"] + if (exists == false || analysisType != "Person"){ + return nil, false, "", "", errors.New("Trying to get metadata from non-person analysis.") + } + + combinedGenomesExist, exists := element["CombinedGenomesExist"] + if (exists == false){ + return nil, false, "", "", errors.New("Malformed analysis map list: Metadata missing CombinedGenomesExist") + } + if (combinedGenomesExist == "No"){ + + mainGenomeIdentifier, exists := element["GenomeIdentifier"] + if (exists == false){ + return nil, false, "", "", errors.New("Malformed analysis map list: Single-genome analysis Metadata missing GenomeIdentifier") + } + + allRawGenomeIdentifiersList := []string{mainGenomeIdentifier} + + return allRawGenomeIdentifiersList, false, "", "", nil + } + + onlyExcludeConflictsGenomeIdentifier, exists := element["OnlyExcludeConflictsGenomeIdentifier"] + if (exists == false){ + return nil, false, "", "", errors.New("Malformed analysis map list: Metadata missing OnlyExcludeConflictsGenomeIdentifier") + } + onlyIncludeSharedGenomeIdentifier, exists := element["OnlyIncludeSharedGenomeIdentifier"] + if (exists == false){ + return nil, false, "", "", errors.New("Malformed genetic analysis map list: Metadata missing OnlyIncludeSharedGenomeIdentifier") + } + + allRawGenomeIdentifiersListString, exists := element["AllRawGenomeIdentifiersList"] + if (exists == false){ + return nil, false, "", "", errors.New("Malformed analysis map list: Multi-genome Metadata missing AllRawGenomeIdentifiersList") + } + + allRawGenomeIdentifiersList := strings.Split(allRawGenomeIdentifiersListString, "+") + + if (len(allRawGenomeIdentifiersList) < 2){ + return nil, false, "", "", errors.New("Malformed analysis map list: Multi-genome Metadata contains AllRawGenomeIdentifiersList with less than two identifiers") + } + + return allRawGenomeIdentifiersList, true, onlyExcludeConflictsGenomeIdentifier, onlyIncludeSharedGenomeIdentifier, nil + } + + return nil, false, "", "", errors.New("GetMetadataFromPersonGeneticAnalysis called with malformed person analysis map list: missing Metadata item.") +} + +//Outputs: +// -string: Pair 1 Person A Genome Identifier +// -string: Pair 1 Person B Genome Identifier +// -bool: Second Genome pair exists +// -string: Pair 2 Person A Genome Identifier +// -string: Pair 2 Person B Genome Identifier +// -bool: Person A Has Multiple Genomes +// -string: Person A OnlyExcludeConflicts Genome Identifier +// -string: Person A OnlyIncludeShared Genome Identifier +// -bool: Person B Has Multiple Genomes +// -string: Person B OnlyExcludeConflicts Genome Identifier +// -string: Person B OnlyIncludeShared Genome Identifier +// -error +func GetMetadataFromCoupleGeneticAnalysis(inputGeneticAnalysisMapList []map[string]string)(string, string, bool, string, string, bool, string, string, bool, string, string, error){ + + for _, element := range inputGeneticAnalysisMapList{ + + itemType, exists := element["ItemType"] + if (exists == false){ + return "", "", false, "", "", false, "", "", false, "", "", errors.New("Malformed genetic analysis map list: Item missing itemType") + } + if (itemType != "Metadata"){ + continue + } + + analysisVersion, exists := element["AnalysisVersion"] + if (exists == false){ + return "", "", false, "", "", false, "", "", false, "", "", errors.New("Cannot read analysis: Metadata missing analysisVersion") + } + + if (analysisVersion != "1"){ + // This analysis must have been created by a newer version of Seekia + // We cannot read it + return "", "", false, "", "", false, "", "", false, "", "", errors.New("Cannot read analysis: Is a newer analysis version.") + } + + analysisType, exists := element["AnalysisType"] + if (exists == false || analysisType != "Couple"){ + return "", "", false, "", "", false, "", "", false, "", "", errors.New("Trying to get metadata from non-couple analysis.") + } + + pair1PersonAGenomeIdentifier, exists := element["Pair1PersonAGenomeIdentifier"] + if (exists == false){ + return "", "", false, "", "", false, "", "", false, "", "", errors.New("Malformed couple analysis: Metadata missing Pair1PersonAGenomeIdentifier") + } + + pair1PersonBGenomeIdentifier, exists := element["Pair1PersonBGenomeIdentifier"] + if (exists == false){ + return "", "", false, "", "", false, "", "", false, "", "", errors.New("Malformed couple analysis: Metadata missing Pair1PersonBGenomeIdentifier") + } + + secondPairExists, exists := element["SecondPairExists"] + if (exists == false){ + return "", "", false, "", "", false, "", "", false, "", "", errors.New("Malformed analysis map list: Metadata missing CombinedGenomesExist") + } + if (secondPairExists == "No"){ + + return pair1PersonAGenomeIdentifier, pair1PersonBGenomeIdentifier, false, "", "", false, "", "", false, "", "", nil + } + + pair2PersonAGenomeIdentifier, exists := element["Pair2PersonAGenomeIdentifier"] + if (exists == false){ + return "", "", false, "", "", false, "", "", false, "", "", errors.New("Malformed couple analysis map list: Metadata missing Pair2PersonAGenomeIdentifier") + } + + pair2PersonBGenomeIdentifier, exists := element["Pair2PersonBGenomeIdentifier"] + if (exists == false){ + return "", "", false, "", "", false, "", "", false, "", "", errors.New("Malformed couple analysis map list: Metadata missing Pair2PersonBGenomeIdentifier") + } + + getPersonAMultipleGenomesInfo := func()(bool, string, string, error){ + + personAHasMultipleGenomes, exists := element["PersonAHasMultipleGenomes"] + if (exists == false){ + return false, "", "", errors.New("Couple analysis malformed: Missing PersonAHasMultipleGenomes") + } + + if (personAHasMultipleGenomes == "No"){ + return false, "", "", nil + } + + personAOnlyExcludeConflictsGenomeIdentifier, exists := element["PersonAOnlyExcludeConflictsGenomeIdentifier"] + if (exists == false) { + return false, "", "", errors.New("Couple analysis malformed: Missing PersonAOnlyExcludeConflictsGenomeIdentifier") + } + + personAOnlyIncludeSharedGenomeIdentifier, exists := element["PersonAOnlyIncludeSharedGenomeIdentifier"] + if (exists == false) { + return false, "", "", errors.New("Couple analysis malformed: Missing PersonAOnlyIncludeSharedGenomeIdentifier") + } + + return true, personAOnlyExcludeConflictsGenomeIdentifier, personAOnlyIncludeSharedGenomeIdentifier, nil + } + + getPersonBMultipleGenomesInfo := func()(bool, string, string, error){ + + personBHasMultipleGenomes, exists := element["PersonBHasMultipleGenomes"] + if (exists == false){ + return false, "", "", errors.New("Couple analysis malformed: Missing PersonBHasMultipleGenomes") + } + + if (personBHasMultipleGenomes == "No"){ + return false, "", "", nil + } + + personBOnlyExcludeConflictsGenomeIdentifier, exists := element["PersonBOnlyExcludeConflictsGenomeIdentifier"] + if (exists == false) { + return false, "", "", errors.New("Couple analysis malformed: Missing PersonBOnlyExcludeConflictsGenomeIdentifier") + } + + personBOnlyIncludeSharedGenomeIdentifier, exists := element["PersonBOnlyIncludeSharedGenomeIdentifier"] + if (exists == false) { + return false, "", "", errors.New("Couple analysis malformed: Missing PersonBOnlyIncludeSharedGenomeIdentifier") + } + + return true, personBOnlyExcludeConflictsGenomeIdentifier, personBOnlyIncludeSharedGenomeIdentifier, nil + } + + personAHasMultipleGenomes, personAOnlyExcludeConflictsGenomeIdentifier, personAOnlyIncludeSharedGenomeIdentifier, err := getPersonAMultipleGenomesInfo() + if (err != nil) { return "", "", false, "", "", false, "", "", false, "", "", err } + + personBHasMultipleGenomes, personBOnlyExcludeConflictsGenomeIdentifier, personBOnlyIncludeSharedGenomeIdentifier, err := getPersonBMultipleGenomesInfo() + if (err != nil) { return "", "", false, "", "", false, "", "", false, "", "", err } + + return pair1PersonAGenomeIdentifier, pair1PersonBGenomeIdentifier, true, pair2PersonAGenomeIdentifier, pair2PersonBGenomeIdentifier, personAHasMultipleGenomes, personAOnlyExcludeConflictsGenomeIdentifier, personAOnlyIncludeSharedGenomeIdentifier, personBHasMultipleGenomes, personBOnlyExcludeConflictsGenomeIdentifier, personBOnlyIncludeSharedGenomeIdentifier, nil + } + + return "", "", false, "", "", false, "", "", false, "", "", errors.New("GetMetadataFromCoupleGeneticAnalysis called with malformed couple analysis map list: missing Metadata item.") +} + + +// This function will take a couple and person analysis +// It takes in a genome identifier from the couple analysis +// It returns the equivalent genome identifier from the person analysis +// This is needed for combined genomes, because their identifiers are generated randomly for each analysis +//Inputs: +// -bool: Is person A +// -[]map[string]string: Person A Analysis Map List +// -[]map[string]string: Person B Analysis Map List +// -[]map[string]string: Couple Analysis Map List +// -string: Input Genome Identifier (Should be from Couple identifier) +//Outputs: +// -string: Genome identifier from person analysis +// -bool: Person analysis has multiple genomes +// -bool: Genome is a combined genome +// -string: Genome combined type ("Only Exclude Conflicts"/"Only Include Shared") +// -error +func GetMatchingPersonAnalysisGenomeIdentifierFromCoupleAnalysis(isPersonA bool, personAAnalysisMapList []map[string]string, personBAnalysisMapList []map[string]string, coupleAnalysisMapList []map[string]string, inputGenomeIdentifier string)(string, bool, bool, string, error){ + + // We need to figure out which genome identifier the current genome identifier corresponds to within the person analysis + // If the genome is not a combined genome, then the genome identifier should be identical between each analysis + // If it is a combined genome, we need to determine which genome it corresponds to + + _, _, secondGenomePairExists, _, _, personAHasMultipleGenomes, personAOnlyExcludeConflictsGenomeIdentifier, personAOnlyIncludeSharedGenomeIdentifier, personBHasMultipleGenomes, personBOnlyExcludeConflictsGenomeIdentifier, personBOnlyIncludeSharedGenomeIdentifier, err := GetMetadataFromCoupleGeneticAnalysis(coupleAnalysisMapList) + if (err != nil) { return "", false, false, "", err } + + if (isPersonA == true){ + + if (personAHasMultipleGenomes == false){ + // This person does not have multiple genomes. The genome identifier is the same between both analyses + return inputGenomeIdentifier, false, false, "", nil + } + + if (secondGenomePairExists == false){ + return inputGenomeIdentifier, true, false, "", nil + } + + _, multipleGenomesExist, onlyExcludeConflictsGenomeIdentifier, onlyIncludeSharedGenomeIdentifier, err := GetMetadataFromPersonGeneticAnalysis(personAAnalysisMapList) + if (err != nil) { return "", false, false, "", err } + if (multipleGenomesExist == false){ + return "", false, false, "", errors.New("Couple analysis says person has multiple genomes, person analysis does not.") + } + + if (inputGenomeIdentifier == personAOnlyExcludeConflictsGenomeIdentifier){ + return onlyExcludeConflictsGenomeIdentifier, true, true, "Only Exclude Conflicts", nil + } + if (inputGenomeIdentifier == personAOnlyIncludeSharedGenomeIdentifier){ + return onlyIncludeSharedGenomeIdentifier, true, true, "Only Include Shared", nil + } + return "", false, false, "", errors.New("Combined genome identifier from couple analysis does not correspond to either combined genome from person analysis.") + } + + // isPersonA == false + + if (personBHasMultipleGenomes == false){ + // This person does not have multiple genomes. The genome identifier is the same between both analyses + return inputGenomeIdentifier, false, false, "", nil + } + + if (secondGenomePairExists == false){ + return inputGenomeIdentifier, true, false, "", nil + } + + _, multipleGenomesExist, onlyExcludeConflictsGenomeIdentifier, onlyIncludeSharedGenomeIdentifier, err := GetMetadataFromPersonGeneticAnalysis(personBAnalysisMapList) + if (err != nil) { return "", false, false, "", err } + if (multipleGenomesExist == false){ + return "", false, false, "", errors.New("Couple analysis says person has multiple genomes, person analysis does not.") + } + + if (inputGenomeIdentifier == personBOnlyExcludeConflictsGenomeIdentifier){ + return onlyExcludeConflictsGenomeIdentifier, true, true, "Only Exclude Conflicts", nil + } + if (inputGenomeIdentifier == personBOnlyIncludeSharedGenomeIdentifier){ + return onlyIncludeSharedGenomeIdentifier, true, true, "Only Include Shared", nil + } + + return "", false, false, "", errors.New("Combined genome identifier from couple analysis does not correspond to either combined genome from person analysis.") +} + + +//Outputs: +// -bool: Probabilities known +// -int: Probability of having disease +// -string: Probability of having disease formatted (with % suffix) +// -int: Probability of passing a disease variant +// -string: Probability of passing a disease variant formatted (with % suffix) +// -int: Number of variants tested +// -bool: Conflict exists +// -error +func GetPersonMonogenicDiseaseInfoFromGeneticAnalysis(personAnalysisMapList []map[string]string, diseaseName string, genomeIdentifier string, personHasMultipleGenomes bool)(bool, int, string, int, string, int, bool, error){ + + for _, element := range personAnalysisMapList{ + + itemType, exists := element["ItemType"] + if (exists == false) { + return false, 0, "", 0, "", 0, false, errors.New("Malformed analysisMapList: Item missing itemType") + } + + if (itemType != "MonogenicDisease"){ + continue + } + + currentDiseaseName, exists := element["DiseaseName"] + if (exists == false){ + return false, 0, "", 0, "", 0, false, errors.New("Malformed analysisMapList: Monogenic Disease item missing DiseaseName") + } + + if (currentDiseaseName != diseaseName){ + continue + } + + numberOfVariantsTested, exists := element[genomeIdentifier + "_NumberOfVariantsTested"] + if (exists == false){ + return false, 0, "", 0, "", 0, false, errors.New("Malformed person analysis: Monogenic Disease item missing _NumberOfVariantsTested") + } + + numberOfVariantsTestedInt, err := helpers.ConvertStringToInt(numberOfVariantsTested) + if (err != nil){ + return false, 0, "", 0, "", 0, false, errors.New("Malformed person analysis: Monogenic Disease item contains invalid _NumberOfVariantsTested: " + numberOfVariantsTested) + } + + getConflictExistsBool := func()(bool, error){ + + if (personHasMultipleGenomes == false){ + return false, nil + } + + conflictExists, exists := element["ConflictExists"] + if (exists == false) { + return false, errors.New("Malformed analysisMapList: Missing ConflictExists") + } + + if (conflictExists == "Yes"){ + return true, nil + } + return false, nil + } + + conflictExistsBool, err := getConflictExistsBool() + if (err != nil) { return false, 0, "", 0, "", 0, false, err } + + probabilityOfHavingDisease, exists := element[genomeIdentifier + "_ProbabilityOfHavingDisease"] + if (exists == false) { + return false, 0, "", 0, "", 0, false, errors.New("Malformed analysisMapList: Monogenic Disease map missing _ProbabilityOfHavingDisease") + } + + if (probabilityOfHavingDisease == "Unknown"){ + return false, 0, "", 0, "", numberOfVariantsTestedInt, conflictExistsBool, nil + } + + probabilityOfHavingDiseaseInt, err := helpers.ConvertStringToInt(probabilityOfHavingDisease) + if (err != nil){ + return false, 0, "", 0, "", 0, false, errors.New("Malformed person analysis: Monogenic Disease map contains invalid _ProbabilityOfHavingDisease: " + probabilityOfHavingDisease) + } + + probabilityOfHavingDiseaseFormatted := probabilityOfHavingDisease + "%" + + probabilityOfPassingAVariant, exists := element[genomeIdentifier + "_ProbabilityOfPassingADiseaseVariant"] + if (exists == false) { + return false, 0, "", 0, "", 0, false, errors.New("Malformed analysisMapList: Monogenic Disease map missing _ProbabilityOfPassingADiseaseVariant") + } + + probabilityOfPassingAVariantInt, err := helpers.ConvertStringToInt(probabilityOfPassingAVariant) + if (err != nil){ + return false, 0, "", 0, "", 0, false, errors.New("Malformed person analysis: Monogenic Disease map contains invalid _ProbabilityOfPassingADiseaseVariant: " + probabilityOfPassingAVariant) + } + + probabilityOfPassingAVariantFormatted := probabilityOfPassingAVariant + "%" + + return true, probabilityOfHavingDiseaseInt, probabilityOfHavingDiseaseFormatted, probabilityOfPassingAVariantInt, probabilityOfPassingAVariantFormatted, numberOfVariantsTestedInt, conflictExistsBool, nil + } + + return false, 0, "", 0, "", 0, false, errors.New("GetPersonMonogenicDiseaseInfoFromGeneticAnalysis failed: Cannot find monogenicDisease item in person analysis. Disease name: " + diseaseName) +} + +//Outputs: +// -bool: Probabilities known +// -int: Percentage Probability of offspring having disease +// -string: Probability of offspring having disease formatted (with % suffix) +// -int: Percentage probability of offspring having a disease variant +// -string: Percentage probability of offspring having a disease variant formatted (with % suffix) +// -bool: Conflict exists between genome pairs +// -error +func GetOffspringMonogenicDiseaseInfoFromGeneticAnalysis(coupleAnalysisMapList []map[string]string, diseaseName string, genomePairIdentifier string, secondGenomePairExists bool)(bool, int, string, int, string, bool, error){ + + for _, element := range coupleAnalysisMapList{ + + itemType, exists := element["ItemType"] + if (exists == false) { + return false, 0, "", 0, "", false, errors.New("Malformed couple analysis: Item missing itemType") + } + + if (itemType != "MonogenicDisease"){ + continue + } + + currentDiseaseName, exists := element["DiseaseName"] + if (exists == false){ + return false, 0, "", 0, "", false, errors.New("Malformed couple analysis: Monogenic Disease item missing DiseaseName") + } + + if (diseaseName != currentDiseaseName){ + continue + } + + getConflictExistsBool := func()(bool, error){ + + if (secondGenomePairExists == false){ + return false, nil + } + + conflictExists, exists := element["ConflictExists"] + if (exists == false) { + return false, errors.New("Malformed couple analysis: Monogenic disease map missing ConflictExists") + } + + if (conflictExists == "Yes"){ + return true, nil + } + + return false, nil + } + + conflictExistsBool, err := getConflictExistsBool() + if (err != nil) { return false, 0, "", 0, "", false, err } + + probabilityOfOffspringHavingDisease, exists := element[genomePairIdentifier + "_ProbabilityOffspringHasDisease"] + if (exists == false) { + return false, 0, "", 0, "", false, errors.New("Malformed couple analysis: Monogenic Disease map missing _ProbabilityOffspringHasDisease") + } + + if (probabilityOfOffspringHavingDisease == "Unknown"){ + return false, 0, "", 0, "", conflictExistsBool, nil + } + + probabilityOfOffspringHavingDiseaseInt, err := helpers.ConvertStringToInt(probabilityOfOffspringHavingDisease) + if (err != nil){ + return false, 0, "", 0, "", false, errors.New("Malformed couple analysis: Monogenic Disease map contains invalid _ProbabilityOffspringHasDisease: " + probabilityOfOffspringHavingDisease) + } + + probabilityOfOffspringHavingDiseaseFormatted := probabilityOfOffspringHavingDisease + "%" + + probabilityOfOffspringHavingVariant, exists := element[genomePairIdentifier + "_ProbabilityOffspringHasVariant"] + if (exists == false) { + return false, 0, "", 0, "", false, errors.New("Malformed analysisMapList: Monogenic Disease map missing _ProbabilityOffspringHasVariant") + } + + probabilityOfOffspringHavingVariantInt, err := helpers.ConvertStringToInt(probabilityOfOffspringHavingVariant) + if (err != nil){ + return false, 0, "", 0, "", false, errors.New("Malformed analysisMapList: Monogenic Disease map missing _ProbabilityOffspringHasVariant") + } + + probabilityOfOffspringHavingVariantFormatted := probabilityOfOffspringHavingVariant + "%" + + return true, probabilityOfOffspringHavingDiseaseInt, probabilityOfOffspringHavingDiseaseFormatted, probabilityOfOffspringHavingVariantInt, probabilityOfOffspringHavingVariantFormatted, conflictExistsBool, nil + } + + return false, 0, "", 0, "", false, errors.New("GetOffspringMonogenicDiseaseInfoFromGeneticAnalysis called with invalid analysis: Cannot find disease info in couple analysis: " + diseaseName) +} + +//Outputs: +// -bool: Number of mutations known +// -int: Number of mutations +// -error +func GetPersonMonogenicDiseaseVariantInfoFromGeneticAnalysis(personAnalysisMapList []map[string]string, diseaseName string, variantIdentifier string, genomeIdentifier string)(bool, int, error){ + + for _, element := range personAnalysisMapList{ + + itemType, exists := element["ItemType"] + if (exists == false) { + return false, 0, errors.New("Malformed person analysis: Item missing itemType") + } + + if (itemType != "MonogenicDiseaseVariant"){ + continue + } + + currentDiseaseName, exists := element["DiseaseName"] + if (exists == false){ + return false, 0, errors.New("Malformed person analysis: Monogenic Disease item missing DiseaseName") + } + + if (currentDiseaseName != diseaseName){ + continue + } + + currentVariantIdentifier, exists := element["VariantIdentifier"] + if (exists == false) { + return false, 0, errors.New("Malformed person analysis: MonogenicDiseaseVariant item missing VariantIdentifier") + } + + if (currentVariantIdentifier != variantIdentifier){ + continue + } + + genomeHasVariantValue, exists := element[genomeIdentifier + "_HasVariant"] + if (exists == false){ + return false, 0, errors.New("Malformed person analysis: MonogenicDiseaseVariant item missing _HasVariant key for genome") + } + + if (genomeHasVariantValue == "Unknown"){ + return false, 0, nil + } + if (genomeHasVariantValue == "No;No"){ + return true, 0, nil + } + if (genomeHasVariantValue == "Yes;No" || genomeHasVariantValue == "No;Yes"){ + return true, 1, nil + } + if (genomeHasVariantValue == "Yes;Yes"){ + return true, 2, nil + } + + return false, 0, errors.New("Malformed person analysis: Invalid genomeHasVariantValue: " + genomeHasVariantValue) + } + + return false, 0, errors.New("GetPersonMonogenicDiseaseVariantInfoFromGeneticAnalysis failed: Info not found.") +} + + +//Outputs: +// -bool: Offspring Probabilities Known +// -int: Lower Bound Percentage Probability of 0 mutations +// -int: Upper Bound Percentage Probability of 0 mutations +// -string: Percentage probability of 0 mutations formatted (has % suffix and - between non-identical bounds) +// -int: Lower Bound Percentage probability of only 1 mutation +// -int: Upper Bound Percentage probability of only 1 mutation +// -string: Percentage probability of 1 mutation formatted (has % suffix and - between non-identical bounds) +// -int: Lower Bound Percentage probability of 2 mutations +// -int: Upper Bound Percentage probability of 2 mutations +// -string: Percentage probability of 2 mutations formatted (has % suffix and - between non-identical bounds) +// -error +func GetOffspringMonogenicDiseaseVariantInfoFromGeneticAnalysis(coupleAnalysisMapList []map[string]string, diseaseName string, variantIdentifier string, genomePairIdentifier string)(bool, int, int, string, int, int, string, int, int, string, error){ + + for _, element := range coupleAnalysisMapList{ + + itemType, exists := element["ItemType"] + if (exists == false) { + return false, 0, 0, "", 0, 0, "", 0, 0, "", errors.New("Malformed coupleAnalysisMapList: Item missing itemType") + } + + if (itemType != "MonogenicDiseaseVariant"){ + continue + } + currentDiseaseName, exists := element["DiseaseName"] + if (exists == false) { + return false, 0, 0, "", 0, 0, "", 0, 0, "", errors.New("Malformed couple Analysis: MonogenicDiseaseVariant item missing DiseaseName") + } + + if (currentDiseaseName != diseaseName){ + continue + } + + currentVariantIdentifier, exists := element["VariantIdentifier"] + if (exists == false) { + return false, 0, 0, "", 0, 0, "", 0, 0, "", errors.New("Malformed couple analysis: MonogenicDiseaseVariant item missing VariantIdentifier") + } + + if (currentVariantIdentifier != variantIdentifier){ + continue + } + + probabilityOf0MutationsLowerBound, exists := element[genomePairIdentifier + "_ProbabilityOf0MutationsLowerBound"] + if (exists == false){ + return false, 0, 0, "", 0, 0, "", 0, 0, "", errors.New("Malformed analysisMapList: MonogenicDiseaseVariant item missing _ProbabilityOf0MutationsLowerBound key") + } + if (probabilityOf0MutationsLowerBound == "Unknown"){ + return false, 0, 0, "", 0, 0, "", 0, 0, "", nil + } + + probabilityOf0MutationsUpperBound, exists := element[genomePairIdentifier + "_ProbabilityOf0MutationsUpperBound"] + if (exists == false){ + return false, 0, 0, "", 0, 0, "", 0, 0, "", errors.New("Malformed analysisMapList: MonogenicDiseaseVariant item missing _ProbabilityOf0MutationsUpperBound") + } + + probabilityOf1MutationLowerBound, exists := element[genomePairIdentifier + "_ProbabilityOf1MutationLowerBound"] + if (exists == false){ + return false, 0, 0, "", 0, 0, "", 0, 0, "", errors.New("Malformed analysisMapList: MonogenicDiseaseVariant item missing _ProbabilityOf1MutationLowerBound key") + } + + probabilityOf1MutationUpperBound, exists := element[genomePairIdentifier + "_ProbabilityOf1MutationUpperBound"] + if (exists == false){ + return false, 0, 0, "", 0, 0, "", 0, 0, "", errors.New("Malformed analysisMapList: MonogenicDiseaseVariant item missing _ProbabilityOf1MutationUpperBound") + } + + probabilityOf2MutationsLowerBound, exists := element[genomePairIdentifier + "_ProbabilityOf2MutationsLowerBound"] + if (exists == false){ + return false, 0, 0, "", 0, 0, "", 0, 0, "", errors.New("Malformed analysisMapList: MonogenicDiseaseVariant item missing _ProbabilityOf2MutationsLowerBound key") + } + + probabilityOf2MutationsUpperBound, exists := element[genomePairIdentifier + "_ProbabilityOf2MutationsUpperBound"] + if (exists == false){ + return false, 0, 0, "", 0, 0, "", 0, 0, "", errors.New("Malformed analysisMapList: MonogenicDiseaseVariant item missing _ProbabilityOf2MutationsUpperBound") + } + + probabilityOf0MutationsLowerBoundInt, err := helpers.ConvertStringToInt(probabilityOf0MutationsLowerBound) + if (err != nil){ + return false, 0, 0, "", 0, 0, "", 0, 0, "", errors.New("Malformed couple analysis: MonogenicDiseaseVariant contains invalid _ProbabilityOf0MutationsLowerBound: " + probabilityOf0MutationsLowerBound) + } + + probabilityOf0MutationsUpperBoundInt, err := helpers.ConvertStringToInt(probabilityOf0MutationsUpperBound) + if (err != nil){ + return false, 0, 0, "", 0, 0, "", 0, 0, "", errors.New("Malformed couple analysis: MonogenicDiseaseVariant contains invalid _ProbabilityOf0MutationsUpperBound: " + probabilityOf0MutationsUpperBound) + } + + probabilityOf1MutationLowerBoundInt, err := helpers.ConvertStringToInt(probabilityOf1MutationLowerBound) + if (err != nil){ + return false, 0, 0, "", 0, 0, "", 0, 0, "", errors.New("Malformed couple analysis: MonogenicDiseaseVariant contains invalid _ProbabilityOf1MutationLowerBound: " + probabilityOf1MutationLowerBound) + } + + probabilityOf1MutationUpperBoundInt, err := helpers.ConvertStringToInt(probabilityOf1MutationUpperBound) + if (err != nil){ + return false, 0, 0, "", 0, 0, "", 0, 0, "", errors.New("Malformed couple analysis: MonogenicDiseaseVariant contains invalid _ProbabilityOf1MutationUpperBound: " + probabilityOf1MutationUpperBound) + } + + probabilityOf2MutationsLowerBoundInt, err := helpers.ConvertStringToInt(probabilityOf2MutationsLowerBound) + if (err != nil){ + return false, 0, 0, "", 0, 0, "", 0, 0, "", errors.New("Malformed couple analysis: MonogenicDiseaseVariant contains invalid _ProbabilityOf2MutationsLowerBound: " + probabilityOf2MutationsLowerBound) + } + + probabilityOf2MutationsUpperBoundInt, err := helpers.ConvertStringToInt(probabilityOf2MutationsUpperBound) + if (err != nil){ + return false, 0, 0, "", 0, 0, "", 0, 0, "", errors.New("Malformed couple analysis: MonogenicDiseaseVariant contains invalid _ProbabilityOf2MutationsUpperBound: " + probabilityOf2MutationsUpperBound) + } + + getOffspringProbabilityOf0MutationsFormatted := func()string{ + + probabilityOf0MutationsLowerBoundFormatted := probabilityOf0MutationsLowerBound + "%" + + if (probabilityOf0MutationsLowerBoundInt == probabilityOf0MutationsUpperBoundInt){ + return probabilityOf0MutationsLowerBoundFormatted + } + + probabilityOf0MutationsUpperBoundFormatted := probabilityOf0MutationsUpperBound + "%" + + formattedResult := probabilityOf0MutationsLowerBoundFormatted + " - " + probabilityOf0MutationsUpperBoundFormatted + + return formattedResult + } + + probabilityOf0MutationsFormatted := getOffspringProbabilityOf0MutationsFormatted() + + getOffspringProbabilityOf1MutationFormatted := func()string{ + + probabilityOf1MutationLowerBoundFormatted := probabilityOf1MutationLowerBound + "%" + + if (probabilityOf1MutationLowerBoundInt == probabilityOf1MutationUpperBoundInt){ + return probabilityOf1MutationLowerBoundFormatted + } + + probabilityOf1MutationUpperBoundFormatted := probabilityOf1MutationUpperBound + "%" + + formattedResult := probabilityOf1MutationLowerBoundFormatted + " - " + probabilityOf1MutationUpperBoundFormatted + + return formattedResult + } + + probabilityOf1MutationFormatted := getOffspringProbabilityOf1MutationFormatted() + + getOffspringProbabilityOf2MutationsFormatted := func()string{ + + probabilityOf2MutationsLowerBoundFormatted := probabilityOf2MutationsLowerBound + "%" + + if (probabilityOf2MutationsLowerBoundInt == probabilityOf2MutationsUpperBoundInt){ + return probabilityOf2MutationsLowerBoundFormatted + } + + probabilityOf2MutationsUpperBoundFormatted := probabilityOf2MutationsUpperBound + "%" + + formattedResult := probabilityOf2MutationsLowerBoundFormatted + " - " + probabilityOf2MutationsUpperBoundFormatted + + return formattedResult + } + + probabilityOf2MutationsFormatted := getOffspringProbabilityOf2MutationsFormatted() + + return true, probabilityOf0MutationsLowerBoundInt, probabilityOf0MutationsUpperBoundInt, probabilityOf0MutationsFormatted, probabilityOf1MutationLowerBoundInt, probabilityOf1MutationUpperBoundInt, probabilityOf1MutationFormatted, probabilityOf2MutationsLowerBoundInt, probabilityOf2MutationsUpperBoundInt, probabilityOf2MutationsFormatted, nil + } + + return false, 0, 0, "", 0, 0, "", 0, 0, "", errors.New("GetOffspringMonogenicDiseaseVariantInfoFromGeneticAnalysis failed: Info not found.") +} + + +//Outputs: +// -bool: Polygenic Disease Risk Score known +// -int: Disease risk score +// -string: Disease risk score formatted (contains "/10" suffix) +// -int: Number of loci tested +// -bool: Conflict exists +// -error +func GetPersonPolygenicDiseaseInfoFromGeneticAnalysis(personAnalysisMapList []map[string]string, diseaseName string, genomeIdentifier string, personHasMultipleGenomes bool)(bool, int, string, int, bool, error){ + + for _, element := range personAnalysisMapList{ + + analysisItemType, exists := element["ItemType"] + if (exists == false) { + return false, 0, "", 0, false, errors.New("Malformed person analysis: Item missing ItemType") + } + + if (analysisItemType != "PolygenicDisease"){ + continue + } + + itemDiseaseName, exists := element["DiseaseName"] + if (exists == false) { + return false, 0, "", 0, false, errors.New("Malformed person analysis: PolygenicDisease item missing DiseaseName") + } + + if (itemDiseaseName != diseaseName){ + continue + } + + numberOfLociTested, exists := element[genomeIdentifier + "_NumberOfLociTested"] + if (exists == false){ + return false, 0, "", 0, false, errors.New("Malformed person analysis: PolygenicDisease item missing _NumberOfLociTested") + } + + numberOfLociTestedInt, err := helpers.ConvertStringToInt(numberOfLociTested) + if (err != nil){ + return false, 0, "", 0, false, errors.New("Malformed person analysis: PolygenicDisease item contains invalid _NumberOfLociTested: " + numberOfLociTested) + } + + getConflictExistsBool := func()(bool, error){ + + if (personHasMultipleGenomes == false){ + return false, nil + } + + conflictExists, exists := element["ConflictExists"] + if (exists == false) { + return false, errors.New("Malformed analysisMapList: PolygenicDisease analysis missing ConflictExists") + } + + if (conflictExists == "Yes"){ + return true, nil + } + + return false, nil + } + + conflictExistsBool, err := getConflictExistsBool() + if (err != nil) { return false, 0, "", 0, false, err } + + if (numberOfLociTestedInt == 0){ + return false, 0, "", numberOfLociTestedInt, conflictExistsBool, nil + } + + personRiskScore, exists := element[genomeIdentifier + "_RiskScore"] + if (exists == false){ + return false, 0, "", 0, false, errors.New("Person analysis missing _RiskScore key.") + } + + personRiskScoreInt, err := helpers.ConvertStringToInt(personRiskScore) + if (err != nil) { + return false, 0, "", 0, false, errors.New("Person analysis contains invalid _RiskScore: " + personRiskScore) + } + + personRiskScoreFormatted := personRiskScore + "/10" + + return true, personRiskScoreInt, personRiskScoreFormatted, numberOfLociTestedInt, conflictExistsBool, nil + } + + return false, 0, "", 0, false, errors.New("GetPersonPolygenicDiseaseInfoFromGeneticAnalysis failed: Disease info not found.") +} + + +//Outputs: +// -bool: Disease Risk Score known +// -int: Disease risk score +// -string: Disease risk score formatted (contains "/10" suffix) +// -int: Number of loci tested +// -bool: Conflict exists +// -error +func GetOffspringPolygenicDiseaseInfoFromGeneticAnalysis(coupleAnalysisMapList []map[string]string, diseaseName string, genomePairIdentifier string, secondGenomePairExists bool)(bool, int, string, int, bool, error){ + + for _, element := range coupleAnalysisMapList{ + + analysisItemType, exists := element["ItemType"] + if (exists == false) { + return false, 0, "", 0, false, errors.New("Malformed couple analysis: Item missing ItemType") + } + + if (analysisItemType != "PolygenicDisease"){ + continue + } + + itemDiseaseName, exists := element["DiseaseName"] + if (exists == false) { + return false, 0, "", 0, false, errors.New("Malformed couple analysis: PolygenicDisease item missing DiseaseName") + } + + if (itemDiseaseName != diseaseName){ + continue + } + + numberOfLociTested, exists := element[genomePairIdentifier + "_NumberOfLociTested"] + if (exists == false){ + return false, 0, "", 0, false, errors.New("Malformed couple analysis: PolygenicDisease item missing _NumberOfLociTested") + } + + numberOfLociTestedInt, err := helpers.ConvertStringToInt(numberOfLociTested) + if (err != nil){ + return false, 0, "", 0, false, errors.New("Malformed couple analysis: PolygenicDisease item contains invalid _NumberOfLociTested: " + numberOfLociTested) + } + + getConflictExistsBool := func()(bool, error){ + + if (secondGenomePairExists == false){ + return false, nil + } + + conflictExists, exists := element["ConflictExists"] + if (exists == false) { + return false, errors.New("Malformed analysisMapList: PolygenicDisease analysis missing ConflictExists") + } + + if (conflictExists == "Yes"){ + return true, nil + } + + return false, nil + } + + conflictExistsBool, err := getConflictExistsBool() + if (err != nil) { return false, 0, "", 0, false, err } + + offspringRiskScore, exists := element[genomePairIdentifier + "_OffspringRiskScore"] + if (exists == false){ + return false, 0, "", 0, false, errors.New("Couple analysis missing _RiskScore key.") + } + + if (offspringRiskScore == "Unknown"){ + return false, 0, "", numberOfLociTestedInt, conflictExistsBool, nil + } + + offspringRiskScoreInt, err := helpers.ConvertStringToInt(offspringRiskScore) + if (err != nil) { + return false, 0, "", 0, false, errors.New("Couple analysis contains invalid _RiskScore: " + offspringRiskScore) + } + + offspringRiskScoreFormatted := offspringRiskScore + "/10" + + return true, offspringRiskScoreInt, offspringRiskScoreFormatted, numberOfLociTestedInt, conflictExistsBool, nil + } + + return false, 0, "", 0, false, errors.New("GetOffspringPolygenicDiseaseInfoFromGeneticAnalysis failed: Cannot find disease info for disease: " + diseaseName) +} + +//Outputs: +// -bool: Risk Weight and base pair known +// -int: Locus risk weight +// -string: Locus base pair +// -bool: Locus odds ratio known +// -float64: Locus odds ratio +// -string: Locus odds ratio formatted (with x suffix) +// -error +func GetPersonPolygenicDiseaseLocusInfoFromGeneticAnalysis(personAnalyisMapList []map[string]string, diseaseName string, locusIdentifier string, genomeIdentifier string)(bool, int, string, bool, float64, string, error){ + + for _, element := range personAnalyisMapList{ + + itemType, exists := element["ItemType"] + if (exists == false) { + return false, 0, "", false, 0, "", errors.New("Malformed person analysis: Item missing itemType") + } + + if (itemType != "PolygenicDiseaseLocus"){ + continue + } + + currentDiseaseName, exists := element["DiseaseName"] + if (exists == false){ + return false, 0, "", false, 0, "", errors.New("Malformed person analysis: PolygenicDiseaseLocus item missing DiseaseName") + } + + if (currentDiseaseName != diseaseName){ + continue + } + + currentLocusIdentifier, exists := element["LocusIdentifier"] + if (exists == false) { + return false, 0, "", false, 0, "", errors.New("Malformed person analysis: PolygenicDiseaseLocus item missing LocusIdentifier") + } + + if (currentLocusIdentifier != locusIdentifier){ + continue + } + + genomeLocusRiskWeight, exists := element[genomeIdentifier + "_RiskWeight"] + if (exists == false){ + return false, 0, "", false, 0, "", errors.New("Malformed person analysis: PolygenicDiseaseLocus item missing _RiskWeight") + } + + if (genomeLocusRiskWeight == "Unknown"){ + return false, 0, "", false, 0, "", nil + } + + locusBasePair, exists := element[genomeIdentifier + "_LocusBasePair"] + if (exists == false){ + return false, 0, "", false, 0, "", errors.New("Malformed person analysis: PolygenicDiseaseLocus item missing LocusBasePair") + } + + genomeLocusRiskWeightInt, err := helpers.ConvertStringToInt(genomeLocusRiskWeight) + if (err != nil){ + return false, 0, "", false, 0, "", errors.New("Malformed person analysis: PolygenicDiseaseLocus item contains invalid _RiskWeight: " + genomeLocusRiskWeight) + } + + genomeLocusOddsRatio, exists := element[genomeIdentifier + "_OddsRatio"] + if (exists == false){ + return false, 0, "", false, 0, "", errors.New("Malformed person analysis: PolygenicDiseaseLocus item missing _OddsRatio") + } + + if (genomeLocusOddsRatio == "Unknown"){ + return true, genomeLocusRiskWeightInt, locusBasePair, false, 0, "", nil + } + + locusOddsRatioFloat64, err := helpers.ConvertStringToFloat64(genomeLocusOddsRatio) + if (err != nil){ + return false, 0, "", false, 0, "", errors.New("Malformed person analysis: PolygenicDiseaseLocus item contains invalid _OddsRatio: " + genomeLocusOddsRatio) + } + + genomeLocusOddsRatioString := helpers.ConvertFloat64ToStringRounded(locusOddsRatioFloat64, 2) + + locusOddsRatioFormatted := genomeLocusOddsRatioString + "x" + + return true, genomeLocusRiskWeightInt, locusBasePair, true, locusOddsRatioFloat64, locusOddsRatioFormatted, nil + } + + return false, 0, "", false, 0, "", errors.New("GetPersonPolygenicDiseaseLocusInfoFromGeneticAnalysis failed: Locus info not found.") +} + +//Outputs: +// -bool: Offspring risk weight known +// -int: Offspring risk weight +// -bool: Offspring odds ratio known +// -float64: Offspring odds ratio +// -string: Offspring odds ratio formatted (with + and < from unknownFactors weight sum and x suffix) +// -error +func GetOffspringPolygenicDiseaseLocusInfoFromGeneticAnalysis(coupleAnalysisMapList []map[string]string, diseaseName string, locusIdentifier string, genomePairIdentifier string)(bool, int, bool, float64, string, error){ + + for _, element := range coupleAnalysisMapList{ + + itemType, exists := element["ItemType"] + if (exists == false) { + return false, 0, false, 0, "", errors.New("Malformed couple analysis: Item missing itemType") + } + + if (itemType != "PolygenicDiseaseLocus"){ + continue + } + currentDiseaseName, exists := element["DiseaseName"] + if (exists == false) { + return false, 0, false, 0, "", errors.New("Malformed couple analysis: PolygenicDiseaseLocus item missing DiseaseName") + } + + if (currentDiseaseName != diseaseName){ + continue + } + + currentLocusIdentifier, exists := element["LocusIdentifier"] + if (exists == false) { + return false, 0, false, 0, "", errors.New("Malformed couple analysis: PolygenicDiseaseLocus item missing LocusIdentifier") + } + + if (currentLocusIdentifier != locusIdentifier){ + continue + } + + offspringRiskWeight, exists := element[genomePairIdentifier + "_OffspringRiskWeight"] + if (exists == false){ + return false, 0, false, 0, "", errors.New("Malformed couple analysis: PolygenicDiseaseLocus item missing _OffspringRiskWeight key") + } + + if (offspringRiskWeight == "Unknown"){ + return false, 0, false, 0, "", nil + } + + offspringRiskWeightInt, err := helpers.ConvertStringToInt(offspringRiskWeight) + if (err != nil){ + return false, 0, false, 0, "", errors.New("Malformed couple analysis: PolygenicDiseaseLocus item contains invalid _OffspringRiskWeight key: " + offspringRiskWeight) + } + + offspringOddsRatio, exists := element[genomePairIdentifier + "_OffspringOddsRatio"] + if (exists == false){ + return false, 0, false, 0, "", errors.New("Malformed couple analysis: PolygenicDiseaseLocus item missing _OffspringOddsRatio key") + } + if (offspringOddsRatio == "Unknown"){ + return true, offspringRiskWeightInt, false, 0, "", nil + } + + offspringOddsRatioFloat64, err := helpers.ConvertStringToFloat64(offspringOddsRatio) + if (err != nil){ + return false, 0, false, 0, "", errors.New("Malformed couple analysis: Contains invalid _OffspringOddsRatio: " + offspringOddsRatio) + } + + offspringUnknownOddsRatiosWeightSum, exists := element[genomePairIdentifier + "_OffspringUnknownOddsRatiosWeightSum"] + if (exists == false){ + return false, 0, false, 0, "", errors.New("Malformed couple analysis: PolygenicDiseaseLocus item missing _OffspringUnknownOddsRatiosWeightSum key") + } + + offspringUnknownOddsRatiosWeightSumInt, err := helpers.ConvertStringToInt(offspringUnknownOddsRatiosWeightSum) + if (err != nil){ + return false, 0, false, 0, "", errors.New("Malformed couple analysis: PolygenicDiseaseLocus item contains invalid _OffspringUnknownOddsRatiosWeightSum key: " + offspringUnknownOddsRatiosWeightSum) + } + + getOddsRatioFormatted := func()string{ + + if (offspringUnknownOddsRatiosWeightSumInt == 0){ + result := offspringOddsRatio + "x" + return result + } + if (offspringUnknownOddsRatiosWeightSumInt < 0){ + result := "<" + offspringOddsRatio + "x" + return result + } + // offspringUnknownOddsRatiosWeightSumInt > 0 + result := offspringOddsRatio + "x+" + return result + } + + oddsRatioFormatted := getOddsRatioFormatted() + + return true, offspringRiskWeightInt, true, offspringOddsRatioFloat64, oddsRatioFormatted, nil + } + + return false, 0, false, 0, "", errors.New("Malformed couple analysis: Polygenic Disease locus not found.") +} + +type LocusValue struct{ + + Base1 string + Base2 string + + // Are the bases ordered or in random order? + LocusIsPhased bool +} + +//Outputs: +// -map[int64]LocusValue (rsID -> Base pair) (missing rsIDs represent unknown values) +// -bool: Any Trait Rule known/tested +// -map[string]int: Trait outcomes scores map (outcome -> Number of points) +// -int: Number of rules tested +// -bool: Conflict exists +// -error +func GetPersonTraitInfoFromGeneticAnalysis(personAnalysisMapList []map[string]string, traitName string, genomeIdentifier string, personHasMultipleGenomes bool)(map[int64]LocusValue, bool, map[string]int, int, bool, error){ + + for _, element := range personAnalysisMapList{ + + analysisItemType, exists := element["ItemType"] + if (exists == false) { + return nil, false, nil, 0, false, errors.New("Malformed person analysis: Item missing ItemType") + } + + if (analysisItemType != "Trait"){ + continue + } + + itemTraitName, exists := element["TraitName"] + if (exists == false) { + return nil, false, nil, 0, false, errors.New("Malformed person analysis: Trait item missing TraitName") + } + + if (itemTraitName != traitName){ + continue + } + + traitObject, err := traits.GetTraitObject(traitName) + if (err != nil) { return nil, false, nil, 0, false, err } + + traitLociList := traitObject.LociList + + // Map Structure: rsID -> Locus Value + locusValuesMap := make(map[int64]LocusValue) + + for _, rsID := range traitLociList{ + + rsidString := helpers.ConvertInt64ToString(rsID) + + locusValue, exists := element[genomeIdentifier + "_LocusValue_rs" + rsidString] + if (exists == false){ + return nil, false, nil, 0, false, errors.New("Malformed person analysis: missing trait locus value for locus: " + rsidString) + } + + if (locusValue == "Unknown"){ + // No value exists for this locus + continue + } + + locusIsPhasedString, exists := element[genomeIdentifier + "_LocusIsPhased_rs" + rsidString] + if (exists == false){ + return nil, false, nil, 0, false, errors.New("Malformed person analysis: Contains trait locus value which is missing locusIsPhased information.") + } + + locusIsPhased, err := helpers.ConvertYesOrNoStringToBool(locusIsPhasedString) + if (err != nil){ + return nil, false, nil, 0, false, errors.New("Malformed person analysis: Contains invalid trait locusIsPhased rsID value: " + locusIsPhasedString) + } + + locusBase1, locusBase2, semicolonExists := strings.Cut(locusValue, ";") + if (semicolonExists == false){ + return nil, false, nil, 0, false, errors.New("Malformed person analysis: Contains invalid trait locus value: " + locusValue) + } + + locusValueObject := LocusValue{ + Base1: locusBase1, + Base2: locusBase2, + LocusIsPhased: locusIsPhased, + } + + locusValuesMap[rsID] = locusValueObject + } + + numberOfRulesTestedString, exists := element[genomeIdentifier + "_NumberOfRulesTested"] + if (exists == false){ + return nil, false, nil, 0, false, errors.New("Malformed person analysis: Trait item missing _NumberOfRulesTested") + } + + numberOfRulesTested, err := helpers.ConvertStringToInt(numberOfRulesTestedString) + if (err != nil){ + return nil, false, nil, 0, false, errors.New("Malformed person analysis: Trait item contains invalid _NumberOfRulesTested: " + numberOfRulesTestedString) + } + + getConflictExistsBool := func()(bool, error){ + + if (personHasMultipleGenomes == false){ + return false, nil + } + + conflictExists, exists := element["ConflictExists"] + if (exists == false) { + return false, errors.New("Malformed analysisMapList: Trait analysis missing ConflictExists") + } + + if (conflictExists == "Yes"){ + return true, nil + } + + return false, nil + } + + conflictExistsBool, err := getConflictExistsBool() + if (err != nil) { return nil, false, nil, 0, false, err } + + if (numberOfRulesTested == 0){ + return locusValuesMap, false, nil, 0, conflictExistsBool, nil + } + + traitOutcomesList := traitObject.OutcomesList + + traitOutcomeScoresMap := make(map[string]int) + + for _, traitOutcome := range traitOutcomesList{ + + outcomeScoreString, exists := element[genomeIdentifier + "_OutcomeScore_" + traitOutcome] + if (exists == false){ + return nil, false, nil, 0, false, errors.New("Person analysis missing _OutcomePoints_" + traitOutcome + " key.") + } + + outcomeScoreInt, err := helpers.ConvertStringToInt(outcomeScoreString) + if (err != nil){ + return nil, false, nil, 0, false, errors.New("Person analysis contains invalid _OutcomeScore_ value: " + outcomeScoreString) + } + + traitOutcomeScoresMap[traitOutcome] = outcomeScoreInt + } + + return locusValuesMap, true, traitOutcomeScoresMap, numberOfRulesTested, conflictExistsBool, nil + } + + return nil, false, nil, 0, false, errors.New("GetPersonTraitInfoFromGeneticAnalysis failed: Cannot find trait info for trait: " + traitName) +} + + + +//Outputs: +// -bool: Trait Outcome Scores known +// -map[string]float64: Trait average outcome scores map (OutcomeName -> AverageScore) +// -int: Number of rules tested +// -bool: Conflict exists +// -error +func GetOffspringTraitInfoFromGeneticAnalysis(coupleAnalysisMapList []map[string]string, traitName string, genomePairIdentifier string, secondGenomePairExists bool)(bool, map[string]float64, int, bool, error){ + + for _, element := range coupleAnalysisMapList{ + + analysisItemType, exists := element["ItemType"] + if (exists == false) { + return false, nil, 0, false, errors.New("Malformed couple analysis: Item missing ItemType") + } + + if (analysisItemType != "Trait"){ + continue + } + + itemTraitName, exists := element["TraitName"] + if (exists == false) { + return false, nil, 0, false, errors.New("Malformed couple analysis: Trait item missing TraitName") + } + + if (itemTraitName != traitName){ + continue + } + + numberOfRulesTestedString, exists := element[genomePairIdentifier + "_NumberOfRulesTested"] + if (exists == false){ + return false, nil, 0, false, errors.New("Malformed couple analysis: Trait item missing _NumberOfRulesTested") + } + + numberOfRulesTested, err := helpers.ConvertStringToInt(numberOfRulesTestedString) + if (err != nil){ + return false, nil, 0, false, errors.New("Malformed couple analysis: Trait item contains invalid _NumberOfRulesTested: " + numberOfRulesTestedString) + } + + getConflictExistsBool := func()(bool, error){ + + if (secondGenomePairExists == false){ + return false, nil + } + + conflictExists, exists := element["ConflictExists"] + if (exists == false) { + return false, errors.New("Malformed analysisMapList: Trait analysis missing ConflictExists") + } + + if (conflictExists == "Yes"){ + return true, nil + } + + return false, nil + } + + conflictExistsBool, err := getConflictExistsBool() + if (err != nil) { return false, nil, 0, false, err } + + if (numberOfRulesTested == 0){ + return false, nil, 0, conflictExistsBool, nil + } + + traitObject, err := traits.GetTraitObject(traitName) + if (err != nil) { return false, nil, 0, false, err } + + traitOutcomesList := traitObject.OutcomesList + + traitOutcomeScoresMap := make(map[string]float64) + + for _, traitOutcome := range traitOutcomesList{ + + averageOutcomeScoreString, exists := element[genomePairIdentifier + "_OffspringAverageOutcomeScore_" + traitOutcome] + if (exists == false){ + return false, nil, 0, false, errors.New("Couple analysis missing _OutcomePoints_" + traitOutcome + " key.") + } + + averageOutcomeScoreFloat64, err := helpers.ConvertStringToFloat64(averageOutcomeScoreString) + if (err != nil){ + return false, nil, 0, false, errors.New("Couple analysis contains invalid _OutcomeScore_ value: " + averageOutcomeScoreString) + } + + traitOutcomeScoresMap[traitOutcome] = averageOutcomeScoreFloat64 + } + + return true, traitOutcomeScoresMap, numberOfRulesTested, conflictExistsBool, nil + } + + return false, nil, 0, false, errors.New("GetOffspringTraitInfoFromGeneticAnalysis failed: Cannot find trait info for trait: " + traitName) +} + +//Outputs: +// -bool: Rule status known (we know if the rule is passed or not) +// -bool: Genome passes rule +// -map[string]string: Rule locus identifier -> genome base pair +// -error +func GetPersonTraitRuleInfoFromGeneticAnalysis(personAnalyisMapList []map[string]string, traitName string, ruleIdentifier string, genomeIdentifier string)(bool, bool, map[string]string, error){ + + for _, element := range personAnalyisMapList{ + + itemType, exists := element["ItemType"] + if (exists == false) { + return false, false, nil, errors.New("Malformed person analysis: Item missing itemType") + } + + if (itemType != "TraitRule"){ + continue + } + + currentTraitName, exists := element["TraitName"] + if (exists == false){ + return false, false, nil, errors.New("Malformed person analysis: Trait item missing TraitName") + } + + if (currentTraitName != traitName){ + continue + } + + currentRuleIdentifier, exists := element["RuleIdentifier"] + if (exists == false) { + return false, false, nil, errors.New("Malformed person analysis: TraitRule item missing RuleIdentifier") + } + + if (currentRuleIdentifier != ruleIdentifier){ + continue + } + + genomePassesRuleString, exists := element[genomeIdentifier + "_PassesRule"] + if (exists == false){ + return false, false, nil, errors.New("Malformed person analysis: TraitRule item missing _PassesRule") + } + + // Map Structure: Locus Identifier -> Locus base pair + ruleLocusBasePairsMap := make(map[string]string) + + locusKeysPrefix := genomeIdentifier + "_RuleLocusBasePair_" + + for key, value := range element{ + + isLocusValueEntry := strings.HasPrefix(key, locusKeysPrefix) + if (isLocusValueEntry == false){ + continue + } + + locusIdentifier := strings.TrimPrefix(key, locusKeysPrefix) + + if (value == "Unknown"){ + continue + } + + ruleLocusBasePairsMap[locusIdentifier] = value + } + + if (genomePassesRuleString == "Unknown"){ + return false, false, ruleLocusBasePairsMap, nil + } + + genomePassesRuleBool, err := helpers.ConvertYesOrNoStringToBool(genomePassesRuleString) + if (err != nil){ + return false, false, nil, errors.New("Malformed person analysis: TraitRule item contains invalid _PassesRule: " + genomePassesRuleString) + } + + return true, genomePassesRuleBool, ruleLocusBasePairsMap, nil + } + + return false, false, nil, errors.New("GetPersonTraitRuleInfoFromGeneticAnalysis failed: Trait rule info not found.") +} + + +//Outputs: +// -bool: Offspring trait rule probability known +// -int: Offspring probability of passing rule (0 - 100) +// -string: Offspring probability of passing rule formatted (with % suffix) +// -error +func GetOffspringTraitRuleInfoFromGeneticAnalysis(coupleAnalysisMapList []map[string]string, traitName string, ruleIdentifier string, genomePairIdentifier string)(bool, int, string, error){ + + for _, element := range coupleAnalysisMapList{ + + itemType, exists := element["ItemType"] + if (exists == false) { + return false, 0, "", errors.New("Malformed couple analysis: Item missing itemType") + } + + if (itemType != "TraitRule"){ + continue + } + currentTraitName, exists := element["TraitName"] + if (exists == false) { + return false, 0, "", errors.New("Malformed couple analysis: TraitRule item missing TraitName") + } + + if (currentTraitName != traitName){ + continue + } + + currentRuleIdentifier, exists := element["RuleIdentifier"] + if (exists == false) { + return false, 0, "", errors.New("Malformed couple analysis: TraitRule item missing RuleIdentifier") + } + + if (currentRuleIdentifier != ruleIdentifier){ + continue + } + + offspringProbabilityOfPassingRule, exists := element[genomePairIdentifier + "_OffspringProbabilityOfPassingRule"] + if (exists == false){ + return false, 0, "", errors.New("Malformed couple analysis: TraitRule item missing _OffspringProbabilityOfPassingRule key") + } + + if (offspringProbabilityOfPassingRule == "Unknown"){ + return false, 0, "", nil + } + + offspringProbabilityOfPassingRuleInt, err := helpers.ConvertStringToInt(offspringProbabilityOfPassingRule) + if (err != nil) { + return false, 0, "", errors.New("Malformed couple analysis: TraitRule item contains invalid _OffspringProbabilityOfPassingRule: " + offspringProbabilityOfPassingRule) + } + + offspringProbabilityOfPassingRuleFormatted := offspringProbabilityOfPassingRule + "%" + + return true, offspringProbabilityOfPassingRuleInt, offspringProbabilityOfPassingRuleFormatted, nil + } + + return false, 0, "", errors.New("GetOffspringTraitRuleInfoFromGeneticAnalysis failed: Trait rule info not found.") +} + + +// We use this function to verify a person genetic analysis is well formed +func ReadPersonGeneticAnalysisForTests(personAnalysisMapList []map[string]string)error{ + + allRawGenomeIdentifiersList, personHasMultipleGenomes, onlyExcludeConflictsGenomeIdentifier, onlyIncludeSharedGenomeIdentifier, err := GetMetadataFromPersonGeneticAnalysis(personAnalysisMapList) + if (err != nil) { return err } + + allGenomeIdentifiersList := allRawGenomeIdentifiersList + if (personHasMultipleGenomes == true){ + allGenomeIdentifiersList = append(allGenomeIdentifiersList, onlyExcludeConflictsGenomeIdentifier, onlyIncludeSharedGenomeIdentifier) + } + + monogenicDiseaseObjectsList, err := monogenicDiseases.GetMonogenicDiseaseObjectsList() + if (err != nil) { return err } + + for _, monogenicDiseaseObject := range monogenicDiseaseObjectsList{ + + diseaseName := monogenicDiseaseObject.DiseaseName + + for _, genomeIdentifier := range allGenomeIdentifiersList{ + + _, _, _, _, _, _, _, err := GetPersonMonogenicDiseaseInfoFromGeneticAnalysis(personAnalysisMapList, diseaseName, genomeIdentifier, personHasMultipleGenomes) + if (err != nil) { return err } + } + + diseaseVariantObjectsList := monogenicDiseaseObject.VariantsList + + for _, diseaseVariantObject := range diseaseVariantObjectsList{ + + variantIdentifier := diseaseVariantObject.VariantIdentifier + + for _, genomeIdentifier := range allGenomeIdentifiersList{ + + _, _, err := GetPersonMonogenicDiseaseVariantInfoFromGeneticAnalysis(personAnalysisMapList, diseaseName, variantIdentifier, genomeIdentifier) + if (err != nil) { return err } + } + } + } + + polygenicDiseaseObjectsList, err := polygenicDiseases.GetPolygenicDiseaseObjectsList() + if (err != nil) { return err } + + for _, diseaseObject := range polygenicDiseaseObjectsList{ + + diseaseName := diseaseObject.DiseaseName + + for _, genomeIdentifier := range allGenomeIdentifiersList{ + + _, _, _, _, _, err := GetPersonPolygenicDiseaseInfoFromGeneticAnalysis(personAnalysisMapList, diseaseName, genomeIdentifier, personHasMultipleGenomes) + if (err != nil) { return err } + } + + diseaseLocusObjectsList := diseaseObject.LociList + + for _, diseaseLocusObject := range diseaseLocusObjectsList{ + + locusIdentifier := diseaseLocusObject.LocusIdentifier + + for _, genomeIdentifier := range allGenomeIdentifiersList{ + + _, _, _, _, _, _, err := GetPersonPolygenicDiseaseLocusInfoFromGeneticAnalysis(personAnalysisMapList, diseaseName, locusIdentifier, genomeIdentifier) + if (err != nil) { return err } + } + } + } + + traitObjectsList, err := traits.GetTraitObjectsList() + if (err != nil) { return err } + + for _, traitObject := range traitObjectsList{ + + traitName := traitObject.TraitName + + for _, genomeIdentifier := range allGenomeIdentifiersList{ + + _, _, _, _, _, err := GetPersonTraitInfoFromGeneticAnalysis(personAnalysisMapList, traitName, genomeIdentifier, personHasMultipleGenomes) + if (err != nil) { return err } + } + + traitRulesList := traitObject.RulesList + + for _, traitRuleObject := range traitRulesList{ + + ruleIdentifier := traitRuleObject.RuleIdentifier + + for _, genomeIdentifier := range allGenomeIdentifiersList{ + + _, _, _, err := GetPersonTraitRuleInfoFromGeneticAnalysis(personAnalysisMapList, traitName, ruleIdentifier, genomeIdentifier) + if (err != nil) { return err } + } + } + } + + return nil +} + + +// We use this function to verify a person genetic analysis is well formed +func ReadCoupleGeneticAnalysisForTests(coupleAnalysisMapList []map[string]string)error{ + + pair1PersonAGenomeIdentifier, pair1PersonBGenomeIdentifier, secondGenomePairExists, pair2PersonAGenomeIdentifier, pair2PersonBGenomeIdentifier, _, _, _, _, _, _, err := GetMetadataFromCoupleGeneticAnalysis(coupleAnalysisMapList) + if (err != nil) { return err } + + pair1GenomeIdentifier := pair1PersonAGenomeIdentifier + "+" + pair1PersonBGenomeIdentifier + + allGenomePairIdentifiersList := []string{pair1GenomeIdentifier} + + if (secondGenomePairExists == true){ + + pair2GenomeIdentifier := pair2PersonAGenomeIdentifier + "+" + pair2PersonBGenomeIdentifier + + allGenomePairIdentifiersList = append(allGenomePairIdentifiersList, pair2GenomeIdentifier) + } + + monogenicDiseaseObjectsList, err := monogenicDiseases.GetMonogenicDiseaseObjectsList() + if (err != nil) { return err } + + for _, monogenicDiseaseObject := range monogenicDiseaseObjectsList{ + + diseaseName := monogenicDiseaseObject.DiseaseName + + for _, genomePairIdentifier := range allGenomePairIdentifiersList{ + + _, _, _, _, _, _, err = GetOffspringMonogenicDiseaseInfoFromGeneticAnalysis(coupleAnalysisMapList, diseaseName, genomePairIdentifier, secondGenomePairExists) + if (err != nil) { return err } + } + + diseaseVariantObjectsList := monogenicDiseaseObject.VariantsList + + for _, diseaseVariantObject := range diseaseVariantObjectsList{ + + variantIdentifier := diseaseVariantObject.VariantIdentifier + + for _, genomePairIdentifier := range allGenomePairIdentifiersList{ + + _, _, _, _, _, _, _, _, _, _, err := GetOffspringMonogenicDiseaseVariantInfoFromGeneticAnalysis(coupleAnalysisMapList, diseaseName, variantIdentifier, genomePairIdentifier) + if (err != nil) { return err } + } + } + } + + polygenicDiseaseObjectsList, err := polygenicDiseases.GetPolygenicDiseaseObjectsList() + if (err != nil) { return err } + + for _, diseaseObject := range polygenicDiseaseObjectsList{ + + diseaseName := diseaseObject.DiseaseName + + for _, genomePairIdentifier := range allGenomePairIdentifiersList{ + + _, _, _, _, _, err := GetOffspringPolygenicDiseaseInfoFromGeneticAnalysis(coupleAnalysisMapList, diseaseName, genomePairIdentifier, secondGenomePairExists) + if (err != nil) { return err } + } + + diseaseLocusObjectsList := diseaseObject.LociList + + for _, diseaseLocusObject := range diseaseLocusObjectsList{ + + locusIdentifier := diseaseLocusObject.LocusIdentifier + + for _, genomePairIdentifier := range allGenomePairIdentifiersList{ + + _, _, _, _, _, err := GetOffspringPolygenicDiseaseLocusInfoFromGeneticAnalysis(coupleAnalysisMapList, diseaseName, locusIdentifier, genomePairIdentifier) + if (err != nil) { return err } + } + } + } + + traitObjectsList, err := traits.GetTraitObjectsList() + if (err != nil) { return err } + + for _, traitObject := range traitObjectsList{ + + traitName := traitObject.TraitName + + for _, genomePairIdentifier := range allGenomePairIdentifiersList{ + + _, _, _, _, err := GetOffspringTraitInfoFromGeneticAnalysis(coupleAnalysisMapList, traitName, genomePairIdentifier, secondGenomePairExists) + if (err != nil) { return err } + } + + traitRulesList := traitObject.RulesList + + for _, traitRuleObject := range traitRulesList{ + + ruleIdentifier := traitRuleObject.RuleIdentifier + + for _, genomePairIdentifier := range allGenomePairIdentifiersList{ + + _, _, _, err := GetOffspringTraitRuleInfoFromGeneticAnalysis(coupleAnalysisMapList, traitName, ruleIdentifier, genomePairIdentifier) + if (err != nil) { return err } + } + } + } + + return nil +} + + diff --git a/internal/genetics/readRawGenomes/readRawGenomes.go b/internal/genetics/readRawGenomes/readRawGenomes.go new file mode 100644 index 0000000..056879f --- /dev/null +++ b/internal/genetics/readRawGenomes/readRawGenomes.go @@ -0,0 +1,561 @@ + +// readRawGenomes provides functions to read raw genome files from companies like 23andMe and AncestryDNA. +// These are used to create genetic analyses. + +package readRawGenomes + +import "seekia/resources/geneticReferences/locusMetadata" + +import "seekia/internal/helpers" + +import "bufio" +import "strings" +import "errors" +import "io" +import "time" + + +// Returns the import version performed by the current version of Seekia +// Seekia will continue to improve the ability to scan raw genome files +// This includes building a database of the rs identifiers for company-specific snp identifiers +// The raw genome metadata within myGenomes.go must be updated whenever the user updates Seekia and a newer import version exists +func GetCurrentCompanyImportVersion(companyName string)(int, error){ + + if (companyName == "23andMe"){ + return 1, nil + } + if (companyName == "AncestryDNA"){ + return 1, nil + } + return 0, errors.New("GetCurrentCompanyImportVersion called with invalid company name: " + companyName) +} + +type RawGenomeLocusValue struct{ + + // This is one of the following: "C", "A", "T", "G", "I", "D" + Allele1 string + + // This stores if a second base exists + // Base2 will not exist for some chromosomes (Example: Y chromosome) + Allele2Exists bool + + // This is one of the following: "C", "A", "T", "G", "I", "D" + Allele2 string +} + +// This function will always adjust all locus values to represent the Plus strand +// We only import genes into the genomeMap which are used for the analysis +// +// TODO: The ability to process genomes with a different strand will be added +// A database to know which SNPs are Plus/Minus for different HG reference genomes needs to be added +//Outputs: +// -string: Company name ("23andMe", "AncestryDNA") +// -int: ImportVersion (This will help when Seekia has a new way of importing the files. Each company has its own importVersion) +// -int64: Time file was generated +// -int64: Total number of loci (we will only read a tiny fraction of these into the genome map) +// -This does not count loci which have no value (Example: "--") +// -bool: IsPhased (allele order corresponds to haplotype) +// -map[int64]RawGenomeLocusValue: RSID -> Locus allele value(s) +// -error (file not readable) +func ReadRawGenomeFile(fileReader io.Reader) (string, int, int64, int64, bool, map[int64]RawGenomeLocusValue, error) { + + validBasesList := []string{"C", "A", "T", "G", "I", "D"} + + verifyBase := func(inputBase string)bool{ + + for _, baseString := range validBasesList{ + if (inputBase == baseString){ + return true + } + } + return false + } + + //Outputs: + // -bool: Able to read + // -int64: RSID int64 + readRSIDString := func(inputRSID string)(bool, int64){ + + stringWithoutPrefix, prefixExists := strings.CutPrefix(inputRSID, "rs") + if (prefixExists == false){ + return false, 0 + } + + result, err := helpers.ConvertStringToInt64(stringWithoutPrefix) + if (err != nil){ + return false, 0 + } + + return true, result + } + + fileBufioReader := bufio.NewReader(fileReader) + + firstLine, err := fileBufioReader.ReadString('\n') + if (err != nil){ + // File does not have another line + return "", 0, 0, 0, false, nil, errors.New("Malformed genome file: Too short.") + } + + fileIsAncestryDNA := strings.HasPrefix(firstLine, "#AncestryDNA raw data download") + if (fileIsAncestryDNA == true){ + + // AncestryDNA represents all locus values on the Plus strand. (allegedly) + + secondLine, err := fileBufioReader.ReadString('\n') + if (err != nil){ + // File does not have another line + return "", 0, 0, 0, false, nil, errors.New("Malformed genome file: Too short.") + } + + // Second line contains time of creation + // Example: + // "#This file was generated by AncestryDNA at: 02/22/2022 10:10:10 UTC" + // Time is Month -> Day -> Year + timeOfCreationString := strings.TrimPrefix(secondLine, "#This file was generated by AncestryDNA at: ") + + monthString, remainingString, found := strings.Cut(timeOfCreationString, "/") + if (found == false){ + return "", 0, 0, 0, false, nil, errors.New("Malformed AncestryDNA genome file: Missing Month") + } + dayString, remainingString, found := strings.Cut(remainingString, "/") + if (found == false){ + return "", 0, 0, 0, false, nil, errors.New("Malformed AncestryDNA genome file: Missing Day") + } + yearString, _, found := strings.Cut(remainingString, " ") + if (found == false){ + return "", 0, 0, 0, false, nil, errors.New("Malformed AncestryDNA genome file: Missing Year") + } + + getMonthObject := func()(time.Month, error){ + if (monthString == "01"){ + return time.January, nil + } + if (monthString == "02"){ + return time.February, nil + } + if (monthString == "03"){ + return time.March, nil + } + if (monthString == "04"){ + return time.April, nil + } + if (monthString == "05"){ + return time.May, nil + } + if (monthString == "06"){ + return time.June, nil + } + if (monthString == "07"){ + return time.July, nil + } + if (monthString == "08"){ + return time.August, nil + } + if (monthString == "09"){ + return time.September, nil + } + if (monthString == "10"){ + return time.October, nil + } + if (monthString == "11"){ + return time.November, nil + } + if (monthString == "12"){ + return time.December, nil + } + return time.January, errors.New("Malformed AncestryDNA genome file: Invalid month: " + monthString) + } + + monthObject, err := getMonthObject() + if (err != nil) { return "", 0, 0, 0, false, nil, err } + + yearInt, err := helpers.ConvertStringToInt(yearString) + if (err != nil) { + return "", 0, 0, 0, false, nil, errors.New("Malformed AncestryDNA genome file: Invalid year: " + yearString) + } + + dayInt, err := helpers.ConvertStringToInt(dayString) + if (err != nil) { + return "", 0, 0, 0, false, nil, errors.New("Malformed AncestryDNA genome file: Invalid day: " + dayString) + } + + fileTimeObject := time.Date(yearInt, monthObject, dayInt, 0, 0, 0, 0, time.UTC) + + fileTimeUnix := fileTimeObject.Unix() + + // Now we advance bufio reader to the SNP rows + + for { + + fileLineString, err := fileBufioReader.ReadString('\n') + if (err != nil){ + // File does not have another line + return "", 0, 0, 0, false, nil, errors.New("Malformed genome file: Too short.") + } + + // All SNP rows comes after this line: + // "rsid chromosome position allele1 allele2" + lineReached := strings.HasPrefix(fileLineString, "rsid") + if (lineReached == true){ + break + } + } + + numberOfLoci := int64(0) + + genomeMap := make(map[int64]RawGenomeLocusValue) + + for { + + fileLineString, err := fileBufioReader.ReadString('\n') + if (err != nil){ + // File does not have another line + break + } + if (fileLineString == "\n"){ + // This is the final line + break + } + + numberOfLoci += 1 + + fileLineWithoutNewline := strings.TrimSuffix(fileLineString, "\n") + + // Example row (tab delimited) + // "rs9777703 1 928836 T T" + + rowColumnsSlice := strings.Split(fileLineWithoutNewline, "\t") + if (len(rowColumnsSlice) != 5){ + return "", 0, 0, 0, false, nil, errors.New("Malformed AncestryDNA genome file: Invalid row detected: " + fileLineString) + } + + snpIdentifier := rowColumnsSlice[0] + + isValid, locusRSID := readRSIDString(snpIdentifier) + if (isValid == false){ + // This must be a custom identifier, not an RSID + // TODO: Create database to convert them to RSIDs, like the one we have for 23andMe + continue + } + + // We check to see if this rsID is used in any part of the genetic analysis, and skip it if it is not. + + locusFound, _, err := locusMetadata.GetLocusMetadata(locusRSID) + if (err != nil) { return "", 0, 0, 0, false, nil, err } + if (locusFound == false){ + // This locus is not used for any part of the genetic analysis + // We don't need to include it in our genome map + continue + } + + alleleABase := rowColumnsSlice[3] + alleleBBaseRaw := rowColumnsSlice[4] + + // Allele B has a control character suffix + // We will remove it + alleleBBase := string(alleleBBaseRaw[0]) + + if (alleleABase == "0" || alleleBBase == "0"){ + // The data was not successfully recorded + // We will skip this line + continue + } + + aIsValid := verifyBase(alleleABase) + if (aIsValid == false){ + return "", 0, 0, 0, false, nil, errors.New("Malformed AncestryDNA genome file: Invalid base detected: " + alleleABase) + } + bIsValid := verifyBase(alleleBBase) + if (bIsValid == false){ + return "", 0, 0, 0, false, nil, errors.New("Malformed AncestryDNA genome file: Invalid base detected: " + alleleBBase) + } + + locusValueObject := RawGenomeLocusValue{ + + Allele1: alleleABase, + Allele2Exists: true, + Allele2: alleleBBase, + } + + genomeMap[locusRSID] = locusValueObject + } + + // Each file usually has 500,000+ SNPs + if (numberOfLoci < 1000){ + return "", 0, 0, 0, false, nil, errors.New("Malformed AncestryDNA genome file: Not enough bases.") + } + + return "AncestryDNA", 1, fileTimeUnix, numberOfLoci, false, genomeMap, nil + } + + // 23andMe files have the following first line format: + // "# This data file generated by 23andMe at: Mon Jan 01 01:00:00 2022" + fileIs23andMe := strings.HasPrefix(firstLine, "# This data file generated by 23andMe at:") + if (fileIs23andMe == true){ + + // 23andMe represents all locus values on the Plus strand. + + timeOfGenerationString := strings.TrimPrefix(firstLine, "# This data file generated by 23andMe at: ") + + timeSlice := strings.Split(timeOfGenerationString, " ") + if (len(timeSlice) != 5 && len(timeSlice) != 6){ + return "", 0, 0, 0, false, nil, errors.New("Malformed 23andMe genome file: Invalid time: " + timeOfGenerationString) + } + + monthString := timeSlice[1] + + getDayAndYearString := func()(string, string){ + + if (len(timeSlice) == 5){ + + dayString := timeSlice[2] + yearString := timeSlice[4] + + return dayString, yearString + } + + // len(timeSlice) == 6 + + dayString := timeSlice[3] + yearString := timeSlice[5] + + return dayString, yearString + } + + dayString, yearString := getDayAndYearString() + + getMonthObject := func()(time.Month, error){ + + switch monthString{ + case "Jan":{ + return time.January, nil + } + case "Feb":{ + return time.February, nil + } + case "Mar":{ + return time.March, nil + } + case "Apr":{ + return time.April, nil + } + case "May":{ + return time.May, nil + } + case "Jun":{ + return time.June, nil + } + case "Jul":{ + return time.July, nil + } + case "Aug":{ + return time.August, nil + } + case "Sep":{ + return time.September, nil + } + case "Oct":{ + return time.October, nil + } + case "Nov":{ + return time.November, nil + } + case "Dec":{ + return time.December, nil + } + } + return time.January, errors.New("Malformed 23andMe genome file: Invalid month: " + monthString) + } + + monthObject, err := getMonthObject() + if (err != nil) { return "", 0, 0, 0, false, nil, err } + + // We have to remove control character and newline suffix + yearExtractedString := string(yearString[:4]) + + yearInt, err := helpers.ConvertStringToInt(yearExtractedString) + if (err != nil) { + return "", 0, 0, 0, false, nil, errors.New("Malformed 23andMe genome file: Invalid year: " + yearExtractedString) + } + + dayInt, err := helpers.ConvertStringToInt(dayString) + if (err != nil) { + return "", 0, 0, 0, false, nil, errors.New("Malformed 23andMe genome file: Invalid day: " + dayString) + } + + fileTimeObject := time.Date(yearInt, monthObject, dayInt, 0, 0, 0, 0, time.UTC) + + fileTimeUnix := fileTimeObject.Unix() + + // Now we advance bufio reader to the snp rows + + for { + + fileLineString, err := fileBufioReader.ReadString('\n') + if (err != nil){ + // File does not have another line + return "", 0, 0, 0, false, nil, errors.New("Malformed genome file: Too short.") + } + + // All SNP rows come after this line: + // "# rsid chromosome position genotype" + lineReached := strings.HasPrefix(fileLineString, "# rsid") + if (lineReached == true){ + break + } + } + + numberOfLoci := int64(0) + + genomeMap := make(map[int64]RawGenomeLocusValue) + + for { + + fileLineString, err := fileBufioReader.ReadString('\n') + if (err != nil){ + // File does not have another line + break + } + if (fileLineString == "\n"){ + // This is the final line + break + } + + fileLineWithoutNewline := strings.TrimSuffix(fileLineString, "\n") + + // Rows look like this + // "rs4477212 1 82154 GG" + // "rs571313759 1 1181945 --" (-- means no entry) + // "i3001920 MT 16470 G" (one base is possible) + + rowSlice := strings.Split(fileLineWithoutNewline, "\t") + + if (len(rowSlice) != 4){ + return "", 0, 0, 0, false, nil, errors.New("Malformed 23andMe genome data: Invalid SNP row: " + fileLineString) + } + + snpIdentifier := rowSlice[0] + snpValueRaw := rowSlice[3] + + if (snpValueRaw[0] != byte('-')){ + // Locus value is not "--" + // Locus value exists + numberOfLoci += 1 + } + + //Outputs: + // -bool: rsID found + // -int64: rsID value + getRSIDIdentifier := func()(bool, int64, error){ + + isRSID, rsidInt64 := readRSIDString(snpIdentifier) + if (isRSID == true){ + return true, rsidInt64, nil + } + + // We see if it is a custom locus alias + + aliasFound, rsidInt64, err := locusMetadata.GetCompanyAliasRSID("23andMe", snpIdentifier) + if (err != nil) { return false, 0, err } + if (aliasFound == true){ + return true, rsidInt64, nil + } + + return false, 0, nil + } + + rsidFound, locusRSID, err := getRSIDIdentifier() + if (err != nil) { return "", 0, 0, 0, false, nil, err } + if (rsidFound == false){ + // RSID is unknown. + // It is probably a custom identifier that represents a locus we don't use for the analysis. + continue + } + + locusFound, _, err := locusMetadata.GetLocusMetadata(locusRSID) + if (err != nil) { return "", 0, 0, 0, false, nil, err } + if (locusFound == false){ + // This locus is not used for any part of the genetic analysis + // We don't need to include it in our genome map + continue + } + + // This will return either a base pair or a single base + // Base pair can be "--" + getLocusValueString := func()(string, error){ + + // This value has a control character suffix + // Final index is always a control character + // We remove the control character suffix + + if (len(snpValueRaw) == 2){ + + singleBase := string(snpValueRaw[0]) + return singleBase, nil + } + + if (len(snpValueRaw) == 3){ + + basePair := snpValueRaw[:2] + return basePair, nil + } + + return "", errors.New("Malformed 23andMe genome file: Invalid SNP value: " + snpValueRaw) + } + + basesString, err := getLocusValueString() + if (err != nil) { return "", 0, 0, 0, false, nil, err } + + if (basesString == "--"){ + // No data exists, skip. + continue + } + + for _, baseRune := range basesString{ + + baseIsValid := verifyBase(string(baseRune)) + if (baseIsValid == false){ + return "", 0, 0, 0, false, nil, errors.New("Malformed 23andMe genome file: Invalid SNP base: " + string(baseRune)) + } + } + + getMapEntryValue := func()RawGenomeLocusValue{ + + if (len(basesString) == 1){ + + locusValueObject := RawGenomeLocusValue{ + + Allele1: basesString, + Allele2Exists: false, + Allele2: "", + } + + return locusValueObject + } + + baseAString := string(basesString[0]) + baseBString := string(basesString[1]) + + locusValueObject := RawGenomeLocusValue{ + + Allele1: baseAString, + Allele2Exists: true, + Allele2: baseBString, + } + + return locusValueObject + } + + mapEntryValue := getMapEntryValue() + + genomeMap[locusRSID] = mapEntryValue + } + + return "23andMe", 1, fileTimeUnix, numberOfLoci, false, genomeMap, nil + } + + return "", 0, 0, 0, false, nil, errors.New("Cannot read genome file: File format not known.") +} + + diff --git a/internal/genetics/readRawGenomes/readRawGenomes_test.go b/internal/genetics/readRawGenomes/readRawGenomes_test.go new file mode 100644 index 0000000..d7ea597 --- /dev/null +++ b/internal/genetics/readRawGenomes/readRawGenomes_test.go @@ -0,0 +1,115 @@ +package readRawGenomes_test + + +import "seekia/internal/genetics/readRawGenomes" + +import "seekia/internal/genetics/createRawGenomes" +import "seekia/internal/helpers" + +import "testing" + +import "strings" + + +//TODO: Add some of the locusMetadata rsids to the fake files + +func TestAncestryDNAFileReading(t *testing.T){ + + fileString, expectedFileTimeUnix, numberOfAddedLoci, fileRSIDsMap, err := createRawGenomes.CreateFakeRawGenome_AncestryDNA() + if (err != nil){ + t.Fatalf("Failed to create fake AncestryDNA genome: " + err.Error()) + } + + fileReader := strings.NewReader(fileString) + + companyName, importVersion, fileGenerationTime, fileNumberOfLoci, isPhased, genomeMap, err := readRawGenomes.ReadRawGenomeFile(fileReader) + if (err != nil){ + t.Fatalf("Failed to read AncestryDNA file: " + err.Error()) + } + if (companyName != "AncestryDNA"){ + t.Fatalf("Failed to read AncestryDNA file: Invalid companyName: " + companyName) + } + expectedImportVersion, err := readRawGenomes.GetCurrentCompanyImportVersion("AncestryDNA") + if (err != nil){ + t.Fatalf("Failed to get AncestryDNA import version: " + err.Error()) + } + if (importVersion != expectedImportVersion){ + t.Fatalf("AncestryDNA import version does not match.") + } + if (fileGenerationTime != expectedFileTimeUnix){ + t.Fatalf("AncestryDNA file generation time does not match.") + } + + if (fileNumberOfLoci != numberOfAddedLoci){ + fileNumberOfLociString := helpers.ConvertInt64ToString(fileNumberOfLoci) + numberOfAddedLociString := helpers.ConvertInt64ToString(numberOfAddedLoci) + t.Fatalf("AncestryDNA number of loci does not match: " + fileNumberOfLociString + " != " + numberOfAddedLociString) + } + if (isPhased != false){ + t.Fatalf("AncestryDNA file isPhased status is unexpected.") + } + + for rsid, locusValue := range genomeMap{ + + expectedLocusValue, exists := fileRSIDsMap[rsid] + if (exists == false){ + rsidString := helpers.ConvertInt64ToString(rsid) + t.Fatalf("genomeMap contains unexpected rsid: " + rsidString) + } + if (locusValue != expectedLocusValue){ + t.Fatalf("genomeMap contains unexpected rsid locus value.") + } + } +} + + +func Test23andMeFileReading(t *testing.T){ + + newRawGenome, fileCreationTime, fileNumberOfLoci, fileRSIDsMap, err := createRawGenomes.CreateFakeRawGenome_23andMe() + if (err != nil){ + t.Fatalf("Failed to create fake 23andMe Genome: " + err.Error()) + } + + fileReader := strings.NewReader(newRawGenome) + + companyName, importVersion, genomeGenerationTime, genomeNumberOfLoci, isPhased, genomeMap, err := readRawGenomes.ReadRawGenomeFile(fileReader) + if (err != nil){ + t.Fatalf("Failed to read 23andMe file: " + err.Error()) + } + if (companyName != "23andMe"){ + t.Fatalf("Failed to read 23andMe file: Invalid companyName: " + companyName) + } + expectedImportVersion, err := readRawGenomes.GetCurrentCompanyImportVersion("23andMe") + if (err != nil){ + t.Fatalf("Failed to get 23andMe import version: " + err.Error()) + } + if (importVersion != expectedImportVersion){ + t.Fatalf("23andMe import version does not match.") + } + if (fileCreationTime != genomeGenerationTime){ + t.Fatalf("23andMe file generation time does not match.") + } + + if (fileNumberOfLoci != genomeNumberOfLoci){ + fileNumberOfLociString := helpers.ConvertInt64ToString(fileNumberOfLoci) + genomeNumberOfLociString := helpers.ConvertInt64ToString(genomeNumberOfLoci) + t.Fatalf("23andMe number of loci does not match: " + fileNumberOfLociString + " != " + genomeNumberOfLociString) + } + if (isPhased != false){ + t.Fatalf("23andMe file isPhased status is unexpected.") + } + + for rsid, locusValue := range genomeMap{ + + expectedLocusValue, exists := fileRSIDsMap[rsid] + if (exists == false){ + rsidString := helpers.ConvertInt64ToString(rsid) + t.Fatalf("genomeMap contains unexpected rsid: " + rsidString) + } + if (locusValue != expectedLocusValue){ + t.Fatalf("genomeMap contains unexpected rsid locus value.") + } + } +} + + diff --git a/internal/genetics/sampleAnalyses/SampleCoupleAnalysis.json b/internal/genetics/sampleAnalyses/SampleCoupleAnalysis.json new file mode 100644 index 0000000..97ceca1 --- /dev/null +++ b/internal/genetics/sampleAnalyses/SampleCoupleAnalysis.json @@ -0,0 +1,712 @@ +[ + { + "AnalysisType": "Couple", + "AnalysisVersion": "1", + "ItemType": "Metadata", + "Pair1PersonAGenomeIdentifier": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "Pair1PersonBGenomeIdentifier": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", + "PersonAHasMultipleGenomes": "No", + "PersonBHasMultipleGenomes": "No", + "SecondPairExists": "No" + }, + { + "DiseaseName": "Cystic Fibrosis", + "ItemType": "MonogenicDisease", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa+bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_ProbabilityOffspringHasDisease": "25", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa+bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_ProbabilityOffspringHasVariant": "75", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_NumberOfVariantsTested": "26", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_NumberOfVariantsTested": "26" + }, + { + "DiseaseName": "Cystic Fibrosis", + "ItemType": "MonogenicDiseaseVariant", + "VariantIdentifier": "36965d", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa+bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_ProbabilityOf0MutationsLowerBound": "50", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa+bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_ProbabilityOf0MutationsUpperBound": "50", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa+bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_ProbabilityOf1MutationLowerBound": "50", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa+bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_ProbabilityOf1MutationUpperBound": "50", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa+bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_ProbabilityOf2MutationsLowerBound": "0", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa+bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_ProbabilityOf2MutationsUpperBound": "0" + }, + { + "DiseaseName": "Cystic Fibrosis", + "ItemType": "MonogenicDiseaseVariant", + "VariantIdentifier": "5706b0", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa+bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_ProbabilityOf0MutationsLowerBound": "100", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa+bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_ProbabilityOf0MutationsUpperBound": "100", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa+bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_ProbabilityOf1MutationLowerBound": "0", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa+bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_ProbabilityOf1MutationUpperBound": "0", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa+bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_ProbabilityOf2MutationsLowerBound": "0", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa+bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_ProbabilityOf2MutationsUpperBound": "0" + }, + { + "DiseaseName": "Cystic Fibrosis", + "ItemType": "MonogenicDiseaseVariant", + "VariantIdentifier": "01a2a0", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa+bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_ProbabilityOf0MutationsLowerBound": "100", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa+bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_ProbabilityOf0MutationsUpperBound": "100", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa+bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_ProbabilityOf1MutationLowerBound": "0", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa+bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_ProbabilityOf1MutationUpperBound": "0", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa+bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_ProbabilityOf2MutationsLowerBound": "0", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa+bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_ProbabilityOf2MutationsUpperBound": "0" + }, + { + "DiseaseName": "Cystic Fibrosis", + "ItemType": "MonogenicDiseaseVariant", + "VariantIdentifier": "bd4106", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa+bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_ProbabilityOf0MutationsLowerBound": "100", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa+bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_ProbabilityOf0MutationsUpperBound": "100", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa+bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_ProbabilityOf1MutationLowerBound": "0", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa+bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_ProbabilityOf1MutationUpperBound": "0", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa+bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_ProbabilityOf2MutationsLowerBound": "0", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa+bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_ProbabilityOf2MutationsUpperBound": "0" + }, + { + "DiseaseName": "Cystic Fibrosis", + "ItemType": "MonogenicDiseaseVariant", + "VariantIdentifier": "4d6f38", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa+bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_ProbabilityOf0MutationsLowerBound": "100", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa+bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_ProbabilityOf0MutationsUpperBound": "100", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa+bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_ProbabilityOf1MutationLowerBound": "0", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa+bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_ProbabilityOf1MutationUpperBound": "0", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa+bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_ProbabilityOf2MutationsLowerBound": "0", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa+bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_ProbabilityOf2MutationsUpperBound": "0" + }, + { + "DiseaseName": "Cystic Fibrosis", + "ItemType": "MonogenicDiseaseVariant", + "VariantIdentifier": "c6135a", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa+bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_ProbabilityOf0MutationsLowerBound": "50", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa+bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_ProbabilityOf0MutationsUpperBound": "50", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa+bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_ProbabilityOf1MutationLowerBound": "50", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa+bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_ProbabilityOf1MutationUpperBound": "50", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa+bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_ProbabilityOf2MutationsLowerBound": "0", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa+bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_ProbabilityOf2MutationsUpperBound": "0" + }, + { + "DiseaseName": "Cystic Fibrosis", + "ItemType": "MonogenicDiseaseVariant", + "VariantIdentifier": "88a7f4", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa+bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_ProbabilityOf0MutationsLowerBound": "100", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa+bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_ProbabilityOf0MutationsUpperBound": "100", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa+bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_ProbabilityOf1MutationLowerBound": "0", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa+bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_ProbabilityOf1MutationUpperBound": "0", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa+bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_ProbabilityOf2MutationsLowerBound": "0", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa+bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_ProbabilityOf2MutationsUpperBound": "0" + }, + { + "DiseaseName": "Cystic Fibrosis", + "ItemType": "MonogenicDiseaseVariant", + "VariantIdentifier": "058a4d", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa+bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_ProbabilityOf0MutationsLowerBound": "Unknown", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa+bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_ProbabilityOf0MutationsUpperBound": "Unknown", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa+bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_ProbabilityOf1MutationLowerBound": "Unknown", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa+bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_ProbabilityOf1MutationUpperBound": "Unknown", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa+bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_ProbabilityOf2MutationsLowerBound": "Unknown", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa+bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_ProbabilityOf2MutationsUpperBound": "Unknown" + }, + { + "DiseaseName": "Cystic Fibrosis", + "ItemType": "MonogenicDiseaseVariant", + "VariantIdentifier": "2a4ddf", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa+bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_ProbabilityOf0MutationsLowerBound": "Unknown", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa+bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_ProbabilityOf0MutationsUpperBound": "Unknown", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa+bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_ProbabilityOf1MutationLowerBound": "Unknown", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa+bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_ProbabilityOf1MutationUpperBound": "Unknown", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa+bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_ProbabilityOf2MutationsLowerBound": "Unknown", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa+bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_ProbabilityOf2MutationsUpperBound": "Unknown" + }, + { + "DiseaseName": "Cystic Fibrosis", + "ItemType": "MonogenicDiseaseVariant", + "VariantIdentifier": "e1bcfb", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa+bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_ProbabilityOf0MutationsLowerBound": "100", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa+bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_ProbabilityOf0MutationsUpperBound": "100", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa+bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_ProbabilityOf1MutationLowerBound": "0", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa+bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_ProbabilityOf1MutationUpperBound": "0", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa+bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_ProbabilityOf2MutationsLowerBound": "0", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa+bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_ProbabilityOf2MutationsUpperBound": "0" + }, + { + "DiseaseName": "Cystic Fibrosis", + "ItemType": "MonogenicDiseaseVariant", + "VariantIdentifier": "c28795", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa+bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_ProbabilityOf0MutationsLowerBound": "100", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa+bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_ProbabilityOf0MutationsUpperBound": "100", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa+bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_ProbabilityOf1MutationLowerBound": "0", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa+bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_ProbabilityOf1MutationUpperBound": "0", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa+bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_ProbabilityOf2MutationsLowerBound": "0", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa+bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_ProbabilityOf2MutationsUpperBound": "0" + }, + { + "DiseaseName": "Cystic Fibrosis", + "ItemType": "MonogenicDiseaseVariant", + "VariantIdentifier": "f1965c", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa+bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_ProbabilityOf0MutationsLowerBound": "100", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa+bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_ProbabilityOf0MutationsUpperBound": "100", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa+bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_ProbabilityOf1MutationLowerBound": "0", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa+bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_ProbabilityOf1MutationUpperBound": "0", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa+bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_ProbabilityOf2MutationsLowerBound": "0", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa+bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_ProbabilityOf2MutationsUpperBound": "0" + }, + { + "DiseaseName": "Cystic Fibrosis", + "ItemType": "MonogenicDiseaseVariant", + "VariantIdentifier": "3420c1", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa+bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_ProbabilityOf0MutationsLowerBound": "100", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa+bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_ProbabilityOf0MutationsUpperBound": "100", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa+bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_ProbabilityOf1MutationLowerBound": "0", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa+bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_ProbabilityOf1MutationUpperBound": "0", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa+bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_ProbabilityOf2MutationsLowerBound": "0", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa+bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_ProbabilityOf2MutationsUpperBound": "0" + }, + { + "DiseaseName": "Cystic Fibrosis", + "ItemType": "MonogenicDiseaseVariant", + "VariantIdentifier": "25d0b4", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa+bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_ProbabilityOf0MutationsLowerBound": "100", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa+bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_ProbabilityOf0MutationsUpperBound": "100", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa+bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_ProbabilityOf1MutationLowerBound": "0", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa+bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_ProbabilityOf1MutationUpperBound": "0", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa+bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_ProbabilityOf2MutationsLowerBound": "0", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa+bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_ProbabilityOf2MutationsUpperBound": "0" + }, + { + "DiseaseName": "Cystic Fibrosis", + "ItemType": "MonogenicDiseaseVariant", + "VariantIdentifier": "139ab2", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa+bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_ProbabilityOf0MutationsLowerBound": "100", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa+bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_ProbabilityOf0MutationsUpperBound": "100", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa+bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_ProbabilityOf1MutationLowerBound": "0", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa+bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_ProbabilityOf1MutationUpperBound": "0", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa+bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_ProbabilityOf2MutationsLowerBound": "0", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa+bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_ProbabilityOf2MutationsUpperBound": "0" + }, + { + "DiseaseName": "Cystic Fibrosis", + "ItemType": "MonogenicDiseaseVariant", + "VariantIdentifier": "f7a12e", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa+bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_ProbabilityOf0MutationsLowerBound": "100", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa+bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_ProbabilityOf0MutationsUpperBound": "100", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa+bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_ProbabilityOf1MutationLowerBound": "0", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa+bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_ProbabilityOf1MutationUpperBound": "0", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa+bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_ProbabilityOf2MutationsLowerBound": "0", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa+bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_ProbabilityOf2MutationsUpperBound": "0" + }, + { + "DiseaseName": "Cystic Fibrosis", + "ItemType": "MonogenicDiseaseVariant", + "VariantIdentifier": "deb2e2", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa+bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_ProbabilityOf0MutationsLowerBound": "100", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa+bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_ProbabilityOf0MutationsUpperBound": "100", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa+bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_ProbabilityOf1MutationLowerBound": "0", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa+bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_ProbabilityOf1MutationUpperBound": "0", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa+bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_ProbabilityOf2MutationsLowerBound": "0", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa+bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_ProbabilityOf2MutationsUpperBound": "0" + }, + { + "DiseaseName": "Cystic Fibrosis", + "ItemType": "MonogenicDiseaseVariant", + "VariantIdentifier": "884cf0", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa+bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_ProbabilityOf0MutationsLowerBound": "100", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa+bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_ProbabilityOf0MutationsUpperBound": "100", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa+bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_ProbabilityOf1MutationLowerBound": "0", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa+bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_ProbabilityOf1MutationUpperBound": "0", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa+bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_ProbabilityOf2MutationsLowerBound": "0", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa+bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_ProbabilityOf2MutationsUpperBound": "0" + }, + { + "DiseaseName": "Cystic Fibrosis", + "ItemType": "MonogenicDiseaseVariant", + "VariantIdentifier": "b9fad1", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa+bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_ProbabilityOf0MutationsLowerBound": "100", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa+bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_ProbabilityOf0MutationsUpperBound": "100", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa+bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_ProbabilityOf1MutationLowerBound": "0", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa+bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_ProbabilityOf1MutationUpperBound": "0", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa+bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_ProbabilityOf2MutationsLowerBound": "0", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa+bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_ProbabilityOf2MutationsUpperBound": "0" + }, + { + "DiseaseName": "Cystic Fibrosis", + "ItemType": "MonogenicDiseaseVariant", + "VariantIdentifier": "f2448d", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa+bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_ProbabilityOf0MutationsLowerBound": "100", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa+bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_ProbabilityOf0MutationsUpperBound": "100", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa+bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_ProbabilityOf1MutationLowerBound": "0", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa+bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_ProbabilityOf1MutationUpperBound": "0", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa+bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_ProbabilityOf2MutationsLowerBound": "0", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa+bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_ProbabilityOf2MutationsUpperBound": "0" + }, + { + "DiseaseName": "Cystic Fibrosis", + "ItemType": "MonogenicDiseaseVariant", + "VariantIdentifier": "09b96f", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa+bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_ProbabilityOf0MutationsLowerBound": "100", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa+bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_ProbabilityOf0MutationsUpperBound": "100", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa+bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_ProbabilityOf1MutationLowerBound": "0", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa+bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_ProbabilityOf1MutationUpperBound": "0", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa+bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_ProbabilityOf2MutationsLowerBound": "0", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa+bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_ProbabilityOf2MutationsUpperBound": "0" + }, + { + "DiseaseName": "Cystic Fibrosis", + "ItemType": "MonogenicDiseaseVariant", + "VariantIdentifier": "2f8651", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa+bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_ProbabilityOf0MutationsLowerBound": "Unknown", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa+bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_ProbabilityOf0MutationsUpperBound": "Unknown", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa+bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_ProbabilityOf1MutationLowerBound": "Unknown", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa+bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_ProbabilityOf1MutationUpperBound": "Unknown", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa+bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_ProbabilityOf2MutationsLowerBound": "Unknown", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa+bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_ProbabilityOf2MutationsUpperBound": "Unknown" + }, + { + "DiseaseName": "Cystic Fibrosis", + "ItemType": "MonogenicDiseaseVariant", + "VariantIdentifier": "46efe1", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa+bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_ProbabilityOf0MutationsLowerBound": "Unknown", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa+bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_ProbabilityOf0MutationsUpperBound": "Unknown", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa+bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_ProbabilityOf1MutationLowerBound": "Unknown", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa+bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_ProbabilityOf1MutationUpperBound": "Unknown", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa+bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_ProbabilityOf2MutationsLowerBound": "Unknown", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa+bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_ProbabilityOf2MutationsUpperBound": "Unknown" + }, + { + "DiseaseName": "Cystic Fibrosis", + "ItemType": "MonogenicDiseaseVariant", + "VariantIdentifier": "528406", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa+bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_ProbabilityOf0MutationsLowerBound": "100", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa+bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_ProbabilityOf0MutationsUpperBound": "100", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa+bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_ProbabilityOf1MutationLowerBound": "0", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa+bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_ProbabilityOf1MutationUpperBound": "0", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa+bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_ProbabilityOf2MutationsLowerBound": "0", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa+bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_ProbabilityOf2MutationsUpperBound": "0" + }, + { + "DiseaseName": "Cystic Fibrosis", + "ItemType": "MonogenicDiseaseVariant", + "VariantIdentifier": "e8e8fc", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa+bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_ProbabilityOf0MutationsLowerBound": "100", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa+bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_ProbabilityOf0MutationsUpperBound": "100", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa+bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_ProbabilityOf1MutationLowerBound": "0", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa+bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_ProbabilityOf1MutationUpperBound": "0", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa+bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_ProbabilityOf2MutationsLowerBound": "0", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa+bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_ProbabilityOf2MutationsUpperBound": "0" + }, + { + "DiseaseName": "Cystic Fibrosis", + "ItemType": "MonogenicDiseaseVariant", + "VariantIdentifier": "e60633", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa+bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_ProbabilityOf0MutationsLowerBound": "100", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa+bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_ProbabilityOf0MutationsUpperBound": "100", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa+bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_ProbabilityOf1MutationLowerBound": "0", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa+bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_ProbabilityOf1MutationUpperBound": "0", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa+bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_ProbabilityOf2MutationsLowerBound": "0", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa+bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_ProbabilityOf2MutationsUpperBound": "0" + }, + { + "DiseaseName": "Cystic Fibrosis", + "ItemType": "MonogenicDiseaseVariant", + "VariantIdentifier": "d72d30", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa+bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_ProbabilityOf0MutationsLowerBound": "100", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa+bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_ProbabilityOf0MutationsUpperBound": "100", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa+bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_ProbabilityOf1MutationLowerBound": "0", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa+bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_ProbabilityOf1MutationUpperBound": "0", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa+bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_ProbabilityOf2MutationsLowerBound": "0", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa+bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_ProbabilityOf2MutationsUpperBound": "0" + }, + { + "DiseaseName": "Cystic Fibrosis", + "ItemType": "MonogenicDiseaseVariant", + "VariantIdentifier": "a3b068", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa+bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_ProbabilityOf0MutationsLowerBound": "Unknown", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa+bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_ProbabilityOf0MutationsUpperBound": "Unknown", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa+bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_ProbabilityOf1MutationLowerBound": "Unknown", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa+bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_ProbabilityOf1MutationUpperBound": "Unknown", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa+bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_ProbabilityOf2MutationsLowerBound": "Unknown", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa+bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_ProbabilityOf2MutationsUpperBound": "Unknown" + }, + { + "DiseaseName": "Cystic Fibrosis", + "ItemType": "MonogenicDiseaseVariant", + "VariantIdentifier": "770086", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa+bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_ProbabilityOf0MutationsLowerBound": "100", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa+bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_ProbabilityOf0MutationsUpperBound": "100", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa+bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_ProbabilityOf1MutationLowerBound": "0", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa+bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_ProbabilityOf1MutationUpperBound": "0", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa+bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_ProbabilityOf2MutationsLowerBound": "0", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa+bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_ProbabilityOf2MutationsUpperBound": "0" + }, + { + "DiseaseName": "Cystic Fibrosis", + "ItemType": "MonogenicDiseaseVariant", + "VariantIdentifier": "a00f11", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa+bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_ProbabilityOf0MutationsLowerBound": "100", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa+bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_ProbabilityOf0MutationsUpperBound": "100", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa+bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_ProbabilityOf1MutationLowerBound": "0", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa+bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_ProbabilityOf1MutationUpperBound": "0", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa+bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_ProbabilityOf2MutationsLowerBound": "0", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa+bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_ProbabilityOf2MutationsUpperBound": "0" + }, + { + "DiseaseName": "Cystic Fibrosis", + "ItemType": "MonogenicDiseaseVariant", + "VariantIdentifier": "79d73b", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa+bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_ProbabilityOf0MutationsLowerBound": "100", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa+bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_ProbabilityOf0MutationsUpperBound": "100", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa+bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_ProbabilityOf1MutationLowerBound": "0", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa+bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_ProbabilityOf1MutationUpperBound": "0", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa+bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_ProbabilityOf2MutationsLowerBound": "0", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa+bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_ProbabilityOf2MutationsUpperBound": "0" + }, + { + "DiseaseName": "Sickle Cell Anemia", + "ItemType": "MonogenicDisease", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa+bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_ProbabilityOffspringHasDisease": "0", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa+bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_ProbabilityOffspringHasVariant": "0", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_NumberOfVariantsTested": "1", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_NumberOfVariantsTested": "1" + }, + { + "DiseaseName": "Sickle Cell Anemia", + "ItemType": "MonogenicDiseaseVariant", + "VariantIdentifier": "50e857", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa+bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_ProbabilityOf0MutationsLowerBound": "100", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa+bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_ProbabilityOf0MutationsUpperBound": "100", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa+bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_ProbabilityOf1MutationLowerBound": "0", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa+bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_ProbabilityOf1MutationUpperBound": "0", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa+bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_ProbabilityOf2MutationsLowerBound": "0", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa+bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_ProbabilityOf2MutationsUpperBound": "0" + }, + { + "DiseaseName": "Breast Cancer", + "ItemType": "PolygenicDisease", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa+bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_NumberOfLociTested": "24", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa+bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_OffspringRiskScore": "1" + }, + { + "DiseaseName": "Breast Cancer", + "ItemType": "PolygenicDiseaseLocus", + "LocusIdentifier": "d7891c", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa+bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_OffspringOddsRatio": "1.14000", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa+bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_OffspringRiskWeight": "1", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa+bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_OffspringUnknownOddsRatiosWeightSum": "0" + }, + { + "DiseaseName": "Breast Cancer", + "ItemType": "PolygenicDiseaseLocus", + "LocusIdentifier": "41c164", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa+bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_OffspringOddsRatio": "1.00000", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa+bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_OffspringRiskWeight": "0", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa+bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_OffspringUnknownOddsRatiosWeightSum": "0" + }, + { + "DiseaseName": "Breast Cancer", + "ItemType": "PolygenicDiseaseLocus", + "LocusIdentifier": "f3a097", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa+bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_OffspringOddsRatio": "0.50000", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa+bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_OffspringRiskWeight": "0", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa+bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_OffspringUnknownOddsRatiosWeightSum": "0" + }, + { + "DiseaseName": "Breast Cancer", + "ItemType": "PolygenicDiseaseLocus", + "LocusIdentifier": "d4626f", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa+bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_OffspringOddsRatio": "1.07000", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa+bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_OffspringRiskWeight": "0", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa+bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_OffspringUnknownOddsRatiosWeightSum": "0" + }, + { + "DiseaseName": "Breast Cancer", + "ItemType": "PolygenicDiseaseLocus", + "LocusIdentifier": "84aaa4", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa+bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_OffspringOddsRatio": "1.00000", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa+bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_OffspringRiskWeight": "0", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa+bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_OffspringUnknownOddsRatiosWeightSum": "0" + }, + { + "DiseaseName": "Breast Cancer", + "ItemType": "PolygenicDiseaseLocus", + "LocusIdentifier": "c8de7a", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa+bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_OffspringOddsRatio": "1.50000", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa+bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_OffspringRiskWeight": "1", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa+bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_OffspringUnknownOddsRatiosWeightSum": "0" + }, + { + "DiseaseName": "Breast Cancer", + "ItemType": "PolygenicDiseaseLocus", + "LocusIdentifier": "d30087", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa+bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_OffspringOddsRatio": "1.07000", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa+bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_OffspringRiskWeight": "0", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa+bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_OffspringUnknownOddsRatiosWeightSum": "0" + }, + { + "DiseaseName": "Breast Cancer", + "ItemType": "PolygenicDiseaseLocus", + "LocusIdentifier": "cafa72", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa+bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_OffspringOddsRatio": "1.00000", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa+bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_OffspringRiskWeight": "0", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa+bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_OffspringUnknownOddsRatiosWeightSum": "0" + }, + { + "DiseaseName": "Breast Cancer", + "ItemType": "PolygenicDiseaseLocus", + "LocusIdentifier": "8f671c", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa+bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_OffspringOddsRatio": "1.14000", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa+bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_OffspringRiskWeight": "1", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa+bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_OffspringUnknownOddsRatiosWeightSum": "0" + }, + { + "DiseaseName": "Breast Cancer", + "ItemType": "PolygenicDiseaseLocus", + "LocusIdentifier": "b3e49a", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa+bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_OffspringOddsRatio": "1.00000", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa+bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_OffspringRiskWeight": "0", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa+bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_OffspringUnknownOddsRatiosWeightSum": "0" + }, + { + "DiseaseName": "Breast Cancer", + "ItemType": "PolygenicDiseaseLocus", + "LocusIdentifier": "8b0b02", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa+bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_OffspringOddsRatio": "1.00000", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa+bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_OffspringRiskWeight": "0", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa+bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_OffspringUnknownOddsRatiosWeightSum": "0" + }, + { + "DiseaseName": "Breast Cancer", + "ItemType": "PolygenicDiseaseLocus", + "LocusIdentifier": "25cafc", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa+bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_OffspringOddsRatio": "1.00000", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa+bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_OffspringRiskWeight": "0", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa+bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_OffspringUnknownOddsRatiosWeightSum": "0" + }, + { + "DiseaseName": "Breast Cancer", + "ItemType": "PolygenicDiseaseLocus", + "LocusIdentifier": "34c7e5", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa+bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_OffspringOddsRatio": "1.00000", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa+bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_OffspringRiskWeight": "0", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa+bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_OffspringUnknownOddsRatiosWeightSum": "0" + }, + { + "DiseaseName": "Breast Cancer", + "ItemType": "PolygenicDiseaseLocus", + "LocusIdentifier": "60ce27", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa+bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_OffspringOddsRatio": "1.00000", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa+bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_OffspringRiskWeight": "0", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa+bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_OffspringUnknownOddsRatiosWeightSum": "0" + }, + { + "DiseaseName": "Breast Cancer", + "ItemType": "PolygenicDiseaseLocus", + "LocusIdentifier": "328cdf", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa+bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_OffspringOddsRatio": "1.00000", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa+bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_OffspringRiskWeight": "0", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa+bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_OffspringUnknownOddsRatiosWeightSum": "0" + }, + { + "DiseaseName": "Breast Cancer", + "ItemType": "PolygenicDiseaseLocus", + "LocusIdentifier": "849bc7", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa+bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_OffspringOddsRatio": "1.00000", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa+bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_OffspringRiskWeight": "0", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa+bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_OffspringUnknownOddsRatiosWeightSum": "0" + }, + { + "DiseaseName": "Breast Cancer", + "ItemType": "PolygenicDiseaseLocus", + "LocusIdentifier": "5af5e3", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa+bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_OffspringOddsRatio": "1.00000", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa+bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_OffspringRiskWeight": "0", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa+bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_OffspringUnknownOddsRatiosWeightSum": "0" + }, + { + "DiseaseName": "Breast Cancer", + "ItemType": "PolygenicDiseaseLocus", + "LocusIdentifier": "c354fa", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa+bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_OffspringOddsRatio": "1.00000", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa+bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_OffspringRiskWeight": "0", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa+bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_OffspringUnknownOddsRatiosWeightSum": "0" + }, + { + "DiseaseName": "Breast Cancer", + "ItemType": "PolygenicDiseaseLocus", + "LocusIdentifier": "eedc23", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa+bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_OffspringOddsRatio": "1.02500", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa+bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_OffspringRiskWeight": "0", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa+bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_OffspringUnknownOddsRatiosWeightSum": "0" + }, + { + "DiseaseName": "Breast Cancer", + "ItemType": "PolygenicDiseaseLocus", + "LocusIdentifier": "2ee027", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa+bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_OffspringOddsRatio": "1.00000", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa+bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_OffspringRiskWeight": "0", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa+bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_OffspringUnknownOddsRatiosWeightSum": "0" + }, + { + "DiseaseName": "Breast Cancer", + "ItemType": "PolygenicDiseaseLocus", + "LocusIdentifier": "fc4bab", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa+bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_OffspringOddsRatio": "1.00000", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa+bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_OffspringRiskWeight": "0", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa+bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_OffspringUnknownOddsRatiosWeightSum": "0" + }, + { + "DiseaseName": "Breast Cancer", + "ItemType": "PolygenicDiseaseLocus", + "LocusIdentifier": "f8b225", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa+bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_OffspringOddsRatio": "1.00000", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa+bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_OffspringRiskWeight": "0", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa+bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_OffspringUnknownOddsRatiosWeightSum": "0" + }, + { + "DiseaseName": "Breast Cancer", + "ItemType": "PolygenicDiseaseLocus", + "LocusIdentifier": "4a072c", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa+bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_OffspringOddsRatio": "1.00000", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa+bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_OffspringRiskWeight": "0", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa+bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_OffspringUnknownOddsRatiosWeightSum": "0" + }, + { + "DiseaseName": "Breast Cancer", + "ItemType": "PolygenicDiseaseLocus", + "LocusIdentifier": "070f24", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa+bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_OffspringOddsRatio": "1.00000", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa+bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_OffspringRiskWeight": "0", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa+bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_OffspringUnknownOddsRatiosWeightSum": "0" + }, + { + "DiseaseName": "Breast Cancer", + "ItemType": "PolygenicDiseaseLocus", + "LocusIdentifier": "d08516", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa+bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_OffspringOddsRatio": "Unknown", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa+bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_OffspringRiskWeight": "Unknown", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa+bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_OffspringUnknownOddsRatiosWeightSum": "Unknown" + }, + { + "DiseaseName": "Breast Cancer", + "ItemType": "PolygenicDiseaseLocus", + "LocusIdentifier": "047b84", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa+bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_OffspringOddsRatio": "Unknown", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa+bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_OffspringRiskWeight": "Unknown", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa+bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_OffspringUnknownOddsRatiosWeightSum": "Unknown" + }, + { + "ItemType": "Trait", + "TraitName": "Lactose Tolerance", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa+bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_NumberOfRulesTested": "5", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa+bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_OffspringAverageOutcomeScore_Intolerant": "0", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa+bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_OffspringAverageOutcomeScore_Tolerant": "2.50" + }, + { + "ItemType": "TraitRule", + "RuleIdentifier": "f4e02c", + "TraitName": "Lactose Tolerance", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa+bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_OffspringProbabilityOfPassingRule": "0" + }, + { + "ItemType": "TraitRule", + "RuleIdentifier": "cc3df0", + "TraitName": "Lactose Tolerance", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa+bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_OffspringProbabilityOfPassingRule": "100" + }, + { + "ItemType": "TraitRule", + "RuleIdentifier": "8170ee", + "TraitName": "Lactose Tolerance", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa+bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_OffspringProbabilityOfPassingRule": "0" + }, + { + "ItemType": "TraitRule", + "RuleIdentifier": "52425f", + "TraitName": "Lactose Tolerance", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa+bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_OffspringProbabilityOfPassingRule": "50" + }, + { + "ItemType": "TraitRule", + "RuleIdentifier": "4b5c35", + "TraitName": "Lactose Tolerance", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa+bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_OffspringProbabilityOfPassingRule": "50" + }, + { + "ItemType": "Trait", + "TraitName": "Hair Texture", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa+bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_NumberOfRulesTested": "9", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa+bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_OffspringAverageOutcomeScore_Curly": "1.50", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa+bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_OffspringAverageOutcomeScore_Straight": "3" + }, + { + "ItemType": "TraitRule", + "RuleIdentifier": "fde405", + "TraitName": "Hair Texture", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa+bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_OffspringProbabilityOfPassingRule": "50" + }, + { + "ItemType": "TraitRule", + "RuleIdentifier": "6bd1da", + "TraitName": "Hair Texture", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa+bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_OffspringProbabilityOfPassingRule": "50" + }, + { + "ItemType": "TraitRule", + "RuleIdentifier": "32e377", + "TraitName": "Hair Texture", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa+bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_OffspringProbabilityOfPassingRule": "0" + }, + { + "ItemType": "TraitRule", + "RuleIdentifier": "34e6d2", + "TraitName": "Hair Texture", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa+bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_OffspringProbabilityOfPassingRule": "50" + }, + { + "ItemType": "TraitRule", + "RuleIdentifier": "cf6cb5", + "TraitName": "Hair Texture", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa+bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_OffspringProbabilityOfPassingRule": "50" + }, + { + "ItemType": "TraitRule", + "RuleIdentifier": "2ba65b", + "TraitName": "Hair Texture", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa+bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_OffspringProbabilityOfPassingRule": "0" + }, + { + "ItemType": "TraitRule", + "RuleIdentifier": "ae3274", + "TraitName": "Hair Texture", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa+bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_OffspringProbabilityOfPassingRule": "50" + }, + { + "ItemType": "TraitRule", + "RuleIdentifier": "a546bf", + "TraitName": "Hair Texture", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa+bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_OffspringProbabilityOfPassingRule": "50" + }, + { + "ItemType": "TraitRule", + "RuleIdentifier": "b8dc0a", + "TraitName": "Hair Texture", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa+bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_OffspringProbabilityOfPassingRule": "0" + }, + { + "ItemType": "Trait", + "TraitName": "Facial Structure", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa+bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_NumberOfRulesTested": "0" + }, + { + "ItemType": "Trait", + "TraitName": "Eye Color", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa+bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_NumberOfRulesTested": "0" + }, + { + "ItemType": "Trait", + "TraitName": "Hair Color", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa+bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_NumberOfRulesTested": "0" + }, + { + "ItemType": "Trait", + "TraitName": "Skin Color", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa+bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_NumberOfRulesTested": "0" + } +] \ No newline at end of file diff --git a/internal/genetics/sampleAnalyses/SamplePerson1Analysis.json b/internal/genetics/sampleAnalyses/SamplePerson1Analysis.json new file mode 100644 index 0000000..7229b03 --- /dev/null +++ b/internal/genetics/sampleAnalyses/SamplePerson1Analysis.json @@ -0,0 +1,922 @@ +[ + { + "AnalysisType": "Person", + "AnalysisVersion": "1", + "CombinedGenomesExist": "No", + "GenomeIdentifier": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "ItemType": "Metadata" + }, + { + "DiseaseName": "Cystic Fibrosis", + "ItemType": "MonogenicDisease", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_NumberOfVariantsTested": "26", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_ProbabilityOfHavingDisease": "0", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_ProbabilityOfPassingADiseaseVariant": "50" + }, + { + "DiseaseName": "Cystic Fibrosis", + "ItemType": "MonogenicDiseaseVariant", + "VariantIdentifier": "36965d", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_HasVariant": "No;Yes", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_LocusIsPhased": "No" + }, + { + "DiseaseName": "Cystic Fibrosis", + "ItemType": "MonogenicDiseaseVariant", + "VariantIdentifier": "5706b0", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_HasVariant": "No;No", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_LocusIsPhased": "No" + }, + { + "DiseaseName": "Cystic Fibrosis", + "ItemType": "MonogenicDiseaseVariant", + "VariantIdentifier": "01a2a0", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_HasVariant": "No;No", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_LocusIsPhased": "No" + }, + { + "DiseaseName": "Cystic Fibrosis", + "ItemType": "MonogenicDiseaseVariant", + "VariantIdentifier": "bd4106", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_HasVariant": "No;No", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_LocusIsPhased": "No" + }, + { + "DiseaseName": "Cystic Fibrosis", + "ItemType": "MonogenicDiseaseVariant", + "VariantIdentifier": "4d6f38", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_HasVariant": "No;No", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_LocusIsPhased": "No" + }, + { + "DiseaseName": "Cystic Fibrosis", + "ItemType": "MonogenicDiseaseVariant", + "VariantIdentifier": "c6135a", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_HasVariant": "No;No", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_LocusIsPhased": "No" + }, + { + "DiseaseName": "Cystic Fibrosis", + "ItemType": "MonogenicDiseaseVariant", + "VariantIdentifier": "88a7f4", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_HasVariant": "No;No", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_LocusIsPhased": "No" + }, + { + "DiseaseName": "Cystic Fibrosis", + "ItemType": "MonogenicDiseaseVariant", + "VariantIdentifier": "058a4d", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_HasVariant": "Unknown" + }, + { + "DiseaseName": "Cystic Fibrosis", + "ItemType": "MonogenicDiseaseVariant", + "VariantIdentifier": "2a4ddf", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_HasVariant": "Unknown" + }, + { + "DiseaseName": "Cystic Fibrosis", + "ItemType": "MonogenicDiseaseVariant", + "VariantIdentifier": "e1bcfb", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_HasVariant": "No;No", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_LocusIsPhased": "No" + }, + { + "DiseaseName": "Cystic Fibrosis", + "ItemType": "MonogenicDiseaseVariant", + "VariantIdentifier": "c28795", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_HasVariant": "No;No", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_LocusIsPhased": "No" + }, + { + "DiseaseName": "Cystic Fibrosis", + "ItemType": "MonogenicDiseaseVariant", + "VariantIdentifier": "f1965c", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_HasVariant": "No;No", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_LocusIsPhased": "No" + }, + { + "DiseaseName": "Cystic Fibrosis", + "ItemType": "MonogenicDiseaseVariant", + "VariantIdentifier": "3420c1", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_HasVariant": "No;No", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_LocusIsPhased": "No" + }, + { + "DiseaseName": "Cystic Fibrosis", + "ItemType": "MonogenicDiseaseVariant", + "VariantIdentifier": "25d0b4", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_HasVariant": "No;No", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_LocusIsPhased": "No" + }, + { + "DiseaseName": "Cystic Fibrosis", + "ItemType": "MonogenicDiseaseVariant", + "VariantIdentifier": "139ab2", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_HasVariant": "No;No", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_LocusIsPhased": "No" + }, + { + "DiseaseName": "Cystic Fibrosis", + "ItemType": "MonogenicDiseaseVariant", + "VariantIdentifier": "f7a12e", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_HasVariant": "No;No", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_LocusIsPhased": "No" + }, + { + "DiseaseName": "Cystic Fibrosis", + "ItemType": "MonogenicDiseaseVariant", + "VariantIdentifier": "deb2e2", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_HasVariant": "No;No", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_LocusIsPhased": "No" + }, + { + "DiseaseName": "Cystic Fibrosis", + "ItemType": "MonogenicDiseaseVariant", + "VariantIdentifier": "884cf0", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_HasVariant": "No;No", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_LocusIsPhased": "No" + }, + { + "DiseaseName": "Cystic Fibrosis", + "ItemType": "MonogenicDiseaseVariant", + "VariantIdentifier": "b9fad1", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_HasVariant": "No;No", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_LocusIsPhased": "No" + }, + { + "DiseaseName": "Cystic Fibrosis", + "ItemType": "MonogenicDiseaseVariant", + "VariantIdentifier": "f2448d", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_HasVariant": "No;No", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_LocusIsPhased": "No" + }, + { + "DiseaseName": "Cystic Fibrosis", + "ItemType": "MonogenicDiseaseVariant", + "VariantIdentifier": "09b96f", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_HasVariant": "No;No", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_LocusIsPhased": "No" + }, + { + "DiseaseName": "Cystic Fibrosis", + "ItemType": "MonogenicDiseaseVariant", + "VariantIdentifier": "2f8651", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_HasVariant": "Unknown" + }, + { + "DiseaseName": "Cystic Fibrosis", + "ItemType": "MonogenicDiseaseVariant", + "VariantIdentifier": "46efe1", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_HasVariant": "Unknown" + }, + { + "DiseaseName": "Cystic Fibrosis", + "ItemType": "MonogenicDiseaseVariant", + "VariantIdentifier": "528406", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_HasVariant": "No;No", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_LocusIsPhased": "No" + }, + { + "DiseaseName": "Cystic Fibrosis", + "ItemType": "MonogenicDiseaseVariant", + "VariantIdentifier": "e8e8fc", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_HasVariant": "No;No", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_LocusIsPhased": "No" + }, + { + "DiseaseName": "Cystic Fibrosis", + "ItemType": "MonogenicDiseaseVariant", + "VariantIdentifier": "e60633", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_HasVariant": "No;No", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_LocusIsPhased": "No" + }, + { + "DiseaseName": "Cystic Fibrosis", + "ItemType": "MonogenicDiseaseVariant", + "VariantIdentifier": "d72d30", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_HasVariant": "No;No", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_LocusIsPhased": "No" + }, + { + "DiseaseName": "Cystic Fibrosis", + "ItemType": "MonogenicDiseaseVariant", + "VariantIdentifier": "a3b068", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_HasVariant": "Unknown" + }, + { + "DiseaseName": "Cystic Fibrosis", + "ItemType": "MonogenicDiseaseVariant", + "VariantIdentifier": "770086", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_HasVariant": "No;No", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_LocusIsPhased": "No" + }, + { + "DiseaseName": "Cystic Fibrosis", + "ItemType": "MonogenicDiseaseVariant", + "VariantIdentifier": "a00f11", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_HasVariant": "No;No", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_LocusIsPhased": "No" + }, + { + "DiseaseName": "Cystic Fibrosis", + "ItemType": "MonogenicDiseaseVariant", + "VariantIdentifier": "79d73b", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_HasVariant": "No;No", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_LocusIsPhased": "No" + }, + { + "DiseaseName": "Sickle Cell Anemia", + "ItemType": "MonogenicDisease", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_NumberOfVariantsTested": "1", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_ProbabilityOfHavingDisease": "0", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_ProbabilityOfPassingADiseaseVariant": "0" + }, + { + "DiseaseName": "Sickle Cell Anemia", + "ItemType": "MonogenicDiseaseVariant", + "VariantIdentifier": "50e857", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_HasVariant": "No;No", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_LocusIsPhased": "No" + }, + { + "DiseaseName": "Breast Cancer", + "ItemType": "PolygenicDisease", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_NumberOfLociTested": "24", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_RiskScore": "1" + }, + { + "DiseaseName": "Breast Cancer", + "ItemType": "PolygenicDiseaseLocus", + "LocusIdentifier": "d7891c", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_LocusBasePair": "T;T", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_OddsRatio": "1.00000", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_RiskWeight": "0" + }, + { + "DiseaseName": "Breast Cancer", + "ItemType": "PolygenicDiseaseLocus", + "LocusIdentifier": "41c164", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_LocusBasePair": "G;G", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_OddsRatio": "1.00000", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_RiskWeight": "0" + }, + { + "DiseaseName": "Breast Cancer", + "ItemType": "PolygenicDiseaseLocus", + "LocusIdentifier": "f3a097", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_LocusBasePair": "T;C", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_OddsRatio": "1.00000", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_RiskWeight": "0" + }, + { + "DiseaseName": "Breast Cancer", + "ItemType": "PolygenicDiseaseLocus", + "LocusIdentifier": "d4626f", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_LocusBasePair": "A;C", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_OddsRatio": "1.14000", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_RiskWeight": "1" + }, + { + "DiseaseName": "Breast Cancer", + "ItemType": "PolygenicDiseaseLocus", + "LocusIdentifier": "84aaa4", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_LocusBasePair": "A;A", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_OddsRatio": "1.00000", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_RiskWeight": "0" + }, + { + "DiseaseName": "Breast Cancer", + "ItemType": "PolygenicDiseaseLocus", + "LocusIdentifier": "c8de7a", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_LocusBasePair": "C;C", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_OddsRatio": "1.72000", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_RiskWeight": "2" + }, + { + "DiseaseName": "Breast Cancer", + "ItemType": "PolygenicDiseaseLocus", + "LocusIdentifier": "d30087", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_LocusBasePair": "C;C", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_OddsRatio": "1.00000", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_RiskWeight": "0" + }, + { + "DiseaseName": "Breast Cancer", + "ItemType": "PolygenicDiseaseLocus", + "LocusIdentifier": "cafa72", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_LocusBasePair": "T;T", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_OddsRatio": "1.00000", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_RiskWeight": "0" + }, + { + "DiseaseName": "Breast Cancer", + "ItemType": "PolygenicDiseaseLocus", + "LocusIdentifier": "8f671c", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_LocusBasePair": "T;T", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_OddsRatio": "1.00000", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_RiskWeight": "0" + }, + { + "DiseaseName": "Breast Cancer", + "ItemType": "PolygenicDiseaseLocus", + "LocusIdentifier": "b3e49a", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_LocusBasePair": "C;C", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_OddsRatio": "1.00000", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_RiskWeight": "0" + }, + { + "DiseaseName": "Breast Cancer", + "ItemType": "PolygenicDiseaseLocus", + "LocusIdentifier": "8b0b02", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_LocusBasePair": "C;C", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_OddsRatio": "1.00000", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_RiskWeight": "0" + }, + { + "DiseaseName": "Breast Cancer", + "ItemType": "PolygenicDiseaseLocus", + "LocusIdentifier": "25cafc", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_LocusBasePair": "T;T", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_OddsRatio": "1.00000", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_RiskWeight": "0" + }, + { + "DiseaseName": "Breast Cancer", + "ItemType": "PolygenicDiseaseLocus", + "LocusIdentifier": "34c7e5", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_LocusBasePair": "A;A", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_OddsRatio": "1.00000", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_RiskWeight": "0" + }, + { + "DiseaseName": "Breast Cancer", + "ItemType": "PolygenicDiseaseLocus", + "LocusIdentifier": "60ce27", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_LocusBasePair": "A;A", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_OddsRatio": "1.00000", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_RiskWeight": "0" + }, + { + "DiseaseName": "Breast Cancer", + "ItemType": "PolygenicDiseaseLocus", + "LocusIdentifier": "328cdf", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_LocusBasePair": "A;A", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_OddsRatio": "1.00000", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_RiskWeight": "0" + }, + { + "DiseaseName": "Breast Cancer", + "ItemType": "PolygenicDiseaseLocus", + "LocusIdentifier": "849bc7", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_LocusBasePair": "A;A", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_OddsRatio": "1.00000", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_RiskWeight": "0" + }, + { + "DiseaseName": "Breast Cancer", + "ItemType": "PolygenicDiseaseLocus", + "LocusIdentifier": "5af5e3", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_LocusBasePair": "G;G", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_OddsRatio": "1.00000", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_RiskWeight": "0" + }, + { + "DiseaseName": "Breast Cancer", + "ItemType": "PolygenicDiseaseLocus", + "LocusIdentifier": "c354fa", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_LocusBasePair": "C;C", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_OddsRatio": "1.00000", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_RiskWeight": "0" + }, + { + "DiseaseName": "Breast Cancer", + "ItemType": "PolygenicDiseaseLocus", + "LocusIdentifier": "eedc23", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_LocusBasePair": "T;T", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_OddsRatio": "1.00000", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_RiskWeight": "0" + }, + { + "DiseaseName": "Breast Cancer", + "ItemType": "PolygenicDiseaseLocus", + "LocusIdentifier": "2ee027", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_LocusBasePair": "C;C", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_OddsRatio": "1.00000", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_RiskWeight": "0" + }, + { + "DiseaseName": "Breast Cancer", + "ItemType": "PolygenicDiseaseLocus", + "LocusIdentifier": "fc4bab", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_LocusBasePair": "T;T", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_OddsRatio": "1.00000", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_RiskWeight": "0" + }, + { + "DiseaseName": "Breast Cancer", + "ItemType": "PolygenicDiseaseLocus", + "LocusIdentifier": "f8b225", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_LocusBasePair": "C;C", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_OddsRatio": "1.00000", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_RiskWeight": "0" + }, + { + "DiseaseName": "Breast Cancer", + "ItemType": "PolygenicDiseaseLocus", + "LocusIdentifier": "4a072c", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_LocusBasePair": "C;C", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_OddsRatio": "1.00000", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_RiskWeight": "0" + }, + { + "DiseaseName": "Breast Cancer", + "ItemType": "PolygenicDiseaseLocus", + "LocusIdentifier": "070f24", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_LocusBasePair": "C;C", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_OddsRatio": "1.00000", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_RiskWeight": "0" + }, + { + "DiseaseName": "Breast Cancer", + "ItemType": "PolygenicDiseaseLocus", + "LocusIdentifier": "d08516", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_LocusBasePair": "Unknown", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_OddsRatio": "Unknown", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_RiskWeight": "Unknown" + }, + { + "DiseaseName": "Breast Cancer", + "ItemType": "PolygenicDiseaseLocus", + "LocusIdentifier": "047b84", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_LocusBasePair": "Unknown", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_OddsRatio": "Unknown", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_RiskWeight": "Unknown" + }, + { + "ItemType": "Trait", + "TraitName": "Lactose Tolerance", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_LocusIsPhased_rs182549": "No", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_LocusIsPhased_rs4988235": "No", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_LocusValue_rs182549": "T;C", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_LocusValue_rs4988235": "A;G", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_NumberOfRulesTested": "5", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_OutcomeScore_Intolerant": "0", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_OutcomeScore_Tolerant": "2" + }, + { + "ItemType": "TraitRule", + "RuleIdentifier": "f4e02c", + "TraitName": "Lactose Tolerance", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_PassesRule": "No", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_RuleLocusBasePair_43bf19": "T;C" + }, + { + "ItemType": "TraitRule", + "RuleIdentifier": "cc3df0", + "TraitName": "Lactose Tolerance", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_PassesRule": "Yes", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_RuleLocusBasePair_a7feff": "T;C" + }, + { + "ItemType": "TraitRule", + "RuleIdentifier": "8170ee", + "TraitName": "Lactose Tolerance", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_PassesRule": "No", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_RuleLocusBasePair_da6b04": "A;G" + }, + { + "ItemType": "TraitRule", + "RuleIdentifier": "52425f", + "TraitName": "Lactose Tolerance", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_PassesRule": "Yes", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_RuleLocusBasePair_176dde": "A;G" + }, + { + "ItemType": "TraitRule", + "RuleIdentifier": "4b5c35", + "TraitName": "Lactose Tolerance", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_PassesRule": "No", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_RuleLocusBasePair_164acb": "A;G" + }, + { + "ItemType": "Trait", + "TraitName": "Hair Texture", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_LocusIsPhased_rs11803731": "No", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_LocusIsPhased_rs17646946": "No", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_LocusIsPhased_rs7349332": "No", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_LocusValue_rs11803731": "A;T", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_LocusValue_rs17646946": "A;G", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_LocusValue_rs7349332": "T;C", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_NumberOfRulesTested": "9", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_OutcomeScore_Curly": "3", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_OutcomeScore_Straight": "0" + }, + { + "ItemType": "TraitRule", + "RuleIdentifier": "fde405", + "TraitName": "Hair Texture", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_PassesRule": "No", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_RuleLocusBasePair_0e06e2": "T;C" + }, + { + "ItemType": "TraitRule", + "RuleIdentifier": "6bd1da", + "TraitName": "Hair Texture", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_PassesRule": "Yes", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_RuleLocusBasePair_2da1b7": "T;C" + }, + { + "ItemType": "TraitRule", + "RuleIdentifier": "32e377", + "TraitName": "Hair Texture", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_PassesRule": "No", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_RuleLocusBasePair_c6760e": "T;C" + }, + { + "ItemType": "TraitRule", + "RuleIdentifier": "34e6d2", + "TraitName": "Hair Texture", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_PassesRule": "No", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_RuleLocusBasePair_9079c9": "A;T" + }, + { + "ItemType": "TraitRule", + "RuleIdentifier": "cf6cb5", + "TraitName": "Hair Texture", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_PassesRule": "Yes", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_RuleLocusBasePair_d0aad3": "A;T" + }, + { + "ItemType": "TraitRule", + "RuleIdentifier": "2ba65b", + "TraitName": "Hair Texture", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_PassesRule": "No", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_RuleLocusBasePair_f554b5": "A;T" + }, + { + "ItemType": "TraitRule", + "RuleIdentifier": "ae3274", + "TraitName": "Hair Texture", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_PassesRule": "No", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_RuleLocusBasePair_f500c2": "A;G" + }, + { + "ItemType": "TraitRule", + "RuleIdentifier": "a546bf", + "TraitName": "Hair Texture", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_PassesRule": "Yes", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_RuleLocusBasePair_f1144a": "A;G" + }, + { + "ItemType": "TraitRule", + "RuleIdentifier": "b8dc0a", + "TraitName": "Hair Texture", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_PassesRule": "No", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_RuleLocusBasePair_468bb3": "A;G" + }, + { + "ItemType": "Trait", + "TraitName": "Facial Structure", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_LocusIsPhased_rs1005999": "No", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_LocusIsPhased_rs10237838": "No", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_LocusIsPhased_rs11237982": "No", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_LocusIsPhased_rs12694574": "No", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_LocusIsPhased_rs13097965": "No", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_LocusIsPhased_rs17447439": "No", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_LocusIsPhased_rs1747677": "No", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_LocusIsPhased_rs1978859": "No", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_LocusIsPhased_rs2034127": "No", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_LocusIsPhased_rs2327089": "No", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_LocusIsPhased_rs2832438": "No", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_LocusIsPhased_rs2894450": "No", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_LocusIsPhased_rs4552364": "No", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_LocusIsPhased_rs6020940": "No", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_LocusIsPhased_rs6478394": "No", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_LocusIsPhased_rs7516150": "No", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_LocusIsPhased_rs7552331": "No", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_LocusIsPhased_rs7965082": "No", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_LocusIsPhased_rs805722": "No", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_LocusIsPhased_rs9858909": "No", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_LocusValue_rs1005999": "T;T", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_LocusValue_rs1008591": "Unknown", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_LocusValue_rs1015092": "Unknown", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_LocusValue_rs1019212": "Unknown", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_LocusValue_rs10234405": "Unknown", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_LocusValue_rs10237319": "Unknown", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_LocusValue_rs10237488": "Unknown", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_LocusValue_rs10237838": "C;C", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_LocusValue_rs10265937": "Unknown", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_LocusValue_rs10266101": "Unknown", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_LocusValue_rs10278187": "Unknown", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_LocusValue_rs10485860": "Unknown", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_LocusValue_rs10843104": "Unknown", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_LocusValue_rs11191909": "Unknown", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_LocusValue_rs11237982": "T;C", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_LocusValue_rs1158810": "Unknown", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_LocusValue_rs11604811": "Unknown", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_LocusValue_rs12155314": "Unknown", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_LocusValue_rs12358982": "Unknown", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_LocusValue_rs12437560": "Unknown", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_LocusValue_rs12694574": "T;C", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_LocusValue_rs13097965": "T;C", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_LocusValue_rs13098099": "Unknown", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_LocusValue_rs1562005": "Unknown", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_LocusValue_rs1562006": "Unknown", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_LocusValue_rs1572037": "Unknown", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_LocusValue_rs16863422": "Unknown", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_LocusValue_rs16977002": "Unknown", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_LocusValue_rs16977008": "Unknown", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_LocusValue_rs16977009": "Unknown", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_LocusValue_rs17252053": "Unknown", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_LocusValue_rs17447439": "A;A", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_LocusValue_rs1747677": "C;C", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_LocusValue_rs1887276": "Unknown", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_LocusValue_rs1939697": "Unknown", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_LocusValue_rs1939707": "Unknown", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_LocusValue_rs1978859": "T;C", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_LocusValue_rs1999527": "Unknown", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_LocusValue_rs2034127": "G;G", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_LocusValue_rs2034128": "Unknown", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_LocusValue_rs2034129": "Unknown", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_LocusValue_rs2108166": "Unknown", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_LocusValue_rs2168809": "Unknown", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_LocusValue_rs2274107": "Unknown", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_LocusValue_rs2327089": "T;T", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_LocusValue_rs2327101": "Unknown", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_LocusValue_rs2342494": "Unknown", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_LocusValue_rs2422239": "Unknown", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_LocusValue_rs2422241": "Unknown", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_LocusValue_rs2832438": "T;C", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_LocusValue_rs2894450": "A;G", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_LocusValue_rs397723": "Unknown", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_LocusValue_rs4053148": "Unknown", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_LocusValue_rs4353811": "Unknown", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_LocusValue_rs4433629": "Unknown", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_LocusValue_rs4552364": "T;C", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_LocusValue_rs4633993": "Unknown", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_LocusValue_rs4648379": "Unknown", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_LocusValue_rs4648477": "Unknown", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_LocusValue_rs4648478": "Unknown", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_LocusValue_rs4793389": "Unknown", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_LocusValue_rs6020940": "A;A", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_LocusValue_rs6020957": "Unknown", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_LocusValue_rs6039266": "Unknown", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_LocusValue_rs6039272": "Unknown", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_LocusValue_rs6056066": "Unknown", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_LocusValue_rs6056119": "Unknown", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_LocusValue_rs6056126": "Unknown", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_LocusValue_rs6462544": "Unknown", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_LocusValue_rs6462562": "Unknown", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_LocusValue_rs6478394": "A;G", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_LocusValue_rs6555969": "Unknown", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_LocusValue_rs6749293": "Unknown", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_LocusValue_rs6795519": "Unknown", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_LocusValue_rs6950754": "Unknown", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_LocusValue_rs717463": "Unknown", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_LocusValue_rs7214306": "Unknown", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_LocusValue_rs7516150": "C;C", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_LocusValue_rs7552331": "G;G", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_LocusValue_rs7617069": "Unknown", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_LocusValue_rs7628370": "Unknown", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_LocusValue_rs7640340": "Unknown", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_LocusValue_rs7779616": "Unknown", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_LocusValue_rs7781059": "Unknown", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_LocusValue_rs7799331": "Unknown", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_LocusValue_rs7803030": "Unknown", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_LocusValue_rs7807181": "Unknown", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_LocusValue_rs7965082": "C;C", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_LocusValue_rs7966317": "Unknown", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_LocusValue_rs805693": "Unknown", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_LocusValue_rs805694": "Unknown", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_LocusValue_rs805722": "C;C", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_LocusValue_rs8079498": "Unknown", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_LocusValue_rs875143": "Unknown", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_LocusValue_rs894883": "Unknown", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_LocusValue_rs911015": "Unknown", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_LocusValue_rs911020": "Unknown", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_LocusValue_rs9692219": "Unknown", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_LocusValue_rs974448": "Unknown", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_LocusValue_rs975633": "Unknown", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_LocusValue_rs9858909": "G;G", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_LocusValue_rs9971100": "Unknown", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_NumberOfRulesTested": "0" + }, + { + "ItemType": "Trait", + "TraitName": "Eye Color", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_LocusIsPhased_rs1003719": "No", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_LocusIsPhased_rs1105879": "No", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_LocusIsPhased_rs1126809": "No", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_LocusIsPhased_rs1129038": "No", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_LocusIsPhased_rs11957757": "No", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_LocusIsPhased_rs121908120": "No", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_LocusIsPhased_rs12203592": "No", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_LocusIsPhased_rs12452184": "No", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_LocusIsPhased_rs12593929": "No", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_LocusIsPhased_rs12896399": "No", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_LocusIsPhased_rs12906280": "No", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_LocusIsPhased_rs12913823": "No", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_LocusIsPhased_rs12913832": "No", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_LocusIsPhased_rs1325127": "No", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_LocusIsPhased_rs1393350": "No", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_LocusIsPhased_rs1408799": "No", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_LocusIsPhased_rs1426654": "No", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_LocusIsPhased_rs1667394": "No", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_LocusIsPhased_rs16891982": "No", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_LocusIsPhased_rs1800401": "No", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_LocusIsPhased_rs1800407": "No", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_LocusIsPhased_rs1800414": "No", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_LocusIsPhased_rs2070959": "No", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_LocusIsPhased_rs2733832": "No", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_LocusIsPhased_rs2748901": "No", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_LocusIsPhased_rs351385": "No", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_LocusIsPhased_rs3794604": "No", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_LocusIsPhased_rs4778138": "No", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_LocusIsPhased_rs4778218": "No", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_LocusIsPhased_rs4778241": "No", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_LocusIsPhased_rs4911414": "No", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_LocusIsPhased_rs4911442": "No", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_LocusIsPhased_rs6058017": "No", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_LocusIsPhased_rs6828137": "No", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_LocusIsPhased_rs6910861": "No", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_LocusIsPhased_rs7174027": "No", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_LocusIsPhased_rs7183877": "No", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_LocusIsPhased_rs7495174": "No", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_LocusIsPhased_rs8028689": "No", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_LocusIsPhased_rs892839": "No", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_LocusIsPhased_rs916977": "No", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_LocusIsPhased_rs9782955": "No", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_LocusIsPhased_rs9894429": "No", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_LocusIsPhased_rs9971729": "No", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_LocusValue_rs1003719": "G;G", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_LocusValue_rs10209564": "Unknown", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_LocusValue_rs1105879": "A;C", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_LocusValue_rs1126809": "A;G", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_LocusValue_rs112747614": "Unknown", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_LocusValue_rs1129038": "T;C", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_LocusValue_rs11631797": "Unknown", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_LocusValue_rs116359091": "Unknown", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_LocusValue_rs11957757": "G;G", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_LocusValue_rs121908120": "T;T", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_LocusValue_rs12203592": "C;C", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_LocusValue_rs12335410": "Unknown", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_LocusValue_rs12452184": "C;C", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_LocusValue_rs12543326": "Unknown", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_LocusValue_rs12552712": "Unknown", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_LocusValue_rs12593929": "A;G", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_LocusValue_rs12614022": "Unknown", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_LocusValue_rs12896399": "T;T", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_LocusValue_rs12906280": "G;G", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_LocusValue_rs12913823": "T;C", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_LocusValue_rs12913832": "A;G", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_LocusValue_rs13016869": "Unknown", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_LocusValue_rs1325127": "T;T", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_LocusValue_rs13297008": "Unknown", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_LocusValue_rs138777265": "Unknown", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_LocusValue_rs1393350": "A;G", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_LocusValue_rs1408799": "C;C", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_LocusValue_rs141318671": "Unknown", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_LocusValue_rs1426654": "A;A", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_LocusValue_rs147068120": "Unknown", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_LocusValue_rs1667394": "T;C", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_LocusValue_rs16891982": "G;G", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_LocusValue_rs17184180": "Unknown", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_LocusValue_rs1800401": "G;G", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_LocusValue_rs1800407": "C;C", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_LocusValue_rs1800414": "T;T", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_LocusValue_rs2070959": "A;G", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_LocusValue_rs2095645": "Unknown", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_LocusValue_rs2238289": "Unknown", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_LocusValue_rs2240203": "Unknown", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_LocusValue_rs2252893": "Unknown", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_LocusValue_rs2385028": "Unknown", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_LocusValue_rs2733832": "T;T", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_LocusValue_rs2748901": "A;G", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_LocusValue_rs2835621": "Unknown", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_LocusValue_rs2835630": "Unknown", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_LocusValue_rs2835660": "Unknown", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_LocusValue_rs2854746": "Unknown", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_LocusValue_rs341147": "Unknown", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_LocusValue_rs348613": "Unknown", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_LocusValue_rs35051352": "Unknown", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_LocusValue_rs351385": "A;G", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_LocusValue_rs3768056": "Unknown", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_LocusValue_rs3794604": "T;C", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_LocusValue_rs3809761": "Unknown", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_LocusValue_rs3912104": "Unknown", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_LocusValue_rs3935591": "Unknown", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_LocusValue_rs3940272": "Unknown", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_LocusValue_rs4521336": "Unknown", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_LocusValue_rs4778138": "A;G", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_LocusValue_rs4778218": "G;G", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_LocusValue_rs4778241": "A;C", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_LocusValue_rs4790309": "Unknown", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_LocusValue_rs4911414": "T;G", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_LocusValue_rs4911442": "A;A", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_LocusValue_rs6058017": "A;A", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_LocusValue_rs622330": "Unknown", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_LocusValue_rs62330021": "Unknown", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_LocusValue_rs6420484": "Unknown", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_LocusValue_rs6693258": "Unknown", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_LocusValue_rs6828137": "G;G", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_LocusValue_rs6910861": "A;G", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_LocusValue_rs6944702": "Unknown", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_LocusValue_rs6997494": "Unknown", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_LocusValue_rs7170852": "Unknown", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_LocusValue_rs7174027": "A;G", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_LocusValue_rs7183877": "C;C", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_LocusValue_rs7219915": "Unknown", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_LocusValue_rs72777200": "Unknown", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_LocusValue_rs7277820": "Unknown", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_LocusValue_rs728405": "Unknown", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_LocusValue_rs72928978": "Unknown", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_LocusValue_rs73488486": "Unknown", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_LocusValue_rs74409360": "Unknown", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_LocusValue_rs7495174": "A;G", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_LocusValue_rs790464": "Unknown", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_LocusValue_rs8028689": "T;C", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_LocusValue_rs892839": "G;G", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_LocusValue_rs916977": "T;C", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_LocusValue_rs9301973": "Unknown", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_LocusValue_rs9782955": "C;C", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_LocusValue_rs9894429": "C;C", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_LocusValue_rs989869": "Unknown", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_LocusValue_rs9971729": "A;C", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_NumberOfRulesTested": "0" + }, + { + "ItemType": "Trait", + "TraitName": "Hair Color", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_LocusIsPhased_rs11636232": "No", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_LocusIsPhased_rs12203592": "No", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_LocusIsPhased_rs12821256": "No", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_LocusIsPhased_rs12896399": "No", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_LocusIsPhased_rs12913832": "No", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_LocusIsPhased_rs1540771": "No", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_LocusIsPhased_rs1667394": "No", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_LocusIsPhased_rs1805007": "No", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_LocusIsPhased_rs1805008": "No", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_LocusIsPhased_rs35264875": "No", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_LocusIsPhased_rs3829241": "No", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_LocusIsPhased_rs6918152": "No", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_LocusIsPhased_rs7174027": "No", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_LocusIsPhased_rs7183877": "No", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_LocusIsPhased_rs7495174": "No", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_LocusIsPhased_rs8028689": "No", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_LocusValue_rs11636232": "T;C", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_LocusValue_rs11855019": "Unknown", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_LocusValue_rs12203592": "C;C", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_LocusValue_rs12821256": "T;T", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_LocusValue_rs12896399": "T;T", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_LocusValue_rs12913832": "A;G", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_LocusValue_rs1540771": "C;C", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_LocusValue_rs1667394": "T;C", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_LocusValue_rs1805007": "C;C", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_LocusValue_rs1805008": "C;C", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_LocusValue_rs28777": "Unknown", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_LocusValue_rs35264875": "T;A", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_LocusValue_rs3829241": "G;G", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_LocusValue_rs4778211": "Unknown", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_LocusValue_rs6918152": "A;G", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_LocusValue_rs7174027": "A;G", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_LocusValue_rs7183877": "C;C", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_LocusValue_rs7495174": "A;G", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_LocusValue_rs8028689": "T;C", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_LocusValue_rs8039195": "Unknown", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_NumberOfRulesTested": "0" + }, + { + "ItemType": "Trait", + "TraitName": "Skin Color", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_LocusIsPhased_rs1042602": "No", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_LocusIsPhased_rs1126809": "No", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_LocusIsPhased_rs12203592": "No", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_LocusIsPhased_rs1426654": "No", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_LocusIsPhased_rs16891982": "No", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_LocusIsPhased_rs1834640": "No", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_LocusIsPhased_rs26722": "No", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_LocusIsPhased_rs2762462": "No", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_LocusIsPhased_rs642742": "No", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_LocusIsPhased_rs937171": "No", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_LocusValue_rs10424065": "Unknown", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_LocusValue_rs1042602": "C;C", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_LocusValue_rs1126809": "A;G", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_LocusValue_rs12203592": "C;C", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_LocusValue_rs142317543": "Unknown", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_LocusValue_rs1426654": "A;A", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_LocusValue_rs16891982": "G;G", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_LocusValue_rs1800422": "Unknown", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_LocusValue_rs1834640": "A;A", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_LocusValue_rs26722": "C;C", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_LocusValue_rs2762462": "C;C", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_LocusValue_rs3212368": "Unknown", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_LocusValue_rs3212369": "Unknown", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_LocusValue_rs642742": "T;C", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_LocusValue_rs7176696": "Unknown", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_LocusValue_rs7182710": "Unknown", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_LocusValue_rs784416": "Unknown", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_LocusValue_rs937171": "G;G", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_NumberOfRulesTested": "0" + } +] \ No newline at end of file diff --git a/internal/genetics/sampleAnalyses/SamplePerson2Analysis.json b/internal/genetics/sampleAnalyses/SamplePerson2Analysis.json new file mode 100644 index 0000000..873cd30 --- /dev/null +++ b/internal/genetics/sampleAnalyses/SamplePerson2Analysis.json @@ -0,0 +1,923 @@ +[ + { + "AnalysisType": "Person", + "AnalysisVersion": "1", + "CombinedGenomesExist": "No", + "GenomeIdentifier": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", + "ItemType": "Metadata" + }, + { + "DiseaseName": "Cystic Fibrosis", + "ItemType": "MonogenicDisease", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_NumberOfVariantsTested": "26", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_ProbabilityOfHavingDisease": "0", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_ProbabilityOfPassingADiseaseVariant": "50" + }, + { + "DiseaseName": "Cystic Fibrosis", + "ItemType": "MonogenicDiseaseVariant", + "VariantIdentifier": "36965d", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_HasVariant": "No;No", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_LocusIsPhased": "No" + }, + { + "DiseaseName": "Cystic Fibrosis", + "ItemType": "MonogenicDiseaseVariant", + "VariantIdentifier": "5706b0", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_HasVariant": "No;No", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_LocusIsPhased": "No" + }, + { + "DiseaseName": "Cystic Fibrosis", + "ItemType": "MonogenicDiseaseVariant", + "VariantIdentifier": "01a2a0", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_HasVariant": "No;No", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_LocusIsPhased": "No" + }, + { + "DiseaseName": "Cystic Fibrosis", + "ItemType": "MonogenicDiseaseVariant", + "VariantIdentifier": "bd4106", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_HasVariant": "No;No", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_LocusIsPhased": "No" + }, + { + "DiseaseName": "Cystic Fibrosis", + "ItemType": "MonogenicDiseaseVariant", + "VariantIdentifier": "4d6f38", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_HasVariant": "No;No", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_LocusIsPhased": "No" + }, + { + "DiseaseName": "Cystic Fibrosis", + "ItemType": "MonogenicDiseaseVariant", + "VariantIdentifier": "c6135a", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_HasVariant": "Yes;No", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_LocusIsPhased": "No" + }, + { + "DiseaseName": "Cystic Fibrosis", + "ItemType": "MonogenicDiseaseVariant", + "VariantIdentifier": "88a7f4", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_HasVariant": "No;No", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_LocusIsPhased": "No" + }, + { + "DiseaseName": "Cystic Fibrosis", + "ItemType": "MonogenicDiseaseVariant", + "VariantIdentifier": "058a4d", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_HasVariant": "Unknown" + }, + { + "DiseaseName": "Cystic Fibrosis", + "ItemType": "MonogenicDiseaseVariant", + "VariantIdentifier": "2a4ddf", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_HasVariant": "Unknown" + }, + { + "DiseaseName": "Cystic Fibrosis", + "ItemType": "MonogenicDiseaseVariant", + "VariantIdentifier": "e1bcfb", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_HasVariant": "No;No", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_LocusIsPhased": "No" + }, + { + "DiseaseName": "Cystic Fibrosis", + "ItemType": "MonogenicDiseaseVariant", + "VariantIdentifier": "c28795", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_HasVariant": "No;No", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_LocusIsPhased": "No" + }, + { + "DiseaseName": "Cystic Fibrosis", + "ItemType": "MonogenicDiseaseVariant", + "VariantIdentifier": "f1965c", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_HasVariant": "No;No", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_LocusIsPhased": "No" + }, + { + "DiseaseName": "Cystic Fibrosis", + "ItemType": "MonogenicDiseaseVariant", + "VariantIdentifier": "3420c1", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_HasVariant": "No;No", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_LocusIsPhased": "No" + }, + { + "DiseaseName": "Cystic Fibrosis", + "ItemType": "MonogenicDiseaseVariant", + "VariantIdentifier": "25d0b4", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_HasVariant": "No;No", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_LocusIsPhased": "No" + }, + { + "DiseaseName": "Cystic Fibrosis", + "ItemType": "MonogenicDiseaseVariant", + "VariantIdentifier": "139ab2", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_HasVariant": "No;No", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_LocusIsPhased": "No" + }, + { + "DiseaseName": "Cystic Fibrosis", + "ItemType": "MonogenicDiseaseVariant", + "VariantIdentifier": "f7a12e", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_HasVariant": "No;No", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_LocusIsPhased": "No" + }, + { + "DiseaseName": "Cystic Fibrosis", + "ItemType": "MonogenicDiseaseVariant", + "VariantIdentifier": "deb2e2", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_HasVariant": "No;No", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_LocusIsPhased": "No" + }, + { + "DiseaseName": "Cystic Fibrosis", + "ItemType": "MonogenicDiseaseVariant", + "VariantIdentifier": "884cf0", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_HasVariant": "No;No", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_LocusIsPhased": "No" + }, + { + "DiseaseName": "Cystic Fibrosis", + "ItemType": "MonogenicDiseaseVariant", + "VariantIdentifier": "b9fad1", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_HasVariant": "No;No", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_LocusIsPhased": "No" + }, + { + "DiseaseName": "Cystic Fibrosis", + "ItemType": "MonogenicDiseaseVariant", + "VariantIdentifier": "f2448d", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_HasVariant": "No;No", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_LocusIsPhased": "No" + }, + { + "DiseaseName": "Cystic Fibrosis", + "ItemType": "MonogenicDiseaseVariant", + "VariantIdentifier": "09b96f", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_HasVariant": "No;No", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_LocusIsPhased": "No" + }, + { + "DiseaseName": "Cystic Fibrosis", + "ItemType": "MonogenicDiseaseVariant", + "VariantIdentifier": "2f8651", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_HasVariant": "Unknown" + }, + { + "DiseaseName": "Cystic Fibrosis", + "ItemType": "MonogenicDiseaseVariant", + "VariantIdentifier": "46efe1", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_HasVariant": "Unknown" + }, + { + "DiseaseName": "Cystic Fibrosis", + "ItemType": "MonogenicDiseaseVariant", + "VariantIdentifier": "528406", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_HasVariant": "No;No", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_LocusIsPhased": "No" + }, + { + "DiseaseName": "Cystic Fibrosis", + "ItemType": "MonogenicDiseaseVariant", + "VariantIdentifier": "e8e8fc", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_HasVariant": "No;No", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_LocusIsPhased": "No" + }, + { + "DiseaseName": "Cystic Fibrosis", + "ItemType": "MonogenicDiseaseVariant", + "VariantIdentifier": "e60633", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_HasVariant": "No;No", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_LocusIsPhased": "No" + }, + { + "DiseaseName": "Cystic Fibrosis", + "ItemType": "MonogenicDiseaseVariant", + "VariantIdentifier": "d72d30", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_HasVariant": "No;No", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_LocusIsPhased": "No" + }, + { + "DiseaseName": "Cystic Fibrosis", + "ItemType": "MonogenicDiseaseVariant", + "VariantIdentifier": "a3b068", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_HasVariant": "Unknown" + }, + { + "DiseaseName": "Cystic Fibrosis", + "ItemType": "MonogenicDiseaseVariant", + "VariantIdentifier": "770086", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_HasVariant": "No;No", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_LocusIsPhased": "No" + }, + { + "DiseaseName": "Cystic Fibrosis", + "ItemType": "MonogenicDiseaseVariant", + "VariantIdentifier": "a00f11", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_HasVariant": "No;No", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_LocusIsPhased": "No" + }, + { + "DiseaseName": "Cystic Fibrosis", + "ItemType": "MonogenicDiseaseVariant", + "VariantIdentifier": "79d73b", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_HasVariant": "No;No", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_LocusIsPhased": "No" + }, + { + "DiseaseName": "Sickle Cell Anemia", + "ItemType": "MonogenicDisease", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_NumberOfVariantsTested": "1", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_ProbabilityOfHavingDisease": "0", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_ProbabilityOfPassingADiseaseVariant": "0" + }, + { + "DiseaseName": "Sickle Cell Anemia", + "ItemType": "MonogenicDiseaseVariant", + "VariantIdentifier": "50e857", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_HasVariant": "No;No", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_LocusIsPhased": "No" + }, + { + "DiseaseName": "Breast Cancer", + "ItemType": "PolygenicDisease", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_NumberOfLociTested": "24", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_RiskScore": "1" + }, + { + "DiseaseName": "Breast Cancer", + "ItemType": "PolygenicDiseaseLocus", + "LocusIdentifier": "d7891c", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_LocusBasePair": "C;C", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_OddsRatio": "1.28000", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_RiskWeight": "1" + }, + { + "DiseaseName": "Breast Cancer", + "ItemType": "PolygenicDiseaseLocus", + "LocusIdentifier": "41c164", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_LocusBasePair": "G;G", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_OddsRatio": "1.00000", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_RiskWeight": "0" + }, + { + "DiseaseName": "Breast Cancer", + "ItemType": "PolygenicDiseaseLocus", + "LocusIdentifier": "f3a097", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_LocusBasePair": "C;C", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_OddsRatio": "1.00000", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_RiskWeight": "0" + }, + { + "DiseaseName": "Breast Cancer", + "ItemType": "PolygenicDiseaseLocus", + "LocusIdentifier": "d4626f", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_LocusBasePair": "A;A", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_OddsRatio": "1.00000", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_RiskWeight": "0" + }, + { + "DiseaseName": "Breast Cancer", + "ItemType": "PolygenicDiseaseLocus", + "LocusIdentifier": "84aaa4", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_LocusBasePair": "A;A", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_OddsRatio": "1.00000", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_RiskWeight": "0" + }, + { + "DiseaseName": "Breast Cancer", + "ItemType": "PolygenicDiseaseLocus", + "LocusIdentifier": "c8de7a", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_LocusBasePair": "T;T", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_OddsRatio": "1.00000", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_RiskWeight": "0" + }, + { + "DiseaseName": "Breast Cancer", + "ItemType": "PolygenicDiseaseLocus", + "LocusIdentifier": "d30087", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_LocusBasePair": "T;C", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_OddsRatio": "1.14000", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_RiskWeight": "1" + }, + { + "DiseaseName": "Breast Cancer", + "ItemType": "PolygenicDiseaseLocus", + "LocusIdentifier": "cafa72", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_LocusBasePair": "T;T", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_OddsRatio": "1.00000", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_RiskWeight": "0" + }, + { + "DiseaseName": "Breast Cancer", + "ItemType": "PolygenicDiseaseLocus", + "LocusIdentifier": "8f671c", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_LocusBasePair": "C;C", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_OddsRatio": "1.28000", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_RiskWeight": "2" + }, + { + "DiseaseName": "Breast Cancer", + "ItemType": "PolygenicDiseaseLocus", + "LocusIdentifier": "b3e49a", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_LocusBasePair": "C;C", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_OddsRatio": "1.00000", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_RiskWeight": "0" + }, + { + "DiseaseName": "Breast Cancer", + "ItemType": "PolygenicDiseaseLocus", + "LocusIdentifier": "8b0b02", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_LocusBasePair": "C;C", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_OddsRatio": "1.00000", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_RiskWeight": "0" + }, + { + "DiseaseName": "Breast Cancer", + "ItemType": "PolygenicDiseaseLocus", + "LocusIdentifier": "25cafc", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_LocusBasePair": "T;T", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_OddsRatio": "1.00000", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_RiskWeight": "0" + }, + { + "DiseaseName": "Breast Cancer", + "ItemType": "PolygenicDiseaseLocus", + "LocusIdentifier": "34c7e5", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_LocusBasePair": "A;A", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_OddsRatio": "1.00000", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_RiskWeight": "0" + }, + { + "DiseaseName": "Breast Cancer", + "ItemType": "PolygenicDiseaseLocus", + "LocusIdentifier": "60ce27", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_LocusBasePair": "A;A", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_OddsRatio": "1.00000", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_RiskWeight": "0" + }, + { + "DiseaseName": "Breast Cancer", + "ItemType": "PolygenicDiseaseLocus", + "LocusIdentifier": "328cdf", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_LocusBasePair": "A;A", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_OddsRatio": "1.00000", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_RiskWeight": "0" + }, + { + "DiseaseName": "Breast Cancer", + "ItemType": "PolygenicDiseaseLocus", + "LocusIdentifier": "849bc7", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_LocusBasePair": "A;A", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_OddsRatio": "1.00000", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_RiskWeight": "0" + }, + { + "DiseaseName": "Breast Cancer", + "ItemType": "PolygenicDiseaseLocus", + "LocusIdentifier": "5af5e3", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_LocusBasePair": "G;G", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_OddsRatio": "1.00000", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_RiskWeight": "0" + }, + { + "DiseaseName": "Breast Cancer", + "ItemType": "PolygenicDiseaseLocus", + "LocusIdentifier": "c354fa", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_LocusBasePair": "C;C", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_OddsRatio": "1.00000", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_RiskWeight": "0" + }, + { + "DiseaseName": "Breast Cancer", + "ItemType": "PolygenicDiseaseLocus", + "LocusIdentifier": "eedc23", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_LocusBasePair": "T;C", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_OddsRatio": "1.05000", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_RiskWeight": "1" + }, + { + "DiseaseName": "Breast Cancer", + "ItemType": "PolygenicDiseaseLocus", + "LocusIdentifier": "2ee027", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_LocusBasePair": "C;C", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_OddsRatio": "1.00000", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_RiskWeight": "0" + }, + { + "DiseaseName": "Breast Cancer", + "ItemType": "PolygenicDiseaseLocus", + "LocusIdentifier": "fc4bab", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_LocusBasePair": "T;T", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_OddsRatio": "1.00000", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_RiskWeight": "0" + }, + { + "DiseaseName": "Breast Cancer", + "ItemType": "PolygenicDiseaseLocus", + "LocusIdentifier": "f8b225", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_LocusBasePair": "C;C", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_OddsRatio": "1.00000", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_RiskWeight": "0" + }, + { + "DiseaseName": "Breast Cancer", + "ItemType": "PolygenicDiseaseLocus", + "LocusIdentifier": "4a072c", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_LocusBasePair": "C;C", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_OddsRatio": "1.00000", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_RiskWeight": "0" + }, + { + "DiseaseName": "Breast Cancer", + "ItemType": "PolygenicDiseaseLocus", + "LocusIdentifier": "070f24", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_LocusBasePair": "C;C", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_OddsRatio": "1.00000", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_RiskWeight": "0" + }, + { + "DiseaseName": "Breast Cancer", + "ItemType": "PolygenicDiseaseLocus", + "LocusIdentifier": "d08516", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_LocusBasePair": "Unknown", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_OddsRatio": "Unknown", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_RiskWeight": "Unknown" + }, + { + "DiseaseName": "Breast Cancer", + "ItemType": "PolygenicDiseaseLocus", + "LocusIdentifier": "047b84", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_LocusBasePair": "Unknown", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_OddsRatio": "Unknown", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_RiskWeight": "Unknown" + }, + { + "ItemType": "Trait", + "TraitName": "Lactose Tolerance", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_LocusIsPhased_rs182549": "No", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_LocusIsPhased_rs4988235": "No", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_LocusValue_rs182549": "T;T", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_LocusValue_rs4988235": "A;A", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_NumberOfRulesTested": "5", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_OutcomeScore_Intolerant": "0", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_OutcomeScore_Tolerant": "3" + }, + { + "ItemType": "TraitRule", + "RuleIdentifier": "f4e02c", + "TraitName": "Lactose Tolerance", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_PassesRule": "No", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_RuleLocusBasePair_43bf19": "T;T" + }, + { + "ItemType": "TraitRule", + "RuleIdentifier": "cc3df0", + "TraitName": "Lactose Tolerance", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_PassesRule": "Yes", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_RuleLocusBasePair_a7feff": "T;T" + }, + { + "ItemType": "TraitRule", + "RuleIdentifier": "8170ee", + "TraitName": "Lactose Tolerance", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_PassesRule": "No", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_RuleLocusBasePair_da6b04": "A;A" + }, + { + "ItemType": "TraitRule", + "RuleIdentifier": "52425f", + "TraitName": "Lactose Tolerance", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_PassesRule": "No", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_RuleLocusBasePair_176dde": "A;A" + }, + { + "ItemType": "TraitRule", + "RuleIdentifier": "4b5c35", + "TraitName": "Lactose Tolerance", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_PassesRule": "Yes", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_RuleLocusBasePair_164acb": "A;A" + }, + { + "ItemType": "Trait", + "TraitName": "Hair Texture", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_LocusIsPhased_rs11803731": "No", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_LocusIsPhased_rs17646946": "No", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_LocusIsPhased_rs7349332": "No", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_LocusValue_rs11803731": "A;A", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_LocusValue_rs17646946": "G;G", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_LocusValue_rs7349332": "C;C", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_NumberOfRulesTested": "9", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_OutcomeScore_Curly": "0", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_OutcomeScore_Straight": "6" + }, + { + "ItemType": "TraitRule", + "RuleIdentifier": "fde405", + "TraitName": "Hair Texture", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_PassesRule": "Yes", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_RuleLocusBasePair_0e06e2": "C;C" + }, + { + "ItemType": "TraitRule", + "RuleIdentifier": "6bd1da", + "TraitName": "Hair Texture", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_PassesRule": "No", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_RuleLocusBasePair_2da1b7": "C;C" + }, + { + "ItemType": "TraitRule", + "RuleIdentifier": "32e377", + "TraitName": "Hair Texture", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_PassesRule": "No", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_RuleLocusBasePair_c6760e": "C;C" + }, + { + "ItemType": "TraitRule", + "RuleIdentifier": "34e6d2", + "TraitName": "Hair Texture", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_PassesRule": "Yes", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_RuleLocusBasePair_9079c9": "A;A" + }, + { + "ItemType": "TraitRule", + "RuleIdentifier": "cf6cb5", + "TraitName": "Hair Texture", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_PassesRule": "No", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_RuleLocusBasePair_d0aad3": "A;A" + }, + { + "ItemType": "TraitRule", + "RuleIdentifier": "2ba65b", + "TraitName": "Hair Texture", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_PassesRule": "No", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_RuleLocusBasePair_f554b5": "A;A" + }, + { + "ItemType": "TraitRule", + "RuleIdentifier": "ae3274", + "TraitName": "Hair Texture", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_PassesRule": "Yes", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_RuleLocusBasePair_f500c2": "G;G" + }, + { + "ItemType": "TraitRule", + "RuleIdentifier": "a546bf", + "TraitName": "Hair Texture", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_PassesRule": "No", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_RuleLocusBasePair_f1144a": "G;G" + }, + { + "ItemType": "TraitRule", + "RuleIdentifier": "b8dc0a", + "TraitName": "Hair Texture", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_PassesRule": "No", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_RuleLocusBasePair_468bb3": "G;G" + }, + { + "ItemType": "Trait", + "TraitName": "Facial Structure", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_LocusIsPhased_rs1005999": "No", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_LocusIsPhased_rs10237838": "No", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_LocusIsPhased_rs10843104": "No", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_LocusIsPhased_rs11237982": "No", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_LocusIsPhased_rs12694574": "No", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_LocusIsPhased_rs13097965": "No", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_LocusIsPhased_rs17447439": "No", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_LocusIsPhased_rs1747677": "No", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_LocusIsPhased_rs1978859": "No", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_LocusIsPhased_rs2034127": "No", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_LocusIsPhased_rs2327089": "No", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_LocusIsPhased_rs2832438": "No", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_LocusIsPhased_rs2894450": "No", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_LocusIsPhased_rs4552364": "No", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_LocusIsPhased_rs6020940": "No", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_LocusIsPhased_rs6478394": "No", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_LocusIsPhased_rs7516150": "No", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_LocusIsPhased_rs7552331": "No", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_LocusIsPhased_rs7965082": "No", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_LocusIsPhased_rs805722": "No", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_LocusIsPhased_rs9858909": "No", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_LocusValue_rs1005999": "C;C", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_LocusValue_rs1008591": "Unknown", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_LocusValue_rs1015092": "Unknown", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_LocusValue_rs1019212": "Unknown", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_LocusValue_rs10234405": "Unknown", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_LocusValue_rs10237319": "Unknown", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_LocusValue_rs10237488": "Unknown", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_LocusValue_rs10237838": "T;C", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_LocusValue_rs10265937": "Unknown", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_LocusValue_rs10266101": "Unknown", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_LocusValue_rs10278187": "Unknown", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_LocusValue_rs10485860": "Unknown", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_LocusValue_rs10843104": "T;T", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_LocusValue_rs11191909": "Unknown", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_LocusValue_rs11237982": "T;C", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_LocusValue_rs1158810": "Unknown", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_LocusValue_rs11604811": "Unknown", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_LocusValue_rs12155314": "Unknown", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_LocusValue_rs12358982": "Unknown", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_LocusValue_rs12437560": "Unknown", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_LocusValue_rs12694574": "T;C", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_LocusValue_rs13097965": "T;C", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_LocusValue_rs13098099": "Unknown", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_LocusValue_rs1562005": "Unknown", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_LocusValue_rs1562006": "Unknown", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_LocusValue_rs1572037": "Unknown", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_LocusValue_rs16863422": "Unknown", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_LocusValue_rs16977002": "Unknown", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_LocusValue_rs16977008": "Unknown", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_LocusValue_rs16977009": "Unknown", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_LocusValue_rs17252053": "Unknown", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_LocusValue_rs17447439": "A;A", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_LocusValue_rs1747677": "C;C", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_LocusValue_rs1887276": "Unknown", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_LocusValue_rs1939697": "Unknown", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_LocusValue_rs1939707": "Unknown", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_LocusValue_rs1978859": "T;C", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_LocusValue_rs1999527": "Unknown", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_LocusValue_rs2034127": "G;G", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_LocusValue_rs2034128": "Unknown", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_LocusValue_rs2034129": "Unknown", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_LocusValue_rs2108166": "Unknown", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_LocusValue_rs2168809": "Unknown", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_LocusValue_rs2274107": "Unknown", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_LocusValue_rs2327089": "T;T", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_LocusValue_rs2327101": "Unknown", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_LocusValue_rs2342494": "Unknown", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_LocusValue_rs2422239": "Unknown", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_LocusValue_rs2422241": "Unknown", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_LocusValue_rs2832438": "T;T", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_LocusValue_rs2894450": "G;G", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_LocusValue_rs397723": "Unknown", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_LocusValue_rs4053148": "Unknown", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_LocusValue_rs4353811": "Unknown", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_LocusValue_rs4433629": "Unknown", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_LocusValue_rs4552364": "C;C", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_LocusValue_rs4633993": "Unknown", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_LocusValue_rs4648379": "Unknown", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_LocusValue_rs4648477": "Unknown", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_LocusValue_rs4648478": "Unknown", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_LocusValue_rs4793389": "Unknown", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_LocusValue_rs6020940": "A;A", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_LocusValue_rs6020957": "Unknown", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_LocusValue_rs6039266": "Unknown", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_LocusValue_rs6039272": "Unknown", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_LocusValue_rs6056066": "Unknown", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_LocusValue_rs6056119": "Unknown", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_LocusValue_rs6056126": "Unknown", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_LocusValue_rs6462544": "Unknown", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_LocusValue_rs6462562": "Unknown", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_LocusValue_rs6478394": "A;A", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_LocusValue_rs6555969": "Unknown", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_LocusValue_rs6749293": "Unknown", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_LocusValue_rs6795519": "Unknown", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_LocusValue_rs6950754": "Unknown", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_LocusValue_rs717463": "Unknown", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_LocusValue_rs7214306": "Unknown", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_LocusValue_rs7516150": "C;C", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_LocusValue_rs7552331": "G;G", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_LocusValue_rs7617069": "Unknown", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_LocusValue_rs7628370": "Unknown", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_LocusValue_rs7640340": "Unknown", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_LocusValue_rs7779616": "Unknown", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_LocusValue_rs7781059": "Unknown", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_LocusValue_rs7799331": "Unknown", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_LocusValue_rs7803030": "Unknown", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_LocusValue_rs7807181": "Unknown", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_LocusValue_rs7965082": "T;C", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_LocusValue_rs7966317": "Unknown", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_LocusValue_rs805693": "Unknown", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_LocusValue_rs805694": "Unknown", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_LocusValue_rs805722": "C;C", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_LocusValue_rs8079498": "Unknown", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_LocusValue_rs875143": "Unknown", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_LocusValue_rs894883": "Unknown", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_LocusValue_rs911015": "Unknown", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_LocusValue_rs911020": "Unknown", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_LocusValue_rs9692219": "Unknown", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_LocusValue_rs974448": "Unknown", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_LocusValue_rs975633": "Unknown", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_LocusValue_rs9858909": "G;G", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_LocusValue_rs9971100": "Unknown", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_NumberOfRulesTested": "0" + }, + { + "ItemType": "Trait", + "TraitName": "Eye Color", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_LocusIsPhased_rs1003719": "No", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_LocusIsPhased_rs1105879": "No", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_LocusIsPhased_rs1126809": "No", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_LocusIsPhased_rs1129038": "No", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_LocusIsPhased_rs11957757": "No", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_LocusIsPhased_rs121908120": "No", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_LocusIsPhased_rs12203592": "No", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_LocusIsPhased_rs12452184": "No", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_LocusIsPhased_rs12593929": "No", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_LocusIsPhased_rs12896399": "No", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_LocusIsPhased_rs12906280": "No", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_LocusIsPhased_rs12913823": "No", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_LocusIsPhased_rs12913832": "No", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_LocusIsPhased_rs1325127": "No", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_LocusIsPhased_rs1393350": "No", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_LocusIsPhased_rs1408799": "No", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_LocusIsPhased_rs1426654": "No", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_LocusIsPhased_rs1667394": "No", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_LocusIsPhased_rs16891982": "No", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_LocusIsPhased_rs1800401": "No", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_LocusIsPhased_rs1800407": "No", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_LocusIsPhased_rs1800414": "No", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_LocusIsPhased_rs2070959": "No", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_LocusIsPhased_rs2733832": "No", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_LocusIsPhased_rs2748901": "No", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_LocusIsPhased_rs351385": "No", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_LocusIsPhased_rs3794604": "No", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_LocusIsPhased_rs4778138": "No", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_LocusIsPhased_rs4778218": "No", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_LocusIsPhased_rs4778241": "No", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_LocusIsPhased_rs4911414": "No", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_LocusIsPhased_rs4911442": "No", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_LocusIsPhased_rs6058017": "No", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_LocusIsPhased_rs6828137": "No", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_LocusIsPhased_rs6910861": "No", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_LocusIsPhased_rs7174027": "No", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_LocusIsPhased_rs7183877": "No", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_LocusIsPhased_rs7495174": "No", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_LocusIsPhased_rs8028689": "No", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_LocusIsPhased_rs892839": "No", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_LocusIsPhased_rs916977": "No", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_LocusIsPhased_rs9782955": "No", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_LocusIsPhased_rs9894429": "No", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_LocusIsPhased_rs9971729": "No", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_LocusValue_rs1003719": "A;A", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_LocusValue_rs10209564": "Unknown", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_LocusValue_rs1105879": "A;A", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_LocusValue_rs1126809": "A;G", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_LocusValue_rs112747614": "Unknown", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_LocusValue_rs1129038": "T;C", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_LocusValue_rs11631797": "Unknown", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_LocusValue_rs116359091": "Unknown", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_LocusValue_rs11957757": "A;G", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_LocusValue_rs121908120": "T;T", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_LocusValue_rs12203592": "C;C", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_LocusValue_rs12335410": "Unknown", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_LocusValue_rs12452184": "C;C", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_LocusValue_rs12543326": "Unknown", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_LocusValue_rs12552712": "Unknown", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_LocusValue_rs12593929": "A;A", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_LocusValue_rs12614022": "Unknown", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_LocusValue_rs12896399": "G;G", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_LocusValue_rs12906280": "A;G", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_LocusValue_rs12913823": "T;C", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_LocusValue_rs12913832": "A;G", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_LocusValue_rs13016869": "Unknown", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_LocusValue_rs1325127": "T;T", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_LocusValue_rs13297008": "Unknown", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_LocusValue_rs138777265": "Unknown", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_LocusValue_rs1393350": "A;G", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_LocusValue_rs1408799": "C;C", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_LocusValue_rs141318671": "Unknown", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_LocusValue_rs1426654": "A;G", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_LocusValue_rs147068120": "Unknown", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_LocusValue_rs1667394": "T;C", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_LocusValue_rs16891982": "G;G", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_LocusValue_rs17184180": "Unknown", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_LocusValue_rs1800401": "G;G", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_LocusValue_rs1800407": "C;C", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_LocusValue_rs1800414": "T;T", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_LocusValue_rs2070959": "A;A", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_LocusValue_rs2095645": "Unknown", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_LocusValue_rs2238289": "Unknown", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_LocusValue_rs2240203": "Unknown", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_LocusValue_rs2252893": "Unknown", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_LocusValue_rs2385028": "Unknown", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_LocusValue_rs2733832": "T;T", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_LocusValue_rs2748901": "A;G", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_LocusValue_rs2835621": "Unknown", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_LocusValue_rs2835630": "Unknown", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_LocusValue_rs2835660": "Unknown", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_LocusValue_rs2854746": "Unknown", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_LocusValue_rs341147": "Unknown", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_LocusValue_rs348613": "Unknown", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_LocusValue_rs35051352": "Unknown", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_LocusValue_rs351385": "G;G", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_LocusValue_rs3768056": "Unknown", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_LocusValue_rs3794604": "C;C", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_LocusValue_rs3809761": "Unknown", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_LocusValue_rs3912104": "Unknown", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_LocusValue_rs3935591": "Unknown", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_LocusValue_rs3940272": "Unknown", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_LocusValue_rs4521336": "Unknown", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_LocusValue_rs4778138": "A;G", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_LocusValue_rs4778218": "G;G", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_LocusValue_rs4778241": "C;C", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_LocusValue_rs4790309": "Unknown", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_LocusValue_rs4911414": "T;G", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_LocusValue_rs4911442": "A;G", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_LocusValue_rs6058017": "A;A", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_LocusValue_rs622330": "Unknown", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_LocusValue_rs62330021": "Unknown", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_LocusValue_rs6420484": "Unknown", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_LocusValue_rs6693258": "Unknown", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_LocusValue_rs6828137": "T;G", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_LocusValue_rs6910861": "A;G", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_LocusValue_rs6944702": "Unknown", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_LocusValue_rs6997494": "Unknown", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_LocusValue_rs7170852": "Unknown", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_LocusValue_rs7174027": "G;G", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_LocusValue_rs7183877": "C;C", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_LocusValue_rs7219915": "Unknown", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_LocusValue_rs72777200": "Unknown", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_LocusValue_rs7277820": "Unknown", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_LocusValue_rs728405": "Unknown", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_LocusValue_rs72928978": "Unknown", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_LocusValue_rs73488486": "Unknown", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_LocusValue_rs74409360": "Unknown", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_LocusValue_rs7495174": "A;A", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_LocusValue_rs790464": "Unknown", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_LocusValue_rs8028689": "T;T", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_LocusValue_rs892839": "G;G", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_LocusValue_rs916977": "T;C", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_LocusValue_rs9301973": "Unknown", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_LocusValue_rs9782955": "C;C", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_LocusValue_rs9894429": "T;C", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_LocusValue_rs989869": "Unknown", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_LocusValue_rs9971729": "A;C", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_NumberOfRulesTested": "0" + }, + { + "ItemType": "Trait", + "TraitName": "Hair Color", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_LocusIsPhased_rs11636232": "No", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_LocusIsPhased_rs12203592": "No", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_LocusIsPhased_rs12821256": "No", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_LocusIsPhased_rs12896399": "No", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_LocusIsPhased_rs12913832": "No", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_LocusIsPhased_rs1540771": "No", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_LocusIsPhased_rs1667394": "No", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_LocusIsPhased_rs1805007": "No", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_LocusIsPhased_rs1805008": "No", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_LocusIsPhased_rs35264875": "No", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_LocusIsPhased_rs3829241": "No", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_LocusIsPhased_rs6918152": "No", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_LocusIsPhased_rs7174027": "No", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_LocusIsPhased_rs7183877": "No", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_LocusIsPhased_rs7495174": "No", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_LocusIsPhased_rs8028689": "No", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_LocusValue_rs11636232": "T;C", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_LocusValue_rs11855019": "Unknown", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_LocusValue_rs12203592": "C;C", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_LocusValue_rs12821256": "T;T", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_LocusValue_rs12896399": "G;G", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_LocusValue_rs12913832": "A;G", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_LocusValue_rs1540771": "T;C", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_LocusValue_rs1667394": "T;C", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_LocusValue_rs1805007": "C;C", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_LocusValue_rs1805008": "C;C", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_LocusValue_rs28777": "Unknown", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_LocusValue_rs35264875": "A;A", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_LocusValue_rs3829241": "A;G", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_LocusValue_rs4778211": "Unknown", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_LocusValue_rs6918152": "A;A", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_LocusValue_rs7174027": "G;G", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_LocusValue_rs7183877": "C;C", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_LocusValue_rs7495174": "A;A", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_LocusValue_rs8028689": "T;T", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_LocusValue_rs8039195": "Unknown", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_NumberOfRulesTested": "0" + }, + { + "ItemType": "Trait", + "TraitName": "Skin Color", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_LocusIsPhased_rs1042602": "No", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_LocusIsPhased_rs1126809": "No", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_LocusIsPhased_rs12203592": "No", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_LocusIsPhased_rs1426654": "No", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_LocusIsPhased_rs16891982": "No", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_LocusIsPhased_rs1834640": "No", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_LocusIsPhased_rs26722": "No", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_LocusIsPhased_rs2762462": "No", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_LocusIsPhased_rs642742": "No", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_LocusIsPhased_rs937171": "No", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_LocusValue_rs10424065": "Unknown", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_LocusValue_rs1042602": "C;C", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_LocusValue_rs1126809": "A;G", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_LocusValue_rs12203592": "C;C", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_LocusValue_rs142317543": "Unknown", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_LocusValue_rs1426654": "A;G", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_LocusValue_rs16891982": "G;G", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_LocusValue_rs1800422": "Unknown", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_LocusValue_rs1834640": "A;G", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_LocusValue_rs26722": "C;C", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_LocusValue_rs2762462": "C;C", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_LocusValue_rs3212368": "Unknown", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_LocusValue_rs3212369": "Unknown", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_LocusValue_rs642742": "T;C", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_LocusValue_rs7176696": "Unknown", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_LocusValue_rs7182710": "Unknown", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_LocusValue_rs784416": "Unknown", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_LocusValue_rs937171": "G;G", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_NumberOfRulesTested": "0" + } +] \ No newline at end of file diff --git a/internal/genetics/sampleAnalyses/sampleAnalyses.go b/internal/genetics/sampleAnalyses/sampleAnalyses.go new file mode 100644 index 0000000..45bb4e5 --- /dev/null +++ b/internal/genetics/sampleAnalyses/sampleAnalyses.go @@ -0,0 +1,55 @@ + +// sampleAnalyses provides sample Person and Couple analyses +// These are used to show users what an analysis looks like, without requiring the user to import their genomes. + +package sampleAnalyses + +import "seekia/internal/genetics/readGeneticAnalysis" + +import _ "embed" + +import "errors" + + +//go:embed SamplePerson1Analysis.json +var person1Analysis []byte + +//go:embed SamplePerson2Analysis.json +var person2Analysis []byte + +//go:embed SampleCoupleAnalysis.json +var coupleAnalysis []byte + + +func GetSamplePerson1Analysis()([]map[string]string, error){ + + analysisMapList, err := readGeneticAnalysis.ReadGeneticAnalysisString(string(person1Analysis)) + if (err != nil){ + return nil, errors.New("sampleAnalyses contains invalid person1Analysis: " + err.Error()) + } + + return analysisMapList, nil +} + + +func GetSamplePerson2Analysis()([]map[string]string, error){ + + analysisMapList, err := readGeneticAnalysis.ReadGeneticAnalysisString(string(person2Analysis)) + if (err != nil){ + return nil, errors.New("sampleAnalyses contains invalid person2Analysis: " + err.Error()) + } + + return analysisMapList, nil +} + +func GetSampleCoupleAnalysis()([]map[string]string, error){ + + analysisMapList, err := readGeneticAnalysis.ReadGeneticAnalysisString(string(coupleAnalysis)) + if (err != nil){ + return nil, errors.New("sampleAnalyses contains invalid coupleAnalysis: " + err.Error()) + } + + return analysisMapList, nil +} + + diff --git a/internal/genetics/sampleAnalyses/sampleAnalyses_test.go b/internal/genetics/sampleAnalyses/sampleAnalyses_test.go new file mode 100644 index 0000000..715c363 --- /dev/null +++ b/internal/genetics/sampleAnalyses/sampleAnalyses_test.go @@ -0,0 +1,60 @@ +package sampleAnalyses_test + + +import "seekia/internal/genetics/sampleAnalyses" + +import "seekia/resources/geneticReferences/monogenicDiseases" +import "seekia/resources/geneticReferences/polygenicDiseases" +import "seekia/resources/geneticReferences/traits" + +import "seekia/internal/genetics/readGeneticAnalysis" + +import "testing" + + +func TestPersonSampleAnalyses(t *testing.T){ + + monogenicDiseases.InitializeMonogenicDiseaseVariables() + polygenicDiseases.InitializePolygenicDiseaseVariables() + traits.InitializeTraitVariables() + + person1AnalysisMapList, err := sampleAnalyses.GetSamplePerson1Analysis() + if (err != nil) { + t.Fatalf("GetSamplePerson1Analysis failed: " + err.Error()) + } + + person2AnalysisMapList, err := sampleAnalyses.GetSamplePerson2Analysis() + if (err != nil){ + t.Fatalf("GetSamplePerson2Analysis failed: " + err.Error()) + } + + err = readGeneticAnalysis.ReadPersonGeneticAnalysisForTests(person1AnalysisMapList) + if (err != nil) { + t.Fatalf("SamplePerson1 genetic analysis is malformed: " + err.Error()) + } + + err = readGeneticAnalysis.ReadPersonGeneticAnalysisForTests(person2AnalysisMapList) + if (err != nil) { + t.Fatalf("SamplePerson2 genetic analysis is malformed: " + err.Error()) + } +} + + +func TestCoupleSampleAnalyses(t *testing.T){ + + monogenicDiseases.InitializeMonogenicDiseaseVariables() + polygenicDiseases.InitializePolygenicDiseaseVariables() + traits.InitializeTraitVariables() + + coupleAnalysisMapList, err := sampleAnalyses.GetSampleCoupleAnalysis() + if (err != nil){ + t.Fatalf("GetSampleCoupleAnalysis failed: " + err.Error()) + } + + err = readGeneticAnalysis.ReadCoupleGeneticAnalysisForTests(coupleAnalysisMapList) + if (err != nil) { + t.Fatalf("Sample couple genetic analysis is malformed: " + err.Error()) + } +} + + diff --git a/internal/globalSettings/globalSettings.go b/internal/globalSettings/globalSettings.go new file mode 100644 index 0000000..7df0584 --- /dev/null +++ b/internal/globalSettings/globalSettings.go @@ -0,0 +1,162 @@ + +// globalSettings provides functions to manage global settings, which are the same for all users. + +package globalSettings + +import goFilepath "path/filepath" +import "os" +import "encoding/json" +import "errors" +import "sync" + +func InitializeGlobalSettingsDatastore()error{ + + localFileDirectory, err := os.UserConfigDir() + if (err != nil) { return err } + + globalSettingsFolderpath := goFilepath.Join(localFileDirectory, "SeekiaData", "GlobalSettings") + + // We create the GlobalSettings folder + + file, err := os.Open(globalSettingsFolderpath) + if (err != nil){ + + isNotExistErr := os.IsNotExist(err) + if (isNotExistErr == false) { + return err + } + + // Folder does not exist + err = os.Mkdir(globalSettingsFolderpath, os.ModePerm) + if (err != nil) { return err } + } + file.Close() + + getCurrentSettingsMap := func()(map[string]string, error){ + + globalSettingsFilepath := goFilepath.Join(globalSettingsFolderpath, "GlobalSettings.json") + + fileBytes, err := os.ReadFile(globalSettingsFilepath) + if (err != nil){ + + isNotExistError := os.IsNotExist(err) + if (isNotExistError == false){ + return nil, err + } + + emptyMap := make(map[string]string) + return emptyMap, nil + } + + currentMap := make(map[string]string) + + err = json.Unmarshal(fileBytes, ¤tMap) + if (err != nil) { + return nil, errors.New("Stored globalSettings map is corrupted: " + err.Error()) + } + + return currentMap, nil + } + + currentGlobalSettingsMap, err := getCurrentSettingsMap() + if (err != nil) { return err } + + settingsMap = currentGlobalSettingsMap + + return nil +} + +// This will be locked whenever we are writing the map/local file +var updatingSettingsMutex sync.Mutex + +var settingsMapMutex sync.RWMutex + +var settingsMap map[string]string + +//Outputs: +// -bool: Setting exists +// -string: Setting value +// -error +func GetSetting(settingName string)(bool, string, error){ + + if (settingsMap == nil){ + return false, "", errors.New("GetSetting called when global settings map is not initialized.") + } + + settingsMapMutex.RLock() + settingString, exists := settingsMap[settingName] + settingsMapMutex.RUnlock() + + if (exists == false){ + return false, "", nil + } + return true, settingString, nil +} + +func SetSetting(settingName string, content string) error{ + + if (settingName == ""){ + return errors.New("globalSettings SetSetting called with empty settingName.") + } + if (content == ""){ + return errors.New("globalSettings SetSetting called with empty content.") + } + + updatingSettingsMutex.Lock() + defer updatingSettingsMutex.Unlock() + + settingsMapMutex.Lock() + settingsMap[settingName] = content + settingsMapMutex.Unlock() + + overwriteSettingsFile() + + return nil +} + +func DeleteSetting(settingName string) error { + + if (settingName == ""){ + return errors.New("globalSettings DeleteSetting called with empty settingName.") + } + + updatingSettingsMutex.Lock() + defer updatingSettingsMutex.Unlock() + + settingsMapMutex.Lock() + delete(settingsMap, settingName) + settingsMapMutex.Unlock() + + err := overwriteSettingsFile() + if (err != nil) { return err } + + return nil +} + +func overwriteSettingsFile()error{ + + jsonFileContents, err := json.MarshalIndent(settingsMap, "", "\t") + if (err != nil) { return err } + + fileContentsString := string(jsonFileContents) + + localFileDirectory, err := os.UserConfigDir() + if (err != nil) { return err } + + globalSettingsFilepath := goFilepath.Join(localFileDirectory, "SeekiaData", "GlobalSettings", "GlobalSettings.json") + + newFile, err := os.Create(globalSettingsFilepath) + if (err != nil) { return err } + + _, err = newFile.WriteString(fileContentsString) + if (err != nil) { + newFile.Close() + return err + } + newFile.Close() + + return nil +} + + + diff --git a/internal/helpers/helpers.go b/internal/helpers/helpers.go new file mode 100644 index 0000000..fcdedf1 --- /dev/null +++ b/internal/helpers/helpers.go @@ -0,0 +1,2008 @@ + +// helpers provides miscellaneous functions + +package helpers + +import "seekia/internal/encoding" +import "seekia/internal/identity" +import "seekia/internal/translation" + +import "errors" +import "math" +import "strconv" +import "strings" +import "time" +import "slices" +import "maps" + +import cryptoRand "crypto/rand" +import mathRand "math/rand/v2" + +//Outputs: +// -int: Feet +// -float64: Inches +// -error +func ConvertCentimetersToFeetInches(centimeters float64) (int, float64, error) { + + if (centimeters < 0){ + return 0, 0, errors.New("ConvertCentimetersToFeetInches called with negative centimeters.") + } + + totalInches := centimeters / 2.54 + + feetFloored := math.Floor(totalInches/12) + inches := totalInches - (feetFloored * 12) + + feetInt := int(feetFloored) + + return feetInt, inches, nil +} + + +//Outputs: +// -string: Example: 1 foot, 10 centimeters +// -error +func ConvertCentimetersToFeetInchesTranslatedString(centimeters float64)(string, error){ + + inputFeet, inputInches, err := ConvertCentimetersToFeetInches(centimeters) + if (err != nil) { + return "", errors.New("ConvertCentimetersToFeetInchesTranslatedString called with invalid centimeters.") + } + + getFeetUnits := func()string{ + + if (inputFeet <= 1){ + result := translation.TranslateTextFromEnglishToMyLanguage("foot") + + return result + } + + feetTranslated := translation.TranslateTextFromEnglishToMyLanguage("feet") + + return feetTranslated + } + + feetUnits := getFeetUnits() + + getInchUnits := func()string{ + + if (inputInches == 1){ + result := translation.TranslateTextFromEnglishToMyLanguage("inch") + return result + } + + inchesTranslated := translation.TranslateTextFromEnglishToMyLanguage("inches") + + return inchesTranslated + } + + inchUnits := getInchUnits() + + inputFeetString := ConvertIntToString(inputFeet) + inputInchesString := ConvertFloat64ToStringRounded(inputInches, 1) + + formattedResult := inputFeetString + " " + feetUnits + ", " + inputInchesString + " " + inchUnits + + return formattedResult, nil +} + +func ConvertFeetInchesToCentimeters(feet int, inches float64)(float64, error) { + + if (feet < 0 || inches < 0){ + return 0, errors.New("ConvertFeetInchesToCentimeters called with negative feet/inches.") + } + + totalInches := float64(feet*12) + inches + centimeters := totalInches * 2.54 + + return centimeters, nil +} + + +func ConvertKilometersToMiles(kilometers float64)(float64, error){ + + if (kilometers < 0){ + return 0, errors.New("ConvertKilometersToMiles called with negative kilometers.") + } + + miles := kilometers * 0.621371 + + return miles, nil +} + +func ConvertMilesToKilometers(miles float64)(float64, error){ + + if (miles < 0){ + return 0, errors.New("ConvertMilesToKilometers called with negative miles.") + } + + kilometers := miles * 1.609344 + + return kilometers, nil +} + +func VerifyLatitude(latitude float64)bool{ + + if (latitude >= -90 && latitude <= 90){ + return true + } + + return false +} + +func VerifyLongitude(longitude float64)bool{ + + if (longitude >= -180 && longitude <= 180){ + return true + } + + return false +} + +func VerifyStringIsFloat(input string)bool{ + + _, err := ConvertStringToFloat64(input) + if (err != nil){ + return false + } + + return true +} + +func VerifyStringIsIntWithinRange(inputString string, targetMin int64, targetMax int64)(bool, error){ + + if (targetMin >= targetMax){ + return false, errors.New("VerifyStringIsIntWithinRange called with min greater than or equal to max") + } + + num, err := ConvertStringToInt64(inputString) + if (err != nil) { + return false, nil + } + + if (num < targetMin || num > targetMax){ + return false, nil + } + + return true, nil +} + +func VerifyStringIsFloatWithinRange(inputString string, targetMin float64, targetMax float64)(bool, error){ + + if (targetMin >= targetMax){ + return false, errors.New("VerifyStringIsFloatWithinRange called with min greater than or equal to max") + } + + num, err := ConvertStringToFloat64(inputString) + if (err != nil) { + return false, nil + } + if (num < targetMin || num > targetMax){ + return false, nil + } + + return true, nil +} + +func CheckIfFloat64IsInteger(float float64)bool{ + + floatRounded := float64(int64(float)) + if (float == floatRounded){ + return true + } + + return false +} + +func FloorFloat64ToInt(input float64)(int, error){ + + flooredFloat := math.Floor(input) + + if (flooredFloat < -2147483648 || flooredFloat > 2147483647){ + return 0, errors.New("FloorFloat64ToInt called with float out of range.") + } + + newInt := int(flooredFloat) + + return newInt, nil +} + +func FloorFloat64ToInt64(input float64)(int64, error){ + + flooredFloat := math.Floor(input) + + if (flooredFloat < -9223372036854775808 || flooredFloat > 9223372036854775807){ + return 0, errors.New("FloorFloat64ToInt64 called with float out of range.") + } + + newInt := int64(flooredFloat) + + return newInt, nil +} + +func CeilFloat64ToInt64(input float64)(int64, error){ + + ceiledFloat := math.Ceil(input) + + if (ceiledFloat < -9223372036854775808 || ceiledFloat > 9223372036854775807){ + return 0, errors.New("CeilFloat64ToInt64 called with float out of range.") + } + + ceiledInt := int64(ceiledFloat) + + return ceiledInt, nil +} + +func CeilFloat64ToInt(input float64)(int, error){ + + ceiledFloat := math.Ceil(input) + + if (ceiledFloat < -2147483648 || ceiledFloat > 2147483647){ + return 0, errors.New("CeilFloat64ToInt called with float out of range.") + } + + ceiledInt := int(ceiledFloat) + + return ceiledInt, nil +} + +func ConvertFloat64ToString(input float64) string{ + + result := strconv.FormatFloat(input, 'f', 5, 64) + + return result +} + +func ConvertFloat64ToStringRounded(input float64, outputPrecision int)string{ + + if (outputPrecision < 0){ + outputPrecision = 0 + } + + converted := strconv.FormatFloat(input, 'f', outputPrecision, 64) + + if (outputPrecision == 0){ + return converted + } + + // If all numbers after the decimal are zero, we will remove them + // Example: "5.000" -> "5" + + beforeDecimal, afterDecimal, decimalFound := strings.Cut(converted, ".") + if (decimalFound == false){ + return converted + } + + for _, number := range afterDecimal{ + if (number != '0'){ + return converted + } + } + + return beforeDecimal +} + +func ConvertStringToFloat64(input string)(float64, error){ + + num, err := strconv.ParseFloat(input, 64) + if (err != nil){ + return 0, errors.New("ConvertStringToFloat64 called with invalid input: " + input) + } + + return num, nil +} + +func ConvertInt64ToInt(input int64)(int, error){ + + if (input < -2147483648 || input > 2147483647){ + return 0, errors.New("ConvertInt64ToInt called with input out of range.") + } + + result := int(input) + + return result, nil +} + + +func ConvertByteToString(input byte) string{ + + result := ConvertIntToString(int(input)) + + return result +} + +func ConvertStringToByte(input string)(byte, error){ + + resultInt, err := strconv.Atoi(input) + if (err != nil) { + return 0, errors.New("ConvertStringToByte called with invalid input: " + input) + } + + if (resultInt < 0 || resultInt > 255){ + return 0, errors.New("ConvertStringToByte called with invalid input: " + input) + } + + result := byte(resultInt) + + return result, nil +} + +func ConvertNetworkTypeStringToByte(input string)(byte, error){ + + if (input == "1"){ + return 1, nil + } + if (input == "2"){ + return 2, nil + } + + return 0, errors.New("ConvertNetworkTypeStringToByte called with invalid input: " + input) +} + +func VerifyNetworkType(input byte)bool{ + + if (input != 1 && input != 2){ + return false + } + + return true +} + +func ConvertIntToString(input int) string{ + + str := strconv.Itoa(input) + + return str +} + +func ConvertInt64ToString(input int64)string{ + + str := strconv.FormatInt(input, 10) + + return str +} + +func ConvertStringToInt(input string)(int, error){ + + result, err := strconv.Atoi(input) + if (err != nil) { + return 0, errors.New("ConvertStringToInt called with invalid input: " + input) + } + + return result, nil +} + +func ConvertStringToInt64(input string) (int64, error) { + + result, err := strconv.ParseInt(input, 10, 64) + if (err != nil){ + return 0, errors.New("ConvertStringToInt64 called with invalid input: " + input) + } + + return result, nil +} + +func ConvertBroadcastTimeStringToInt64(input string)(int64, error){ + + inputInt64, err := ConvertStringToInt64(input) + if (err != nil) { + return 0, errors.New("ConvertBroadcastTimeStringToInt64 called with invalid input: " + input) + } + + isValid := VerifyBroadcastTime(inputInt64) + if (isValid == false){ + return 0, errors.New("ConvertBroadcastTimeStringToInt64 called with invalid input. Too early: " + input) + } + + return inputInt64, nil +} + +func VerifyBroadcastTime(input int64)bool{ + + if (input < 1680000000){ + return false + } + + //TODO: Add upper limit + + return true +} + +func ConvertIntToStringWithCommas(num int)string{ + + numAsString := ConvertIntToString(num) + + if (num <= 999){ + return numAsString + } + + resultWithCommas := "" + + finalIndex := len(numAsString)-1 + + counter := 0 + + for i := finalIndex; i >= 0; i--{ + + currentCharacter := string(numAsString[i]) + + if (counter == 3){ + resultWithCommas = currentCharacter + "," + resultWithCommas + counter = 1 + continue + } + + resultWithCommas = currentCharacter + resultWithCommas + counter += 1 + } + + return resultWithCommas +} + +//Outputs: +// -string: Content Type ("Profile"/"Attribute"/"Message"/"Review"/"Report"/"Parameters") +// -error +func GetContentTypeFromContentHash(contentHash []byte)(string, error){ + + hashLength := len(contentHash) + + if (hashLength == 26){ + + return "Message", nil + + } else if (hashLength == 27){ + + metadataByte := contentHash[26] + + if (metadataByte >= 1 && metadataByte <= 6){ + + return "Attribute", nil + } + + } else if (hashLength == 28){ + + metadataByte := contentHash[27] + + if (metadataByte >= 1 && metadataByte <= 6){ + + return "Profile", nil + } + + } else if (hashLength == 29){ + + metadataByte := contentHash[28] + + if (metadataByte >= 1 && metadataByte <= 4){ + + return "Review", nil + } + + } else if (hashLength == 30){ + + metadataByte := contentHash[29] + + if (metadataByte >= 1 && metadataByte <= 4){ + + return "Report", nil + } + + } else if (hashLength == 31){ + + return "Parameters", nil + } + + contentHashHex := encoding.EncodeBytesToHexString(contentHash) + + return "", errors.New("GetContentTypeFromContentHash called with invalid contentHash: " + contentHashHex) +} + +//Outputs: +// -[]byte: Reported hash bytes +// -string: Reported hash type (Identity, Profile, Message, or Attribute) +// -error +func ReadReportedHashString(reportedHash string)([]byte, string, error){ + + reportedHashBytes, reportedHashType, err := ReadReviewedHashString(reportedHash) + if (err != nil){ + return nil, "", errors.New("ReadReportedHashString called with invalid reportedHash: " + err.Error()) + } + + return reportedHashBytes, reportedHashType, nil +} + +//Outputs: +// -[]byte: Reviewed hash bytes +// -string: Reviewed hash type (Identity, Profile, Message, or Attribute) +// -error +func ReadReviewedHashString(reviewedHash string)([]byte, string, error){ + + if (len(reviewedHash) == 27){ + + // Must be identity hash + + identityHashBytes, _, err := identity.ReadIdentityHashString(reviewedHash) + if (err != nil) { + return nil, "", errors.New("ReadReviewedHashString called with invalid reviewedHash: " + reviewedHash) + } + + return identityHashBytes[:], "Identity", nil + } + + // All other reviewedHash types are encoded in Hex + + reviewedHashBytes, err := encoding.DecodeHexStringToBytes(reviewedHash) + if (err != nil) { + return nil, "", errors.New("ReadReviewedHashString called with invalid reviewedHash: " + reviewedHash) + } + + reviewedType, err := GetReviewedTypeFromReviewedHash(reviewedHashBytes) + if (err != nil){ + return nil, "", errors.New("ReadReviewedHashString called with invalid reviewedHash: " + reviewedHash) + } + + return reviewedHashBytes, reviewedType, nil +} + +func EncodeReportedHashBytesToString(reportedHash []byte)(string, error){ + + result, err := EncodeReviewedHashBytesToString(reportedHash) + + return result, err +} + +func EncodeReviewedHashBytesToString(reviewedHash []byte)(string, error){ + + reviewedType, err := GetReviewedTypeFromReviewedHash(reviewedHash) + if (err != nil){ + return "", errors.New("EncodeReviewedHashToString called with invalid reviewedHashBytes: " + err.Error()) + } + + if (reviewedType == "Identity"){ + + if (len(reviewedHash) != 16){ + return "", errors.New("GetReviewedTypeFromReviewedHash returning Identity reviewedType when bytes length is not 16.") + } + + reviewedHashArray := [16]byte(reviewedHash) + + identityHash, _, err := identity.EncodeIdentityHashBytesToString(reviewedHashArray) + if (err != nil) { return "", err } + + return identityHash, nil + } + + reviewedHashString := encoding.EncodeBytesToHexString(reviewedHash) + + return reviewedHashString, nil +} + +func GetReportedTypeFromReportedHash(reportedHash []byte)(string, error){ + + reportedType, err := GetReviewedTypeFromReviewedHash(reportedHash) + if (err != nil) { return "", err } + + return reportedType, nil +} + +func GetReviewedTypeFromReviewedHash(reviewedHash []byte)(string, error){ + + hashLength := len(reviewedHash) + + if (hashLength == 16){ + + metadataByte := reviewedHash[15] + + if (metadataByte >= 1 && metadataByte <= 3){ + + return "Identity", nil + } + + } else if (hashLength == 26){ + + return "Message", nil + + } else if (hashLength == 27){ + + metadataByte := reviewedHash[26] + + if (metadataByte >= 1 && metadataByte <= 6){ + + return "Attribute", nil + } + + } else if (hashLength == 28){ + + metadataByte := reviewedHash[27] + + // We must make sure profile is not disabled + // Disabled profiles cannot be reviewed + + if (metadataByte == 2 || metadataByte == 4 || metadataByte == 6){ + + return "Profile", nil + } + } + + reviewedHashHex := encoding.EncodeBytesToHexString(reviewedHash) + + return "", errors.New("GetReviewedTypeFromReviewedHashBytes called with invalid reviewedHash: " + reviewedHashHex) +} + + +func GetNewRandomProfileHash(profileTypeProvided bool, profileType string, isDisabledProvided bool, isDisabled bool)([28]byte, error){ + + if (profileTypeProvided == true){ + if (profileType != "Mate" && profileType != "Host" && profileType != "Moderator"){ + return [28]byte{}, errors.New("GetNewRandomProfileHash called with invalid profileType: " + profileType) + } + } + + getMetadataByte := func()(byte, error){ + + if (profileTypeProvided == false && isDisabledProvided == false){ + + randomByte, err := GetRandomByteWithinRange(1, 6) + if (err != nil) { return 0, err } + + return randomByte, nil + } + + if (profileTypeProvided == true && isDisabledProvided == false){ + + randomByte, err := GetRandomByteWithinRange(0, 1) + if (err != nil) { return 0, err } + + if (profileType == "Mate"){ + + result := 1 + randomByte + return result, nil + } + if (profileType == "Host"){ + + result := 3 + randomByte + return result, nil + } + if (profileType == "Moderator"){ + + result := 5 + randomByte + return result, nil + } + } + if (profileTypeProvided == false && isDisabledProvided == true){ + + randomInt := GetRandomIntWithinRange(1, 3) + + if (randomInt == 1){ + + // ProfileType == "Mate" + + if (isDisabled == true){ + return 1, nil + } + + return 2, nil + } + if (randomInt == 2){ + + // ProfileType == "Host" + + if (isDisabled == true){ + return 3, nil + } + + return 4, nil + } + + // ProfileType == "Moderator" + + // randomInt == 3 + if (isDisabled == true){ + return 5, nil + } + + return 6, nil + } + + //profileTypeProvided == true && isDisabledProvided == true + + if (profileType == "Mate"){ + + if (isDisabled == true){ + return 1, nil + } + + return 2, nil + } + if (profileType == "Host"){ + + if (isDisabled == true){ + return 3, nil + } + + return 4, nil + } + // profileType == "Moderator" + + if (isDisabled == true){ + return 5, nil + } + + return 6, nil + } + + metadataByte, err := getMetadataByte() + if (err != nil){ return [28]byte{}, err } + + var profileHash [28]byte + _, err = cryptoRand.Read(profileHash[:]) + if (err != nil) { return [28]byte{}, err } + + profileHash[27] = metadataByte + + return profileHash, nil +} + +func GetNewRandomAttributeHash(profileTypeProvided bool, profileType string, isCanonicalProvided bool, isCanonical bool)([27]byte, error){ + + if (profileTypeProvided == true){ + + if (profileType != "Mate" && profileType != "Host" && profileType != "Moderator"){ + return [27]byte{}, errors.New("GetNewRandomAttributeHash called with invalid profileType: " + profileType) + } + } + + getMetadataByte := func()(byte, error){ + + if (profileTypeProvided == false && isCanonicalProvided == false){ + + randomByte, err := GetRandomByteWithinRange(1, 6) + if (err != nil) { return 0, err } + + return randomByte, nil + } + + if (profileTypeProvided == true && isCanonicalProvided == false){ + + randomByte, err := GetRandomByteWithinRange(0, 1) + if (err != nil) { return 0, err } + + if (profileType == "Mate"){ + + result := 1 + randomByte + return result, nil + } + if (profileType == "Host"){ + + result := 3 + randomByte + return result, nil + } + if (profileType == "Moderator"){ + + result := 5 + randomByte + return result, nil + } + } + if (profileTypeProvided == false && isCanonicalProvided == true){ + + randomInt := GetRandomIntWithinRange(1, 3) + + if (randomInt == 1){ + + // ProfileType == "Mate" + + if (isCanonical == true){ + return 1, nil + } + + return 2, nil + } + if (randomInt == 2){ + + // ProfileType == "Host" + + if (isCanonical == true){ + return 3, nil + } + + return 4, nil + } + + // ProfileType == "Moderator" + + // randomInt == 3 + if (isCanonical == true){ + return 5, nil + } + + return 6, nil + } + + //profileTypeProvided == true && isCanonicalProvided == true + + if (profileType == "Mate"){ + + if (isCanonical == true){ + return 1, nil + } + + return 2, nil + } + if (profileType == "Host"){ + + if (isCanonical == true){ + return 3, nil + } + + return 4, nil + } + // profileType == "Moderator" + + if (isCanonical == true){ + return 5, nil + } + + return 6, nil + } + + metadataByte, err := getMetadataByte() + if (err != nil) { return [27]byte{}, err } + + var attributeHash [27]byte + _, err = cryptoRand.Read(attributeHash[:]) + if (err != nil) { return [27]byte{}, err } + + attributeHash[26] = metadataByte + + return attributeHash, nil +} + +func GetNewRandomMessageHash()([26]byte, error){ + + var messageHash [26]byte + + _, err := cryptoRand.Read(messageHash[:]) + if (err != nil) { return [26]byte{}, err } + + return messageHash, nil +} + +func GetNewRandomReviewHash(reviewTypeProvided bool, reviewType string)([29]byte, error){ + + getMetadataByte := func()(byte, error){ + + if (reviewTypeProvided == false){ + + randomByte, err := GetRandomByteWithinRange(1, 4) + if (err != nil) { return 0, err } + + return randomByte, nil + } + if (reviewType == "Identity"){ + return 1, nil + } + if (reviewType == "Profile"){ + return 2, nil + } + if (reviewType == "Attribute"){ + return 3, nil + } + if (reviewType == "Message"){ + return 4, nil + } + + return 0, errors.New("GetNewRandomReviewHash called with invalid reviewType: " + reviewType) + } + + metadataByte, err := getMetadataByte() + if (err != nil){ return [29]byte{}, err } + + var reviewHash [29]byte + + _, err = cryptoRand.Read(reviewHash[:]) + if (err != nil) { return [29]byte{}, err } + + reviewHash[28] = metadataByte + + return reviewHash, nil +} + +func GetNewRandomReportHash(reportTypeProvided bool, reportType string)([30]byte, error){ + + getMetadataByte := func()(byte, error){ + + if (reportTypeProvided == false){ + + randomByte, err := GetRandomByteWithinRange(1, 4) + if (err != nil) { return 0, err } + + return randomByte, nil + } + if (reportType == "Identity"){ + return 1, nil + } + if (reportType == "Profile"){ + return 2, nil + } + if (reportType == "Attribute"){ + return 3, nil + } + if (reportType == "Message"){ + return 4, nil + } + + return 0, errors.New("GetNewRandomReportHash called with invalid reportType: " + reportType) + } + + metadataByte, err := getMetadataByte() + if (err != nil){ return [30]byte{}, err } + + var reportHash [30]byte + + _, err = cryptoRand.Read(reportHash[:]) + if (err != nil) { return [30]byte{}, err } + + reportHash[29] = metadataByte + + return reportHash, nil +} + +func GetNewRandomParametersHash()([31]byte, error){ + + var parametersHash [31]byte + + _, err := cryptoRand.Read(parametersHash[:]) + if (err != nil) { return [31]byte{}, err } + + return parametersHash, nil +} + + +func GetNewRandomBytes(lengthInBytes int)([]byte, error){ + + if (lengthInBytes == 0){ + return nil, errors.New("GetNewRandomBytes called with 0 lengthInBytes.") + } + + randomBytes := make([]byte, lengthInBytes) + + _, err := cryptoRand.Read(randomBytes[:]) + if (err != nil) { return nil, err } + + return randomBytes, nil +} + +func GetNewRandomDeviceIdentifier()([11]byte, error){ + + var newArray [11]byte + + _, err := cryptoRand.Read(newArray[:]) + if (err != nil) { return [11]byte{}, err } + + return newArray, nil +} + +func GetNewRandom32ByteArray()([32]byte, error){ + + var newArray [32]byte + + _, err := cryptoRand.Read(newArray[:]) + if (err != nil) { return [32]byte{}, err } + + return newArray, nil +} + +func GetNewRandom24ByteArray()([24]byte, error){ + + var newArray [24]byte + + _, err := cryptoRand.Read(newArray[:]) + if (err != nil) { return [24]byte{}, err } + + return newArray, nil +} + +func GetNewRandomHexString(lengthInBytes int)(string, error){ + + if (lengthInBytes == 0){ + return "", errors.New("GetNewRandomHexString called with 0 lengthInBytes.") + } + + randomBytes := make([]byte, lengthInBytes) + _, err := cryptoRand.Read(randomBytes[:]) + if (err != nil) { return "", err } + + randomString := encoding.EncodeBytesToHexString(randomBytes) + + return randomString, nil +} + +func VerifyHexString(expectedLengthInBytes uint32, stringToVerify string)bool{ + + inputStringBytes, err := encoding.DecodeHexStringToBytes(stringToVerify) + if (err != nil) { + return false + } + + if (len(inputStringBytes) != int(expectedLengthInBytes)){ + return false + } + + return true +} + + +func ConvertBoolToYesOrNoString(input bool)string{ + + if (input == true){ + return "Yes" + } + + return "No" +} + +func ConvertYesOrNoStringToBool(input string)(bool, error){ + + if (input == "Yes"){ + return true, nil + } + if (input == "No"){ + return false, nil + } + + return false, errors.New("ConvertYesOrNoStringToBool called with invalid yes/no: " + input) +} + + +func DeleteIndexFromStringList(inputList []string, indexToDelete int)([]string, error){ + + if (len(inputList) == 0){ + return nil, errors.New("DeleteIndexFromStringList called with empty list.") + } + + if (indexToDelete < 0){ + return nil, errors.New("DeleteIndexFromStringList called with negative indexToDelete.") + } + + finalIndex := len(inputList) - 1 + + if (indexToDelete > finalIndex){ + return nil, errors.New("DeleteIndexFromStringList called with indexToDelete which is too large.") + } + + newList := slices.Delete(inputList, indexToDelete, indexToDelete+1) + + return newList, nil +} + + +//Outputs: +// -[]string: New list +// -bool: Deleted any items +func DeleteAllMatchingItemsFromStringList(inputList []string, itemToDelete string)([]string, bool){ + + listCopy := slices.Clone(inputList) + + deletionFunction := func(input string)bool{ + if (input == itemToDelete){ + return true + } + return false + } + + newList := slices.DeleteFunc(listCopy, deletionFunction) + + if (len(newList) == len(inputList)){ + return newList, false + } + + return newList, true +} + +//Outputs: +// -[]string: New list +// -bool: Deleted any items +func DeleteAllMatchingItemsFromProfileHashList(inputList [][28]byte, itemToDelete [28]byte)([][28]byte, bool){ + + listCopy := slices.Clone(inputList) + + deletionFunction := func(input [28]byte)bool{ + if (input == itemToDelete){ + return true + } + return false + } + + newList := slices.DeleteFunc(listCopy, deletionFunction) + + if (len(newList) == len(inputList)){ + return newList, false + } + + return newList, true +} + +func GetSharedItemsOfTwoStringLists(listA []string, listB []string)[]string{ + + sharedItemsList := make([]string, 0) + + for _, element := range listA{ + + containsItem := slices.Contains(listB, element) + if (containsItem == true){ + sharedItemsList = append(sharedItemsList, element) + } + } + + return sharedItemsList +} + +// This function will compare two lists. It does not care about item order. +func CheckIfTwoListsContainIdenticalItems[E comparable](listA []E, listB []E)bool{ + + if (len(listA) != len(listB)){ + return false + } + + itemCountMapA := make(map[E]int) + + for _, item := range listA{ + itemCountMapA[item] += 1 + } + + itemCountMapB := make(map[E]int) + + for _, item := range listB{ + itemCountMapB[item] += 1 + } + + areEqual := maps.Equal(itemCountMapA, itemCountMapB) + if (areEqual == false){ + return false + } + + return true +} + +// This function will split a list into sublists +func SplitListIntoSublists[E any](inputList []E, maximumItemsPerSublist int)([][]E, error){ + + if (maximumItemsPerSublist <= 0){ + return nil, errors.New("maximumItemsPerSublist is <= 0") + } + + lengthOfInputList := len(inputList) + + if (lengthOfInputList <= maximumItemsPerSublist){ + + result := [][]E{inputList} + + return result, nil + } + + numberOfSublists := float64(lengthOfInputList)/float64(maximumItemsPerSublist) + + numberOfSublistsCeil := math.Ceil(numberOfSublists) + + itemsPerSublist := float64(lengthOfInputList)/numberOfSublistsCeil + + itemsPerSublistCeiled := math.Ceil(itemsPerSublist) + + if (itemsPerSublistCeiled > 2147483647){ + return nil, errors.New("Items per sublist is out of range.") + } + + sizeOfEachSublist := int(itemsPerSublistCeiled) + + maximumIndex := lengthOfInputList-1 + + listOfSublists := make([][]E, 0) + + currentSublist := make([]E, 0) + + for index, element := range inputList{ + + currentSublist = append(currentSublist, element) + + currentSublistLength := len(currentSublist) + + if (currentSublistLength == sizeOfEachSublist || index == maximumIndex){ + + listOfSublists = append(listOfSublists, currentSublist) + currentSublist = make([]E, 0) + } + } + + return listOfSublists, nil +} + + +func AddItemToStringListAndAvoidDuplicate(inputList []string, newItem string)[]string{ + + for _, element := range inputList{ + + if (element == newItem){ + return inputList + } + } + + inputList = append(inputList, newItem) + + return inputList +} + + +// Will combine two lists, and avoid any duplicate entries +func CombineTwoListsAndAvoidDuplicates[E comparable](listA []E, listB []E)[]E{ + + combinedListsMap := make(map[E]struct{}) + + for _, element := range listA{ + combinedListsMap[element] = struct{}{} + } + for _, element := range listB{ + combinedListsMap[element] = struct{}{} + } + + combinedList := GetListOfMapKeys(combinedListsMap) + + return combinedList +} + + +func RemoveDuplicatesFromStringList(inputList []string)[]string{ + + if (len(inputList) <= 1){ + + listCopy := slices.Clone(inputList) + + return listCopy + } + + listAsMap := make(map[string]struct{}) + + for _, element := range inputList{ + listAsMap[element] = struct{}{} + } + + newList := GetListOfMapKeys(listAsMap) + + return newList +} + +// Outputs: +// -bool: Duplicate exists +// -E: The first duplicate we found +func CheckIfListContainsDuplicates[E comparable](inputList []E)(bool, E){ + + if (len(inputList) <= 1){ + var emptyItem E + return false, emptyItem + } + + listMap := make(map[E]struct{}) + + for index, element := range inputList{ + + if (index != 0){ + _, exists := listMap[element] + if (exists == true){ + return true, element + } + } + + listMap[element] = struct{}{} + } + + var emptyItem E + return false, emptyItem +} + + +// Uses weak randomness +func GetRandomItemFromList[E any](inputList []E)(E, error){ + + lengthOfList := len(inputList) + if (lengthOfList == 0){ + var emptyItem E + return emptyItem, errors.New("GetRandomItemFromList called with empty list.") + } + if (lengthOfList == 1){ + result := inputList[0] + return result, nil + } + + randomIndex := mathRand.IntN(lengthOfList) + + result := inputList[randomIndex] + + return result, nil +} + +// This function will return a list containing a map's keys +func GetListOfMapKeys[M map[K]V, K comparable, V any](inputMap M)[]K{ + + newList := make([]K, 0, len(inputMap)) + + for key, _ := range inputMap{ + + newList = append(newList, key) + } + + return newList +} + +// This function will return a list containing a map's values +func GetListOfMapValues[M map[K]V, K comparable, V any](inputMap M)[]V{ + + newList := make([]V, 0, len(inputMap)) + + for _, value := range inputMap{ + + newList = append(newList, value) + } + + return newList +} + +// This function will return a deep copy of a list of string->string maps +func DeepCopyStringToStringMapList(inputMapList []map[string]string)[]map[string]string{ + + mapListCopy := make([]map[string]string, 0, len(inputMapList)) + + for _, element := range inputMapList{ + + elementCopy := maps.Clone(element) + + mapListCopy = append(mapListCopy, elementCopy) + } + + return mapListCopy +} + +//Uses weak randomness +func RandomizeListOrder[E any](inputList []E){ + + mathRand.Shuffle(len(inputList), func(i int, j int){ + inputList[i], inputList[j] = inputList[j], inputList[i] + }) +} + + +func SortIdentityHashListToUnicodeOrder(inputList [][16]byte)error{ + + // Map Structure: Identity hash -> Identity hash string + identityHashStringsMap := make(map[[16]byte]string) + + for _, identityHash := range inputList{ + + identityHashString, _, err := identity.EncodeIdentityHashBytesToString(identityHash) + if (err != nil){ + identityHashHex := encoding.EncodeBytesToHexString(identityHash[:]) + return errors.New("SortIdentityHashListToUnicodeOrder called with invalid identityHash: " + identityHashHex) + } + + identityHashStringsMap[identityHash] = identityHashString + } + + compareFunction := func(identityHashA [16]byte, identityHashB [16]byte)int{ + + if (identityHashA == identityHashB){ + return 0 + } + + identityHashAString, exists := identityHashStringsMap[identityHashA] + if (exists == false){ + panic("identityHashA is missing from identityHashStringsMap.") + } + + identityHashBString, exists := identityHashStringsMap[identityHashB] + if (exists == false){ + panic("identityHashB is missing from identityHashStringsMap.") + } + + if (identityHashAString < identityHashBString){ + return -1 + } + + return 1 + } + + slices.SortFunc(inputList, compareFunction) + + return nil +} + + +func CopyAndSortStringListToUnicodeOrder(inputList []string)[]string{ + + listCopy := slices.Clone(inputList) + + slices.Sort(listCopy) + + return listCopy +} + +func SortStringListToUnicodeOrder(inputList []string){ + + slices.Sort(inputList) +} + + +func GetRandomByteWithinRange(minimum byte, maximum byte)(byte, error){ + + if (maximum < minimum){ + return 0, errors.New("GetRandomByteWithinRange called with maximum < minimum") + } + + randomInt := GetRandomIntWithinRange(int(minimum), int(maximum)) + + if (randomInt < 0 || randomInt > 255){ + return 0, errors.New("GetRandomIntWithinRange returning out of bounds result.") + } + + randomByte := byte(randomInt) + + return randomByte, nil +} + +// Will return random int between two input numbers +// Returns weak randomness +func GetRandomIntWithinRange(numA int, numB int)int{ + + if (numA == numB){ + return numA + } + + lesserValue := min(numA, numB) + greaterValue := max(numA, numB) + + differenceBetweenValues := greaterValue - lesserValue + + randomValue := mathRand.IntN(differenceBetweenValues + 1) + + resultValue := lesserValue + randomValue + + return resultValue +} + +// This function will return a bool with the provided probabilityOfTrue +// For example, if the probabilityOfTrue is 0.6, then 60% of the time, this function will return true +// +func GetRandomBoolWithProbability(probabilityOfTrue float64)(bool, error){ + + if (probabilityOfTrue < 0 || probabilityOfTrue > 1){ + + probabilityOfTrueString := ConvertFloat64ToString(probabilityOfTrue) + return false, errors.New("GetRandomBoolWithProbability called with invalid probabilityOfTrue: " + probabilityOfTrueString) + } + + if (probabilityOfTrue == 0){ + return false, nil + } + if (probabilityOfTrue == 1){ + return true, nil + } + + // This function returns a random float between 0-1 + randomFloat := mathRand.Float64() + + if (randomFloat <= probabilityOfTrue){ + return true, nil + } + + return false, nil +} + +// Will return random int64 between two input numbers +// Returns weak randomness +func GetRandomInt64WithinRange(numA int64, numB int64)int64{ + + if (numA == numB){ + return numA + } + + lesserValue := min(numA, numB) + greaterValue := max(numA, numB) + + differenceBetweenValues := greaterValue - lesserValue + + randomValue := mathRand.Int64N(differenceBetweenValues + 1) + + resultValue := lesserValue + randomValue + + return resultValue +} + +func GetRandomBool()bool{ + + randomNumber := mathRand.IntN(2) + + if (randomNumber == 1){ + return true + } + return false +} + +// This will replace newline characters with the "⁋" character +// It also counts tabs as being 5 characters long +//Outputs: +// -string: Trimmed/flattened string (or original string if no changes were made) +// -bool: Any trimming/flattening occurred +// -error +func TrimAndFlattenString(inputString string, maximumCharacters int)(string, bool, error){ + + if (maximumCharacters < 1) { + return "", false, errors.New("TrimAndFlattenString called with maximumCharacters which is less than 1.") + } + + // Note that some characters are multiple runes in length. + // This is not a perfect estimate, but it will only overestimate the length + // All english characters are 1 byte long, so they are also 1 rune long + + // We treat tabs as being 5 characters long + tabsReplacedString := strings.ReplaceAll(inputString, "\t", " ") + + getFlattenedString := func()string{ + + containsNewlines := strings.Contains(tabsReplacedString, "\n") + if (containsNewlines == true){ + + // String must be flattened + + newlinesReplacedString := strings.ReplaceAll(tabsReplacedString, "\n", "⁋") + + return newlinesReplacedString + } + + return tabsReplacedString + } + + flattenedString := getFlattenedString() + + numberOfCharacters := len([]rune(flattenedString)) + + if (numberOfCharacters <= maximumCharacters){ + // No trimming is needed. + return flattenedString, false, nil + } + + stringCharactersList := []rune(flattenedString) + + outputString := string(stringCharactersList[:maximumCharacters]) + + outputStringWithEllipsis := outputString + "..." + + return outputStringWithEllipsis, true, nil +} + + +func TranslateAndJoinStringListItems(inputList []string, delimiter string)string{ + + if (len(inputList) == 0){ + return "" + } + + // We use this to build the output + var outputBuilder strings.Builder + + finalIndex := len(inputList) - 1 + + for index, element := range inputList{ + + elementTranslated := translation.TranslateTextFromEnglishToMyLanguage(element) + + outputBuilder.WriteString(elementTranslated) + + if (index != finalIndex){ + outputBuilder.WriteString(delimiter) + } + } + + result := outputBuilder.String() + + return result +} + +// Show remainder will include two units, such as "5 days 10 hours" or "3 months 2 weeks" +func ConvertUnixTimeDurationToUnitsTimeTranslated(unixDuration int64, showRemainder bool)(string, error){ + + // TODO: Add translation + + getDurationWithRemainder := func()(string, int64){ + + if (unixDuration <= 0) { + return "0 seconds", 0 + } + + minuteUnix := int64(60) + + if (unixDuration < minuteUnix){ + numSeconds := unixDuration + + durationSecondsString := ConvertInt64ToString(numSeconds) + if (durationSecondsString == "1"){ + return durationSecondsString + " Second", 0 + } + return durationSecondsString + " Seconds", 0 + } + + hourUnix := int64(3600) + + if (unixDuration < hourUnix){ + numMinutes := unixDuration/minuteUnix + remainderTime := unixDuration - (numMinutes * minuteUnix) + + durationMinutesString := ConvertInt64ToString(numMinutes) + if (durationMinutesString == "1"){ + return durationMinutesString + " Minute", remainderTime + } + return durationMinutesString + " Minutes", remainderTime + } + + dayUnix := int64(86400) + + if (unixDuration < dayUnix){ + numHours := unixDuration/hourUnix + remainderTime := unixDuration - (numHours * hourUnix) + + if (remainderTime < minuteUnix){ + remainderTime = 0 + } + + durationHoursString := ConvertInt64ToString(numHours) + if (durationHoursString == "1"){ + return durationHoursString + " Hour", remainderTime + } + return durationHoursString + " Hours", remainderTime + } + + weekUnix := int64(604800) + + if (unixDuration < weekUnix){ + numDays := unixDuration/dayUnix + remainderTime := unixDuration - (numDays * dayUnix) + + if (remainderTime < hourUnix){ + remainderTime = 0 + } + + durationDaysString := ConvertInt64ToString(numDays) + if (durationDaysString == "1"){ + return durationDaysString + " Day", remainderTime + } + return durationDaysString + " Days", remainderTime + } + + monthUnix := int64(2629743) + + if (unixDuration < monthUnix){ + numWeeks := unixDuration/weekUnix + remainderTime := unixDuration - (numWeeks * weekUnix) + + if (remainderTime < dayUnix){ + remainderTime = 0 + } + + durationWeeksString := ConvertInt64ToString(numWeeks) + if (durationWeeksString == "1"){ + return durationWeeksString + " Week", remainderTime + } + return durationWeeksString + " Weeks", remainderTime + } + + yearUnix := int64(31556926) + + if (unixDuration < yearUnix) { + numMonths := unixDuration/monthUnix + remainderTime := unixDuration - (numMonths * monthUnix) + + if (remainderTime < dayUnix){ + remainderTime = 0 + } + + durationMonthsString := ConvertInt64ToString(numMonths) + if (durationMonthsString == "1"){ + return durationMonthsString + " Month", remainderTime + } + return durationMonthsString + " Months", remainderTime + } + + numYears := unixDuration/yearUnix + remainderTime := unixDuration - (numYears * yearUnix) + + if (remainderTime < dayUnix || numYears > 1){ + remainderTime = 0 + } + + durationYearsString := ConvertInt64ToString(numYears) + if (durationYearsString == "1"){ + return durationYearsString + " Year", remainderTime + } + + return durationYearsString + " Years", remainderTime + } + + timeTranslated, remainder := getDurationWithRemainder() + if (showRemainder == false){ + return timeTranslated, nil + } + if (remainder == 0){ + return timeTranslated, nil + } + + remainderString, err := ConvertUnixTimeDurationToUnitsTimeTranslated(remainder, false) + if (err != nil) { return "", err } + + andTranslated := translation.TranslateTextFromEnglishToMyLanguage("and") + + // Example of resultWithRemainder: "1 week and 5 days" + + resultWithRemainder := timeTranslated + " " + andTranslated + " " + remainderString + + return resultWithRemainder, nil +} + + +func ConvertUnixTimeToTimeAgoTranslated(inputUnixTime int64, showRemainderTime bool)(string, error){ + + currentTime := time.Now().Unix() + + if (inputUnixTime > currentTime){ + return "", errors.New("ConvertUnixTimeToTimeAgoTranslated called with invalid date: Time is not in the past.") + } + + durationUnix := currentTime - inputUnixTime + + durationString, err := ConvertUnixTimeDurationToUnitsTimeTranslated(durationUnix, showRemainderTime) + if (err != nil) { return "", err } + + agoTranslated := translation.TranslateTextFromEnglishToMyLanguage("Ago") + + durationStringAgo := durationString + " " + agoTranslated + + return durationStringAgo, nil +} + +func ConvertUnixTimeToTimeFromNowTranslated(inputUnixTime int64, showRemainderTime bool)(string, error){ + + currentTime := time.Now().Unix() + + getDurationUnixAndSuffix := func()(int64, string){ + + if (currentTime >= inputUnixTime){ + + // inputUnixTime is in the past + + durationUnix := currentTime - inputUnixTime + + return durationUnix, "Ago" + } + + // inputUnixTime is in the future + + durationUnix := inputUnixTime - currentTime + + return durationUnix, "In The Future" + } + + durationUnix, suffix := getDurationUnixAndSuffix() + + durationString, err := ConvertUnixTimeDurationToUnitsTimeTranslated(durationUnix, showRemainderTime) + if (err != nil) { return "", err } + + suffixTranslated := translation.TranslateTextFromEnglishToMyLanguage(suffix) + + timeFromNowTranslated := durationString + " " + suffixTranslated + + return timeFromNowTranslated, nil +} + +// This will return time in the the following format: February 14, 2023 at 2:14. +// It returns 24 hour time +func ConvertUnixTimeToTranslatedTime(inputTime int64)string{ + + timeObject := time.Unix(inputTime, 0) + + timeYear, timeMonth, timeDay := timeObject.Date() + + timeYearString := ConvertIntToString(timeYear) + + timeMonthString := translation.TranslateTextFromEnglishToMyLanguage(timeMonth.String()) + + timeDayString := ConvertIntToString(timeDay) + + atTranslated := translation.TranslateTextFromEnglishToMyLanguage("at") + + timeHour, timeMinute, _ := timeObject.Clock() + + timeHourString := ConvertIntToString(timeHour) + + getTimeMinuteStringFormatted := func()string{ + + timeMinuteString := ConvertIntToString(timeMinute) + + // We have to convert times such as 11:0 -> 11:00 + + if (timeMinute < 10){ + + result := "0" + timeMinuteString + + return result + } + + return timeMinuteString + } + + timeMinuteStringFormatted := getTimeMinuteStringFormatted() + + timeString := timeMonthString + " " + timeDayString + ", " + timeYearString + " " + atTranslated + " " + timeHourString + ":" + timeMinuteStringFormatted + + return timeString +} + + +func ConvertFloat64ToRoundedStringWithTranslatedUnits(inputFloat float64)(string, error){ + + if (inputFloat < 0){ + return "", errors.New("ConvertFloat64ToRoundedStringWithTranslatedUnits called with negative input.") + } + + if (inputFloat < 1000){ + result := ConvertInt64ToString(int64(inputFloat)) + return result, nil + } + + getUnits := func()(float64, string){ + + millionFloat := float64(1000000) + + if (inputFloat < millionFloat){ + divided := inputFloat / 1000 + return divided, "thousand" + } + + billionFloat := float64(1000000000) + + if (inputFloat < billionFloat){ + divided := inputFloat / millionFloat + return divided, "million" + } + + trillionFloat := float64(1000000000000) + + if (inputFloat < trillionFloat){ + divided := inputFloat / billionFloat + return divided, "billion" + } + + quadrillionFloat := float64(1000000000000000) + + if (inputFloat < quadrillionFloat){ + divided := inputFloat / trillionFloat + return divided, "trillion" + } + + quintillionFloat := float64(1000000000000000000) + + if (inputFloat < quintillionFloat){ + divided := inputFloat / quadrillionFloat + return divided, "quadrillion" + } + + sextillionFloat := float64(1000000000000000000000) + + if (inputFloat < sextillionFloat){ + divided := inputFloat / quintillionFloat + return divided, "quintillion" + } + + septillionFloat := float64(1000000000000000000000000) + + if (inputFloat < septillionFloat){ + divided := inputFloat / sextillionFloat + return divided, "sextillion" + } + divided := inputFloat / septillionFloat + return divided, "septillion" + } + + dividedResult, unitsString := getUnits() + + unitsTranslated := translation.TranslateTextFromEnglishToMyLanguage(unitsString) + + resultString := ConvertFloat64ToStringRounded(dividedResult, 1) + + hasSuffix := strings.HasSuffix(resultString, ".0") + if (hasSuffix == true){ + + resultTrimmed := strings.TrimSuffix(resultString, ".0") + + resultWithUnits := resultTrimmed + " " + unitsTranslated + + return resultWithUnits, nil + } + + resultWithUnits := resultString + " " + unitsTranslated + + return resultWithUnits, nil +} + + +// This function takes a number and the min and max range of that number +// It returns a number scaled between a new min and max +func ScaleNumberProportionally(ascending bool, input int, inputMin int, inputMax int, newMin int, newMax int)(int, error){ + + if (inputMin == inputMax) { + return inputMin, nil + } + if (inputMin > inputMax) { + return 0, errors.New("ScaleNumberProportionally error: InputMin is greater than inputMax") + } + + if (input < inputMin) { + return 0, errors.New("ScaleNumberProportionally error: Input is less than inputMin") + } + if (input > inputMax) { + return 0, errors.New("ScaleNumberProportionally error: Input is greater than inputMax") + } + + if (newMin == newMax) { + return newMin, nil + } + if (newMin > newMin){ + return 0, errors.New("ScaleNumberProportionally error: newMin is greater than newMin.") + } + + inputRangePortionLength := input - inputMin + + inputRangeDistance := inputMax - inputMin + + inputRangePortion := float64(inputRangePortionLength)/float64(inputRangeDistance) + + // This represents the portion of our output range that we want to travel across + getOutputRangePortion := func()float64{ + if (ascending == true){ + return inputRangePortion + } + + outputRangePortion := 1 - inputRangePortion + return outputRangePortion + } + + outputRangePortion := getOutputRangePortion() + + outputRangeDistance := newMax - newMin + + outputRangePortionLength := float64(outputRangeDistance) * outputRangePortion + + resultFloat64 := float64(newMin) + outputRangePortionLength + + result := int(math.Floor(resultFloat64)) + + return result, nil +} + +func XORTwo32ByteArrays(array1 [32]byte, array2 [32]byte)[32]byte{ + + var newArray [32]byte + + for i := 0; i < 32; i++ { + + newArray[i] = array1[i] ^ array2[i] + } + + return newArray +} + +func CheckIfStringContainsTabsOrNewlines(inputString string)bool{ + + for _, element := range inputString{ + if (element == '\r' || element == '\n' || element == '\t') { + return true + } + } + return false +} + + + diff --git a/internal/helpers/helpers_test.go b/internal/helpers/helpers_test.go new file mode 100644 index 0000000..174cac2 --- /dev/null +++ b/internal/helpers/helpers_test.go @@ -0,0 +1,440 @@ +package helpers_test + +import "seekia/internal/helpers" + +import "testing" + +import "slices" + + +func TestTimeFunctions(t *testing.T){ + + minuteUnix := int64(60) + hourUnix := int64(3600) + dayUnix := int64(86400) + weekUnix := int64(604800) + monthUnix := int64(2629743) + yearUnix := int64(31556926) + + durationTranslated, err := helpers.ConvertUnixTimeDurationToUnitsTimeTranslated(hourUnix, true) + if (err != nil) { + t.Fatalf("Failed to get duration units time translated: " + err.Error()) + } + + if (durationTranslated != "1 Hour") { + t.Fatalf("Invalid units time translated: " + durationTranslated) + } + + durationTranslated, err = helpers.ConvertUnixTimeDurationToUnitsTimeTranslated(hourUnix + minuteUnix*2, true) + if (err != nil) { + t.Fatalf("Failed to get duration units time translated: " + err.Error()) + } + + if (durationTranslated != "1 Hour and 2 Minutes") { + t.Fatalf("Invalid units time translated: " + durationTranslated) + } + + durationTranslated, err = helpers.ConvertUnixTimeDurationToUnitsTimeTranslated(dayUnix + minuteUnix*2, true) + if (err != nil) { + t.Fatalf("Failed to get duration units time translated: " + err.Error()) + } + + if (durationTranslated != "1 Day") { + t.Fatalf("Invalid units time translated: " + durationTranslated) + } + + durationTranslated, err = helpers.ConvertUnixTimeDurationToUnitsTimeTranslated(weekUnix + dayUnix*2, true) + if (err != nil) { + t.Fatalf("Failed to get duration units time translated: " + err.Error()) + } + + if (durationTranslated != "1 Week and 2 Days") { + t.Fatalf("Invalid units time translated: " + durationTranslated) + } + + durationTranslated, err = helpers.ConvertUnixTimeDurationToUnitsTimeTranslated(monthUnix + dayUnix*2, true) + if (err != nil) { + t.Fatalf("Failed to get duration units time translated: " + err.Error()) + } + + if (durationTranslated != "1 Month and 2 Days") { + t.Fatalf("Invalid units time translated: " + durationTranslated) + } + + durationTranslated, err = helpers.ConvertUnixTimeDurationToUnitsTimeTranslated(monthUnix + weekUnix, true) + if (err != nil) { + t.Fatalf("Failed to get duration units time translated: " + err.Error()) + } + + if (durationTranslated != "1 Month and 1 Week") { + t.Fatalf("Invalid units time translated: " + durationTranslated) + } + + durationTranslated, err = helpers.ConvertUnixTimeDurationToUnitsTimeTranslated(monthUnix + weekUnix*2, true) + if (err != nil) { + t.Fatalf("Failed to get duration units time translated: " + err.Error()) + } + + if (durationTranslated != "1 Month and 2 Weeks") { + t.Fatalf("Invalid units time translated: " + durationTranslated) + } + + durationTranslated, err = helpers.ConvertUnixTimeDurationToUnitsTimeTranslated(yearUnix + weekUnix*2, true) + if (err != nil) { + t.Fatalf("Failed to get duration units time translated: " + err.Error()) + } + + if (durationTranslated != "1 Year and 2 Weeks") { + t.Fatalf("Invalid units time translated: " + durationTranslated) + } + + durationTranslated, err = helpers.ConvertUnixTimeDurationToUnitsTimeTranslated(10 * yearUnix + weekUnix*2, true) + if (err != nil) { + t.Fatalf("Failed to get duration units time translated: " + err.Error()) + } + + if (durationTranslated != "10 Years") { + t.Fatalf("Invalid units time translated: " + durationTranslated) + } + +} + + +func TestIntToStringWithCommas(t *testing.T){ + + num := 1 + + result := helpers.ConvertIntToStringWithCommas(num) + if (result != "1"){ + t.Fatalf("Int to string with commas conversion failed") + } + + num = 12 + + result = helpers.ConvertIntToStringWithCommas(num) + if (result != "12"){ + t.Fatalf("Int to string with commas conversion failed.") + } + + num = 123 + + result = helpers.ConvertIntToStringWithCommas(num) + if (result != "123"){ + t.Fatalf("Int to string with commas conversion failed.") + } + + num = 1234 + + result = helpers.ConvertIntToStringWithCommas(num) + if (result != "1,234"){ + t.Fatalf("Int to string with commas conversion failed.") + } + + num = 12345 + + result = helpers.ConvertIntToStringWithCommas(num) + if (result != "12,345"){ + + t.Fatalf("Int to string with commas conversion failed.") + } + + num = 123456 + + result = helpers.ConvertIntToStringWithCommas(num) + if (result != "123,456"){ + t.Fatalf("Int to string with commas conversion failed.") + } + + num = 1234567 + + result = helpers.ConvertIntToStringWithCommas(num) + if (result != "1,234,567"){ + t.Fatalf("Int to string with commas conversion failed.") + } + + num = 12345678 + + result = helpers.ConvertIntToStringWithCommas(num) + if (result != "12,345,678"){ + t.Fatalf("Int to string with commas conversion failed.") + } + + num = 123456789 + + result = helpers.ConvertIntToStringWithCommas(num) + if (result != "123,456,789"){ + t.Fatalf("Int to string with commas conversion failed.") + } + + num = 1234567891 + + result = helpers.ConvertIntToStringWithCommas(num) + if (result != "1,234,567,891"){ + t.Fatalf("Int to string with commas conversion failed.") + } + + num = 12345678912 + + result = helpers.ConvertIntToStringWithCommas(num) + if (result != "12,345,678,912"){ + t.Fatalf("Int to string with commas conversion failed.") + } + + num = 123456789123 + + result = helpers.ConvertIntToStringWithCommas(num) + if (result != "123,456,789,123"){ + t.Fatalf("Int to string with commas conversion failed.") + } +} + +func TestRandomNumberFunctions(t *testing.T){ + + min := 1 + max := 100 + + for i:=0; i < 100; i++{ + + randomNumber := helpers.GetRandomIntWithinRange(min, max) + if (randomNumber < min || randomNumber > max){ + t.Fatalf("Failed to get random number between two numbers.") + } + } + + min = -10 + max = -5 + + for i:=0; i < 100; i++{ + + randomNumber := helpers.GetRandomIntWithinRange(min, max) + if (randomNumber < min || randomNumber > max){ + t.Fatalf("Failed to get random number between two numbers.") + } + } + + minInt64 := int64(0) + maxInt64 := int64(100) + + for i:=0; i < 100; i++{ + + randomNumber := helpers.GetRandomInt64WithinRange(minInt64, maxInt64) + if (randomNumber < minInt64 || randomNumber > maxInt64){ + t.Fatalf("Failed to get random number between two numbers.") + } + } + + minInt64 = int64(-30) + maxInt64 = int64(50) + + for i:=0; i < 100; i++{ + + randomNumber := helpers.GetRandomInt64WithinRange(minInt64, maxInt64) + if (randomNumber < minInt64 || randomNumber > maxInt64){ + t.Fatalf("Failed to get random number between two numbers.") + } + } +} + +func TestGetRandomSliceElementFunction(t *testing.T){ + + testListA := []string{"A"} + + result, err := helpers.GetRandomItemFromList(testListA) + if (err != nil) { + t.Fatalf("Failed to get random item from string list: " + err.Error()) + } + + if (result != "A"){ + t.Fatalf("Failed to get random item from string list- Invalid result: " + result) + } + + testListB := []string{"A", "B", "C"} + + result, err = helpers.GetRandomItemFromList(testListB) + if (err != nil) { + t.Fatalf("Failed to get random item from string list: " + err.Error()) + } + + isValid := slices.Contains(testListB, result) + if (isValid == false){ + t.Fatalf("Failed to get random item from string list- Invalid result: " + result) + } +} + +func TestSliceFunctions(t *testing.T){ + + testListA := []string{"1", "2"} + + newListA, err := helpers.DeleteIndexFromStringList(testListA, 0) + if (err != nil) { + t.Fatalf("Failed to delete index from string list: " + err.Error()) + } + + expectedListA := []string{"2"} + + areEqual := slices.Equal(newListA, expectedListA) + if (areEqual == false){ + t.Fatalf("Failed to delete index from string list.") + } + + testListB := []string{"1", "2", "3", "4"} + + newListB, err := helpers.DeleteIndexFromStringList(testListB, 3) + if (err != nil) { + t.Fatalf("Failed to delete index from string list: " + err.Error()) + } + + expectedListB := []string{"1", "2", "3"} + + areEqual = slices.Equal(newListB, expectedListB) + if (areEqual == false){ + t.Fatalf("Failed to delete index from string list.") + } + + testListC := []string{"1"} + + newListC, err := helpers.DeleteIndexFromStringList(testListC, 0) + if (err != nil) { + t.Fatalf("Failed to delete index from string list: " + err.Error()) + } + + if (len(newListC) != 0){ + t.Fatalf("Failed to delete index from string list.") + } + + testListD := []string{"1", "2", "3", "4"} + + newListD, err := helpers.DeleteIndexFromStringList(testListD, 0) + if (err != nil) { + t.Fatalf("Failed to delete index from string list: " + err.Error()) + } + + expectedListD := []string{"2", "3", "4"} + + areEqual = slices.Equal(newListD, expectedListD) + if (areEqual == false){ + t.Fatalf("Failed to delete index from string list.") + } +} + + +func TestNumberProportionalScaling(t *testing.T){ + + + result, err := helpers.ScaleNumberProportionally(true, 50, 0, 100, 0, 50) + if (err != nil){ + t.Fatalf("ScaleNumberProportionally failed: " + err.Error()) + } + if (result != 25){ + t.Fatalf("ScaleNumberProportionally failed test 1.") + } + + result, err = helpers.ScaleNumberProportionally(true, 25, 0, 100, 0, 200) + if (err != nil){ + t.Fatalf("ScaleNumberProportionally failed: " + err.Error()) + } + if (result != 50){ + t.Fatalf("ScaleNumberProportionally failed test 2.") + } + + result, err = helpers.ScaleNumberProportionally(false, 25, 0, 100, 0, 200) + if (err != nil){ + t.Fatalf("ScaleNumberProportionally failed: " + err.Error()) + } + if (result != 150){ + t.Fatalf("ScaleNumberProportionally failed test 3.") + } + + result, err = helpers.ScaleNumberProportionally(true, 1, 0, 10, 0, 200) + if (err != nil){ + t.Fatalf("ScaleNumberProportionally failed: " + err.Error()) + } + if (result != 20){ + t.Fatalf("ScaleNumberProportionally failed test 4.") + } + + result, err = helpers.ScaleNumberProportionally(true, -50, -100, 0, 0, 200) + if (err != nil){ + t.Fatalf("ScaleNumberProportionally failed: " + err.Error()) + } + if (result != 100){ + t.Fatalf("ScaleNumberProportionally failed test 5.") + } + + + result, err = helpers.ScaleNumberProportionally(true, -25, -100, 0, 0, 200) + if (err != nil){ + t.Fatalf("ScaleNumberProportionally failed: " + err.Error()) + } + if (result != 150){ + t.Fatalf("ScaleNumberProportionally failed test 6.") + } + + result, err = helpers.ScaleNumberProportionally(true, 50, 0, 100, 0, 2) + if (err != nil){ + t.Fatalf("ScaleNumberProportionally failed: " + err.Error()) + } + if (result != 1){ + t.Fatalf("ScaleNumberProportionally failed test 7.") + } + + + result, err = helpers.ScaleNumberProportionally(true, 10, 0, 100, 5, 25) + if (err != nil){ + t.Fatalf("ScaleNumberProportionally failed: " + err.Error()) + } + if (result != 7){ + t.Fatalf("ScaleNumberProportionally failed test 8.") + } + + result, err = helpers.ScaleNumberProportionally(true, 100, 0, 100, 2, 22) + if (err != nil){ + t.Fatalf("ScaleNumberProportionally failed: " + err.Error()) + } + if (result != 22){ + t.Fatalf("ScaleNumberProportionally failed test 9.") + } + + result, err = helpers.ScaleNumberProportionally(false, 0, 0, 100, 2, 22) + if (err != nil){ + t.Fatalf("ScaleNumberProportionally failed: " + err.Error()) + } + if (result != 22){ + t.Fatalf("ScaleNumberProportionally failed test 10.") + } +} + + +func TestSplitListIntoSublists(t *testing.T){ + + inputList := make([]string, 0, 100) + + for i := 0; i < 100; i++{ + + item := helpers.ConvertIntToString(i+1) + + inputList = append(inputList, item) + } + + sublistsList, err := helpers.SplitListIntoSublists(inputList, 10) + if (err != nil) { + t.Fatalf("Failed to split string list into sublists: " + err.Error()) + } + + totalElements := 0 + + for _, element := range sublistsList{ + + elementsInSublist := len(element) + totalElements += elementsInSublist + + if (elementsInSublist != 10){ + t.Fatalf("Failed to split string list into sublists: invalid sublist length.") + } + } + + if (totalElements != 100){ + t.Fatalf("Failed to split string list into sublists: invalid sublist elements length.") + } +} + diff --git a/internal/identity/identity.go b/internal/identity/identity.go new file mode 100644 index 0000000..501aa1c --- /dev/null +++ b/internal/identity/identity.go @@ -0,0 +1,311 @@ + +// identity provides functions for creating and reading identity keys and identity hashes + +package identity + +// Identity Keys are Edwards Keys used for identity purposes. + +import "seekia/internal/encoding" +import "seekia/internal/cryptography/blake3" +import "seekia/internal/cryptography/edwardsKeys" + +import "bytes" +import "slices" +import cryptoRand "crypto/rand" +import mathRand "math/rand/v2" +import "errors" + + +func VerifyIdentityKeyHex(identityKey string)bool{ + + isValid := edwardsKeys.VerifyPublicKeyHex(identityKey) + if (isValid == false){ + return false + } + + return true +} + +func VerifyIdentityHash(identityHash [16]byte, identityTypeProvided bool, identityType string)(bool, error){ + + if (identityTypeProvided == true){ + if (identityType != "Mate" && identityType != "Host" && identityType != "Moderator"){ + return false, errors.New("VerifyIdentityHash called with invalid provided identityType: " + identityType) + } + } + + userIdentityType, err := GetIdentityTypeFromIdentityHash(identityHash) + if (err != nil){ + return false, nil + } + + if (identityTypeProvided == false){ + return true, nil + } + + if (userIdentityType != identityType){ + return false, nil + } + + return true, nil +} + + +func GetIdentityTypeFromIdentityHash(identityHash [16]byte)(string, error){ + + identityTypeByte := identityHash[15] + + if (identityTypeByte == 1){ + return "Mate", nil + } + if (identityTypeByte == 2){ + return "Host", nil + } + if (identityTypeByte == 3){ + return "Moderator", nil + } + + identityHashHex := encoding.EncodeBytesToHexString(identityHash[:]) + + return "", errors.New("GetIdentityTypeFromIdentityHash called with invalid identity hash: " + identityHashHex) +} + +//Outputs: +// -[16]byte: Bytes representation of identity hash +// -string: Identity type of identity hash +// -error +func ReadIdentityHashString(identityHash string)([16]byte, string, error){ + + if (len(identityHash) != 27){ + return [16]byte{}, "", errors.New("ReadIdentityHashString called with invalid identity hash: Invalid length: " + identityHash) + } + + identityKeyHashString := identityHash[:24] + checksumString := identityHash[24:26] + identityTypeCharacter := string(identityHash[26]) + + identityKeyHashBytes, err := encoding.DecodeBase32StringToBytes(identityKeyHashString) + if (err != nil) { + return [16]byte{}, "", errors.New("ReadIdentityHashString called with invalid identity hash: Contains invalid characters: " + identityHash + ": " + err.Error()) + } + + if (len(identityKeyHashBytes) != 15) { + return [16]byte{}, "", errors.New("ReadIdentityHashString called with invalid identity hash: Invalid bytes length: " + identityHash) + } + + checksumByte, err := encoding.DecodeHexStringToBytes(checksumString) + if (err != nil){ + return [16]byte{}, "", errors.New("ReadIdentityHashString called with invalid identity hash: Invalid checksum: Not Hex: " + identityHash) + } + + if (len(checksumByte) != 1){ + return [16]byte{}, "", errors.New("ReadIdentityHashString called with invalid identity hash: Invalid checksum: Invalid Length: " + identityHash) + } + + expectedChecksumByte, err := blake3.GetBlake3HashAsBytes(1, identityKeyHashBytes) + if (err != nil) { return [16]byte{}, "", err } + + areEqual := bytes.Equal(checksumByte, expectedChecksumByte) + if (areEqual == false){ + return [16]byte{}, "", errors.New("ReadIdentityHashString called with invalid identity hash: Invalid checksum: " + identityHash) + } + + getIdentityTypeNameAndByte := func()(string, byte, error){ + + if (identityTypeCharacter == "m"){ + return "Mate", 1, nil + } + if (identityTypeCharacter == "h"){ + return "Host", 2, nil + } + if (identityTypeCharacter == "r"){ + return "Moderator", 3, nil + } + + return "", 0, errors.New("ReadIdentityHashString called with invalid identity hash: Invalid identityType character: " + identityTypeCharacter + " from " + identityHash) + } + + identityType, identityTypeByte, err := getIdentityTypeNameAndByte() + if (err != nil) { return [16]byte{}, "", err } + + identityHashBytes := append(identityKeyHashBytes, identityTypeByte) + + identityHashBytesArray := [16]byte(identityHashBytes) + + return identityHashBytesArray, identityType, nil +} + + +//Outputs: +// -string: Encoded identity hash +// -string: Identity type +// -error +func EncodeIdentityHashBytesToString(identityHashBytes [16]byte)(string, string, error){ + + identityKeyHashBytes := identityHashBytes[:15] + identityTypeByte := identityHashBytes[15] + + checksumByte, err := blake3.GetBlake3HashAsBytes(1, identityKeyHashBytes) + if (err != nil) { return "", "", err } + + identityKeyHashString := encoding.EncodeBytesToBase32String(identityKeyHashBytes) + + checksumByteHex := encoding.EncodeBytesToHexString(checksumByte) + + //Outputs: + // -string: Identity type + // -string: Identity type character + // -error + getIdentityTypeWithCharacter := func()(string, string, error){ + + if (identityTypeByte == 1){ + + return "Mate", "m", nil + } + if (identityTypeByte == 2){ + + return "Host", "h", nil + } + if (identityTypeByte == 3){ + + return "Moderator", "r", nil + } + + return "", "", errors.New("EncodeIdentityHashBytesToString called with invalid identityHashBytes: Contains invalid identityTypeByte.") + } + + identityType, identityTypeCharacter, err := getIdentityTypeWithCharacter() + if (err != nil) { return "", "", err } + + identityHash := identityKeyHashString + checksumByteHex + identityTypeCharacter + + return identityHash, identityType, nil +} + +func ConvertIdentityKeyToIdentityHash(identityKey [32]byte, identityType string)([16]byte, error){ + + if (identityType != "Mate" && identityType != "Host" && identityType != "Moderator"){ + return [16]byte{}, errors.New("ConvertIdentityKeyToIdentityHash called with invalid identityType: " + identityType) + } + + identityKeyHashBytes, err := blake3.GetBlake3HashAsBytes(15, identityKey[:]) + if (err != nil) { return [16]byte{}, err } + + getIdentityTypeByte := func()byte{ + + if (identityType == "Mate"){ + return 1 + } + if (identityType == "Host"){ + return 2 + } + + return 3 + } + + identityTypeByte := getIdentityTypeByte() + + identityHashBytes := append(identityKeyHashBytes, identityTypeByte) + + identityHashArray := [16]byte(identityHashBytes) + + return identityHashArray, nil +} + + +func GetNewRandomIdentityHash(identityTypeProvided bool, identityType string)([16]byte, error){ + + identityKeyHashBytes := make([]byte, 15) + _, err := cryptoRand.Read(identityKeyHashBytes[:]) + if (err != nil){ return [16]byte{}, err } + + getIdentityTypeByte := func()(byte, error){ + + if (identityTypeProvided == false){ + + randomIndex := mathRand.IntN(3) + + if (randomIndex == 0){ + return 1, nil + } + if (randomIndex == 1){ + return 2, nil + } + // randomIndex == 2 + return 3, nil + } + + if (identityType == "Mate"){ + return 1, nil + } + if (identityType == "Host"){ + return 2, nil + } + if (identityType == "Moderator"){ + return 3, nil + } + + return 0, errors.New("GetNewRandomIdentityHash called with invalid identityType: " + identityType) + } + + identityTypeByte, err := getIdentityTypeByte() + if (err != nil) { return [16]byte{}, err } + + identityHashBytes := append(identityKeyHashBytes, identityTypeByte) + + identityHashArray := [16]byte(identityHashBytes) + + return identityHashArray, nil +} + +func GetPublicPrivateIdentityKeysFromSeedPhraseHash(inputSeedPhraseHash [32]byte)([32]byte, [64]byte, error){ + + // This is the salt used to derive identity hashes from seed phrase hashes + // It is the string "identitykeysseed" decoded from base32 to bytes. + identityKeysSalt := []byte{64, 200, 217, 162, 120, 81, 49, 41, 16, 131} + + hashInput := slices.Concat(inputSeedPhraseHash[:], identityKeysSalt) + + seedBytes, err := blake3.Get32ByteBlake3Hash(hashInput) + if (err != nil) { return [32]byte{}, [64]byte{}, err } + + publicKeyArray, privateKeyArray := edwardsKeys.GetSeededEdwardsPublicAndPrivateKeys(seedBytes) + + return publicKeyArray, privateKeyArray, nil +} + +func GetNewRandomPublicPrivateIdentityKeys()([32]byte, [64]byte, error){ + + publicKeyArray, privateKeyArray, err := edwardsKeys.GetNewRandomPublicAndPrivateEdwardsKeys() + if (err != nil) { return [32]byte{}, [64]byte{}, err } + + return publicKeyArray, privateKeyArray, nil +} + +func GetIdentityHashFromSeedPhraseHash(seedPhraseHash [32]byte, identityType string)([16]byte, error){ + + publicIdentityKey, _, err := GetPublicPrivateIdentityKeysFromSeedPhraseHash(seedPhraseHash) + if (err != nil) { return [16]byte{}, err } + + identityHash, err := ConvertIdentityKeyToIdentityHash(publicIdentityKey, identityType) + if (err != nil) { return [16]byte{}, err } + + return identityHash, nil +} + + +// We use this function to generate custom identity hashes +// This is faster, because we only have to generate the first 10 bytes of the identity hash, and we don't have to calculate the checksum +func GetIdentityHash16CharacterPrefixFromSeedPhraseHash(seedPhraseHash [32]byte)(string, error){ + + publicIdentityKey, _, err := GetPublicPrivateIdentityKeysFromSeedPhraseHash(seedPhraseHash) + if (err != nil) { return "", err } + + identityKeyHashBytes, err := blake3.GetBlake3HashAsBytes(10, publicIdentityKey[:]) + if (err != nil) { return "", err } + + identityHashPrefix := encoding.EncodeBytesToBase32String(identityKeyHashBytes) + + return identityHashPrefix, nil +} + diff --git a/internal/identity/identity_test.go b/internal/identity/identity_test.go new file mode 100644 index 0000000..80e5cff --- /dev/null +++ b/internal/identity/identity_test.go @@ -0,0 +1,133 @@ +package identity_test + +import "seekia/internal/identity" + +import "seekia/internal/encoding" +import "seekia/internal/helpers" + +import "testing" +import "bytes" + +func TestRandomIdentity(t *testing.T){ + + randomIdentityType, err := helpers.GetRandomItemFromList([]string{"Mate", "Host", "Moderator"}) + if (err != nil){ + t.Fatalf("Failed to get random identityType: " + err.Error()) + } + + testIdentityHash, err := identity.GetNewRandomIdentityHash(true, randomIdentityType) + if (err != nil){ + t.Fatalf("GetNewRandomIdentityHash failed: " + err.Error()) + } + + identityType, err := identity.GetIdentityTypeFromIdentityHash(testIdentityHash) + if (err != nil){ + t.Fatalf("GetNewRandomIdentityHash returning invalid identity hash. Reason: " + err.Error()) + } + + if (identityType != randomIdentityType){ + t.Fatalf("GetNewRandomIdentityHash returning identity hash of different identityType: " + identityType) + } +} + +func TestIdentityDerivation(t *testing.T){ + + testSeedPhraseHashHex := "6c63469b0581a2633d37721d466b68ed9c3b3744352a5409792f84e24d19203d" + + testSeedPhraseHashBytes, err := encoding.DecodeHexStringToBytes(testSeedPhraseHashHex) + if (err != nil){ + t.Fatalf("testSeedPhraseHashHex is invalid: Not Hex: " + err.Error()) + } + + testSeedPhraseHashArray := [32]byte(testSeedPhraseHashBytes) + + expectedPublicIdentityKeyHex := "69b6735f22a3991215a139a666dcbb42e7a33fbab32109a185dd60ba2a5a150d" + + expectedPublicIdentityKeyBytes, err := encoding.DecodeHexStringToBytes(expectedPublicIdentityKeyHex) + if (err != nil){ + t.Fatalf("expectedPublicIdentityKeyHex is invalid: Not Hex: " + err.Error()) + } + + expectedPrivateIdentityKeyHex := "38a84046125d37b00343c281a91ea3e9b7edee50b82df2b5350b66f44985423369b6735f22a3991215a139a666dcbb42e7a33fbab32109a185dd60ba2a5a150d" + + expectedPrivateIdentityKeyBytes, err := encoding.DecodeHexStringToBytes(expectedPrivateIdentityKeyHex) + if (err != nil){ + t.Fatalf("expectedPrivateIdentityKeyHex is invalid: Not Hex: " + err.Error()) + } + + publicIdentityKey, privateIdentityKey, err := identity.GetPublicPrivateIdentityKeysFromSeedPhraseHash(testSeedPhraseHashArray) + if (err != nil){ + t.Fatalf("GetPublicPrivateIdentityKeysFromSeedPhraseHash failed: " + err.Error()) + } + areEqual := bytes.Equal(publicIdentityKey[:], expectedPublicIdentityKeyBytes) + if (areEqual == false){ + publicKeyHex := encoding.EncodeBytesToHexString(publicIdentityKey[:]) + t.Fatalf("GetPublicPrivateIdentityKeysFromSeedPhraseHash returning invalid public identity key: " + publicKeyHex) + } + areEqual = bytes.Equal(privateIdentityKey[:], expectedPrivateIdentityKeyBytes) + if (areEqual == false){ + privateKeyHex := encoding.EncodeBytesToHexString(privateIdentityKey[:]) + t.Fatalf("GetPublicPrivateIdentityKeysFromSeedPhraseHash returning invalid private identity key: " + privateKeyHex) + } + + identityHash, err := identity.ConvertIdentityKeyToIdentityHash(publicIdentityKey, "Mate") + if (err != nil){ + t.Fatalf("ConvertIdentityKeyToIdentityHash failed: " + err.Error()) + } + + expectedIdentityHash := "orvstztt4hp6rqjcpv2icocf5dm" + + expectedIdentityHashArray, identityType, err := identity.ReadIdentityHashString(expectedIdentityHash) + if (err != nil){ + t.Fatalf("Cannot read expectedIdentityHash: " + err.Error()) + } + + if (identityHash != expectedIdentityHashArray){ + + identityHashString, _, err := identity.EncodeIdentityHashBytesToString(identityHash) + if (err != nil){ + t.Fatalf("ConvertIdentityKeyToIdentityHash returning invalid identity hash: " + err.Error()) + } + + t.Fatalf("ReadIdentityHashString returning unexpected identity hash: " + identityHashString) + } + if (identityType != "Mate"){ + t.Fatalf("ReadIdentityHashString returning unexpected identityType: " + identityType) + } + + identityType, err = identity.GetIdentityTypeFromIdentityHash(identityHash) + if (err != nil){ + t.Fatalf("GetIdentityTypeFromIdentityHash fails to read valid mate identity hash.") + } + + if (identityType != "Mate"){ + t.Fatalf("GetIdentityTypeFromIdentityHash fails to read valid mate identity hash.") + } + +} + + +func TestIdentityEncoding(t *testing.T){ + + testIdentityHash, err := identity.GetNewRandomIdentityHash(false, "") + if (err != nil){ + t.Fatalf("GetNewRandomIdentityHash failed: " + err.Error()) + } + + encodedIdentityHash, identityType, err := identity.EncodeIdentityHashBytesToString(testIdentityHash) + if (err != nil){ + t.Fatalf("EncodeIdentityHashBytesToString failed: " + err.Error()) + } + + identityHashBytes, identityType_Received, err := identity.ReadIdentityHashString(encodedIdentityHash) + if (err != nil){ + t.Fatalf("ReadIdentityHash failed: " + err.Error()) + } + + if (testIdentityHash != identityHashBytes){ + t.Fatalf("Decoded identity hash does not match.") + } + if (identityType != identityType_Received){ + t.Fatalf("Decoded identityType does not match.") + } +} diff --git a/internal/imageEffects/imageEffects.go b/internal/imageEffects/imageEffects.go new file mode 100644 index 0000000..06a6882 --- /dev/null +++ b/internal/imageEffects/imageEffects.go @@ -0,0 +1,156 @@ + +// imageEffects provides functions to perform image effects + +package imageEffects + +import "seekia/imported/goeffects" + +import "seekia/internal/imagery" +import "seekia/internal/helpers" + +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 inputImage") + } + + if (effectStrength <= 0){ + return inputImage, nil + } + if (effectStrength > 100) { + effectStrength = 100 + } + + result, err := goeffects.ApplyCartoonEffect(inputImage, effectStrength) + + return result, err +} + +func ApplyPencilEffect(inputImage image.Image, effectStrength int) (image.Image, error){ + + if (inputImage == nil){ + return nil, errors.New("ApplyPencilEffect called with nil inputImage") + } + + if (effectStrength <= 0){ + return inputImage, nil + } + if (effectStrength > 100) { + effectStrength = 100 + } + + result, err := goeffects.ApplyPencilEffect(inputImage, effectStrength) + + return result, err +} + +func ApplyOilPaintingEffect(inputImage image.Image, effectStrength int) (image.Image, error){ + + if (inputImage == nil){ + return nil, errors.New("ApplyOilPaintingEffect called with nil inputImage") + } + + if (effectStrength <= 0){ + return inputImage, nil + } + if (effectStrength > 100) { + effectStrength = 100 + } + + result, err := goeffects.ApplyOilPaintingEffect(inputImage, effectStrength) + + return result, err +} + +func ApplyWireframeEffect(inputImage image.Image, effectStrength int) (image.Image, error){ + + if (inputImage == nil){ + return nil, errors.New("ApplyWireframeEffect called with nil inputImage") + } + + if (effectStrength <= 0){ + return inputImage, nil + } + if (effectStrength > 100) { + effectStrength = 100 + } + + result, err := goeffects.ApplyWireframeEffect(inputImage, effectStrength, false) + + return result, err +} + +func ApplyStrokeEffect(inputImage image.Image, effectStrength int) (image.Image, error){ + + if (inputImage == nil){ + return nil, errors.New("ApplyStrokeEffect called with nil inputImage") + } + + if (effectStrength <= 0){ + return inputImage, nil + } + if (effectStrength > 100) { + effectStrength = 100 + } + + result, err := goeffects.ApplyWireframeEffect(inputImage, effectStrength, true) + + return result, err +} + +func GetImageWithEmojiOverlay(inputImage image.Image, emojiImage image.Image, emojiScale int, xAxisPercentage int, yAxisPercentage int)(image.Image, error){ + + if (inputImage == nil) { + return nil, errors.New("GetImageWithEmojiOverlay called with nil inputImage.") + } + if (emojiImage == nil) { + return nil, errors.New("GetImageWithEmojiOverlay called with nil emojiImage.") + } + + newImageRGBA := image.NewRGBA(inputImage.Bounds()) + draw.Draw(newImageRGBA, inputImage.Bounds(), inputImage, image.Point{}, draw.Over) + + imageWidth, imageHeight, err := imagery.GetImageWidthAndHeightPixels(inputImage) + if (err != nil) { return nil, err } + + imageLongerSideLength := max(imageWidth, imageHeight) + + // This is the length of the longest side of the emoji we will draw + emojiLongerSideMaximumLength, err := helpers.ScaleNumberProportionally(true, emojiScale, 0, 100, 1, imageLongerSideLength) + if (err != nil) { return nil, err } + + newEmoji, err := imagery.ResizeGolangImage(emojiImage, emojiLongerSideMaximumLength) + if (err != nil) { return nil, err } + + // Now we get the X and Y Coordinate point for where we will draw the emoji + // We first find the center coordinate of the emoji we are drawing + + emojiCenterXCoordinate, err := helpers.ScaleNumberProportionally(true, xAxisPercentage, 0, 100, 0, imageWidth) + if (err != nil) { return nil, err } + + emojiCenterYCoordinate, err := helpers.ScaleNumberProportionally(false, yAxisPercentage, 0, 100, 0, imageHeight) + if (err != nil) { return nil, err } + + emojiWidth, emojiHeight, err := imagery.GetImageWidthAndHeightPixels(newEmoji) + if (err != nil) { return nil, err } + + // Now we find the coordinates of the top left corner of the emoji we are drawing. + + pointXCoordinate := -(emojiCenterXCoordinate - (emojiWidth/2)) + pointYCoordinate := -(emojiCenterYCoordinate - (emojiHeight/2)) + + emojiDrawPoint := image.Point{ X : pointXCoordinate, Y : pointYCoordinate } + + draw.Draw(newImageRGBA, inputImage.Bounds(), newEmoji, emojiDrawPoint, draw.Over) + + return newImageRGBA, nil +} + + + + diff --git a/internal/imageEffects/imageEffects_test.go b/internal/imageEffects/imageEffects_test.go new file mode 100644 index 0000000..9a7e73e --- /dev/null +++ b/internal/imageEffects/imageEffects_test.go @@ -0,0 +1,70 @@ +package imageEffects_test + +import "seekia/resources/imageFiles" +import "seekia/internal/helpers" +import "seekia/internal/imageEffects" +import "seekia/internal/imagery" + +import "testing" + + +func TestImageEffects(t *testing.T){ + + testImageBytes := imageFiles.PNG_SmileyA + + testImage, err := imagery.ConvertPNGImageFileBytesToGolangImage(testImageBytes) + if (err != nil){ + t.Fatalf("ConvertPNGImageFileBytesToGolangImage failed: " + err.Error()) + } + + for strength:=0; strength <= 100; strength += 20{ + + _, err := imageEffects.ApplyCartoonEffect(testImage, strength) + if (err != nil){ + t.Fatalf("ApplyCartoonEffect failed: " + err.Error()) + } + + _, err = imageEffects.ApplyPencilEffect(testImage, strength) + if (err != nil){ + t.Fatalf("ApplyPencilEffect failed: " + err.Error()) + } + + _, err = imageEffects.ApplyOilPaintingEffect(testImage, strength) + if (err != nil){ + t.Fatalf("ApplyOilPaintingEffect failed: " + err.Error()) + } + + _, err = imageEffects.ApplyWireframeEffect(testImage, strength) + if (err != nil){ + t.Fatalf("ApplyWireframeEffect failed: " + err.Error()) + } + _, err = imageEffects.ApplyStrokeEffect(testImage, strength) + if (err != nil){ + t.Fatalf("ApplyStrokeEffect failed: " + err.Error()) + } + } + + testEmojiBytes := imageFiles.Emoji_3629 + + testEmoji, err := imagery.ConvertSVGImageFileBytesToGolangImage(testEmojiBytes) + if (err != nil){ + t.Fatalf("ConvertSVGImageFileBytesToGolangImage failed: " + err.Error()) + } + + for i:=0; i < 100; i++{ + + randomEmojiScale := helpers.GetRandomIntWithinRange(0, 100) + + randomXPercentage := helpers.GetRandomIntWithinRange(0, 100) + + randomYPercentage := helpers.GetRandomIntWithinRange(0, 100) + + _, err := imageEffects.GetImageWithEmojiOverlay(testImage, testEmoji, randomEmojiScale, randomXPercentage, randomYPercentage) + if (err != nil){ + t.Fatalf("GetImageWithEmojiOverlay failed: " + err.Error()) + } + } +} + + + diff --git a/internal/imagery/imagery.go b/internal/imagery/imagery.go new file mode 100644 index 0000000..099c615 --- /dev/null +++ b/internal/imagery/imagery.go @@ -0,0 +1,545 @@ + +// imagery provides functions to read, edit and export images + +package imagery + +import "seekia/internal/encoding" +import "seekia/internal/helpers" +import "seekia/internal/appValues" +import "seekia/internal/localFilesystem" + +import "github.com/disintegration/gift" +import "github.com/srwiley/oksvg" +import "github.com/srwiley/rasterx" + +import chaiWebp "github.com/chai2010/webp" + +import goJpeg "image/jpeg" +import goWebp "golang.org/x/image/webp" +import goPng "image/png" + +import goFilepath "path/filepath" +import "image" +import "image/color" +import "image/draw" +import "strings" +import "bytes" +import "errors" + + +// Image can be either jpeg, jpg, webp or png +//Outputs: +// -bool: File found +// -bool: Able to read image +// -image.Image: golang image +// -error +func ReadImageFile(filepath string) (bool, bool, image.Image, error){ + + exists, fileBytes, err := localFilesystem.GetFileContents(filepath) + if (err != nil) { return false, false, nil, err } + if (exists == false){ + return false, false, nil, nil + } + + filename := goFilepath.Base(filepath) + filenameLowercase := strings.ToLower(filename) + + isJPEG := strings.HasSuffix(filenameLowercase, "jpeg") + isJPG := strings.HasSuffix(filenameLowercase, "jpg") + if (isJPEG == true || isJPG == true){ + imageObject, err := ConvertJPEGImageFileBytesToGolangImage(fileBytes) + if (err == nil) { + return true, true, imageObject, nil + } + } + + isPNG := strings.HasSuffix(filenameLowercase, "png") + if (isPNG == true){ + + imageObject, err := ConvertPNGImageFileBytesToGolangImage(fileBytes) + if (err == nil) { + return true, true, imageObject, nil + } + } + + isWEBP := strings.HasSuffix(filenameLowercase, "webp") + if (isWEBP == true){ + + imageObject, err := ConvertWEBPImageFileBytesToGolangImage(fileBytes) + if (err == nil){ + return true, true, imageObject, nil + } + } + + // File extention is unknown, or it does not correspond to the image file's true format + // We will try every method we haven't already tried. + + if (isJPEG == false && isJPG == false){ + + imageObject, err := ConvertJPEGImageFileBytesToGolangImage(fileBytes) + if (err == nil) { + return true, true, imageObject, nil + } + } + if (isPNG == false){ + + imageObject, err := ConvertPNGImageFileBytesToGolangImage(fileBytes) + if (err == nil) { + return true, true, imageObject, nil + } + } + if (isWEBP == false){ + imageObject, err := ConvertWEBPImageFileBytesToGolangImage(fileBytes) + if (err == nil){ + return true, true, imageObject, nil + } + } + + return true, false, nil, nil +} + +func ConvertJPEGImageFileBytesToGolangImage(input []byte)(image.Image, error){ + + bytesBuffer := bytes.NewBuffer(input) + + imageObject, err := goJpeg.Decode(bytesBuffer) + if (err != nil) { return nil, err } + + return imageObject, nil +} + +func ConvertPNGImageFileBytesToGolangImage(input []byte)(image.Image, error){ + + bytesBuffer := bytes.NewBuffer(input) + + imageObject, err := goPng.Decode(bytesBuffer) + if (err != nil) { return nil, err } + + return imageObject, nil +} + +func ConvertWEBPImageFileBytesToGolangImage(input []byte)(image.Image, error){ + + bytesBuffer := bytes.NewBuffer(input) + + imageObject, err := goWebp.Decode(bytesBuffer) + if (err != nil) { return nil, err } + + return imageObject, nil +} + +// This function only works for square images +func ConvertSVGImageFileBytesToGolangImage(inputBytes []byte)(image.Image, error){ + + //TODO: Use a different svg package, because oksvg cannot render many complex openmoji icons + + fileReader := bytes.NewReader(inputBytes) + + svgIconObject, err := oksvg.ReadIconStream(fileReader, oksvg.StrictErrorMode) + if (err != nil) { return nil, err } + + svgWidth := int(svgIconObject.ViewBox.W) + svgHeight := int(svgIconObject.ViewBox.H) + + svgIconObject.SetTarget(0, 0, 400, 400) + + imageObject := image.NewRGBA(image.Rect(0, 0, 400, 400)) + + scannerGV := rasterx.NewScannerGV(svgWidth, svgHeight, imageObject, imageObject.Bounds()) + + raster := rasterx.NewDasher(400, 400, scannerGV) + + svgIconObject.Draw(raster, 1) + + return imageObject, nil +} + +// This function creates a 1 pixel image of a provided color +func GetColorSquare(colorCode string)(image.Image, error){ + + colorObject, err := GetColorObjectFromColorCode(colorCode) + if (err != nil){ + return nil, errors.New("GetColorSquare called with invalid color code: " + colorCode) + } + + imageRectangle := image.Rect(0, 0, 1, 1) + imageObject := image.NewRGBA(imageRectangle) + imageObject.Set(0, 0, colorObject) + + return imageObject, nil +} + +// Example color codes: +// -Black: "000000" +// -White: "ffffff" +// -Blue: "0000ff" +// -Red: "ff0000" +func GetColorObjectFromColorCode(colorCode string)(color.Color, error){ + + colorCodeBytes, err := encoding.DecodeHexStringToBytes(colorCode) + if (err != nil) { + return nil, errors.New("GetColorObjectFromColorCode called with invalid color code: " + colorCode) + } + + if (len(colorCodeBytes) != 3){ + return nil, errors.New("GetColorObjectFromColorCode called with invalid color code: " + colorCode) + } + + colorRed := colorCodeBytes[0] + colorGreen := colorCodeBytes[1] + colorBlue := colorCodeBytes[2] + + // Color Alpha (opacity) + // It is always ff, which represents 100% opacity + colorAlpha := uint8(0xff) + + colorObject := color.RGBA{colorRed, colorGreen, colorBlue, colorAlpha} + + return colorObject, nil +} + +// This converts a profile/message webp base64 image string to a viewable, cropped image +// The cropping adds transparent bars so it will conform to a reasonable ratio +// The images are encoded into profiles/messages without the bars to save space +func ConvertWEBPBase64StringToCroppedDownsizedImageObject(base64Input string)(image.Image, error){ + + imageObject, err := ConvertWebpBase64StringToImageObject(base64Input) + if (err != nil) { return nil, err } + + croppedImage, err := cropGolangImageToMaximumRatio(imageObject) + if (err != nil) { return nil, err } + + maximumSideLength := appValues.GetStandardImageMaximumSideLength() + + downsizedImage, err := DownsizeGolangImage(croppedImage, maximumSideLength) + if (err != nil){ return nil, err } + + return downsizedImage, nil +} + +func ConvertWebpBase64StringToImageObject(base64Input string)(image.Image, error){ + + imageBytes, err := encoding.DecodeBase64StringToBytes(base64Input) + if (err != nil) { return nil, err } + + imageObject, err := ConvertWEBPImageFileBytesToGolangImage(imageBytes) + if (err != nil) { return nil, err } + + return imageObject, nil +} + +func GetImageWidthAndHeightPixels(inputImage image.Image)(int, int, error){ + + if (inputImage == nil) { + return 0, 0, errors.New("GetImageWidthAndHeightPixels called with nil image.") + } + + minX := inputImage.Bounds().Min.X + maxX := inputImage.Bounds().Max.X + minY := inputImage.Bounds().Min.Y + maxY := inputImage.Bounds().Max.Y + + width := maxX-minX + height := maxY-minY + + if (width <= 0 || height <= 0) { + return 0, 0, errors.New("Failed to derive image width and height.") + } + + return width, height, nil +} + +// This will downscale an image to the maximum side length +// If the image is already small enough, it will do nothing +// It preserves aspect ratio +func DownsizeGolangImage(inputImage image.Image, maximumSideLength int)(image.Image, error){ + + inputImageWidth, inputImageHeight, err := GetImageWidthAndHeightPixels(inputImage) + if (err != nil) { return nil, err } + + if (inputImageWidth <= maximumSideLength && inputImageHeight <= maximumSideLength) { + return inputImage, nil + } + + resizedImage, err := ResizeGolangImage(inputImage, maximumSideLength) + if (err != nil) { return nil, err } + + return resizedImage, nil +} + +//This function can upscale or downscale an image. It preserves aspect ratio. +func ResizeGolangImage(inputImage image.Image, maximumSideLength int) (image.Image, error){ + + if (inputImage == nil) { + return nil, errors.New("ResizeGolangImage called with nil image.") + } + + imageWidth, imageHeight, err := GetImageWidthAndHeightPixels(inputImage) + if (err != nil) { return nil, err } + + if (imageWidth == 0 || imageHeight == 0){ + return nil, errors.New("ResizeGolangImage called with input image of zero width and length.") + } + if (maximumSideLength <= 0){ + return nil, errors.New("ResizeGolangImage called with invalid maximumSideLength") + } + + //Outputs: + // -int: New width + // -int: New height + // -error + getResizedImageWidthAndHeight := func()(int, int, error){ + + if (imageWidth == imageHeight){ + return maximumSideLength, maximumSideLength, nil + } + + // oldWidth/oldHeight = newWidth/newHeight + // We set either newWidth or newHeight to maximumSideLength + // Then we solve for either newHeight or newWidth + + if (imageWidth > imageHeight) { + + newHeight := float64(maximumSideLength) * (float64(imageHeight)/float64(imageWidth)) + + newHeightInt, err := helpers.FloorFloat64ToInt(newHeight) + if (err != nil) { return 0, 0, err } + + return maximumSideLength, newHeightInt, nil + } + + newWidth := float64(maximumSideLength) * (float64(imageWidth)/float64(imageHeight)) + + newWidthInt, err := helpers.FloorFloat64ToInt(newWidth) + if (err != nil) { return 0, 0, err } + + return newWidthInt, maximumSideLength, nil + } + + newWidth, newHeight, err := getResizedImageWidthAndHeight() + if (err != nil) { return nil, err } + + rectangle := image.Rect(0, 0, newWidth, newHeight) + resizedImage := image.NewRGBA(rectangle) + + giftResizeFilterList := gift.New(gift.Resize(newWidth, newHeight, gift.LanczosResampling)) + giftResizeFilterList.Draw(resizedImage, inputImage) + + return resizedImage, nil +} + + +// This function crops image to maximum allowed aspect ratio. +// It adds transparent space to top/bottom or left/right of image. +// (Smaller Side)/(Larger side) must be > 0.7 +func cropGolangImageToMaximumRatio(inputImage image.Image)(image.Image, error){ + + if (inputImage == nil) { + return nil, errors.New("cropGolangImageToMaximumRatio called with nil image.") + } + + widthPixels, heightPixels, err := GetImageWidthAndHeightPixels(inputImage) + if (err != nil) { return nil, err } + + shorterSide := min(widthPixels, heightPixels) + longerSide := max(widthPixels, heightPixels) + + currentImageRatio := float64(shorterSide)/float64(longerSide) + + if (currentImageRatio > 0.7) { + + //Image does not exceed maximum allowed ratio, image does not need cropping. + return inputImage, nil + } + + // We must increase the length of the smaller side so that the image has a ratio of .7 + // smaller/larger = .7 + // smaller = (.7)*(larger) + + getNewWidthAndHeight := func()(int, int, error){ + + minimumRatio := float64(0.7) + + if (widthPixels > heightPixels){ + newHeightFloat := float64(widthPixels) * minimumRatio + + newHeight, err := helpers.CeilFloat64ToInt(newHeightFloat) + if (err != nil) { return 0, 0, err } + + return widthPixels, newHeight, nil + } + + newWidthFloat := float64(heightPixels) * minimumRatio + + newWidth, err := helpers.CeilFloat64ToInt(newWidthFloat) + if (err != nil) { return 0, 0, err } + + return newWidth, heightPixels, nil + } + + newWidthPixels, newHeightPixels, err := getNewWidthAndHeight() + if (err != nil) { return nil, err } + + newLongerSideLength := max(newWidthPixels, newHeightPixels) + + resizedImage, err := ResizeGolangImage(inputImage, newLongerSideLength) + if (err != nil) { return nil, err } + + resizedImageWidth, resizedImageHeight, err := GetImageWidthAndHeightPixels(resizedImage) + if (err != nil) { return nil, err } + + croppedImageRectangle := image.Rect(0, 0, newWidthPixels, newHeightPixels) + croppedImage := image.NewRGBA(croppedImageRectangle) + + // We find the coordinates of the top left corner of the image we are placing within our new image + + xAxisPlacementPoint := -((newWidthPixels - resizedImageWidth)/2) + yAxisPlacementPoint := -((newHeightPixels - resizedImageHeight)/2) + + croppedImagePlacementPoint := image.Point{ + X: xAxisPlacementPoint, + Y: yAxisPlacementPoint, + } + + draw.Draw(croppedImage, croppedImageRectangle, resizedImage, croppedImagePlacementPoint, draw.Over) + + return croppedImage, nil +} + +// Performs downsizing and compression to maximum standard size +func ConvertImageObjectToStandardWebpBase64String(inputImage image.Image)(string, error){ + + if (inputImage == nil) { + return "", errors.New("ConvertImageObjectToStandardWebpBase64String called with nil image.") + } + + maximumSideLength := appValues.GetStandardImageMaximumSideLength() + + sideLength := maximumSideLength + + for sideLength > 1 { + + resizedImage, err := ResizeGolangImage(inputImage, sideLength) + if (err != nil) { return "", err } + + imageMaximumBytes := appValues.GetStandardImageMaximumBytes() + + imageQuality := float32(100) + + for { + + bytesBuffer := new(bytes.Buffer) + + webpOptions := &chaiWebp.Options{ + Lossless: false, + Quality: imageQuality, + } + + err := chaiWebp.Encode(bytesBuffer, resizedImage, webpOptions) + if (err != nil) { return "", err } + + if (&bytesBuffer == nil) { + return "", errors.New("Nil image buffer after chaiWebp.Encode()") + } + + imageBytes := bytesBuffer.Bytes() + + if (len(imageBytes) == 0){ + return "", errors.New("Empty image buffer after chaiWebp.Encode()") + } + + imageSize := len(imageBytes) + + if (imageSize < imageMaximumBytes){ + + imageBase64String := encoding.EncodeBytesToBase64String(imageBytes) + + return imageBase64String, nil + } + + if (imageQuality > 10){ + + imageQuality -= 10 + + } else if (imageQuality > 4){ + + imageQuality -= 1 + + } else { + // imageQuality is <= 4 + // We cannot compress the image while retaining good quality at these dimensions + // We will downsize the image by 100 pixels and try again + sideLength -= 100 + break + } + } + } + + return "", errors.New("Failed to create standard webp image.") +} + + +func PixelateGolangImage(inputImage image.Image, amount0to100 int)(image.Image, error){ + + if (inputImage == nil) { + return nil, errors.New("PixelateGolangImage called with nil image.") + } + if (amount0to100 < 0 || amount0to100 > 100) { + return nil, errors.New("PixelateGolangImage called with input not between 0 and 100.") + } + + if (amount0to100 == 0) { + return inputImage, nil + } + + widthPixels, heightPixels, err := GetImageWidthAndHeightPixels(inputImage) + if (err != nil) { return nil, err } + + longerSideLength := max(widthPixels, heightPixels) + + pixelationAmountInt, err := helpers.ScaleNumberProportionally(true, amount0to100, 0, 100, 0, longerSideLength/5) + if (err != nil) { return nil, err } + + rectangle := image.Rect(0, 0, widthPixels, heightPixels) + pixelatedImage := image.NewRGBA(rectangle) + + giftPixelateFilterList := gift.New(gift.Pixelate(pixelationAmountInt)) + giftPixelateFilterList.Draw(pixelatedImage, inputImage) + + return pixelatedImage, nil +} + +// This will verify an image conforms to Seekia's image size and ratio requirements +// All images in profiles and messages must conform to these requirements +//Outputs: +// -bool: Image is valid +// -error +func VerifyStandardImageBytes(inputBytes []byte)(bool, error){ + + imageMaximumBytes := appValues.GetStandardImageMaximumBytes() + + if (len(inputBytes) > imageMaximumBytes){ + return false, nil + } + + imageObject, err := ConvertWEBPImageFileBytesToGolangImage(inputBytes) + if (err != nil) { + return false, nil + } + + imageWidth, imageHeight, err := GetImageWidthAndHeightPixels(imageObject) + if (err != nil) { + return false, errors.New("ConvertWEBPImageFileBytesToGolangImage returning image with invalid width and height: " + err.Error()) + } + + maximumAllowedSideLength := appValues.GetStandardImageMaximumSideLength() + + if (imageWidth > maximumAllowedSideLength || imageHeight > maximumAllowedSideLength){ + return false, nil + } + + return true, nil +} + + + diff --git a/internal/imagery/imagery_test.go b/internal/imagery/imagery_test.go new file mode 100644 index 0000000..4361b1d --- /dev/null +++ b/internal/imagery/imagery_test.go @@ -0,0 +1,75 @@ +package imagery_test + +import "seekia/resources/imageFiles" +import "seekia/internal/encoding" +//import "seekia/internal/helpers" +import "seekia/internal/imagery" + +import "image" +import "testing" +//import "os" + +func TestReadWriteImages(t *testing.T){ + + ultrawideImageFileBytes := imageFiles.PNG_Ultrawide + + ultrawideGoImage, err := imagery.ConvertPNGImageFileBytesToGolangImage(ultrawideImageFileBytes) + if (err != nil) { + t.Fatalf("Failed to read JPG image file: " + err.Error()) + } + + pumpkinFileBytes := imageFiles.PNG_Pumpkin + + pumpkinGoImage, err := imagery.ConvertPNGImageFileBytesToGolangImage(pumpkinFileBytes) + if (err != nil) { + t.Fatalf("Failed to read png image file: " + err.Error()) + } + + goImagesList := []image.Image{ultrawideGoImage, pumpkinGoImage} + + for _, imageObject := range goImagesList{ + + standardImageBase64, err := imagery.ConvertImageObjectToStandardWebpBase64String(imageObject) + if (err != nil) { + t.Fatalf("Failed to crop and compress standard image: " + err.Error()) + } + + standardImageBytes, err := encoding.DecodeBase64StringToBytes(standardImageBase64) + if (err != nil) { + t.Fatalf("Failed to decode standard image base64 string: " + err.Error()) + } + + isValid, err := imagery.VerifyStandardImageBytes(standardImageBytes) + if (err != nil) { + t.Fatalf("Failed to VerifyStandardImageBytes: " + err.Error()) + } + if (isValid == false){ + t.Fatalf("Produced standard image is not valid.") + } + + /* + + fileNameIndex := helpers.ConvertIntToString(index+1) + + fileMode := os.FileMode(0755) + + fileName := "Image" + fileNameIndex + ".webp" + + err = os.WriteFile(fileName, standardImageBytes, fileMode) + if (err != nil) { + t.Fatalf("Failed to write image: " + err.Error()) + } + + */ + } + + isValid, err := imagery.VerifyStandardImageBytes(ultrawideImageFileBytes) + if (err != nil) { + t.Fatalf("Failed to VerifyStandardImageBytes: " + err.Error()) + } + if (isValid == true){ + t.Fatalf("VerifyStandardImageBytes allowing ultrawide image.") + } +} + + diff --git a/internal/localFilesystem/localFilesystem.go b/internal/localFilesystem/localFilesystem.go new file mode 100644 index 0000000..0a00146 --- /dev/null +++ b/internal/localFilesystem/localFilesystem.go @@ -0,0 +1,435 @@ + +// localFilesystem provides functions to read and write to the filesystem + +package localFilesystem + +// localFilesystem tries to prevent the concurrent writing of files +// Each filepath and folderpath has its own mutex, which prevents the concurrent write of a file/folder +// This only works properly if each file is deleted individually using DeleteFileOrFolder, as opposed to using something like os.RemoveAll +// +// It is not perfect, because a file could be created within a folder that is being deleted +// To avoid this, we need to avoid deleting folders when the user is signed in + +import "seekia/internal/globalSettings" +import "seekia/internal/appMemory" + +import goFilepath "path/filepath" + +import "sync" +import "os" +import "errors" + +var filesystemPathMutexesMapMutex sync.RWMutex + +var filesystemPathMutexesMap map[string]*sync.RWMutex = make(map[string]*sync.RWMutex) + +func getFilesystemPathMutex(filepath string)*sync.RWMutex{ + + filesystemPathMutexesMapMutex.RLock() + currentMutex, exists := filesystemPathMutexesMap[filepath] + filesystemPathMutexesMapMutex.RUnlock() + if (exists == true){ + return currentMutex + } + + newMutex := new(sync.RWMutex) + + filesystemPathMutexesMapMutex.Lock() + filesystemPathMutexesMap[filepath] = newMutex + filesystemPathMutexesMapMutex.Unlock() + + return newMutex +} + +// This must be run upon application startup +// It also must be run before certain tests +func InitializeAppDatastores()error{ + + seekiaDirectoryPath, err := GetSeekiaDataFolderPath() + if (err != nil) { return err } + + _, err = CreateFolder(seekiaDirectoryPath) + if (err != nil) { return err } + + err = globalSettings.InitializeGlobalSettingsDatastore() + if (err != nil) { return err } + + databaseFolderPath, err := GetAppDatabaseFolderPath() + if (err != nil) { return err } + + _, err = CreateFolder(databaseFolderPath) + if (err != nil) { return err } + + userDataFolderPath, err := GetAppUsersDataFolderPath() + if (err != nil) { return err } + + _, err = CreateFolder(userDataFolderPath) + if (err != nil) { return err } + + parametersFolderpath := goFilepath.Join(seekiaDirectoryPath, "Parameters") + parametersNetwork1Folderpath := goFilepath.Join(parametersFolderpath, "Network1") + parametersNetwork2Folderpath := goFilepath.Join(parametersFolderpath, "Network2") + + _, err = CreateFolder(parametersFolderpath) + if (err != nil) { return err } + _, err = CreateFolder(parametersNetwork1Folderpath) + if (err != nil) { return err } + _, err = CreateFolder(parametersNetwork2Folderpath) + if (err != nil) { return err } + + return nil +} + +// This returns the folderpath where Seekia globalSettings and userData is stored +// It may also contain the database, unless the user has manually changed the database location +func GetSeekiaDataFolderPath() (string, error) { + + localFileDirectory, err := os.UserConfigDir() + if (err != nil) { return "", err } + + seekiaDirectory := goFilepath.Join(localFileDirectory, "SeekiaData") + + return seekiaDirectory, nil +} + +// This folder stores all app user data within it +// Each user has a folder, the name being the name of the user +func GetAppUsersDataFolderPath()(string, error){ + + seekiaDirectoryPath, err := GetSeekiaDataFolderPath() + if (err != nil) { return "", err } + + appUsersDirectory := goFilepath.Join(seekiaDirectoryPath, "UserData") + + return appUsersDirectory, nil +} + +// This returns the folder where the currently signed-in user's data is stored +func GetAppUserFolderPath() (string, error) { + + exists, appUserName := appMemory.GetMemoryEntry("AppUser") + if (exists == false){ + return "", errors.New("GetUserDirectoryPath called when user is not signed in.") + } + + appUsersFolderPath, err := GetAppUsersDataFolderPath() + if (err != nil) { return "", err } + + userDirectory := goFilepath.Join(appUsersFolderPath, appUserName) + + return userDirectory, nil +} + +// This folder location is customizable in the app. +func GetAppDatabaseFolderPath()(string, error){ + + exists, directoryPath, err := globalSettings.GetSetting("DatabaseFolderpath") + if (err != nil) { return "", err } + if (exists == true){ + return directoryPath, nil + } + + seekiaDirectory, err := GetSeekiaDataFolderPath() + if (err != nil) { return "", err } + + databasePath := goFilepath.Join(seekiaDirectory, "SeekiaDatabase") + + return databasePath, nil +} + + +// Function will create new file or overwite existing: +func CreateOrOverwriteFile(content []byte, folderPath string, filename string) error{ + + _, err := CreateFolder(folderPath) + if (err != nil) { return err } + + filepath := goFilepath.Join(folderPath, filename) + + newFile, err := os.Create(filepath) + if (err != nil) { return err } + + _, err = newFile.Write(content) + if (err != nil) { + newFile.Close() + return err + } + newFile.Close() + + return nil +} + +//Outputs: +// -bool: File exists +// -[]byte: File contents +// -error +func GetFileContents(filePath string) (bool, []byte, error){ + + filepathMutex := getFilesystemPathMutex(filePath) + + filepathMutex.RLock() + fileContents, err := os.ReadFile(filePath) + filepathMutex.RUnlock() + if (err == nil) { + return true, fileContents, nil + } + + isNotExistError := os.IsNotExist(err) + if (isNotExistError == false){ + return false, nil, err + } + + emptyFileContents := make([]byte, 0) + return false, emptyFileContents, nil +} + +//Outputs: +// -[][]byte: List of each file's contents +// -error +func GetAllFilesInFolderAsList(folderPath string)([][]byte, error){ + + folderMap, err := GetFolderContentsAsMap(folderPath) + if (err != nil) { return nil, err } + + filesList := make([][]byte, 0, len(folderMap)) + + for _, fileContents := range folderMap{ + + filesList = append(filesList, fileContents) + } + + return filesList, nil +} + +// Outputs: +// -map[string][]byte: File name -> File Contents +// -error +func GetFolderContentsAsMap(folderPath string)(map[string][]byte, error) { + + folderpathMutex := getFilesystemPathMutex(folderPath) + + folderpathMutex.RLock() + fileList, err := os.ReadDir(folderPath) + folderpathMutex.RUnlock() + if (err != nil) { return nil, err } + + folderMap := make(map[string][]byte) + + for _, filesystemObject := range fileList{ + + filepathIsFolder := filesystemObject.IsDir() + if (filepathIsFolder == true){ + continue + } + + fileName := filesystemObject.Name() + filePath := goFilepath.Join(folderPath, fileName) + + filepathMutex := getFilesystemPathMutex(filePath) + + filepathMutex.RLock() + fileBytes, err := os.ReadFile(filePath) + filepathMutex.RUnlock() + if (err != nil){ return nil, err } + + folderMap[fileName] = fileBytes + } + + return folderMap, nil +} + +// This does not work with nested folders +// You must create all parent folders before calling this function on a folderPath +//Outputs: +// -bool: Folder already exists +// -error +func CreateFolder(folderPath string)(bool, error){ + + folderpathMutex := getFilesystemPathMutex(folderPath) + + folderpathMutex.RLock() + filesystemObject, err := os.Open(folderPath) + folderpathMutex.RUnlock() + if (err == nil){ + // Folder already exists + filesystemObject.Close() + return true, nil + } + + isNotExistErr := os.IsNotExist(err) + if (isNotExistErr == false){ + return false, err + } + + //Folder does not exist, create folder + + folderpathMutex.Lock() + err = os.Mkdir(folderPath, os.ModePerm) + folderpathMutex.Unlock() + if (err != nil){ return false, err } + + return false, nil +} + +func CheckIfFileExists(filepath string)(bool, error){ + + filepathMutex := getFilesystemPathMutex(filepath) + + filepathMutex.RLock() + _, err := os.Stat(filepath) + filepathMutex.RUnlock() + if (err != nil) { + isNotExistErr := os.IsNotExist(err) + if (isNotExistErr == true) { + // File does not exist + return false, nil + } + return false, err + } + + return true, nil +} + +// This function works for files and empty directories +//Outputs: +// -bool: File/Folder existed and was deleted +// -error +func DeleteFileOrFolder(filepath string)(bool, error){ + + filepathMutex := getFilesystemPathMutex(filepath) + + filepathMutex.Lock() + err := os.Remove(filepath) + filepathMutex.Unlock() + if (err != nil) { + isNotExistErr := os.IsNotExist(err) + if (isNotExistErr == true) { + return false, nil + } + return false, err + } + + return true, nil +} + +//Outputs: +// -bool: Folder exists and its contents were deleted +// -error +func DeleteAllFolderContents(inputFolderpath string)(bool, error){ + + folderpathMutex := getFilesystemPathMutex(inputFolderpath) + + folderpathMutex.RLock() + fileList, err := os.ReadDir(inputFolderpath) + folderpathMutex.RUnlock() + if (err != nil) { + isNotExistError := os.IsNotExist(err) + if (isNotExistError == true){ + // Folder does not exist, nothing to delete + return false, nil + } + return false, err + } + + for _, filesystemObject := range fileList { + + fileName := filesystemObject.Name() + filePath := goFilepath.Join(inputFolderpath, fileName) + + filepathMutex := getFilesystemPathMutex(filePath) + + filepathIsFolder := filesystemObject.IsDir() + if (filepathIsFolder == true){ + + filepathMutex.RLock() + fileList, err := os.ReadDir(filePath) + filepathMutex.RUnlock() + if (err != nil) { return false, err } + if (len(fileList) != 0){ + + // Filepath is a folder with items in it + // We recursively delete all items + + folderExists, err := DeleteAllFolderContents(filePath) + if (err != nil) { return false, err } + if (folderExists == false){ + return false, errors.New("Folder not found after being found already during DeleteAllFolderContents.") + } + } + } + + _, err := DeleteFileOrFolder(filePath) + if (err != nil) { return false, err } + } + + return true, nil +} + + +func GetFolderSizeInBytes(folderPath string)(int64, error){ + + sizeInBytes := int64(0) + + filepathMutex := getFilesystemPathMutex(folderPath) + + filepathMutex.RLock() + filesList, err := os.ReadDir(folderPath) + filepathMutex.RUnlock() + if (err != nil) { return 0, err } + + for _, fileObject := range filesList{ + + isFolder := fileObject.IsDir() + if (isFolder == true){ + + subFolderName := fileObject.Name() + + subFolderPath := goFilepath.Join(folderPath, subFolderName) + + subFolderSize, err := GetFolderSizeInBytes(subFolderPath) + if (err != nil) { return 0, err } + + sizeInBytes += subFolderSize + + continue + } + + fileName := fileObject.Name() + filePath := goFilepath.Join(folderPath, fileName) + fileExists, fileSize, err := GetFileSize(filePath) + if (err != nil) { return 0, err } + if (fileExists == false) { + return 0, errors.New("File not found after being found during GetFolderSizeInBytes.") + } + + sizeInBytes += fileSize + } + + return sizeInBytes, nil +} + +//Outputs: +// -bool: File exists +// -int64: Size in bytes +// -error +func GetFileSize(filepath string)(bool, int64, error){ + + filepathMutex := getFilesystemPathMutex(filepath) + + filepathMutex.RLock() + fileObject, err := os.Stat(filepath) + filepathMutex.RUnlock() + if (err != nil){ + isNotExistError := os.IsNotExist(err) + if (isNotExistError == true){ + return false, 0, nil + } + return false, 0, err + } + + fileSizeBytes := fileObject.Size() + + return true, fileSizeBytes, nil +} + + diff --git a/internal/logger/logger.go b/internal/logger/logger.go new file mode 100644 index 0000000..83e1416 --- /dev/null +++ b/internal/logger/logger.go @@ -0,0 +1,103 @@ + +// logger provides a logger to log Seekia client events +// These logs are able to be viewed through the GUI + +package logger + +// Log types: +// -General +// -Network +// -BackgroundJobs + +//TODO: Add more log entries for events that should not happen, but may happen very rarely. +// We want to see if these kinds of events are happening more often than they should (almost never) +// There are also many other places where we should add logging + +// TODO: Add automatic clearing of old logs, and a button to clear logs +// Also, a way to disable logging + +import "seekia/internal/helpers" +import "seekia/internal/myDatastores/myList" + +import "time" +import "errors" + +var myLogListDatastore_General *myList.MyList +var myLogListDatastore_Network *myList.MyList +var myLogListDatastore_BackgroundJobs *myList.MyList + +func getMyLogListDatastore(logsType string)(*myList.MyList, error){ + + if (logsType == "General"){ + return myLogListDatastore_General, nil + } + if (logsType == "Network"){ + return myLogListDatastore_Network, nil + } + if (logsType == "BackgroundJobs"){ + return myLogListDatastore_BackgroundJobs, nil + } + + return nil, errors.New("getMyLogListDatastore called invalid logsType: " + logsType) +} + +// This function must be called whenever an app user signs in +func InitializeMyLogDatastores()error{ + + newMyLogListDatastore_General, err := myList.CreateNewList("GeneralLog") + if (err != nil) { return err } + + newMyLogListDatastore_Network, err := myList.CreateNewList("NetworkLog") + if (err != nil) { return err } + + newMyLogListDatastore_BackgroundJobs, err := myList.CreateNewList("BackgroundJobsLog") + if (err != nil) { return err } + + myLogListDatastore_General = newMyLogListDatastore_General + myLogListDatastore_Network = newMyLogListDatastore_Network + myLogListDatastore_BackgroundJobs = newMyLogListDatastore_BackgroundJobs + + return nil +} + +func GetLogList(logsType string)([]string, error){ + + logListDatastore, err := getMyLogListDatastore(logsType) + if (err != nil) { return nil, err } + + logList, err := logListDatastore.GetList() + if (err != nil) { return nil, err } + + return logList, nil +} + +func AddLogError(logName string, logError error)error{ + + errorString := "ERROR: " + logError.Error() + + err := AddLogEntry(logName, errorString) + if (err != nil) { return err } + + return nil +} + +func AddLogEntry(logsType string, logEntry string)error{ + + currentYear, currentMonth, currentDay := time.Now().Date() + + currentYearString := helpers.ConvertIntToString(currentYear) + currentMonthString := helpers.ConvertIntToString(int(currentMonth)) + currentDayString := helpers.ConvertIntToString(currentDay) + + logEntryWithTime := currentYearString + "-" + currentMonthString + "-" + currentDayString + ": " + logEntry + + myLogListDatastore, err := getMyLogListDatastore(logsType) + if (err != nil) { return err } + + err = myLogListDatastore.AddListItem(logEntryWithTime) + if (err != nil) { return err } + + return nil +} + + diff --git a/internal/mateQuestionnaire/mateQuestionnaire.go b/internal/mateQuestionnaire/mateQuestionnaire.go new file mode 100644 index 0000000..0bdb76c --- /dev/null +++ b/internal/mateQuestionnaire/mateQuestionnaire.go @@ -0,0 +1,310 @@ + +// mateQuestionnaire provides functions to create and read mate questionnaires and questionnaire responses. +// Questionnaires are lists of questions that users can share on their profile. +// Users can filter matches based on their responses. +// Full questionnaire specification is provided in /documentation/Specification.md + +package mateQuestionnaire + +import "seekia/internal/allowedText" +import "seekia/internal/encoding" +import "seekia/internal/helpers" + +import "strings" +import "errors" + +type QuestionObject struct{ + + // 9 or 10 byte hex encoded string + // 9 bytes = Choice + // 10 bytes = Entry + Identifier string + + // "Choice"/"Entry" + Type string + + // The content of the question + // Example: "What is your job?" + Content string + + Options string +} + +//Will also verify questionnaire +func ReadQuestionnaireString(inputQuestionnaire string)([]QuestionObject, error){ + + if (inputQuestionnaire == ""){ + return nil, errors.New("ReadQuestionnaireString called with empty questionnaire.") + } + + questionsRawList := strings.Split(inputQuestionnaire, "+&") + if (len(questionsRawList) > 25){ + return nil, errors.New("ReadQuestionnaireString called with questionnaire containing more than 25 questions.") + } + + // We use this map to detect duplicates + questionIdentifiersMap := make(map[string]struct{}) + + questionnaireQuestionsList := make([]QuestionObject, 0, len(questionsRawList)) + + for _, rawQuestion := range questionsRawList{ + + questionInfoList := strings.Split(rawQuestion, "%¢") + + if (len(questionInfoList) != 4){ + return nil, errors.New("Malformed questionnaire question: " + rawQuestion) + } + + questionIdentifier := questionInfoList[0] + questionType := questionInfoList[1] + questionContent := questionInfoList[2] + questionOptions := questionInfoList[3] + + getExpectedQuestionIdentifierLength := func()uint32{ + if (questionType == "Choice"){ + return 9 + } + return 10 + } + + expectedQuestionIdentifierLength := getExpectedQuestionIdentifierLength() + + isValid := helpers.VerifyHexString(expectedQuestionIdentifierLength, questionIdentifier) + if (isValid == false){ + return nil, errors.New("Malformed question: Invalid identifier: " + questionIdentifier) + } + + _, exists := questionIdentifiersMap[questionIdentifier] + if (exists == true){ + return nil, errors.New("Malformed questionnaire: Duplicate questionIdentifier exists.") + } + + questionIdentifiersMap[questionIdentifier] = struct{}{} + + if (questionType != "Choice" && questionType != "Entry"){ + return nil, errors.New("Malformed question: Invalid question type: " + questionType) + } + + if (questionContent == ""){ + return nil, errors.New("Malformed question: Empty content.") + } + + isAllowed := allowedText.VerifyStringIsAllowed(questionContent) + if (isAllowed == false){ + return nil, errors.New("Malformed question: Content contains unallowed text") + } + + if (len(questionContent) > 500){ + return nil, errors.New("Malformed question: Content is too long.") + } + + if (questionType == "Choice"){ + + maxChoicesAllowed, choicesListString, delimiterFound := strings.Cut(questionOptions, "#") + if (delimiterFound == false){ + return nil, errors.New("Malformed question: Invalid choice question options: Missing #") + } + + maxChoicesAllowedInt, err := helpers.ConvertStringToInt(maxChoicesAllowed) + if (err != nil) { + return nil, errors.New("Malformed question: Invalid maximum choices allowed.") + } + if (maxChoicesAllowedInt < 1 || maxChoicesAllowedInt > 6){ + return nil, errors.New("Malformed question: Invalid max choices allowed") + } + + choicesList := strings.Split(choicesListString, "$¥") + if (len(choicesList) < 2 || len(choicesList) > 6){ + return nil, errors.New("Malformed question: Invalid choices list length.") + } + + // We use this map to check for duplicates + choicesMap := make(map[string]struct{}) + + for _, choiceString := range choicesList{ + + if (choiceString == ""){ + return nil, errors.New("Malformed question: Choice is empty.") + } + + if (len(choiceString) > 100){ + return nil, errors.New("Malformed question: Choice text is too long.") + } + + choiceIsAllowed := allowedText.VerifyStringIsAllowed(choiceString) + if (choiceIsAllowed == false){ + return nil, errors.New("Malformed question: Choice contains not-allowed text.") + } + + _, exists := choicesMap[choiceString] + if (exists == true){ + return nil, errors.New("Malformed question: Duplicate choice exists.") + } + choicesMap[choiceString] = struct{}{} + } + + } else { + // questionType == "Entry" + + if (questionOptions != "Numeric" && questionOptions != "Any"){ + return nil, errors.New("Malformed question: Invalid entry options.") + } + } + + newQuestionObject := QuestionObject{ + + Identifier: questionIdentifier, + Type: questionType, + Content: questionContent, + Options: questionOptions, + } + + questionnaireQuestionsList = append(questionnaireQuestionsList, newQuestionObject) + } + + return questionnaireQuestionsList, nil +} + +func CreateQuestionnaireString(inputQuestionnaireObject []QuestionObject)(string, error){ + + if (len(inputQuestionnaireObject) == 0){ + return "", errors.New("CreateQuestionnaireString called with empty inputQuestionnaireObject.") + } + if (len(inputQuestionnaireObject) > 25){ + return "", errors.New("Cannot create questionnaire: More than 25 questions.") + } + + questionsList := make([]string, 0, len(inputQuestionnaireObject)) + + for _, questionObject := range inputQuestionnaireObject{ + + questionIdentifier := questionObject.Identifier + questionType := questionObject.Type + questionContent := questionObject.Content + questionOptions := questionObject.Options + + questionSlice := []string{questionIdentifier, questionType, questionContent, questionOptions} + + questionString := strings.Join(questionSlice, "%¢") + + questionsList = append(questionsList, questionString) + } + + questionnaireString := strings.Join(questionsList, "+&") + + // Now we verify questionnaire: + _, err := ReadQuestionnaireString(questionnaireString) + if (err != nil){ + return "", errors.New("Cannot create questionnaire: Result questionnaire is invalid: " + err.Error()) + } + + return questionnaireString, nil +} + +//Outputs: +// -map[string]string: Response map (Question identifier -> Encoded question response) +// -error +func ReadQuestionnaireResponse(inputResponseString string)(map[string]string, error){ + + questionsList := strings.Split(inputResponseString, "+&") + + if (len(questionsList) > 25){ + return nil, errors.New("Invalid questionnaire response: More than 25 questions.") + } + + responseMap := make(map[string]string) + + for _, element := range questionsList{ + + questionIdentifier, questionResponse, delimiterFound := strings.Cut(element, "%") + if (delimiterFound == false){ + return nil, errors.New("Invalid questionnaire response: Invalid encoded response") + } + + questionIdentifierBytes, err := encoding.DecodeHexStringToBytes(questionIdentifier) + if (err != nil){ + return nil, errors.New("Invalid questionnaire response: Question identifier is not hex: " + questionIdentifier) + } + + if (len(questionIdentifierBytes) != 9 && len(questionIdentifierBytes) != 10){ + return nil, errors.New("Invalid questionnaire response: Question identifier is invalid length.") + } + + if (len(questionIdentifierBytes) == 10){ + + // Question is of type Entry + + if (questionResponse == ""){ + return nil, errors.New("Invalid questionnaire response: Entry response is empty.") + } + + if (len(questionResponse) > 2000){ + return nil, errors.New("Invalid questionnaire response: Response too long.") + } + + } else { + + // Question is of type Choice + + responseChoiceIndexesList := strings.Split(questionResponse, "$") + if (len(responseChoiceIndexesList) > 6){ + return nil, errors.New("Invalid questionnaire response: Too many choices.") + } + + // We use a map to prevent duplicates + choicesMap := make(map[string]struct{}) + + for _, responseChoiceIndex := range responseChoiceIndexesList{ + + responseChoiceIndexInt, err := helpers.ConvertStringToInt(responseChoiceIndex) + if (err != nil) { + return nil, errors.New("Invalid questionnaire response: Choice index is not an integer.") + } + + if (responseChoiceIndexInt < 1 || responseChoiceIndexInt > 6){ + return nil, errors.New("Invalid questionnaire response: Choice is an invalid int") + } + + _, exists := choicesMap[responseChoiceIndex] + if (exists == true){ + return nil, errors.New("Invalid questionnaire response: Duplicate choice exists.") + } + + choicesMap[responseChoiceIndex] = struct{}{} + } + } + + responseMap[questionIdentifier] = questionResponse + } + + return responseMap, nil +} + +//Inputs: +// -map[string]string: Response map (Question identifier -> Encoded question response) +//Outputs: +// -string: Questionnaire response +// -error +func CreateQuestionnaireResponse(inputResponseMap map[string]string)(string, error){ + + questionsItemsList := make([]string, 0, len(inputResponseMap)) + + for questionIdentifier, questionEncodedResponse := range inputResponseMap{ + + encodedQuestionItem := questionIdentifier + "%" + questionEncodedResponse + + questionsItemsList = append(questionsItemsList, encodedQuestionItem) + } + + questionnaireResponseString := strings.Join(questionsItemsList, "+&") + + _, err := ReadQuestionnaireResponse(questionnaireResponseString) + if (err != nil) { + return "", errors.New("Cannot create questionnaire response: Result is invalid: " + err.Error()) + } + + return questionnaireResponseString, nil +} + + + diff --git a/internal/memos/createMemos/createMemos.go b/internal/memos/createMemos/createMemos.go new file mode 100644 index 0000000..6b89a41 --- /dev/null +++ b/internal/memos/createMemos/createMemos.go @@ -0,0 +1,114 @@ + +// createMemos provides a function to create Seekia memos +// Memos are specially formatted messages that are signed with a Seekia identity key +// They can be shared anywhere, and verified using the Seekia application + +package createMemos + +import "seekia/internal/allowedText" +import "seekia/internal/cryptography/blake3" +import "seekia/internal/cryptography/edwardsKeys" +import "seekia/internal/encoding" +import "seekia/internal/identity" + +import "strings" +import "errors" + +//Outputs: +// -string: Memo +// -error +func CreateMemo(identityPublicKey [32]byte, identityPrivateKey [64]byte, identityType string, decorativePrefix string, decorativeSuffix string, memoMessage string)(string, error){ + + authorIdentityHash, err := identity.ConvertIdentityKeyToIdentityHash(identityPublicKey, identityType) + if (err != nil) { return "", err } + + authorIdentityHashString, _, err := identity.EncodeIdentityHashBytesToString(authorIdentityHash) + if (err != nil) { return "", err } + + textIsAllowed := allowedText.VerifyStringIsAllowed(memoMessage) + if (textIsAllowed == false){ + return "", errors.New("CreateMemo called with unallowed text.") + } + + if (len(memoMessage) > 50000){ + return "", errors.New("CreateMemo called with memoMessage that is too long") + } + + identityPublicKeyHex := encoding.EncodeBytesToHexString(identityPublicKey[:]) + + identityPublicKeyPieceA := identityPublicKeyHex[:32] + identityPublicKeyPieceB := identityPublicKeyHex[32:] + + // We use this to build the memo body + var memoBodyBuilder strings.Builder + + memoBodyBuilder.WriteString("|- Identity Key:\n") + memoBodyBuilder.WriteString("| " + identityPublicKeyPieceA + "\n") + memoBodyBuilder.WriteString("| " + identityPublicKeyPieceB + "\n") + memoBodyBuilder.WriteString("|\n") + memoBodyBuilder.WriteString("|- Author:\n") + memoBodyBuilder.WriteString("| " + authorIdentityHashString + "\n") + memoBodyBuilder.WriteString("|\n") + memoBodyBuilder.WriteString("|- Memo:\n") + memoBodyBuilder.WriteString("|\n") + + memoMessageLinesList := strings.Split(memoMessage, "\n") + + for _, messageLine := range memoMessageLinesList{ + + if (messageLine == ""){ + + // There is no content on this line. It only contains a newline. + + memoLine := "|\n" + + memoBodyBuilder.WriteString(memoLine) + + } else { + + memoLine := "| " + messageLine + "\n" + + memoBodyBuilder.WriteString(memoLine) + } + } + + memoBodyBuilder.WriteString("|\n") + + // The memo body is the part of the memo we will sign with the author's identity key + memoBody := memoBodyBuilder.String() + + memoBodyBytes := []byte(memoBody) + + memoBodyHash, err := blake3.Get32ByteBlake3Hash(memoBodyBytes) + if (err != nil) { return "", err } + + signatureBytes := edwardsKeys.CreateSignature(identityPrivateKey, memoBodyHash) + + signatureString := encoding.EncodeBytesToBase64String(signatureBytes[:]) + + signaturePiece1 := signatureString[:30] + signaturePiece2 := signatureString[30:60] + signaturePiece3 := signatureString[60:] + + // We use this to build the memo header + var memoHeaderBuilder strings.Builder + + memoHeaderBuilder.WriteString("| " + decorativePrefix + " Seekia Memo " + decorativeSuffix + "\n") + memoHeaderBuilder.WriteString("|\n") + memoHeaderBuilder.WriteString("|- Signature:\n") + memoHeaderBuilder.WriteString("| " + signaturePiece1 + "\n") + memoHeaderBuilder.WriteString("| " + signaturePiece2 + "\n") + memoHeaderBuilder.WriteString("| " + signaturePiece3 + "\n") + memoHeaderBuilder.WriteString("|\n") + + memoHeader := memoHeaderBuilder.String() + + memoFooter := "| " + decorativePrefix + " End Of Memo " + decorativeSuffix + + newMemo := memoHeader + memoBody + memoFooter + + return newMemo, nil +} + + + diff --git a/internal/memos/createMemos/createMemos_test.go b/internal/memos/createMemos/createMemos_test.go new file mode 100644 index 0000000..dc6be15 --- /dev/null +++ b/internal/memos/createMemos/createMemos_test.go @@ -0,0 +1,144 @@ +package createMemos_test + +import "seekia/internal/memos/createMemos" +import "seekia/internal/memos/readMemos" +import "seekia/internal/encoding" +import "seekia/internal/generate" +import "seekia/internal/helpers" +import "seekia/internal/identity" + +import "bytes" +import "testing" + + +func TestCreateMemos(t *testing.T){ + + for i := 0; i < 1000; i++{ + + memoContent, err := generate.GetRandomText(100, true) + if (err != nil){ + t.Fatalf("Failed to GetRandomText: " + err.Error()) + } + + authorIdentityType, err := helpers.GetRandomItemFromList([]string{"Mate", "Host", "Moderator"}) + if (err != nil){ + t.Fatalf("Failed to get new random identityType: " + err.Error()) + } + + authorIdentityPublicKey, authorIdentityPrivateKey, err := identity.GetNewRandomPublicPrivateIdentityKeys() + if (err != nil){ + t.Fatalf("Failed to get new random identity public/private keys: " + err.Error()) + } + + authorIdentityHash, err := identity.ConvertIdentityKeyToIdentityHash(authorIdentityPublicKey, authorIdentityType) + if (err != nil){ + t.Fatalf("Failed to get author identity hash: " + err.Error()) + } + + newMemo, err := createMemos.CreateMemo(authorIdentityPublicKey, authorIdentityPrivateKey, authorIdentityType, "««", "»»", memoContent) + if (err != nil){ + t.Fatalf("Failed to create memo: " + err.Error()) + } + + memoIsValid, _, receivedAuthorIdentityHash, unarmoredMemoContent, err := readMemos.ReadMemo(newMemo) + if (err != nil){ + t.Fatalf("Failed to read memo: " + err.Error()) + } + if (memoIsValid == false){ + t.Fatalf("Created memo is not valid.") + } + if (authorIdentityHash != receivedAuthorIdentityHash){ + receivedAuthorIdentityHashHex := encoding.EncodeBytesToHexString(receivedAuthorIdentityHash[:]) + t.Fatalf("Received author identity hash does not match: " + receivedAuthorIdentityHashHex) + } + + if (unarmoredMemoContent != memoContent){ + t.Fatalf("Received unarmoredMemoContents does not match memoContents ") + } + } + + memoMessage := `Cure racial loneliness. + +Beautify the human species. + +Seekia: Be race aware.` + + authorIdentityPublicKeyHex := "b26327a3e00e97a2759ac0a08a39c84f8edb833eec43669a81e27509f4ff6636" + + authorIdentityPublicKeyBytes, err := encoding.DecodeHexStringToBytes(authorIdentityPublicKeyHex) + if (err != nil){ + t.Fatalf("authorIdentityPublicKeyHex is invalid: Not Hex: " + err.Error()) + } + + authorIdentityPublicKeyArray := [32]byte(authorIdentityPublicKeyBytes) + + authorIdentityPrivateKeyHex := "c27bc4be0d7fdf18653f5ea4d81b56c89b9003013abaee9aa3afddc53ebf9726b26327a3e00e97a2759ac0a08a39c84f8edb833eec43669a81e27509f4ff6636" + + authorIdentityPrivateKeyBytes, err := encoding.DecodeHexStringToBytes(authorIdentityPrivateKeyHex) + if (err != nil){ + t.Fatalf("authorIdentityPrivateKeyHex is invalid: Not Hex: " + err.Error()) + } + + authorIdentityPrivateKeyArray := [64]byte(authorIdentityPrivateKeyBytes) + + newMemo, err := createMemos.CreateMemo(authorIdentityPublicKeyArray, authorIdentityPrivateKeyArray, "Mate", "<<", ">>", memoMessage) + if (err != nil){ + t.Fatalf("Failed to create memo: " + err.Error()) + } + + memoIsValid, memoHash, authorIdentityHash, _, err := readMemos.ReadMemo(newMemo) + if (err != nil){ + t.Fatalf("Failed to read memo: " + err.Error()) + } + if (memoIsValid == false){ + t.Fatalf("Created memo is not valid.") + } + + expectedAuthorIdentityHashString := "p2qsphq4m72wr6trbyibwggsfem" + + expectedAuthorIdentityHash, _, err := identity.ReadIdentityHashString(expectedAuthorIdentityHashString) + if (err != nil){ + t.Fatalf("Failed to read expectedAuthorIdentityHashString: " + err.Error()) + } + + if (authorIdentityHash != expectedAuthorIdentityHash){ + authorIdentityHashHex := encoding.EncodeBytesToHexString(authorIdentityHash[:]) + t.Fatalf("Received author identity hash does not match: " + authorIdentityHashHex) + } + + memoHashExpected := "191505401860f00d7a24837dc98130f2e6381f928d848e48720e4af2b72970df" + + memoHashBytes, err := encoding.DecodeHexStringToBytes(memoHashExpected) + if (err != nil){ + t.Fatalf("Unable to read memoHashExpected: " + err.Error()) + } + + areEqual := bytes.Equal(memoHash[:], memoHashBytes) + if (areEqual == false){ + memoHashHex := encoding.EncodeBytesToHexString(memoHash[:]) + t.Fatalf("Received unexpected memo hash: " + memoHashHex) + } + + ethereumAddress, err := readMemos.GetBlockchainAddressFromMemoHash("Ethereum", memoHash) + if (err != nil){ + t.Fatalf("GetBlockchainAddressFromMemoHash failed: " + err.Error()) + } + + if (ethereumAddress != "0xcc55D6D71E93eefF9f4103b8057e0B77F5881605"){ + t.Fatalf("GetBlockchainAddressFromMemoHash returning unexpected Ethereum address: " + ethereumAddress) + } + + cardanoAddress, err := readMemos.GetBlockchainAddressFromMemoHash("Cardano", memoHash) + if (err != nil){ + t.Fatalf("GetBlockchainAddressFromMemoHash failed: " + err.Error()) + } + + if (cardanoAddress != "addr1v9j2ldvmmdxtr2aapgvnaknhw8rkxyv0ky9qwnqd8xlejrgnc83f6"){ + t.Fatalf("GetBlockchainAddressFromMemoHash returning unexpected Cardano address: " + cardanoAddress) + } + +} + + + + diff --git a/internal/memos/readMemos/readMemos.go b/internal/memos/readMemos/readMemos.go new file mode 100644 index 0000000..925990b --- /dev/null +++ b/internal/memos/readMemos/readMemos.go @@ -0,0 +1,293 @@ + +// readMemos provides functions to read Seekia memos and to derive blockchain addresses to timestamp memos +// Memos are specially formatted messages that are signed with a Seekia identity key +// They can be shared anywhere, and verified using the Seekia application + +package readMemos + +//TODO: We can relax some restrictions on the memo format. +// We can add translations to some of the memo terms ("Memo", "Author", "Signature") +// This should be able to be done later, and memos created with with the original memo format will still be valid. + +import "seekia/internal/cryptocurrency/ethereumAddress" +import "seekia/internal/cryptocurrency/cardanoAddress" + +import "seekia/internal/cryptography/blake3" +import "seekia/internal/cryptography/edwardsKeys" +import "seekia/internal/encoding" +import "seekia/internal/identity" + +import "strings" +import "errors" + + +//Outputs: +// -bool: Memo is valid +// -[32]byte: Memo hash (32 bytes long blake3 hash of memo string) +// -[16]byte: Author identity hash +// -string: Memo Unarmored Contents (The contents of the memo without the header, signature, author identity key, padding, and footer) +// -error +func ReadMemo(inputMemo string)(bool, [32]byte, [16]byte, string, error){ + + memoHashBytes, err := blake3.GetBlake3HashAsBytes(32, []byte(inputMemo)) + if (err != nil) { + // Invalid memo: Is empty. + return false, [32]byte{}, [16]byte{}, "", nil + } + + memoHash := [32]byte(memoHashBytes) + + memoLinesList := strings.Split(inputMemo, "\n") + + if (len(memoLinesList) < 18){ + //Invalid memo: Too short + return false, [32]byte{}, [16]byte{}, "", nil + } + + topLine := memoLinesList[0] + + // The decorations on this line (and the footer) can be anything. + // We just make sure "Seekia Memo" exists in the first line of the memo. + containsSeekiaMemo := strings.Contains(topLine, "Seekia Memo") + if (containsSeekiaMemo == false){ + // Invalid memo: Missing Seekia Memo header. + return false, [32]byte{}, [16]byte{}, "", nil + } + + if (memoLinesList[1] != "|"){ + // Memo is malformed + return false, [32]byte{}, [16]byte{}, "", nil + } + + if (memoLinesList[2] != "|- Signature:"){ + // Memo is malformed + return false, [32]byte{}, [16]byte{}, "", nil + } + + signaturePart1, hasPrefix := strings.CutPrefix(memoLinesList[3], "| ") + if (hasPrefix == false){ + // Memo is malformed + return false, [32]byte{}, [16]byte{}, "", nil + } + signaturePart2, hasPrefix := strings.CutPrefix(memoLinesList[4], "| ") + if (hasPrefix == false){ + // Memo is malformed + return false, [32]byte{}, [16]byte{}, "", nil + } + signaturePart3, hasPrefix := strings.CutPrefix(memoLinesList[5], "| ") + if (hasPrefix == false){ + // Memo is malformed + return false, [32]byte{}, [16]byte{}, "", nil + } + + signatureString := signaturePart1 + signaturePart2 + signaturePart3 + + signatureBytes, err := encoding.DecodeBase64StringToBytes(signatureString) + if (err != nil){ + // Memo is malformed + return false, [32]byte{}, [16]byte{}, "", nil + } + + if (len(signatureBytes) != 64){ + // Memo is malformed + return false, [32]byte{}, [16]byte{}, "", nil + } + + signatureArray := [64]byte(signatureBytes) + + if (memoLinesList[6] != "|"){ + // Memo is malformed + return false, [32]byte{}, [16]byte{}, "", nil + } + + // We create memoContent, which is the content string that will be hashed and signed by the author + // We trim the final line + finalIndex := len(memoLinesList)-1 + memoContentList := memoLinesList[7:finalIndex] + memoContent := strings.Join(memoContentList, "\n") + memoContent += "\n" + + memoContentHash, err := blake3.Get32ByteBlake3Hash([]byte(memoContent)) + if (err != nil){ return false, [32]byte{}, [16]byte{}, "", err } + + if (memoLinesList[7] != "|- Identity Key:"){ + // Memo is malformed + return false, [32]byte{}, [16]byte{}, "", nil + } + + identityKeyPart1, hasPrefix := strings.CutPrefix(memoLinesList[8], "| ") + if (hasPrefix == false){ + // Memo is malformed + return false, [32]byte{}, [16]byte{}, "", nil + } + identityKeyPart2, hasPrefix := strings.CutPrefix(memoLinesList[9], "| ") + if (hasPrefix == false){ + // Memo is malformed + return false, [32]byte{}, [16]byte{}, "", nil + } + + identityKeyHex := identityKeyPart1 + identityKeyPart2 + + identityKeyBytes, err := encoding.DecodeHexStringToBytes(identityKeyHex) + if (err != nil){ + // Memo is malformed + // Invalid identity key: Not hex. + return false, [32]byte{}, [16]byte{}, "", nil + } + if (len(identityKeyBytes) != 32){ + // Memo is malformed + // Invalid identity key: Invalid length. + return false, [32]byte{}, [16]byte{}, "", nil + } + + identityKeyArray := [32]byte(identityKeyBytes) + + if (memoLinesList[10] != "|"){ + // Memo is malformed + return false, [32]byte{}, [16]byte{}, "", nil + } + if (memoLinesList[11] != "|- Author:"){ + // Memo is malformed + return false, [32]byte{}, [16]byte{}, "", nil + } + + identityHashString, hasPrefix := strings.CutPrefix(memoLinesList[12], "| ") + if (hasPrefix == false){ + // Memo is malformed + return false, [32]byte{}, [16]byte{}, "", nil + } + + identityHash, identityType, err := identity.ReadIdentityHashString(identityHashString) + if (err != nil){ + // Memo is malformed + // Invalid author identity hash + return false, [32]byte{}, [16]byte{}, "", nil + } + + // Now we verify signature and identity hash + + expectedIdentityHash, err := identity.ConvertIdentityKeyToIdentityHash(identityKeyArray, identityType) + if (err != nil){ + // Memo is malformed + // Invalid identity key + return false, [32]byte{}, [16]byte{}, "", nil + } + + if (identityHash != expectedIdentityHash){ + // Memo is malformed + return false, [32]byte{}, [16]byte{}, "", nil + } + + isValid := edwardsKeys.VerifySignature(identityKeyArray, signatureArray, memoContentHash) + if (isValid == false){ + // Memo is malformed + return false, [32]byte{}, [16]byte{}, "", nil + } + + // Signature is valid. + // We make sure rest of memo is well formed. + + if (memoLinesList[13] != "|"){ + // Memo is malformed + return false, [32]byte{}, [16]byte{}, "", nil + } + if (memoLinesList[14] != "|- Memo:"){ + // Memo is malformed + return false, [32]byte{}, [16]byte{}, "", nil + } + if (memoLinesList[15] != "|"){ + // Memo is malformed + return false, [32]byte{}, [16]byte{}, "", nil + } + + penultimateLine := memoLinesList[finalIndex-1] + if (penultimateLine != "|"){ + // Memo is malformed + // The second to last line must be a single pipe + return false, [32]byte{}, [16]byte{}, "", nil + } + + finalLine := memoLinesList[finalIndex] + + containsEndOfMemo := strings.Contains(finalLine, "End Of Memo") + if (containsEndOfMemo == false){ + // Memo is malformed + return false, [32]byte{}, [16]byte{}, "", nil + } + + // Now we check to make sure each line has the "|" prefix and is well formed + // We use this to build the unarmored memo contents + var unarmoredMemoContentsBuilder strings.Builder + + for index, memoLine := range memoLinesList{ + + if (index < 16){ + continue + } + if (index >= finalIndex-1){ + // We have reached the penultimate line of the memo + // We don't include this line or the memo footer in the unarmored memo contents + break + } + + if (memoLine != "|" && memoLine != "| "){ + + // "|" == The line is a newline without any text content. + + // "| " == This memo was created before the memo format was changed to not include whitespace before empty lines + + // The line should not be empty + // We will add the line contents to the unarmoredContent + + lineWithoutPrefix, hasPrefix := strings.CutPrefix(memoLine, "| ") + if (hasPrefix == false){ + // Memo is malformed + // Each line must either be a single pipe, a pipe with 4 whitespace characters, + // or a pipe with 4 whitespace characters and content afterwards + return false, [32]byte{}, [16]byte{}, "", nil + } + + if (lineWithoutPrefix == ""){ + // Memo is malformed + // Each line must contain some content + return false, [32]byte{}, [16]byte{}, "", nil + } + + unarmoredMemoContentsBuilder.WriteString(lineWithoutPrefix) + } + + if (index < finalIndex-2){ + unarmoredMemoContentsBuilder.WriteString("\n") + } + } + + unarmoredMemoContents := unarmoredMemoContentsBuilder.String() + + return true, memoHash, identityHash, unarmoredMemoContents, nil +} + +//Outputs: +// -string: Blockchain address that can timestamp the memo +// -error +func GetBlockchainAddressFromMemoHash(cryptocurrencyName string, memoHash [32]byte)(string, error){ + + if (cryptocurrencyName != "Ethereum" && cryptocurrencyName != "Cardano"){ + return "", errors.New("GetBlockchainAddressFromMemoHash called with invalid cryptocurrencyName: " + cryptocurrencyName) + } + + if (cryptocurrencyName == "Ethereum"){ + + memoEthereumAddress, err := ethereumAddress.GetMemoEthereumAddressFromMemoHash(memoHash) + if (err != nil) { return "", err } + + return memoEthereumAddress, nil + } + + memoCardanoAddress, err := cardanoAddress.GetMemoCardanoAddressFromMemoHash(memoHash) + if (err != nil) { return "", err } + + return memoCardanoAddress, nil +} + + + diff --git a/internal/memos/readMemos/readMemos_test.go b/internal/memos/readMemos/readMemos_test.go new file mode 100644 index 0000000..03a5377 --- /dev/null +++ b/internal/memos/readMemos/readMemos_test.go @@ -0,0 +1,194 @@ +package readMemos_test + + +import "seekia/internal/memos/readMemos" +import "seekia/internal/identity" +import "seekia/internal/encoding" + +import "bytes" +import "testing" + + +func TestReadMemo(t *testing.T){ + + testMemo := `| «« Seekia Memo »» +| +|- Signature: +| OBaIeYgbN6AqrlKRGo2K9v2y8L4_jk +| hRahKqgYvGbS3zv0cWDbmdZd4h4GMA +| _aZC2rX67dkBHoGOgzvrVguiDA== +| +|- Identity Key: +| 019f50f450efec14d5314e36685e6ccd +| 2a11026d206108a7e46ad1aa5778a36c +| +|- Author: +| ajcn6vesxwejdvwgj57bdo3m3dm +| +|- Memo: +| +| Cure racial loneliness. +| +| Beautify the human species. +| +| Seekia: Be race aware. +| +| «« End Of Memo »»` + + + memoIsValid, memoHash, authorIdentityHash, unarmoredMemoContent, err := readMemos.ReadMemo(testMemo) + if (err != nil){ + t.Fatalf("Failed to read memo: " + err.Error()) + } + if (memoIsValid == false){ + t.Fatalf("Memo is not valid.") + } + + expectedAuthorIdentityHashString := "ajcn6vesxwejdvwgj57bdo3m3dm" + + expectedAuthorIdentityHash, _, err := identity.ReadIdentityHashString(expectedAuthorIdentityHashString) + if (err != nil){ + t.Fatalf("Failed to read expectedAuthorIdentityHashString: " + err.Error()) + } + + if (authorIdentityHash != expectedAuthorIdentityHash){ + authorIdentityHashHex := encoding.EncodeBytesToHexString(authorIdentityHash[:]) + t.Fatalf("Received author identity hash does not match: " + authorIdentityHashHex) + } + + memoHashExpected := "1cc8b5f73ec83b24db18814292ffd3139b6571be0d0a4ce7002de458525b0ea3" + + memoHashBytes, err := encoding.DecodeHexStringToBytes(memoHashExpected) + if (err != nil){ + t.Fatalf("Unable to read memoHashExpected: " + err.Error()) + } + + areEqual := bytes.Equal(memoHash[:], memoHashBytes) + if (areEqual == false){ + memoHashHex := encoding.EncodeBytesToHexString(memoHash[:]) + t.Fatalf("Received unexpected memo hash: " + memoHashHex) + } + + ethereumAddress, err := readMemos.GetBlockchainAddressFromMemoHash("Ethereum", memoHash) + if (err != nil){ + t.Fatalf("GetBlockchainAddressFromMemoHash failed: " + err.Error()) + } + + if (ethereumAddress != "0xc514B072e60F03568F0e136d30de210579ffA1F0"){ + t.Fatalf("GetBlockchainAddressFromMemoHash returning unexpected Ethereum address: " + ethereumAddress) + } + + cardanoAddress, err := readMemos.GetBlockchainAddressFromMemoHash("Cardano", memoHash) + if (err != nil){ + t.Fatalf("GetBlockchainAddressFromMemoHash failed: " + err.Error()) + } + + if (cardanoAddress != "addr1vy0gfzl8muw3cstqhlage0nm5y9tyay3ueez4whya9qulpgpvytyu"){ + t.Fatalf("GetBlockchainAddressFromMemoHash returning unexpected Cardano address: " + cardanoAddress) + } + + expectedUnarmoredMemoContent := `Cure racial loneliness. + +Beautify the human species. + +Seekia: Be race aware.` + + if (unarmoredMemoContent != expectedUnarmoredMemoContent){ + t.Fatalf("Unexpected unarmoredMemoContent: " + unarmoredMemoContent) + } +} + + +// We use this to verify a legacy memo, where lines with no content were preceded with a pipe and 4 whitespace characters +// Now memos do not contain the 4 whitespace characters for these lines. They only contain a single pipe. +func TestReadMemo_Legacy(t *testing.T){ + + testMemo := `| «« Seekia Memo »» +| +|- Signature: +| ND8PfLLNZn7CJlQMokl7jvmd8yL5ee +| OGuJ6Jql_dN2BgANO8n9vYo9h5qPZL +| AUAmWM2D_PXJUwzOTxs6bUV1Bw== +| +|- Identity Key: +| b26327a3e00e97a2759ac0a08a39c84f +| 8edb833eec43669a81e27509f4ff6636 +| +|- Author: +| p2qsphq4m72wr6trbyibwggsfem +| +|- Memo: +| +| Cure racial loneliness. +| +| Facilitate eugenic breeding. +| +| Seekia: Be race aware. +| +| «« End Of Memo »»` + + + memoIsValid, memoHash, authorIdentityHash, unarmoredMemoContent, err := readMemos.ReadMemo(testMemo) + if (err != nil){ + t.Fatalf("Failed to read memo: " + err.Error()) + } + if (memoIsValid == false){ + t.Fatalf("Memo is not valid.") + } + + expectedAuthorIdentityHashString := "p2qsphq4m72wr6trbyibwggsfem" + + expectedAuthorIdentityHash, _, err := identity.ReadIdentityHashString(expectedAuthorIdentityHashString) + if (err != nil){ + t.Fatalf("Failed to read expectedAuthorIdentityHashString: " + err.Error()) + } + + if (authorIdentityHash != expectedAuthorIdentityHash){ + authorIdentityHashHex := encoding.EncodeBytesToHexString(authorIdentityHash[:]) + t.Fatalf("Received author identity hash does not match: " + authorIdentityHashHex) + } + + memoHashExpected := "c624a587458564a272a357c8bda79e51dfe4cbc1a995b612887ceafe54ad3b4f" + + memoHashBytes, err := encoding.DecodeHexStringToBytes(memoHashExpected) + if (err != nil){ + t.Fatalf("Unable to read memoHashExpected: " + err.Error()) + } + + areEqual := bytes.Equal(memoHash[:], memoHashBytes) + if (areEqual == false){ + memoHashHex := encoding.EncodeBytesToHexString(memoHash[:]) + t.Fatalf("Received unexpected memo hash: " + memoHashHex) + } + + ethereumAddress, err := readMemos.GetBlockchainAddressFromMemoHash("Ethereum", memoHash) + if (err != nil){ + t.Fatalf("GetBlockchainAddressFromMemoHash failed: " + err.Error()) + } + + if (ethereumAddress != "0xb56159Dbe37A255915A113a12588447182eDDD06"){ + t.Fatalf("GetBlockchainAddressFromMemoHash returning unexpected Ethereum address: " + ethereumAddress) + } + + cardanoAddress, err := readMemos.GetBlockchainAddressFromMemoHash("Cardano", memoHash) + if (err != nil){ + t.Fatalf("GetBlockchainAddressFromMemoHash failed: " + err.Error()) + } + + if (cardanoAddress != "addr1vxerdvvcwxgvcwf23tjxp8kl9kg0rpe4dsvpsjfcguusk6sfn9c6h"){ + t.Fatalf("GetBlockchainAddressFromMemoHash returning unexpected Cardano address: " + cardanoAddress) + } + + expectedUnarmoredMemoContent := `Cure racial loneliness. + +Facilitate eugenic breeding. + +Seekia: Be race aware.` + + if (unarmoredMemoContent != expectedUnarmoredMemoContent){ + t.Fatalf("Unexpected unarmoredMemoContent: " + unarmoredMemoContent) + } +} + + + diff --git a/internal/messaging/chatMessageStorage/chatMessageStorage.go b/internal/messaging/chatMessageStorage/chatMessageStorage.go new file mode 100644 index 0000000..84babcf --- /dev/null +++ b/internal/messaging/chatMessageStorage/chatMessageStorage.go @@ -0,0 +1,142 @@ + +// chatMessageStorage provides functions for storing and retrieving raw chat messages. +// This package stores encrypted messages. The myChatMessages package is where decrypted messages are stored. + +package chatMessageStorage + +import "seekia/internal/badgerDatabase" +import "seekia/internal/messaging/myInbox" +import "seekia/internal/messaging/readMessages" +import "seekia/internal/moderation/reportStorage" +import "seekia/internal/moderation/reviewStorage" +import "seekia/internal/mySettings" + +import "errors" + + +func GetNumberOfStoredMessages()(int64, error){ + + numberOfMessages, err := badgerDatabase.GetNumberOfChatMessages() + if (err != nil) { return 0, err } + + return numberOfMessages, nil +} + +// Returns all downloaded raw message hashes +// These are different from myChatMessages, which are decrypted +func GetAllDownloadedMessageHashes()([][26]byte, error){ + + messageHashes, err := badgerDatabase.GetAllChatMessageHashes() + if (err != nil) { return nil, err } + + return messageHashes, nil +} + +//Outputs: +// -bool: Message is well formed +// -error +func AddMessage(inputMessage []byte)(bool, error){ + + ableToRead, messageHash, _, messageNetworkType, recipientInbox, _, _, _, _, _, _, err := readMessages.ReadChatMessagePublicDataAndHash(true, inputMessage) + if (err != nil) { return false, err } + if (ableToRead == false){ + // Message is malformed, do nothing. + // This means that the other host is malicious + return false, nil + } + + exists, _, err := badgerDatabase.GetChatMessage(messageHash) + if (err != nil) { return false, err } + if (exists == true){ + // Message already exists in the database. + return true, nil + } + + err = badgerDatabase.AddChatInboxMessage(recipientInbox, messageHash) + if (err != nil) { return false, err } + + err = badgerDatabase.AddChatMessage(messageHash, inputMessage) + if (err != nil) { return false, err } + + err = mySettings.SetSetting("ViewedContentNeedsRefreshYesNo", "Yes") + if (err != nil) { return false, err } + + inboxIsMine, inboxIdentityType, err := myInbox.CheckIfInboxIsMine(recipientInbox, messageNetworkType) + if (err != nil) { return false, err } + if (inboxIsMine == true){ + + err = mySettings.SetSetting(inboxIdentityType + "ChatConversationsNeedRefreshYesNo", "Yes") + if (err != nil) { return false, err } + } + + return true, nil +} + + +// This function will attempt to decrypt a message and return the communication +// We attempt to find a cipher key for the message from any downloaded reviews/reports +//Outputs: +// -bool: Message exists (in storage) +// -bool: Cipher key found +// -bool: Message is decryptable +// -[32]byte: Message Cipher Key +// -[16]byte: Message sender +// -string: Message communication +// -error +func GetDecryptedMessageForModeration(messageHash [26]byte)(bool, bool, bool, [32]byte, [16]byte, string, error){ + + messageExists, messageBytes, err := badgerDatabase.GetChatMessage(messageHash) + if (err != nil) { return false, false, false, [32]byte{}, [16]byte{}, "", err } + if (messageExists == false){ + + return false, false, false, [32]byte{}, [16]byte{}, "", nil + } + + ableToRead, _, messageNetworkType, _, _, _, _, messageCipherKeyHash, _, _, err := readMessages.ReadChatMessagePublicData(false, messageBytes) + if (err != nil) { return false, false, false, [32]byte{}, [16]byte{}, "", err } + if (ableToRead == false){ + return false, false, false, [32]byte{}, [16]byte{}, "", errors.New("Database corrupt: Contains invalid message.") + } + + getMessageCipherKey := func()(bool, [32]byte, error){ + + cipherKeyFound, cipherKey, err := reportStorage.GetMessageCipherKeyFromAnyReport(messageHash, messageNetworkType, messageCipherKeyHash) + if (err != nil) { return false, [32]byte{}, err } + if (cipherKeyFound == true){ + return true, cipherKey, nil + } + + cipherKeyFound, cipherKey, err = reviewStorage.GetMessageCipherKeyFromAnyReview(messageHash, messageNetworkType, messageCipherKeyHash) + if (err != nil) { return false, [32]byte{}, err } + if (cipherKeyFound == true){ + return true, cipherKey, nil + } + + return false, [32]byte{}, nil + } + + cipherKeyFound, messageCipherKey, err := getMessageCipherKey() + if (err != nil) { return false, false, false, [32]byte{}, [16]byte{}, "", err } + if (cipherKeyFound == false){ + return true, false, false, [32]byte{}, [16]byte{}, "", nil + } + + ableToRead, messageHash_Received, _, messageNetworkType_Received, senderIdentityHash, messageCommunication, err := readMessages.ReadChatMessageWithCipherKey(messageBytes, messageCipherKey) + if (err != nil) { return false, false, false, [32]byte{}, [16]byte{}, "", err } + if (ableToRead == false){ + // Message is malformed + // It must have been created by a malicious author. + return true, true, false, [32]byte{}, [16]byte{}, "", nil + } + if (messageHash_Received != messageHash){ + return false, false, false, [32]byte{}, [16]byte{}, "", errors.New("ReadChatMessageWithCipherKey returning different messageHash than ReadChatMessagePublicData") + } + if (messageNetworkType_Received != messageNetworkType){ + return false, false, false, [32]byte{}, [16]byte{}, "", errors.New("Database corrupt: Contains message with different message hash than entry key.") + } + + return true, true, true, messageCipherKey, senderIdentityHash, messageCommunication, nil +} + + + diff --git a/internal/messaging/createMessages/createMessages.go b/internal/messaging/createMessages/createMessages.go new file mode 100644 index 0000000..35919b5 --- /dev/null +++ b/internal/messaging/createMessages/createMessages.go @@ -0,0 +1,237 @@ + +// createMessages provides functions to create Seekia chat messages +// The chat message structure will eventually be documented in /documentation/Specification.md + +package createMessages + +//TODO: Add an upper limit on bytes, and an upper limit for size of communication. +//TODO: Verify all message values and function inputs. + +import "seekia/internal/allowedText" +import "seekia/internal/appValues" +import "seekia/internal/cryptography/blake3" +import "seekia/internal/cryptography/chaPolyShrink" +import "seekia/internal/cryptography/edwardsKeys" +import "seekia/internal/cryptography/kyber" +import "seekia/internal/cryptography/nacl" +import "seekia/internal/encoding" +import "seekia/internal/helpers" +import "seekia/internal/identity" + +import messagepack "github.com/vmihailenco/msgpack/v5" + +import "slices" +import "errors" + + +//Outputs: +// -[]byte: Message bytes +// -[26]byte: Message Hash +// -error +func CreateChatMessage( + networkType byte, + senderIdentityHash [16]byte, + senderIdentityPublicKey [32]byte, + senderIdentityPrivateKey [64]byte, + messageSentTime int64, + senderCurrentSecretInboxSeed [22]byte, + senderNextSecretInboxSeed [22]byte, + senderDeviceIdentifier [11]byte, + senderLatestChatKeysUpdateTime int64, + communication string, + recipientIdentityHash [16]byte, + recipientInbox [10]byte, + recipientNaclKey [32]byte, + recipientKyberKey [1568]byte, + doubleSealedKeysSealerKey [32]byte)([]byte, [26]byte, error){ + + // We create the basaldataEncryptionKey, messageCipherKey, and messageCipherKeyHash + + basaldataEncryptionKey, err := helpers.GetNewRandom32ByteArray() + if (err != nil) { return nil, [26]byte{}, err } + + messageCipherKey, err := blake3.Get32ByteBlake3Hash(basaldataEncryptionKey[:]) + if (err != nil) { return nil, [26]byte{}, err } + + cipherKeyHash, err := blake3.GetBlake3HashAsBytes(25, messageCipherKey[:]) + if (err != nil) { return nil, [26]byte{}, err } + + // We create the noncipheredSection, which contains the message inbox, message version, network type, cipherKeyHash, and the doubleSealedKeys + + keyPieceA, err := helpers.GetNewRandom32ByteArray() + if (err != nil) { return nil, [26]byte{}, err } + + keyPieceB := helpers.XORTwo32ByteArrays(keyPieceA, basaldataEncryptionKey) + + naclEncryptedKeyPieceA, err := nacl.EncryptKeyWithNacl(recipientNaclKey, keyPieceA) + if (err != nil) { return nil, [26]byte{}, err } + + kyberEncryptedKeyPieceB, err := kyber.EncryptKeyWithKyber(recipientKyberKey, keyPieceB) + if (err != nil) { return nil, [26]byte{}, err } + + innerSealedKeyPiecesList := slices.Concat(naclEncryptedKeyPieceA[:], kyberEncryptedKeyPieceB[:]) + + doubleSealedKeysNonce, err := helpers.GetNewRandom24ByteArray() + if (err != nil) { return nil, [26]byte{}, err } + + doubleSealedKeys, err := chaPolyShrink.EncryptChaPolyShrink(innerSealedKeyPiecesList, doubleSealedKeysSealerKey, doubleSealedKeysNonce, false, 0, false, [32]byte{}) + if (err != nil) { return nil, [26]byte{}, err } + + // We create the DoubleSealedKeys to store the encryption keys for the cipheredMessage and the basaldata + + messageVersion := appValues.GetMessageVersion() + + cipheredMessageNonce, err := helpers.GetNewRandom24ByteArray() + if (err != nil) { return nil, [26]byte{}, err } + + messageVersionEncoded, err := encoding.EncodeMessagePackBytes(messageVersion) + if (err != nil) { return nil, [26]byte{}, err } + + networkTypeEncoded, err := encoding.EncodeMessagePackBytes(networkType) + if (err != nil) { return nil, [26]byte{}, err } + + recipientInboxEncoded, err := encoding.EncodeMessagePackBytes(recipientInbox) + if (err != nil) { return nil, [26]byte{}, err } + + doubleSealedKeysNonceEncoded, err := encoding.EncodeMessagePackBytes(doubleSealedKeysNonce) + if (err != nil) { return nil, [26]byte{}, err } + + doubleSealedKeysEncoded, err := encoding.EncodeMessagePackBytes(doubleSealedKeys) + if (err != nil) { return nil, [26]byte{}, err } + + cipherKeyHashEncoded, err := encoding.EncodeMessagePackBytes(cipherKeyHash) + if (err != nil) { return nil, [26]byte{}, err } + + cipheredMessageNonceEncoded, err := encoding.EncodeMessagePackBytes(cipheredMessageNonce) + if (err != nil) { return nil, [26]byte{}, err } + + noncipheredSectionSlice := []messagepack.RawMessage{messageVersionEncoded, networkTypeEncoded, recipientInboxEncoded, doubleSealedKeysNonceEncoded, doubleSealedKeysEncoded, cipherKeyHashEncoded, cipheredMessageNonceEncoded} + + noncipheredSectionEncoded, err := encoding.EncodeMessagePackBytes(noncipheredSectionSlice) + if (err != nil) { return nil, [26]byte{}, err } + + // The purpose of the noncipheredSectionHash is to prevent someone from tampering with the message and replaying the ciphered portion + // By using the noncipheredSectionHash as the additionalData, the noncipheredSection cannot be tampered with + + noncipheredSectionHash, err := blake3.Get32ByteBlake3Hash(noncipheredSectionEncoded) + if (err != nil) { return nil, [26]byte{}, err } + + // We create the inner message + + timeIsValid := helpers.VerifyBroadcastTime(messageSentTime) + if (timeIsValid == false){ + return nil, [26]byte{}, errors.New("CreateChatMessage called with invalid messageSentTime.") + } + timeIsValid = helpers.VerifyBroadcastTime(senderLatestChatKeysUpdateTime) + if (timeIsValid == false){ + return nil, [26]byte{}, errors.New("CreateChatMessage called with invalid senderLatestChatKeysUpdateTime") + } + + recipientIdentityType, err := identity.GetIdentityTypeFromIdentityHash(recipientIdentityHash) + if (err != nil){ + recipientIdentityHashHex := encoding.EncodeBytesToHexString(recipientIdentityHash[:]) + return nil, [26]byte{}, errors.New("CreateChatMessage called with invalid recipientIdentityHash: " + recipientIdentityHashHex) + } + + senderIdentityHashExpected, err := identity.ConvertIdentityKeyToIdentityHash(senderIdentityPublicKey, recipientIdentityType) + if (err != nil) { + senderIdentityPublicKeyHex := encoding.EncodeBytesToHexString(senderIdentityPublicKey[:]) + return nil, [26]byte{}, errors.New("CreateChatMessage called with invalid senderIdentityPublicKey: " + senderIdentityPublicKeyHex) + } + + if (senderIdentityHash != senderIdentityHashExpected){ + senderIdentityHashHex := encoding.EncodeBytesToHexString(senderIdentityHash[:]) + return nil, [26]byte{}, errors.New("CreateChatMessage called with invalid senderIdentityHash: " + senderIdentityHashHex) + } + + recipientIdentityHashEncoded, err := encoding.EncodeMessagePackBytes(recipientIdentityHash) + if (err != nil) { return nil, [26]byte{}, err } + + sentTimeEncoded, err := encoding.EncodeMessagePackBytes(messageSentTime) + if (err != nil) { return nil, [26]byte{}, err } + + senderCurrentSecretInboxSeedEncoded, err := encoding.EncodeMessagePackBytes(senderCurrentSecretInboxSeed) + if (err != nil) { return nil, [26]byte{}, err } + + senderNextSecretInboxSeedEncoded, err := encoding.EncodeMessagePackBytes(senderNextSecretInboxSeed) + if (err != nil) { return nil, [26]byte{}, err } + + senderDeviceIdentifierEncoded, err := encoding.EncodeMessagePackBytes(senderDeviceIdentifier) + if (err != nil) { return nil, [26]byte{}, err } + + senderLatestChatKeysUpdateTimeEncoded, err := encoding.EncodeMessagePackBytes(senderLatestChatKeysUpdateTime) + if (err != nil) { return nil, [26]byte{}, err } + + messageBasaldataSlice := []messagepack.RawMessage{recipientIdentityHashEncoded, sentTimeEncoded, senderCurrentSecretInboxSeedEncoded, senderNextSecretInboxSeedEncoded, senderDeviceIdentifierEncoded, senderLatestChatKeysUpdateTimeEncoded} + + messageBasaldataEncoded, err := encoding.EncodeMessagePackBytes(messageBasaldataSlice) + if (err != nil) { return nil, [26]byte{}, err } + + basaldataEncryptionNonce, err := helpers.GetNewRandom24ByteArray() + if (err != nil) { return nil, [26]byte{}, err } + + encryptedBasaldata, err := chaPolyShrink.EncryptChaPolyShrink(messageBasaldataEncoded, basaldataEncryptionKey, basaldataEncryptionNonce, false, 0, false, [32]byte{}) + if (err != nil) { return nil, [26]byte{}, err } + + communicationIsAllowed := allowedText.VerifyStringIsAllowed(communication) + if (communicationIsAllowed == false){ + return nil, [26]byte{}, errors.New("CreateChatMessage called with communication containing unallowed characters.") + } + + //TODO: Add length restriction to communication + + senderIdentityKeyEncoded, err := encoding.EncodeMessagePackBytes(senderIdentityPublicKey) + if (err != nil) { return nil, [26]byte{}, err } + + senderIdentityTypeEncoded, err := encoding.EncodeMessagePackBytes(recipientIdentityType) + if (err != nil) { return nil, [26]byte{}, err } + + basaldataEncryptionNonceEncoded, err := encoding.EncodeMessagePackBytes(basaldataEncryptionNonce) + if (err != nil) { return nil, [26]byte{}, err } + + encryptedBasaldataEncoded, err := encoding.EncodeMessagePackBytes(encryptedBasaldata) + if (err != nil) { return nil, [26]byte{}, err } + + communicationEncoded, err := encoding.EncodeMessagePackBytes(communication) + if (err != nil) { return nil, [26]byte{}, err } + + innerMessageContentSlice := []messagepack.RawMessage{senderIdentityKeyEncoded, senderIdentityTypeEncoded, basaldataEncryptionNonceEncoded, encryptedBasaldataEncoded, communicationEncoded} + + innerMessageContentEncoded, err := encoding.EncodeMessagePackBytes(innerMessageContentSlice) + if (err != nil) { return nil, [26]byte{}, err } + + innerMessageContentHash, err := blake3.Get32ByteBlake3Hash(innerMessageContentEncoded) + if (err != nil) { return nil, [26]byte{}, err } + + innerMessageSignature := edwardsKeys.CreateSignature(senderIdentityPrivateKey, innerMessageContentHash) + + signatureEncoded, err := encoding.EncodeMessagePackBytes(innerMessageSignature) + if (err != nil) { return nil, [26]byte{}, err } + + innerMessageSlice := []messagepack.RawMessage{signatureEncoded, innerMessageContentEncoded} + + innerMessageBytes, err := encoding.EncodeMessagePackBytes(innerMessageSlice) + if (err != nil) { return nil, [26]byte{}, err } + + cipheredMessage, err := chaPolyShrink.EncryptChaPolyShrink(innerMessageBytes, messageCipherKey, cipheredMessageNonce, true, 1000, true, noncipheredSectionHash) + if (err != nil) { return nil, [26]byte{}, err } + + cipheredMessageEncoded, err := encoding.EncodeMessagePackBytes(cipheredMessage) + if (err != nil) { return nil, [26]byte{}, err } + + // We encode the outer message + + finalMessageSlice := []messagepack.RawMessage{noncipheredSectionEncoded, cipheredMessageEncoded} + + finalMessageBytes, err := encoding.EncodeMessagePackBytes(finalMessageSlice) + if (err != nil) { return nil, [26]byte{}, err } + + messageHash, err := blake3.GetBlake3HashAsBytes(26, finalMessageBytes) + if (err != nil) { return nil, [26]byte{}, err } + + messageHashArray := [26]byte(messageHash) + + return finalMessageBytes, messageHashArray, nil +} + + diff --git a/internal/messaging/createMessages/createMessages_test.go b/internal/messaging/createMessages/createMessages_test.go new file mode 100644 index 0000000..9337b62 --- /dev/null +++ b/internal/messaging/createMessages/createMessages_test.go @@ -0,0 +1,267 @@ +package createMessages_test + +import "seekia/internal/cryptography/kyber" +import "seekia/internal/cryptography/nacl" +import "seekia/internal/helpers" +import "seekia/internal/identity" +import "seekia/internal/messaging/readMessages" +import "seekia/internal/messaging/createMessages" +import "seekia/internal/messaging/inbox" + +import "time" +import "testing" + +func TestReadMessagePublicData(t *testing.T){ + + messageNetworkType, err := helpers.GetRandomByteWithinRange(1, 2) + if (err != nil) { + t.Fatalf("Failed to get random network type: " + err.Error()) + } + + identityType, err := helpers.GetRandomItemFromList([]string{"Mate", "Moderator"}) + if (err != nil) { + t.Fatalf("Failed to get random identity type: " + err.Error()) + } + + senderIdentityPublicKey, senderIdentityPrivateKey, err := identity.GetNewRandomPublicPrivateIdentityKeys() + if (err != nil) { + t.Fatalf("Failed to derive identity keys: " + err.Error()) + } + + senderIdentityHash, err := identity.ConvertIdentityKeyToIdentityHash(senderIdentityPublicKey, identityType) + if (err != nil) { + t.Fatalf("Failed to derive identity hash: " + err.Error()) + } + + recipientInbox, err := inbox.GetNewRandomInbox() + if (err != nil) { + t.Fatalf("GetNewRandomInbox failed: " + err.Error()) + } + + recipientIdentityHash, err := identity.GetNewRandomIdentityHash(true, identityType) + if (err != nil) { + t.Fatalf("Failed to get random identity hash: " + err.Error()) + } + + senderLatestChatKeysUpdateTime := int64(12345678900) + + recipientNaclKey, err := nacl.GetNewRandomPublicNaclKey() + if (err != nil) { + t.Fatalf("GetNewRandomPublicNaclKey failed: " + err.Error()) + } + + recipientKyberKey, err := kyber.GetNewRandomPublicKyberKey() + if (err != nil) { + t.Fatalf("GetNewRandomPublicKyberKey failed: " + err.Error()) + } + + messageSentTime := time.Now().Unix() + + senderCurrentSecretInboxSeed, err := inbox.GetNewRandomSecretInboxSeed() + if (err != nil) { + t.Fatalf("GetNewRandomSecretInboxSeed failed: " + err.Error()) + } + + senderNextSecretInboxSeed, err := inbox.GetNewRandomSecretInboxSeed() + if (err != nil) { + t.Fatalf("GetNewRandomSecretInboxSeed failed: " + err.Error()) + } + + senderDeviceIdentifier, err := helpers.GetNewRandomDeviceIdentifier() + if (err != nil) { + t.Fatalf("GetNewRandomDeviceIdentifier failed: " + err.Error()) + } + + testCommunication := "This is a test message." + + doubleSealedKeysSealerKey, err := helpers.GetNewRandom32ByteArray() + if (err != nil) { + t.Fatalf("GetNewRandom32ByteArray failed: " + err.Error()) + } + + finalMessage, messageHash, err := createMessages.CreateChatMessage(messageNetworkType, senderIdentityHash, senderIdentityPublicKey, senderIdentityPrivateKey, messageSentTime, senderCurrentSecretInboxSeed, senderNextSecretInboxSeed, senderDeviceIdentifier, senderLatestChatKeysUpdateTime, testCommunication, recipientIdentityHash, recipientInbox, recipientNaclKey, recipientKyberKey, doubleSealedKeysSealerKey) + if (err != nil) { + t.Fatalf("Failed to create chat message: " + err.Error()) + } + + ableToRead, messageHash_Received, messageVersion_Received, messageNetworkType_Received, recipientInbox_Received, _, _, _, _, _, _, err := readMessages.ReadChatMessagePublicDataAndHash(true, finalMessage) + if (err != nil) { + t.Fatalf("Failed to read message public data: " + err.Error()) + } + if (ableToRead == false){ + t.Fatalf("Failed to read message public data.") + } + if (messageVersion_Received != 1){ + t.Fatalf("Message version does not match.") + } + if (messageNetworkType_Received != messageNetworkType){ + t.Fatalf("Message network type does not match.") + } + if (messageHash != messageHash_Received){ + t.Fatalf("Message hash does not match.") + } + if (recipientInbox != recipientInbox_Received){ + t.Fatalf("Message recipient identity hash does not match.") + } +} + + +func TestEncryptDecryptMessage(t *testing.T) { + + messageNetworkType, err := helpers.GetRandomByteWithinRange(1, 2) + if (err != nil) { + t.Fatalf("Failed to get random network type: " + err.Error()) + } + + identityType, err := helpers.GetRandomItemFromList([]string{"Mate", "Moderator"}) + if (err != nil) { + t.Fatalf("Failed to get random identity type: " + err.Error()) + } + + senderIdentityPublicKey, senderIdentityPrivateKey, err := identity.GetNewRandomPublicPrivateIdentityKeys() + if (err != nil) { + t.Fatalf("Failed to derive identity keys: " + err.Error()) + } + + senderIdentityHash, err := identity.ConvertIdentityKeyToIdentityHash(senderIdentityPublicKey, identityType) + if (err != nil) { + t.Fatalf("Failed to derive identity hash: " + err.Error()) + } + + recipientInbox, err := inbox.GetNewRandomInbox() + if (err != nil) { + t.Fatalf("GetNewRandomInbox failed: " + err.Error()) + } + + recipientIdentityHash, err := identity.GetNewRandomIdentityHash(true, identityType) + if (err != nil) { + t.Fatalf("Failed to get random identity hash: " + err.Error()) + } + + senderLatestChatKeysUpdateTime := int64(1234567899900) + + recipientNaclPublicKey, recipientNaclPrivateKey, err := nacl.GetNewRandomPublicPrivateNaclKeys() + if (err != nil) { + t.Fatalf("Failed to derive Nacl keys: " + err.Error()) + } + + recipientKyberPublicKey, recipientKyberPrivateKey, err := kyber.GetNewRandomPublicPrivateKyberKeys() + if (err != nil) { + t.Fatalf("Failed to derive Kyber keys: " + err.Error()) + } + + messageSentTime := time.Now().Unix() + + senderCurrentSecretInboxSeed, err := inbox.GetNewRandomSecretInboxSeed() + if (err != nil) { + t.Fatalf("GetNewRandomSecretInboxSeed failed: " + err.Error()) + } + + senderNextSecretInboxSeed, err := inbox.GetNewRandomSecretInboxSeed() + if (err != nil) { + t.Fatalf("GetNewRandomSecretInboxSeed failed: " + err.Error()) + } + + senderDeviceIdentifier, err := helpers.GetNewRandomDeviceIdentifier() + if (err != nil) { + t.Fatalf("GetNewRandomDeviceIdentifier failed: " + err.Error()) + } + + testCommunication := "This is a test message." + + doubleSealedKeysSealerKey, err := helpers.GetNewRandom32ByteArray() + if (err != nil) { + t.Fatalf("GetNewRandom32ByteArray failed: " + err.Error()) + } + + finalMessage, messageHash, err := createMessages.CreateChatMessage(messageNetworkType, senderIdentityHash, senderIdentityPublicKey, senderIdentityPrivateKey, messageSentTime, senderCurrentSecretInboxSeed, senderNextSecretInboxSeed, senderDeviceIdentifier, senderLatestChatKeysUpdateTime, testCommunication, recipientIdentityHash, recipientInbox, recipientNaclPublicKey, recipientKyberPublicKey, doubleSealedKeysSealerKey,) + if (err != nil) { + t.Fatalf("Failed to create chat message: " + err.Error()) + } + + newChatKeySet := readMessages.ChatKeySet{ + + NaclPublicKey: recipientNaclPublicKey, + NaclPrivateKey: recipientNaclPrivateKey, + KyberPrivateKey: recipientKyberPrivateKey, + } + newRecipientChatDecryptionKeySetsList := []readMessages.ChatKeySet{newChatKeySet} + + ableToRead, messageHash_Received, messageVersion, messageNetworkType_Received, messageCipherKey, senderIdentityHash_Received, recipientIdentityHash_Received, messageSentTime_Received, communication_Received, senderCurrentSecretInboxSeed_Received, senderNextSecretInboxSeed_Received, senderDeviceIdentifier_Received, senderLatestChatKeysUpdateTime_Received, err := readMessages.ReadChatMessage(finalMessage, doubleSealedKeysSealerKey, newRecipientChatDecryptionKeySetsList) + if (err != nil) { + t.Fatalf("Failed to read chat message: " + err.Error()) + } + if (ableToRead == false) { + t.Fatalf("Failed to read chat message.") + } + if (messageVersion != 1){ + t.Fatalf("Message version is not valid.") + } + if (messageNetworkType_Received != messageNetworkType){ + t.Fatalf("Message network type does not match.") + } + if (messageHash != messageHash_Received){ + t.Fatalf("Message hash does not match.") + } + if (senderIdentityHash != senderIdentityHash_Received){ + t.Fatalf("Sender identity hash does not match.") + } + if (recipientIdentityHash != recipientIdentityHash_Received){ + t.Fatalf("Recipient identity hash does not match.") + } + if (messageSentTime != messageSentTime_Received){ + t.Fatalf("Message sent time is incorrect.") + } + + if (testCommunication != communication_Received){ + t.Fatalf("Message communication does not match.") + } + + if (senderCurrentSecretInboxSeed != senderCurrentSecretInboxSeed_Received){ + t.Fatalf("senderCurrentSecretInboxSeed does not match.") + } + + if (senderNextSecretInboxSeed != senderNextSecretInboxSeed_Received){ + t.Fatalf("senderNextSecretInboxSeed does not match.") + } + + if (senderLatestChatKeysUpdateTime != senderLatestChatKeysUpdateTime_Received){ + t.Fatalf("Sender latest chat keys update time does not match.") + } + + if (senderDeviceIdentifier != senderDeviceIdentifier_Received){ + t.Fatalf("Sender device identifier does not match.") + } + + if (len(messageCipherKey) != 32){ + t.Fatalf("ReadChatMessage returning invalid messageCipherKey: Invalid length.") + } + + messageCipherKeyArray := [32]byte(messageCipherKey) + + ableToRead, messageHash_Received, messageVersion_Received, messageNetworkType_Received, senderIdentityHash_Received, communication_Received, err := readMessages.ReadChatMessageWithCipherKey(finalMessage, messageCipherKeyArray) + if (err != nil){ + t.Fatalf("ReadChatMessageWithCipherKey failed: " + err.Error()) + } + if (ableToRead == false){ + t.Fatalf("ReadChatMessageWithCipherKey failed.") + } + if (messageVersion_Received != 1){ + t.Fatalf("ReadChatMessageWithCipherKey failed: returning mismatched version.") + } + if (messageNetworkType_Received != messageNetworkType){ + t.Fatalf("ReadChatMessageWithCipherKey failed: returning mismatched network type.") + } + if (messageHash != messageHash_Received){ + t.Fatalf("ReadChatMessageWithCipherKey failed: returning mismatched messageHash.") + } + + if (senderIdentityHash != senderIdentityHash_Received){ + t.Fatalf("ReadChatMessageWithCipherKey failed: Sender identity hash does not match.") + } + + if (testCommunication != communication_Received){ + t.Fatalf("ReadChatMessageWithCipherKey failed: Message communication does not match.") + } +} + diff --git a/internal/messaging/inbox/inbox.go b/internal/messaging/inbox/inbox.go new file mode 100644 index 0000000..3161fb1 --- /dev/null +++ b/internal/messaging/inbox/inbox.go @@ -0,0 +1,126 @@ + +// inbox provides functions to read inboxes, create random inboxes, and to derive inboxes and sealer keys + +package inbox + +import "seekia/internal/cryptography/blake3" +import "seekia/internal/encoding" +import "seekia/internal/identity" +import "crypto/rand" + +import "slices" +import "errors" + + +func GetNewRandomInbox()([10]byte, error){ + + var inboxArray [10]byte + + _, err := rand.Read(inboxArray[:]) + if (err != nil) { return [10]byte{}, err } + + return inboxArray, nil +} + +func GetNewRandomSecretInboxSeed()([22]byte, error){ + + var newSecretInboxSeed [22]byte + + _, err := rand.Read(newSecretInboxSeed[:]) + if (err != nil) { return [22]byte{}, err } + + return newSecretInboxSeed, nil +} + +func ReadInboxString(inputInbox string)([10]byte, error){ + + inboxBytes, err := encoding.DecodeBase32StringToBytes(inputInbox) + if (err != nil){ + return [10]byte{}, errors.New("ReadInboxString called with invalid inbox: " + inputInbox + ". Not Base32: " + err.Error()) + } + + if (len(inboxBytes) != 10){ + return [10]byte{}, errors.New("ReadInboxString called with invalid inbox: " + inputInbox + ". Invalid length.") + } + + inboxArray := [10]byte(inboxBytes) + + return inboxArray, nil +} + +func GetPublicInboxFromIdentityHash(inputIdentityHash [16]byte)([10]byte, error){ + + identityType, err := identity.GetIdentityTypeFromIdentityHash(inputIdentityHash) + if (err != nil) { + identityHashHex := encoding.EncodeBytesToHexString(inputIdentityHash[:]) + return [10]byte{}, errors.New("GetPublicInboxFromIdentityHash called with invalid identity hash: " + identityHashHex) + } + + if (identityType != "Mate" && identityType != "Moderator"){ + return [10]byte{}, errors.New("GetPublicInboxFromIdentityHash called with Host identity hash.") + } + + // This sequence of bytes is the string "identitypubinbox" decoded from base32 to bytes + hashInputSuffix := []byte{64, 200, 217, 162, 120, 125, 2, 134, 133, 215} + + hashInput := slices.Concat(inputIdentityHash[:], hashInputSuffix) + + inboxBytes, err := blake3.GetBlake3HashAsBytes(10, hashInput) + if (err != nil) { return [10]byte{}, err } + + inboxArray := [10]byte(inboxBytes) + + return inboxArray, nil +} + +// Messages sent to a user's public inbox use a doublesealedkeys sealer key that is derived from the user's identity hash +func GetPublicInboxSealerKeyFromIdentityHash(inputIdentityHash [16]byte)([32]byte, error){ + + identityType, err := identity.GetIdentityTypeFromIdentityHash(inputIdentityHash) + if (err != nil) { + inputIdentityHashHex := encoding.EncodeBytesToHexString(inputIdentityHash[:]) + return [32]byte{}, errors.New("GetPublicInboxSealerKeyFromIdentityHash called with invalid identity hash: " + inputIdentityHashHex) + } + + if (identityType != "Mate" && identityType != "Moderator"){ + return [32]byte{}, errors.New("GetPublicInboxSealerKeyFromIdentityHash called with Host identityType identity hash.") + } + + // This sequence of bytes is the string "identityhashpublicinboxsealerkey" decoded from base32 to bytes + hashInputSuffix := []byte{64, 200, 217, 162, 120, 56, 36, 119, 208, 43, 64, 144, 208, 186, 242, 32, 22, 72, 168, 152} + + hashInput := slices.Concat(inputIdentityHash[:], hashInputSuffix) + + doubleSealedKeysSealerKey, err := blake3.GetBlake3HashAsBytes(32, hashInput) + if (err != nil) { return [32]byte{}, err } + + doubleSealedKeysSealerKeyArray := [32]byte(doubleSealedKeysSealerKey) + + return doubleSealedKeysSealerKeyArray, nil +} + + +// Outputs: +// -[10]byte: Secret inbox +// -[32]byte: Secret inbox doubleSealedKeys sealer key +// -error +func GetSecretInboxAndSealerKeyFromSecretInboxSeed(inputSecretInboxSeed [22]byte)([10]byte, [32]byte, error){ + + secretInboxHashInput := append(inputSecretInboxSeed[:], 1) + sealerKeyHashInput := append(inputSecretInboxSeed[:], 2) + + secretInbox, err := blake3.GetBlake3HashAsBytes(10, secretInboxHashInput) + if (err != nil) { return [10]byte{}, [32]byte{}, err } + + secretInboxArray := [10]byte(secretInbox) + + sealerKey, err := blake3.GetBlake3HashAsBytes(32, sealerKeyHashInput) + if (err != nil) { return [10]byte{}, [32]byte{}, err } + + sealerKeyArray := [32]byte(sealerKey) + + return secretInboxArray, sealerKeyArray, nil +} + + + diff --git a/internal/messaging/inbox/inbox_test.go b/internal/messaging/inbox/inbox_test.go new file mode 100644 index 0000000..5d1a0de --- /dev/null +++ b/internal/messaging/inbox/inbox_test.go @@ -0,0 +1,96 @@ +package inbox_test + +import "seekia/internal/messaging/inbox" + +import "seekia/internal/identity" +import "seekia/internal/encoding" + +import "bytes" +import "testing" + +func TestPublicInboxDerivationFunctions(t *testing.T){ + + testIdentityHashString := "wgkxplfvju7yhpoadvxju4kmfdm" + + testIdentityHash, _, err := identity.ReadIdentityHashString(testIdentityHashString) + if (err != nil){ + t.Fatalf("Failed to read testIdentityHashString: " + err.Error()) + } + + expectedPublicInboxString := "7sjss2s73pbqnhhe" + + expectedPublicInbox, err := inbox.ReadInboxString(expectedPublicInboxString) + if (err != nil){ + t.Fatalf("Failed to decode expectedPublicInbox: " + err.Error()) + } + + publicInbox, err := inbox.GetPublicInboxFromIdentityHash(testIdentityHash) + if (err != nil){ + t.Fatalf("Failed to get public inbox from identity hash: " + err.Error()) + } + + if (publicInbox != expectedPublicInbox){ + publicInboxString := encoding.EncodeBytesToBase32String(publicInbox[:]) + t.Fatalf("Failed to get public inbox from identity hash: Unexpected result: " + publicInboxString) + } + + publicInboxSealerKey, err := inbox.GetPublicInboxSealerKeyFromIdentityHash(testIdentityHash) + if (err != nil){ + t.Fatalf("GetPublicInboxSealerKeyFromIdentityHash failed: " + err.Error()) + } + + expectedPublicInboxSealerKey := "7d3fadbc59f692dead5ae794ddd0492cfef2990357f09301a7560966dd955d8b" + + expectedPublicInboxSealerKeyBytes, err := encoding.DecodeHexStringToBytes(expectedPublicInboxSealerKey) + if (err != nil){ + t.Fatalf("expectedPublicInboxSealerKey is not hex: " + err.Error()) + } + + areEqual := bytes.Equal(publicInboxSealerKey[:], expectedPublicInboxSealerKeyBytes) + if (areEqual == false){ + resultHex := encoding.EncodeBytesToHexString(publicInboxSealerKey[:]) + t.Fatalf("GetPublicInboxSealerKeyFromIdentityHash failed: Unexpected result: " + resultHex) + } +} + +func TestSecretInboxDerivationFunction(t *testing.T){ + + testSecretInboxSeed := "f574640613cb21cbb92e549e6a0412a152cca1a19ad3" + + testSecretInboxSeedBytes, err := encoding.DecodeHexStringToBytes(testSecretInboxSeed) + if (err != nil){ + t.Fatalf("testSecretInboxSeed is not hex: " + err.Error()) + } + if (len(testSecretInboxSeedBytes) != 22){ + t.Fatalf("testSecretInboxSeed has an invalid length.") + } + + testSecretInboxSeedArray := [22]byte(testSecretInboxSeedBytes) + + secretInbox, secretInboxSealerKey, err := inbox.GetSecretInboxAndSealerKeyFromSecretInboxSeed(testSecretInboxSeedArray) + if (err != nil){ + t.Fatalf("GetSecretInboxAndSealerKeyFromSecretInboxSeed failed: " + err.Error()) + } + + expectedSecretInboxString := "q7it4ziecjxigop6" + + expectedSecretInbox, err := inbox.ReadInboxString(expectedSecretInboxString) + if (err != nil){ + t.Fatalf("Failed to decode expectedSecretInbox: " + err.Error()) + } + + if (secretInbox != expectedSecretInbox){ + t.Fatalf("GetSecretInboxAndSealerKeyFromSecretInboxSeed returning unexpected secretInbox.") + } + + expectedSecretInboxSealerKey := "070a8c4a9c9d8a46951f02fafab3bbe94e958b3d5db21425ef842141c691d231" + expectedSecretInboxSealerKeyBytes, err := encoding.DecodeHexStringToBytes(expectedSecretInboxSealerKey) + if (err != nil){ + t.Fatalf("expectedSecretInboxSealerKey is not hex: " + err.Error()) + } + areEqual := bytes.Equal(secretInboxSealerKey[:], expectedSecretInboxSealerKeyBytes) + if (areEqual == false){ + t.Fatalf("GetSecretInboxAndSealerKeyFromSecretInboxSeed returning unexpected secretInboxSealerKey") + } +} + diff --git a/internal/messaging/myChatConversations/myChatConversations.go b/internal/messaging/myChatConversations/myChatConversations.go new file mode 100644 index 0000000..3097c56 --- /dev/null +++ b/internal/messaging/myChatConversations/myChatConversations.go @@ -0,0 +1,824 @@ + +// myChatConversations provides functions to generate and retrieve a user's chat conversations +// These conversations can be filtered and sorted based on the recipient's qualities +// Examples: Sort recipients by age, filter recipients who do not fulfill our desires + +package myChatConversations + +import "seekia/internal/appMemory" +import "seekia/internal/encoding" +import "seekia/internal/helpers" +import "seekia/internal/identity" +import "seekia/internal/messaging/myChatFilters" +import "seekia/internal/messaging/myChatMessages" +import "seekia/internal/messaging/myReadStatus" +import "seekia/internal/myBlockedUsers" +import "seekia/internal/myDatastores/myMapList" +import "seekia/internal/myIdentity" +import "seekia/internal/mySettings" +import "seekia/internal/profiles/viewableProfiles" + +import "slices" +import "sync" +import "errors" + + +// This mutex will be locked whenever we update chat conversations +var updatingMyChatConversationsMutex sync.Mutex + +var myMateChatConversationsMapListDatastore *myMapList.MyMapList +var myModeratorChatConversationsMapListDatastore *myMapList.MyMapList + +// This function must be called whenever an app user signs in +func InitializeMyChatConversationsDatastores()error{ + + updatingMyChatConversationsMutex.Lock() + defer updatingMyChatConversationsMutex.Unlock() + + newMyMateChatConversationsMapListDatastore, err := myMapList.CreateNewMapList("MyMateChatConversations") + if (err != nil) { return err } + + newMyModeratorChatConversationsMapListDatastore, err := myMapList.CreateNewMapList("MyModeratorChatConversations") + if (err != nil) { return err } + + myMateChatConversationsMapListDatastore = newMyMateChatConversationsMapListDatastore + myModeratorChatConversationsMapListDatastore = newMyModeratorChatConversationsMapListDatastore + + return nil +} + +func getMyConversationsMapListDatastore(identityType string)(*myMapList.MyMapList, error) { + + if (identityType == "Mate"){ + return myMateChatConversationsMapListDatastore, nil + } + if (identityType == "Moderator"){ + return myModeratorChatConversationsMapListDatastore, nil + } + + return nil, errors.New("getMyConversationsMapListDatastore called with invalid identity type: " + identityType) +} + +// This function checks if conversations are generated and sorted +func GetMyChatConversationsReadyStatus(identityType string, networkType byte)(bool, error){ + + isValid := helpers.VerifyNetworkType(networkType) + if (isValid == false){ + networkTypeString := helpers.ConvertByteToString(networkType) + return false, errors.New("GetMyChatConversationsReadyStatus called with invalid networkType: " + networkTypeString) + } + + generatedStatus, err := getMyChatConversationsGeneratedStatus(identityType) + if (err != nil) { return false, err } + if (generatedStatus == false) { + return false, nil + } + + exists, sortedStatus, err := mySettings.GetSetting(identityType + "ChatConversationsSortedStatus") + if (err != nil) { return false, err } + if (exists == false || sortedStatus != "Yes") { + return false, nil + } + + exists, conversationsNetworkTypeString, err := mySettings.GetSetting(identityType + "ChatConversationsNetworkType") + if (err != nil) { return false, err } + if (exists == false){ + // This should not happen, because ...ChatConversationsNetworkType is set whenever conversations are generated + return false, errors.New("mySettings is missing " + identityType + "ChatConversationsNetworkType when " + identityType + "ChatConversationsGeneratedStatus exists.") + } + conversationsNetworkType, err := helpers.ConvertNetworkTypeStringToByte(conversationsNetworkTypeString) + if (err != nil) { + return false, errors.New("MySettings contains invalid " + identityType + "ChatConversationsNetworkType: " + conversationsNetworkTypeString) + } + if (conversationsNetworkType != networkType) { + + // Conversations must have been generated for a different networkType. + // This should not happen, because we should only call this function using our current appNetworkType + // Whenever we change network types, we reset the ChatConversationsGeneratedStatus to No. + + //TODO: Log this. + + err := mySettings.SetSetting(identityType + "ChatConversationsGeneratedStatus", "No") + if (err != nil) { return false, err } + + return false, nil + } + + return true, nil +} + +// Our conversations will need a refresh any time a new message sent to the user's inboxes is downloaded +func CheckIfMyChatConversationsNeedRefresh(identityType string)(bool, error){ + + exists, needsRefresh, err := mySettings.GetSetting(identityType + "ChatConversationsNeedRefreshYesNo") + if (err != nil) { return true, err } + if (exists == true && needsRefresh == "No") { + return false, nil + } + + return true, nil +} + +func getMyChatConversationsGeneratedStatus(identityType string)(bool, error){ + + chatMessagesReady, err := myChatMessages.GetMyChatMessagesReadyStatus(identityType) + if (err != nil) { return false, err } + if (chatMessagesReady == false) { + return false, nil + } + + exists, readyStatus, err := mySettings.GetSetting(identityType + "ChatConversationsGeneratedStatus") + if (err != nil) { return false, err } + if (exists == false || readyStatus != "Yes"){ + return false, nil + } + + return true, nil +} + +// This function returns the ready (generated and sorted) chat conversations map list +//Outputs: +// -bool: Chat conversations are ready +// -[]map[string]string +// -error +func GetMyChatConversationsMapList(identityType string, networkType byte)(bool, []map[string]string, error){ + + if (identityType != "Mate" && identityType != "Moderator"){ + return false, nil, errors.New("GetMyChatConversationsMapList called with invalid identityType: " + identityType) + } + + isValid := helpers.VerifyNetworkType(networkType) + if (isValid == false){ + networkTypeString := helpers.ConvertByteToString(networkType) + return false, nil, errors.New("GetMyChatConversationsMapList called with invalid networkType: " + networkTypeString) + } + + conversationsReady, err := GetMyChatConversationsReadyStatus(identityType, networkType) + if (err != nil) { return false, nil, err } + if (conversationsReady == false) { + return false, nil, nil + } + + myReadyConversationsMapListDatastore, err := getMyConversationsMapListDatastore(identityType) + if (err != nil) { return false, nil, err } + + myReadyChatConversationsMapList, err := myReadyConversationsMapListDatastore.GetMapList() + if (err != nil) { return false, nil, err } + + return true, myReadyChatConversationsMapList, nil +} + +//Outputs: +// -bool: Conversations ready +// -string: Number of conversations +// -error +func GetNumberOfConversations(identityType string, networkType byte)(bool, int, error){ + + isValid := helpers.VerifyNetworkType(networkType) + if (isValid == false){ + networkTypeString := helpers.ConvertByteToString(networkType) + return false, 0, errors.New("GetNumberOfConversations called with invalid networkType: " + networkTypeString) + } + + conversationsReady, conversationsMap, err := GetMyChatConversationsMapList(identityType, networkType) + if (err != nil) { return false, 0, err } + if (conversationsReady == false){ + return false, 0, nil + } + + numberOfConversations := len(conversationsMap) + + return true, numberOfConversations, nil +} + +func GetConversationsSortByAttribute(identityType string) (string, error){ + + exists, currentAttribute, err := mySettings.GetSetting(identityType + "ChatConversations_SortByAttribute") + if (err != nil) { return "", err } + if (exists == false){ + return "MatchScore", nil + } + + return currentAttribute, nil +} + +func GetConversationsSortDirection(identityType string) (string, error){ + + exists, sortDirection, err := mySettings.GetSetting(identityType + "ChatConversations_SortDirection") + if (err != nil) { return "", err } + if (exists == false){ + return "Descending", nil + } + + if (sortDirection != "Ascending" && sortDirection != "Descending"){ + return "", errors.New("MySettings malformed: Invalid ChatConversations_SortDirection: " + sortDirection) + } + + return sortDirection, nil +} + +//Outputs: +// -bool: Conversations ready +// -string: Number of unread conversations +// -error +func GetNumberOfUnreadConversations(identityType string, networkType byte)(bool, int, error){ + + isValid := helpers.VerifyNetworkType(networkType) + if (isValid == false){ + networkTypeString := helpers.ConvertByteToString(networkType) + return false, 0, errors.New("GetNumberOfUnreadConversations called with invalid networkType: " + networkTypeString) + } + + conversationsReady, conversationsMapList, err := GetMyChatConversationsMapList(identityType, networkType) + if (err != nil) { return false, 0, err } + if (conversationsReady == false){ + return false, 0, nil + } + + numberOfUnreadConversations := 0 + + for _, conversationMap := range conversationsMapList { + + myIdentityHashString, exists := conversationMap["MyIdentityHash"] + if (exists == false) { + return false, 0, errors.New("Invalid conversation map: Item missing MyIdentityHash") + } + + theirIdentityHashString, exists := conversationMap["TheirIdentityHash"] + if (exists == false) { + return false, 0, errors.New("Invalid conversation map: Item missing TheirIdentityHash") + } + + myIdentityHash, myIdentityType, err := identity.ReadIdentityHashString(myIdentityHashString) + if (err != nil){ + return false, 0, errors.New("Invalid conversation map: Item contains invalid MyIdentityHash: " + myIdentityHashString) + } + + theirIdentityHash, theirIdentityType, err := identity.ReadIdentityHashString(theirIdentityHashString) + if (err != nil){ + return false, 0, errors.New("Invalid conversation map: Item contains invalid TheirIdentityHash: " + theirIdentityHashString) + } + if (myIdentityType != theirIdentityType){ + return false, 0, errors.New("Invalid conversation map: Item contains mismatched My and Their identityTypes.") + } + if (myIdentityType != identityType){ + return false, 0, errors.New("GetMyChatConversationsMapList returning conversation map for different identityType participants.") + } + + readUnreadStatus, err := myReadStatus.GetConversationReadUnreadStatus(myIdentityHash, theirIdentityHash, networkType) + if (err != nil) { return false, 0, err } + if (readUnreadStatus == "Unread"){ + numberOfUnreadConversations += 1 + } + } + + return true, numberOfUnreadConversations, nil +} + + +//Outputs: +// -bool: Build encountered error +// -string: Error encountered +// -bool: Build is stopped (will be stopped if user went to different page) +// -bool: Conversations are ready +// -float64: Percentage Progress (0 - 1) +// -error +func GetChatConversationsBuildStatus(identityType string, networkType byte)(bool, string, bool, bool, float64, error){ + + isValid := helpers.VerifyNetworkType(networkType) + if (isValid == false){ + networkTypeString := helpers.ConvertByteToString(networkType) + return false, "", false, false, 0, errors.New("GetChatConversationsBuildStatus called with invalid networkType: " + networkTypeString) + } + + exists, encounteredError := appMemory.GetMemoryEntry(identityType + "ChatConversationsBuildEncounteredError") + if (exists == false){ + // No build exists. A build has not been started since Seekia was started + return false, "", false, false, 0, nil + } + if (encounteredError == "Yes"){ + exists, errorEncountered := appMemory.GetMemoryEntry(identityType + "ChatConversationsBuildError") + if (exists == false){ + return false, "", false, false, 0, errors.New("Chat conversations build error encountered error is yes, but no error exists.") + } + + return true, errorEncountered, false, false, 0, nil + } + + isStopped := CheckIfBuildMyConversationsIsStopped() + if (isStopped == true){ + return false, "", true, false, 0, nil + } + + conversationsReadyBool, err := GetMyChatConversationsReadyStatus(identityType, networkType) + if (err != nil) { return false, "", false, false, 0, err } + if (conversationsReadyBool == true){ + + return false, "", false, true, 1, nil + } + + exists, currentPercentageString := appMemory.GetMemoryEntry(identityType + "ChatConversationsReadyProgressStatus") + if (exists == false){ + // No build exists. A build has not been started since Seekia was started + return false, "", false, false, 0, nil + } + + currentPercentageFloat, err := helpers.ConvertStringToFloat64(currentPercentageString) + if (err != nil){ + return false, "", false, false, 0, errors.New("ChatConversationsReadyProgressStatus is invalid: Not a float: " + currentPercentageString) + } + + return false, "", false, false, currentPercentageFloat, nil +} + +func CheckIfBuildMyConversationsIsStopped()bool{ + + exists, buildStoppedStatus := appMemory.GetMemoryEntry("StopBuildMyConversationsYesNo") + if (exists == false || buildStoppedStatus != "No") { + return true + } + + return false +} + + +// This function will cancel the current build (if one is running) +// It will then start updating our chat messages and conversations list +func StartUpdatingMyConversations(identityType string, networkType byte) error{ + + if (identityType != "Mate" && identityType != "Moderator") { + return errors.New("StartUpdatingMyConversations called with invalid identity type: " + identityType) + } + + isValid := helpers.VerifyNetworkType(networkType) + if (isValid == false){ + networkTypeString := helpers.ConvertByteToString(networkType) + return errors.New("StartUpdatingMyConversations called with invalid networkType: " + networkTypeString) + } + + networkTypeString := helpers.ConvertByteToString(networkType) + + appMemory.SetMemoryEntry("StopBuildMyConversationsYesNo", "Yes") + + // We wait for any existing build to stop + updatingMyChatConversationsMutex.Lock() + + appMemory.SetMemoryEntry(identityType + "ChatConversationsBuildEncounteredError", "No") + appMemory.SetMemoryEntry(identityType + "ChatConversationsBuildError", "") + appMemory.SetMemoryEntry(identityType + "ChatConversationsReadyProgressStatus", "0") + + appMemory.SetMemoryEntry("StopBuildMyConversationsYesNo", "No") + + updateMyChatConversations := func()error{ + + myIdentityExists, myIdentityHash, err := myIdentity.GetMyIdentityHash(identityType) + if (err != nil) { return err } + if (myIdentityExists == false){ + + // Our identity does not exist. + + // We first update our myChatMessagesMapList ready status by running the below function + + updateProgressFunction := func(_ int)error{ + return nil + } + + _, err := myChatMessages.GetUpdatedMyChatMessagesMapList(identityType, networkType, updateProgressFunction) + if (err != nil) { return err } + + // Now we overwrite our conversation datastores with empty map lists. + // Using the DeleteMapList function achieves this. + + myChatConversationsMapListDatastore, err := getMyConversationsMapListDatastore(identityType) + if (err != nil) { return err } + + err = myChatConversationsMapListDatastore.DeleteMapList() + if (err != nil) { return err } + + err = mySettings.SetSetting(identityType + "ChatConversationsGeneratedStatus", "Yes") + if (err != nil) { return err } + + err = mySettings.SetSetting(identityType + "ChatConversationsNetworkType", networkTypeString) + if (err != nil) { return err } + + err = mySettings.SetSetting(identityType + "ChatConversationsSortedStatus", "Yes") + if (err != nil) { return err } + + appMemory.SetMemoryEntry(identityType + "ChatConversationsReadyProgressStatus", "1") + + err = mySettings.SetSetting(identityType + "ChatConversationsNeedRefreshYesNo", "No") + if (err != nil) { return err } + + return nil + } + + isStopped := CheckIfBuildMyConversationsIsStopped() + if (isStopped == true) { + // User has moved to a different page. Stop generating. + return nil + } + + getConversationsNeedToBeGeneratedStatus := func()(bool, error){ + + messagesReadyStatus, err := myChatMessages.GetMyChatMessagesReadyStatus(identityType) + if (err != nil) { return false, err } + if (messagesReadyStatus == false){ + return true, nil + } + + conversationsGenerated, err := getMyChatConversationsGeneratedStatus(identityType) + if (err != nil) { return false, err } + if (conversationsGenerated == false){ + return true, nil + } + + exists, chatConversationsNetworkTypeString, err := mySettings.GetSetting(identityType + "ChatConversationsNetworkType") + if (err != nil) { return false, err } + if (exists == false){ + // This should not happen, because MatchesNetworkType is set to Yes whenever matches are generated + return false, errors.New("...ChatConversationsNetworkType missing when ...ChatConversationsGeneratedStatus exists.") + } + + chatConversationsNetworkType, err := helpers.ConvertNetworkTypeStringToByte(chatConversationsNetworkTypeString) + if (err != nil) { + return false, errors.New("mySettings contains invalid ...ChatConversationsNetworkType: " + chatConversationsNetworkTypeString) + } + if (chatConversationsNetworkType != networkType){ + // This should not happen, because ...ChatConversationsGeneratedStatus should be set to No whenever app network + // type is changed, and StartUpdatingMyConversations should only be called with the current app network type. + return true, nil + } + + return false, nil + } + + conversationsNeedToBeGenerated, err := getConversationsNeedToBeGeneratedStatus() + if (err != nil) { return err } + if (conversationsNeedToBeGenerated == true){ + + // We generate conversations + + err := mySettings.SetSetting(identityType + "ChatConversationsGeneratedStatus", "No") + if (err != nil) { return err } + + err = mySettings.SetSetting(identityType + "ChatConversationsSortedStatus", "No") + if (err != nil) { return err } + + updatePercentageProgressFunction := func(input int)error{ + + // Input is a value between 0-100 + // We reduce it down to a value between 0-50 + + newProgressInt, err := helpers.ScaleNumberProportionally(true, input, 0, 100, 0, 50) + if (err != nil) { return err } + + newPercentageProgressFloat := float64(newProgressInt)/100 + + newPercentageProgressString := helpers.ConvertFloat64ToString(newPercentageProgressFloat) + + appMemory.SetMemoryEntry(identityType + "ChatConversationsReadyProgressStatus", newPercentageProgressString) + + return nil + } + + myChatMessagesMapList, err := myChatMessages.GetUpdatedMyChatMessagesMapList(identityType, networkType, updatePercentageProgressFunction) + if (err != nil) { return err } + + appMemory.SetMemoryEntry(identityType + "ChatConversationsReadyProgressStatus", ".50") + + // Conversations map stores data as TheirIdentityHash -> MostRecentMessageTime + conversationsMap := make(map[[16]byte]int64) + + for _, messageMap := range myChatMessagesMapList { + + isStopped := CheckIfBuildMyConversationsIsStopped() + if (isStopped == true) { + // User has moved to a different page. Stop generating. + return nil + } + + messageMyIdentityHashString, exists := messageMap["MyIdentityHash"] + if (exists == false){ + return errors.New("Malformed message map: missing MyIdentityHash.") + } + + messageMyIdentityHash, messageMyIdentityHashIdentityType, err := identity.ReadIdentityHashString(messageMyIdentityHashString) + if (err != nil){ + return errors.New("Malformed message map: contains invalid MyIdentityHash: " + messageMyIdentityHashString) + } + if (messageMyIdentityHashIdentityType != identityType){ + return errors.New("GetUpdatedMyChatMessagesMapList returning message with different identity type.") + } + + if (messageMyIdentityHash != myIdentityHash){ + // This should not happen, because our old identity's messages should have been deleted. + return errors.New("GetUpdatedMyChatMessagesMapList returning map list containing message from a different identityHash than our current one for provided identityType.") + } + + messageTheirIdentityHashString, exists := messageMap["TheirIdentityHash"] + if (exists == false){ + return errors.New("Malformed message map: Missing TheirIdentityHash.") + } + + messageTheirIdentityHash, theirIdentityType, err := identity.ReadIdentityHashString(messageTheirIdentityHashString) + if (err != nil) { + return errors.New("Malformed message map: Contains invalid TheirIdentityHash: " + messageTheirIdentityHashString) + } + + if (theirIdentityType != identityType){ + return errors.New("myChatMessagesMapList contains message map between different identity types.") + } + + messageNetworkType, exists := messageMap["NetworkType"] + if (exists == false){ + return errors.New("myChatMessagesMapList returning messageMap missing NetworkType.") + } + if (messageNetworkType != networkTypeString){ + return errors.New("myChatMessagesMapList returning messageMap with different NetworkType") + } + + messageSentTimeString, exists := messageMap["TimeSent"] + if (exists == false){ + return errors.New("Malformed message map: Missing TimeSent.") + } + + currentMessageSentTimeInt64, err := helpers.ConvertStringToInt64(messageSentTimeString) + if (err != nil) { + return errors.New("Malformed message map: Contains invalid TimeSent: " + messageSentTimeString) + } + + existingMostRecentMessageSentTime, exists := conversationsMap[messageTheirIdentityHash] + if (exists == false || currentMessageSentTimeInt64 > existingMostRecentMessageSentTime){ + + conversationsMap[messageTheirIdentityHash] = currentMessageSentTimeInt64 + } + } + + appMemory.SetMemoryEntry(identityType + "ChatConversationsReadyProgressStatus", ".60") + + myIdentityHashString, _, err := identity.EncodeIdentityHashBytesToString(myIdentityHash) + if (err != nil){ + myIdentityHashHex := encoding.EncodeBytesToHexString(myIdentityHash[:]) + return errors.New("GetMyIdentityHash returning invalid myIdentityHash: " + myIdentityHashHex) + } + + newConversationsMapList := make([]map[string]string, 0) + + for theirIdentityHash, conversationMostRecentMessageTime := range conversationsMap { + + isStopped := CheckIfBuildMyConversationsIsStopped() + if (isStopped == true) { + // User has moved to a different page. Stop generating. + return nil + } + + theyAreBlocked, _, _, _, err := myBlockedUsers.CheckIfUserIsBlocked(theirIdentityHash) + if (err != nil) { return err } + if (theyAreBlocked == true) { + continue + } + + userPassesChatFilters, err := myChatFilters.CheckIfUserPassesAllMyChatFilters(theirIdentityHash, networkType) + if (err != nil) { return err } + if (userPassesChatFilters == false){ + continue + } + + theirIdentityHashString, _, err := identity.EncodeIdentityHashBytesToString(theirIdentityHash) + if (err != nil){ + theirIdentityHashHex := encoding.EncodeBytesToHexString(theirIdentityHash[:]) + return errors.New("conversationsMap contains invalid theirIdentityHash: " + theirIdentityHashHex) + } + + mostRecentMessageSentTimeString := helpers.ConvertInt64ToString(conversationMostRecentMessageTime) + + newConversationMap := map[string]string{ + "MyIdentityHash": myIdentityHashString, + "TheirIdentityHash": theirIdentityHashString, + "NetworkType": networkTypeString, + "MostRecentMessageTime": mostRecentMessageSentTimeString, + } + + newConversationsMapList = append(newConversationsMapList, newConversationMap) + } + + myChatConversationsMapListDatastore, err := getMyConversationsMapListDatastore(identityType) + if (err != nil) { return err } + + err = myChatConversationsMapListDatastore.OverwriteMapList(newConversationsMapList) + if (err != nil) { return err } + + err = mySettings.SetSetting(identityType + "ChatConversationsGeneratedStatus", "Yes") + if (err != nil) { return err } + + err = mySettings.SetSetting(identityType + "ChatConversationsNetworkType", networkTypeString) + if (err != nil) { return err } + } + + isStopped = CheckIfBuildMyConversationsIsStopped() + if (isStopped == true) { + // User has moved to a different page. Stop generating. + return nil + } + + appMemory.SetMemoryEntry(identityType + "ChatConversationsReadyProgressStatus", ".70") + + conversationsReady, err := GetMyChatConversationsReadyStatus(identityType, networkType) + if (err != nil) { return err } + if (conversationsReady == false){ + + // Now we sort conversations. + + currentSortByAttribute, err := GetConversationsSortByAttribute(identityType) + if (err != nil) { return err } + currentSortDirection, err := GetConversationsSortDirection(identityType) + if (err != nil) { return err } + + myChatConversationsMapListDatastore, err := getMyConversationsMapListDatastore(identityType) + if (err != nil) { return err } + + currentConversationsMapList, err := myChatConversationsMapListDatastore.GetMapList() + if (err != nil) { return err } + + // We use this map to make sure there are no duplicate recipients + // This should never happen, unless the user's stored map list was edited or there is a bug + recipientsMap := make(map[[16]byte]struct{}) + + // Map structure: Their Identity Hash -> Sort By Attribute Value + recipientAttributeValuesMap := make(map[string]float64) + + getAllowUnknownViewableStatusBool := func()bool{ + + if (identityType == "Mate"){ + return false + } + return true + } + + allowUnknownViewableStatusBool := getAllowUnknownViewableStatusBool() + + maximumIndex := len(currentConversationsMapList) - 1 + + for index, conversationMap := range currentConversationsMapList{ + + conversationMyIdentityHashString, exists := conversationMap["MyIdentityHash"] + if (exists == false) { + return errors.New("Malformed conversation map during sort: item missing MyIdentityHash.") + } + conversationMyIdentityHash, conversationMyIdentityHashIdentityType, err := identity.ReadIdentityHashString(conversationMyIdentityHashString) + if (err != nil){ + return errors.New("Malformed conversation map during sort: contains invalid MyIdentityHash: " + conversationMyIdentityHashString) + } + if (conversationMyIdentityHashIdentityType != identityType){ + return errors.New("Malformed conversation map during sort: contains conversation with different identity type.") + } + + if (conversationMyIdentityHash != myIdentityHash){ + // This should not happen, because our conversations should be regenerated whenever we change our identity + return errors.New("Malformed conversation map during sort: Contains different identity hash.") + } + + theirIdentityHashString, exists := conversationMap["TheirIdentityHash"] + if (exists == false) { + return errors.New("ChatConversations map list item missing TheirIdentityHash") + } + + theirIdentityHash, theirIdentityType, err := identity.ReadIdentityHashString(theirIdentityHashString) + if (err != nil){ + return errors.New("ChatConversations map list item contains invalid theirIdentityHash: " + theirIdentityHashString) + } + if (theirIdentityType != identityType){ + return errors.New("ChatConversations map list item contains invalid theirIdentityHash: Different identityType: " + theirIdentityType) + } + + _, exists = recipientsMap[theirIdentityHash] + if (exists == true){ + return errors.New("ChatConversations map list is malformed: Contains two conversation maps with the same recipient") + } + recipientsMap[theirIdentityHash] = struct{}{} + + profileExists, _, attributeExists, attributeValue, err := viewableProfiles.GetAnyAttributeFromNewestViewableUserProfile(theirIdentityHash, networkType, currentSortByAttribute, true, allowUnknownViewableStatusBool, true) + if (err != nil) { return err } + if (profileExists == true && attributeExists == true){ + + attributeValueFloat, err := helpers.ConvertStringToFloat64(attributeValue) + if (err != nil) { + return errors.New("User attribute cannot be converted to float during conversation build: " + attributeValue) + } + + recipientAttributeValuesMap[theirIdentityHashString] = attributeValueFloat + } + + isStopped := CheckIfBuildMyConversationsIsStopped() + if (isStopped == true){ + // User has moved to a different page. Stop generating. + return nil + } + + newScaledPercentageInt, err := helpers.ScaleNumberProportionally(true, index, 0, maximumIndex, 70, 85) + if (err != nil) { return err } + + newProgressFloat := float64(newScaledPercentageInt)/100 + + newProgressString := helpers.ConvertFloat64ToString(newProgressFloat) + + appMemory.SetMemoryEntry(identityType + "ChatConversationsReadyProgressStatus", newProgressString) + } + + compareConversationMapsFunction := func(conversationMapA map[string]string, conversationMapB map[string]string) int { + + identityHashA, exists := conversationMapA["TheirIdentityHash"] + if (exists == false) { + panic("Malformed conversations map list: Item missing TheirIdentityHash") + } + identityHashB, exists := conversationMapB["TheirIdentityHash"] + if (exists == false) { + panic("Malformed conversations map list: Item missing TheirIdentityHash") + } + + if (identityHashA == identityHashB){ + panic("Malformed conversations map list: Two conversations contain the same TheirIdentityHash.") + } + + attributeValueA, attributeValueAExists := recipientAttributeValuesMap[identityHashA] + + attributeValueB, attributeValueBExists := recipientAttributeValuesMap[identityHashB] + + if (attributeValueAExists == false && attributeValueBExists == false){ + + // We don't know the attribute value for either recipient + // We sort recipients in unicode order + if (identityHashA < identityHashB){ + return -1 + } + return 1 + + } else if (attributeValueAExists == true && attributeValueBExists == false){ + + // We sort unknown attribute recipient conversations to the back of the list + + return -1 + + } else if (attributeValueAExists == false && attributeValueBExists == true){ + + return 1 + } + + // Both recipient attribute values exist + + if (attributeValueA == attributeValueB){ + // If values are equal, we want the result to be the same with each refresh + // We sort the identity hashses in unicode order + if (identityHashA < identityHashB){ + return -1 + } + return 1 + } + + if (attributeValueA < attributeValueB){ + if (currentSortDirection == "Ascending"){ + return -1 + } + return 1 + } + if (currentSortDirection == "Ascending"){ + return 1 + } + return -1 + } + + slices.SortFunc(currentConversationsMapList, compareConversationMapsFunction) + + err = myChatConversationsMapListDatastore.OverwriteMapList(currentConversationsMapList) + if (err != nil) { return err } + + err = mySettings.SetSetting(identityType + "ChatConversationsSortedStatus", "Yes") + if (err != nil) { return err } + } + + appMemory.SetMemoryEntry(identityType + "ChatConversationsReadyProgressStatus", "1") + + err = mySettings.SetSetting(identityType + "ChatConversationsNeedRefreshYesNo", "No") + if (err != nil) { return err } + + return nil + } + + updateFunction := func(){ + + err := updateMyChatConversations() + if (err != nil){ + appMemory.SetMemoryEntry(identityType + "ChatConversationsBuildEncounteredError", "Yes") + appMemory.SetMemoryEntry(identityType + "ChatConversationsBuildError", err.Error()) + } + + updatingMyChatConversationsMutex.Unlock() + } + + go updateFunction() + + return nil +} + + + diff --git a/internal/messaging/myChatFilterStatistics/myChatFilterStatistics.go b/internal/messaging/myChatFilterStatistics/myChatFilterStatistics.go new file mode 100644 index 0000000..abd6d26 --- /dev/null +++ b/internal/messaging/myChatFilterStatistics/myChatFilterStatistics.go @@ -0,0 +1,236 @@ + +// myChatFilterStatistics provides functions to create statistics based on a user's chat filters. +// These statistics will show the user what percentage of their conversations are being filtered by their chat filters. + +package myChatFilterStatistics + +import "seekia/internal/helpers" +import "seekia/internal/identity" +import "seekia/internal/messaging/myChatFilters" +import "seekia/internal/messaging/myChatMessages" +import "seekia/internal/myBlockedUsers" +import "seekia/internal/myIdentity" + +import "errors" + +type ChatFilterStatisticsItem struct{ + + // The name of the filter + // Example: "ShowMyMatchesOnly" + FilterName string + + // How many recipients pass the filter + NumberOfRecipientsWhoPassFilter int + + // Percentage of all chat recipients who pass filter + PercentageOfRecipientsWhoPassFilter float64 + + // How many filtered recipients you would have if this filter was deactivated + NumberOfFilterExcludedRecipients int + + // What percentage of filter-excluded filtered recipients pass this filter + PercentageOfFilterExcludedRecipientsWhoPassFilter float64 +} + +//Outputs: +// -int: Number of conversations (Number of Recipients) +// -int: Number of conversations which pass all chat filters +// -[]ChatFilterStatisticsItem +// -error +func GetAllMyChatFilterStatistics(myIdentityType string, networkType byte)(int, int, []ChatFilterStatisticsItem, error){ + + if (myIdentityType != "Mate" && myIdentityType != "Moderator"){ + return 0, 0, nil, errors.New("Invalid myIdentityType in call to GetAllMyChatFilterStatistics: " + myIdentityType) + } + + isValid := helpers.VerifyNetworkType(networkType) + if (isValid == false){ + networkTypeString := helpers.ConvertByteToString(networkType) + return 0, 0, nil, errors.New("GetAllMyChatFilterStatistics called with invalid networkType: " + networkTypeString) + } + + myIdentityExists, myIdentityHash, err := myIdentity.GetMyIdentityHash(myIdentityType) + if (err != nil) { return 0, 0, nil, err } + + updatePercentageProgress := func(_ int)error{ + return nil + } + + myChatMessagesMapList, err := myChatMessages.GetUpdatedMyChatMessagesMapList(myIdentityType, networkType, updatePercentageProgress) + if (err != nil) { return 0, 0, nil, err } + + // We use a map to avoid duplicates + allRecipientsMap := make(map[[16]byte]struct{}) + + for _, chatMessageMap := range myChatMessagesMapList{ + + if (myIdentityExists == false){ + // No messages or conversations exist. + break + } + + messageMyIdentityHashString, exists := chatMessageMap["MyIdentityHash"] + if (exists == false){ + return 0, 0, nil, errors.New("Malformed message map: missing MyIdentityHash.") + } + + messageMyIdentityHash, _, err := identity.ReadIdentityHashString(messageMyIdentityHashString) + if (err != nil){ + return 0, 0, nil, errors.New("Malformed message map: Contains invalid MyIdentityHash: " + messageMyIdentityHashString) + } + + if (messageMyIdentityHash != myIdentityHash){ + // This should not happen + // If we delete our identity, we will prune any messages for that identity from our chat messages map + //TODO: Log this + continue + } + + theirIdentityHashString, exists := chatMessageMap["TheirIdentityHash"] + if (exists == false){ + return 0, 0, nil, errors.New("MyChatMessages map malformed: missing TheirIdentityHash") + } + + theirIdentityHash, _, err := identity.ReadIdentityHashString(theirIdentityHashString) + if (err != nil){ + return 0, 0, nil, errors.New("Malformed message map: Contains invalid TheirIdentityHash: " + theirIdentityHashString) + } + + theyAreBlocked, _, _, _, err := myBlockedUsers.CheckIfUserIsBlocked(theirIdentityHash) + if (err != nil) { return 0, 0, nil, err } + if (theyAreBlocked == true) { + continue + } + + allRecipientsMap[theirIdentityHash] = struct{}{} + } + + allRecipientsList := helpers.GetListOfMapKeys(allRecipientsMap) + + numberOfRecipients := len(allRecipientsList) + + // Below will store the number of recipients who pass all chat filters + numberOfFilteredRecipients := 0 + + //Map Structure: Filter name -> Number of recipients who pass filter + numberOfRecipientsWhoPassFilterMap := make(map[string]int) + + //Map Structure: Filter name -> Number of filtered recipients the user would have excluding the filter + numberOfFilterExcludedRecipientsMap := make(map[string]int) + + for _, recipientIdentityHash := range allRecipientsList{ + + recipientPassesAllFilters := true + + userPassesMyFiltersMap, err := myChatFilters.GetUserPassesMyChatFiltersMap(recipientIdentityHash, networkType) + if (err != nil) { return 0, 0, nil, err } + + for filterName, userPassesFilter := range userPassesMyFiltersMap{ + + if (userPassesFilter == true){ + + numberOfRecipientsWhoPassFilterMap[filterName] += 1 + } else { + recipientPassesAllFilters = false + } + + // Now we find out if recipient would pass all filters excluding current filter + + checkIfRecipientPassesFiltersExcludingCurrent := func()bool{ + + for filterNameInner, userPassesFilterInner := range userPassesMyFiltersMap{ + + if (filterNameInner == filterName){ + continue + } + if (userPassesFilterInner == false){ + return false + } + } + + return true + } + + recipientPassesFiltersExcludingCurrent := checkIfRecipientPassesFiltersExcludingCurrent() + + if (recipientPassesFiltersExcludingCurrent == true){ + numberOfFilterExcludedRecipientsMap[filterName] += 1 + } + } + + if (recipientPassesAllFilters == true){ + numberOfFilteredRecipients += 1 + } + } + + chatFiltersList, err := myChatFilters.GetChatFiltersList(myIdentityType) + if (err != nil) { return 0, 0, nil, err } + + allMyChatFilterStatisticsItemsList := make([]ChatFilterStatisticsItem, 0, len(chatFiltersList)) + + for _, filterName := range chatFiltersList{ + + getNumberOfRecipientsWhoPassFilter := func()int{ + + numberOfRecipientsWhoPassFilter, exists := numberOfRecipientsWhoPassFilterMap[filterName] + if (exists == false) { + return 0 + } + return numberOfRecipientsWhoPassFilter + } + + numberOfRecipientsWhoPassFilter := getNumberOfRecipientsWhoPassFilter() + + getNumberOfFilterExcludedRecipients := func()int{ + + numberOfFilterExcludedRecipients, exists := numberOfFilterExcludedRecipientsMap[filterName] + if (exists == false) { + return 0 + } + + return numberOfFilterExcludedRecipients + } + + numberOfFilterExcludedRecipients := getNumberOfFilterExcludedRecipients() + + getPercentageOfRecipientsWhoPassFilter := func()float64{ + if (numberOfRecipients == 0){ + return 0 + } + + percentageOfRecipientsWhoPassFilter := 100 * (float64(numberOfRecipientsWhoPassFilter)/float64(numberOfRecipients)) + + return percentageOfRecipientsWhoPassFilter + } + + percentageOfRecipientsWhoPassFilter := getPercentageOfRecipientsWhoPassFilter() + + getPercentageOfFilterExcludedRecipientsWhoPassFilter := func()float64{ + if (numberOfRecipients == 0){ + return 0 + } + + percentageOfFilterExcludedRecipientsWhoPassFilter := 100 * (float64(numberOfFilteredRecipients)/float64(numberOfFilterExcludedRecipients)) + + return percentageOfFilterExcludedRecipientsWhoPassFilter + } + + percentageOfFilterExcludedRecipientsWhoPassFilter := getPercentageOfFilterExcludedRecipientsWhoPassFilter() + + newStatisticsItem := ChatFilterStatisticsItem{ + FilterName: filterName, + NumberOfRecipientsWhoPassFilter: numberOfRecipientsWhoPassFilter, + PercentageOfRecipientsWhoPassFilter: percentageOfRecipientsWhoPassFilter, + NumberOfFilterExcludedRecipients: numberOfFilterExcludedRecipients, + PercentageOfFilterExcludedRecipientsWhoPassFilter: percentageOfFilterExcludedRecipientsWhoPassFilter, + } + + allMyChatFilterStatisticsItemsList = append(allMyChatFilterStatisticsItemsList, newStatisticsItem) + } + + return numberOfRecipients, numberOfFilteredRecipients, allMyChatFilterStatisticsItemsList, nil +} + + + + diff --git a/internal/messaging/myChatFilters/myChatFilters.go b/internal/messaging/myChatFilters/myChatFilters.go new file mode 100644 index 0000000..5f33e2f --- /dev/null +++ b/internal/messaging/myChatFilters/myChatFilters.go @@ -0,0 +1,312 @@ + +// myChatFilters provides functions to determine if a peer passes a user's chat filters. +// It also provides functions to edit a user's chat filters. + +package myChatFilters + +import "seekia/internal/desires/myMateDesires" +import "seekia/internal/encoding" +import "seekia/internal/helpers" +import "seekia/internal/identity" +import "seekia/internal/messaging/myChatMessages" +import "seekia/internal/myContacts" +import "seekia/internal/myDatastores/myMap" +import "seekia/internal/myIgnoredUsers" +import "seekia/internal/myLikedUsers" +import "seekia/internal/mySettings" +import "seekia/internal/profiles/viewableProfiles" + +import "errors" + +var myMateChatFiltersMapDatastore *myMap.MyMap +var myModeratorChatFiltersMapDatastore *myMap.MyMap + +// This function must be called whenever an app user signs in +func InitializeMyChatFiltersDatastores()error{ + + newMyMateChatFiltersMapDatastore, err := myMap.CreateNewMap("MyMateChatFilters") + if (err != nil){ return err } + + newMyModeratorChatFiltersMapDatastore, err := myMap.CreateNewMap("MyModeratorChatFilters") + if (err != nil){ return err } + + myMateChatFiltersMapDatastore = newMyMateChatFiltersMapDatastore + myModeratorChatFiltersMapDatastore = newMyModeratorChatFiltersMapDatastore + + return nil +} + +func getMyChatFiltersMapDatastore(identityType string)(*myMap.MyMap, error){ + + if (identityType == "Mate"){ + return myMateChatFiltersMapDatastore, nil + } + if (identityType == "Moderator"){ + return myModeratorChatFiltersMapDatastore, nil + } + + return nil, errors.New("getMyChatFilterMapDatastore called with invalid identityType: " + identityType) +} + +// Returns a list of all chat filters for the requested identityType +func GetChatFiltersList(identityType string)([]string, error){ + + if (identityType != "Mate" && identityType != "Moderator"){ + return nil, errors.New("GetChatFiltersList called with invalid identityType: " + identityType) + } + + if (identityType == "Mate"){ + + allMateChatFiltersList := []string{"ShowMyContactsOnly", "ShowMyMatchesOnly", "ShowHasMessagedMeOnly", "OnlyShowLikedUsers", "HideIgnoredUsers"} + + return allMateChatFiltersList, nil + } + + allModeratorChatFiltersList := []string{"ShowMyContactsOnly", "ShowHasMessagedMeOnly"} + + return allModeratorChatFiltersList, nil +} + +// Outputs: +// -bool: User passes filters +// -error +func CheckIfUserPassesAllMyChatFilters(userIdentityHash [16]byte, networkType byte)(bool, error){ + + userIdentityType, err := identity.GetIdentityTypeFromIdentityHash(userIdentityHash) + if (err != nil){ + userIdentityHashHex := encoding.EncodeBytesToHexString(userIdentityHash[:]) + return false, errors.New("CheckIfUserPassesAllMyChatFilters called with invalid identity hash: " + userIdentityHashHex) + } + + if (userIdentityType == "Host"){ + return false, errors.New("CheckIfUserPassesAllMyChatFilters called with Host user.") + } + + isValid := helpers.VerifyNetworkType(networkType) + if (isValid == false){ + networkTypeString := helpers.ConvertByteToString(networkType) + return false, errors.New("CheckIfUserPassesAllMyChatFilters called with invalid networkType: " + networkTypeString) + } + + chatFiltersList, err := GetChatFiltersList(userIdentityType) + if (err != nil) { return false, err } + + for _, chatFilterName := range chatFiltersList{ + + passesDesire, err := checkIfUserPassesMyChatFilter(userIdentityHash, networkType, chatFilterName) + if (err != nil) { return false, err } + if (passesDesire == false){ + return false, nil + } + } + + return true, nil +} + + +//Outputs: +// -map[string]bool: Chat filter name -> User passes chat filter +// -error +func GetUserPassesMyChatFiltersMap(userIdentityHash [16]byte, networkType byte)(map[string]bool, error){ + + userIdentityType, err := identity.GetIdentityTypeFromIdentityHash(userIdentityHash) + if (err != nil){ + userIdentityHashHex := encoding.EncodeBytesToHexString(userIdentityHash[:]) + return nil, errors.New("GetUserPassesMyChatFiltersMap called with invalid identity hash: " + userIdentityHashHex) + } + + if (userIdentityType == "Host"){ + return nil, errors.New("GetUserPassesMyChatFiltersMap called with Host user.") + } + + isValid := helpers.VerifyNetworkType(networkType) + if (isValid == false){ + networkTypeString := helpers.ConvertByteToString(networkType) + return nil, errors.New("GetUserPassesMyChatFiltersMap called with invalid networkType: " + networkTypeString) + } + + chatFiltersList, err := GetChatFiltersList(userIdentityType) + if (err != nil) { return nil, err } + + // Map Structure: Chat filter name -> User passes filter + userPassesChatFiltersMap := make(map[string]bool) + + for _, chatFilterName := range chatFiltersList{ + + userPassesFilter, err := checkIfUserPassesMyChatFilter(userIdentityHash, networkType, chatFilterName) + if (err != nil) { return nil, err } + + userPassesChatFiltersMap[chatFilterName] = userPassesFilter + } + + return userPassesChatFiltersMap, nil +} + + +func checkIfUserPassesMyChatFilter(userIdentityHash [16]byte, networkType byte, chatFilterName string)(bool, error){ + + userIdentityType, err := identity.GetIdentityTypeFromIdentityHash(userIdentityHash) + if (err != nil) { + userIdentityHashHex := encoding.EncodeBytesToHexString(userIdentityHash[:]) + return false, errors.New("checkIfUserPassesMyChatFilter called with invalid identity hash: " + userIdentityHashHex) + } + + if (userIdentityType == "Host"){ + return false, errors.New("checkIfUserPassesMyChatFilter called with Host user.") + } + + isValid := helpers.VerifyNetworkType(networkType) + if (isValid == false){ + networkTypeString := helpers.ConvertByteToString(networkType) + return false, errors.New("checkIfUserPassesMyChatFilter called with invalid networkType: " + networkTypeString) + } + + chatFilterIsEnabled, err := GetChatFilterOnOffStatus(userIdentityType, chatFilterName) + if (err != nil) { return false, err } + if (chatFilterIsEnabled == false){ + return true, nil + } + + if (chatFilterName == "ShowMyContactsOnly"){ + + isMyContact, err := myContacts.CheckIfUserIsMyContact(userIdentityHash) + if (err != nil) { return false, err } + if (isMyContact == false){ + return false, nil + } + + return true, nil + } + if (chatFilterName == "ShowMyMatchesOnly"){ + + profileExists, _, getAnyUserAttributeFunction, err := viewableProfiles.GetRetrieveAnyNewestViewableUserProfileAttributeFunction(userIdentityHash, networkType, true, false, false) + if (err != nil) { return false, err } + if (profileExists == false) { + // Profile not found. + return false, nil + } + + passesMyDesires, err := myMateDesires.CheckIfMateProfilePassesAllMyDesires(false, "", getAnyUserAttributeFunction) + if (err != nil) { return false, err } + if (passesMyDesires == false){ + return false, nil + } + + return true, nil + } + if (chatFilterName == "ShowHasMessagedMeOnly"){ + + hasMessagedMeStatus, err := myChatMessages.CheckIfUserHasMessagedMe(userIdentityHash, networkType) + if (err != nil) { return false, err } + if (hasMessagedMeStatus == false){ + return false, nil + } + + return true, nil + } + + if (chatFilterName == "OnlyShowLikedUsers"){ + + userIsLiked, _, err := myLikedUsers.CheckIfUserIsLiked(userIdentityHash) + if (err != nil) { return false, err } + if (userIsLiked == false){ + return false, nil + } + + return true, nil + } + if (chatFilterName == "HideIgnoredUsers"){ + + userIsIgnored, _, _, _, err := myIgnoredUsers.CheckIfUserIsIgnored(userIdentityHash) + if (err != nil) { return false, err } + if (userIsIgnored == true){ + return false, nil + } + + return true, nil + } + + return false, errors.New("checkIfUserPassesMyChatFilter called with invalid chatFilterName: " + chatFilterName) +} + + +func GetNumberOfEnabledChatFilters(myIdentityType string)(int, error){ + + myEnabledChatFiltersList, err := GetAllMyEnabledChatFiltersList(myIdentityType) + if (err != nil) { return 0, err } + + numberOfEnabledFilters := len(myEnabledChatFiltersList) + + return numberOfEnabledFilters, nil +} + +func GetAllMyEnabledChatFiltersList(myIdentityType string)([]string, error){ + + chatFiltersList, err := GetChatFiltersList(myIdentityType) + if (err != nil) { return nil, err } + + myEnabledChatFiltersList := make([]string, 0) + + for _, chatFilterName := range chatFiltersList{ + + currentOnOffStatus, err := GetChatFilterOnOffStatus(myIdentityType, chatFilterName) + if (err != nil) { return nil, err } + + if (currentOnOffStatus == true){ + myEnabledChatFiltersList = append(myEnabledChatFiltersList, chatFilterName) + } + } + + return myEnabledChatFiltersList, nil +} + +func SetChatFilterOnOffStatus(myIdentityType string, chatFilterName string, chatFilterStatus bool)error{ + + if (myIdentityType != "Mate" && myIdentityType != "Moderator") { + return errors.New("SetChatFilterOnOffStatus called with invalid identity type: " + myIdentityType) + } + + filterOnOffStatusString := helpers.ConvertBoolToYesOrNoString(chatFilterStatus) + + myChatFiltersMapDatastore, err := getMyChatFiltersMapDatastore(myIdentityType) + if (err != nil){ return err } + + err = myChatFiltersMapDatastore.SetMapEntry(chatFilterName, filterOnOffStatusString) + if (err != nil) { return err } + + err = mySettings.SetSetting(myIdentityType + "ChatConversationsGeneratedStatus", "No") + if (err != nil) { return err } + + return nil +} + + +func GetChatFilterOnOffStatus(myIdentityType string, chatFilterName string)(bool, error){ + + if (myIdentityType != "Mate" && myIdentityType != "Moderator") { + return false, errors.New("GetChatFilterOnOffStatus called with invalid identity type: " + myIdentityType) + } + + myChatFiltersMapDatastore, err := getMyChatFiltersMapDatastore(myIdentityType) + if (err != nil){ return false, err } + + filterStatusExists, currentFilterStatus, err := myChatFiltersMapDatastore.GetMapEntry(chatFilterName) + if (err != nil) { return false, err } + if (filterStatusExists == false){ + + if (chatFilterName == "HideIgnoredUsers"){ + return true, nil + } + return false, nil + } + + filterOnOffStatusBool, err := helpers.ConvertYesOrNoStringToBool(currentFilterStatus) + if (err != nil) { return false, err } + + return filterOnOffStatusBool, nil +} + + + + + diff --git a/internal/messaging/myChatKeys/myChatKeys.go b/internal/messaging/myChatKeys/myChatKeys.go new file mode 100644 index 0000000..35cbb0a --- /dev/null +++ b/internal/messaging/myChatKeys/myChatKeys.go @@ -0,0 +1,585 @@ + +// myChatKeys provides functions to manage a user's chat keys +// New keys are created after a set duration has passed +// Old keys are deleted after the user has downloaded all of the messages during the period when they were used + +package myChatKeys + +import "seekia/internal/cryptography/kyber" +import "seekia/internal/cryptography/nacl" +import "seekia/internal/encoding" +import "seekia/internal/helpers" +import "seekia/internal/identity" +import "seekia/internal/logger" +import "seekia/internal/messaging/readMessages" +import "seekia/internal/myDatastores/myMap" +import "seekia/internal/myDatastores/myMapList" +import "seekia/internal/myIdentity" + +import "sync" +import "time" +import "strings" +import "errors" + +//TODO: Add pruning function to get rid of old keys + +// This mutex will be locked whenever we update the chat keys maplist +var updatingMyChatKeysMutex sync.Mutex + +// This mutex will be locked whenever we update the chatKeysLatestUpdateTime map object +var updatingMyChatKeysLatestUpdateTimeMutex sync.Mutex + +var myChatKeysMapListDatastore *myMapList.MyMapList + +// Map Structure: Identity Hash + "@" + Network Type -> Latest update unix time +var myChatKeysLatestUpdateTimeMapDatastore *myMap.MyMap + +// Map Structure: Identity Hash + "@" + Network Type -> Nacl Key + "+" + Kyber Key +var myNewestBroadcastPublicChatKeysMapDatastore *myMap.MyMap + + +// This function must be called whenever an app user signs in +func InitializeMyChatKeysDatastores()error{ + + updatingMyChatKeysMutex.Lock() + defer updatingMyChatKeysMutex.Unlock() + + updatingMyChatKeysLatestUpdateTimeMutex.Lock() + defer updatingMyChatKeysLatestUpdateTimeMutex.Unlock() + + newMyChatKeysLatestUpdateTimeMapDatastore, err := myMap.CreateNewMap("MyChatKeysLatestUpdateTime") + if (err != nil) { return err } + + newMyChatKeysMapListDatastore, err := myMapList.CreateNewMapList("MyChatKeys") + if (err != nil) { return err } + + newMyNewestBroadcastPublicChatKeysMapDatastore, err := myMap.CreateNewMap("MyNewestBroadcastPublicChatKeys") + if (err != nil) { return err } + + myChatKeysLatestUpdateTimeMapDatastore = newMyChatKeysLatestUpdateTimeMapDatastore + + myChatKeysMapListDatastore = newMyChatKeysMapListDatastore + + myNewestBroadcastPublicChatKeysMapDatastore = newMyNewestBroadcastPublicChatKeysMapDatastore + + return nil +} + +// This function provides the decryption keys for a particular identity hash + +//Outputs: +// -bool: My identity found +// -bool: Any chat keys found (will be false if user has never broadcasted their profile) +// -[]ChatKeySet: Chat keys list +// -error +func GetMyChatDecryptionKeySetsList(myIdentityHash [16]byte, networkType byte)(bool, bool, []readMessages.ChatKeySet, error){ + + isMine, myIdentityType, err := myIdentity.CheckIfIdentityHashIsMine(myIdentityHash) + if (err != nil) { return false, false, nil, err } + if (isMine == false) { + return false, false, nil, nil + } + + if (myIdentityType != "Mate" && myIdentityType != "Moderator"){ + return false, false, nil, errors.New("GetMyChatDecryptionKeysMapList called with Host identity.") + } + + isValid := helpers.VerifyNetworkType(networkType) + if (isValid == false){ + networkTypeString := helpers.ConvertByteToString(networkType) + return false, false, nil, errors.New("GetMyChatDecryptionKeySetsList called with invalid networkType: " + networkTypeString) + } + + myIdentityHashString, _, err := identity.EncodeIdentityHashBytesToString(myIdentityHash) + if (err != nil) { + myIdentityHashHex := encoding.EncodeBytesToHexString(myIdentityHash[:]) + return false, false, nil, errors.New("CheckIfIdentityHashIsMine not verifying identity hash: " + myIdentityHashHex) + } + + networkTypeString := helpers.ConvertByteToString(networkType) + + lookupMap := map[string]string{ + "MyIdentityHash": myIdentityHashString, + "NetworkType": networkTypeString, + } + + anyExist, myChatKeysMapList, err := myChatKeysMapListDatastore.GetMapListItems(lookupMap) + if (err != nil){ return false, false, nil, err } + if (anyExist == false){ + return true, false, nil, nil + } + + chatDecryptionKeySetsList := make([]readMessages.ChatKeySet, 0, len(myChatKeysMapList)) + + for _, keySetMap := range myChatKeysMapList{ + + myNaclPublicKey, exists := keySetMap["NaclPublicKey"] + if (exists == false){ + return false, false, nil, errors.New("Malformed myChatKeys map list: Item missing NaclPublicKey") + } + myNaclPrivateKey, exists := keySetMap["NaclPrivateKey"] + if (exists == false){ + return false, false, nil, errors.New("Malformed myChatKeys map list: Item missing NaclPrivateKey") + } + myKyberPrivateKey, exists := keySetMap["KyberPrivateKey"] + if (exists == false){ + return false, false, nil, errors.New("Malformed myChatKeys map list: Item missing KyberPrivateKey") + } + + myNaclPublicKeyBytes, err := encoding.DecodeBase64StringToBytes(myNaclPublicKey) + if (err != nil){ + return false, false, nil, errors.New("Malformed myChatKeys map list: NaclPublicKey is not Base64.") + } + + if (len(myNaclPublicKeyBytes) != 32){ + return false, false, nil, errors.New("Malformed myChatKeys map list: NaclPublicKey is invalid length.") + } + + myNaclPublicKeyArray := [32]byte(myNaclPublicKeyBytes) + + myNaclPrivateKeyBytes, err := encoding.DecodeHexStringToBytes(myNaclPrivateKey) + if (err != nil){ + return false, false, nil, errors.New("Malformed myChatKeys map list: NaclPrivateKey is not Hex.") + } + + if (len(myNaclPrivateKeyBytes) != 32){ + return false, false, nil, errors.New("Malformed myChatKeys map list: NaclPrivateKey is invalid length.") + } + + myNaclPrivateKeyArray := [32]byte(myNaclPrivateKeyBytes) + + myKyberPrivateKeyBytes, err := encoding.DecodeHexStringToBytes(myKyberPrivateKey) + if (err != nil){ + return false, false, nil, errors.New("Malformed myChatKeys map list: KyberPrivateKey is not Hex.") + } + + if (len(myKyberPrivateKeyBytes) != 1536){ + return false, false, nil, errors.New("Malformed myChatKeys map list: KyberPrivateKey is invalid length.") + } + + myKyberPrivateKeyArray := [1536]byte(myKyberPrivateKeyBytes) + + newChatKeySetObject := readMessages.ChatKeySet{ + NaclPublicKey: myNaclPublicKeyArray, + NaclPrivateKey: myNaclPrivateKeyArray, + KyberPrivateKey: myKyberPrivateKeyArray, + } + + chatDecryptionKeySetsList = append(chatDecryptionKeySetsList, newChatKeySetObject) + } + + return true, true, chatDecryptionKeySetsList, nil +} + +// This function generates new chat keys for an identity. +func GenerateNewChatKeys(myIdentityHash [16]byte, networkType byte)error{ + + myIdentityHashString, _, err := identity.EncodeIdentityHashBytesToString(myIdentityHash) + if (err != nil) { + myIdentityHashHex := encoding.EncodeBytesToHexString(myIdentityHash[:]) + return errors.New("GenerateNewChatKeys called with invalid identity hash: " + myIdentityHashHex) + } + + isValid := helpers.VerifyNetworkType(networkType) + if (isValid == false){ + networkTypeString := helpers.ConvertByteToString(networkType) + return errors.New("GenerateNewChatKeys called with invalid networkType: " + networkTypeString) + } + + updatingMyChatKeysMutex.Lock() + defer updatingMyChatKeysMutex.Unlock() + + currentTime := time.Now().Unix() + currentTimeString := helpers.ConvertInt64ToString(currentTime) + + networkTypeString := helpers.ConvertByteToString(networkType) + + newNaclPublicKey, newNaclPrivateKey, err := nacl.GetNewRandomPublicPrivateNaclKeys() + if (err != nil) { return err } + + newKyberPublicKey, newKyberPrivateKey, err := kyber.GetNewRandomPublicPrivateKyberKeys() + if (err != nil) { return err } + + newNaclPublicKeyString := encoding.EncodeBytesToBase64String(newNaclPublicKey[:]) + newNaclPrivateKeyString := encoding.EncodeBytesToHexString(newNaclPrivateKey[:]) + + newKyberPublicKeyString := encoding.EncodeBytesToBase64String(newKyberPublicKey[:]) + newKyberPrivateKeyString := encoding.EncodeBytesToHexString(newKyberPrivateKey[:]) + + newMapListItem := map[string]string{ + "MyIdentityHash": myIdentityHashString, + "CreatedTime": currentTimeString, + "NetworkType": networkTypeString, + "NaclPublicKey": newNaclPublicKeyString, + "NaclPrivateKey": newNaclPrivateKeyString, + "KyberPublicKey": newKyberPublicKeyString, + "KyberPrivateKey": newKyberPrivateKeyString, + } + + err = myChatKeysMapListDatastore.AddMapListItem(newMapListItem) + if (err != nil) { return err } + + return nil +} + + +// This function will get a user's newest public chat keys +// It will create new chat keys for the provided identity if none exist +//Outputs: +// -bool: My identity exists +// -[32]byte: My Nacl Public key +// -[1568]byte: My Kyber Public key +// -error +func GetMyNewestPublicChatKeys(myIdentityHash [16]byte, networkType byte)(bool, [32]byte, [1568]byte, error){ + + isMine, myIdentityType, err := myIdentity.CheckIfIdentityHashIsMine(myIdentityHash) + if (err != nil) { return false, [32]byte{}, [1568]byte{}, err } + if (isMine == false){ + return false, [32]byte{}, [1568]byte{}, nil + } + if (myIdentityType != "Mate" && myIdentityType != "Moderator"){ + return false, [32]byte{}, [1568]byte{}, errors.New("GetMyNewestPublicChatKeys called with host identity.") + } + + isValid := helpers.VerifyNetworkType(networkType) + if (isValid == false){ + networkTypeString := helpers.ConvertByteToString(networkType) + return false, [32]byte{}, [1568]byte{}, errors.New("GetMyNewestPublicChatKeys called with invalid networkType: " + networkTypeString) + } + + myIdentityHashString, _, err := identity.EncodeIdentityHashBytesToString(myIdentityHash) + if (err != nil){ + myIdentityHashHex := encoding.EncodeBytesToHexString(myIdentityHash[:]) + return false, [32]byte{}, [1568]byte{}, errors.New("CheckIfIdentityHashIsMine not verifying identity hash: " + myIdentityHashHex) + } + + updatingMyChatKeysMutex.Lock() + defer updatingMyChatKeysMutex.Unlock() + + networkTypeString := helpers.ConvertByteToString(networkType) + + lookupMap := map[string]string{ + "MyIdentityHash": myIdentityHashString, + "NetworkType": networkTypeString, + } + + anyItemsFound, myChatKeysMapList, err := myChatKeysMapListDatastore.GetMapListItems(lookupMap) + if (err != nil){ return false, [32]byte{}, [1568]byte{}, err } + if (anyItemsFound == true){ + + if (len(myChatKeysMapList) == 0){ + return false, [32]byte{}, [1568]byte{}, errors.New("GetMapListItems returning empty items list after claiming items were found.") + } + + newestChatKeysCreatedTime := int64(0) + var newestNaclPublicKey [32]byte + var newestKyberPublicKey [1568]byte + + for index, chatKeysSetMap := range myChatKeysMapList{ + + itemMyIdentityHash, exists := chatKeysSetMap["MyIdentityHash"] + if (exists == false) { + return false, [32]byte{}, [1568]byte{}, errors.New("myMapList lookup not working properly.") + } + + if (itemMyIdentityHash != myIdentityHashString){ + return false, [32]byte{}, [1568]byte{}, errors.New("myMapList lookup not working properly.") + } + + timeKeysCreated, exists := chatKeysSetMap["CreatedTime"] + if (exists == false) { + return false, [32]byte{}, [1568]byte{}, errors.New("Malformed myChatKeys map list: Item missing CreatedTime") + } + naclPublicKey, exists := chatKeysSetMap["NaclPublicKey"] + if (exists == false) { + return false, [32]byte{}, [1568]byte{}, errors.New("Malformed myChatKeys map list: Item missing NaclPublicKey") + } + kyberPublicKey, exists := chatKeysSetMap["KyberPublicKey"] + if (exists == false) { + return false, [32]byte{}, [1568]byte{}, errors.New("Malformed myChatKeys map list: Item missing KyberPublicKey") + } + + naclPublicKeyBytes, err := encoding.DecodeBase64StringToBytes(naclPublicKey) + if (err != nil){ + return false, [32]byte{}, [1568]byte{}, errors.New("Malformed myChatKeys map list: Item contains invalid NaclPublicKey: Not hex.") + } + + if (len(naclPublicKeyBytes) != 32){ + return false, [32]byte{}, [1568]byte{}, errors.New("Malformed myChatKeys map list: Item contains invalid NaclPublicKey: Invalid length.") + } + + naclPublicKeyArray := [32]byte(naclPublicKeyBytes) + + kyberPublicKeyBytes, err := encoding.DecodeBase64StringToBytes(kyberPublicKey) + if (err != nil){ + return false, [32]byte{}, [1568]byte{}, errors.New("Malformed myChatKeys map list: Item contains invalid KyberPublicKey: Not hex.") + } + + if (len(kyberPublicKeyBytes) != 1568){ + return false, [32]byte{}, [1568]byte{}, errors.New("Malformed myChatKeys map list: Item contains invalid KyberPublicKey: Invalid length.") + } + + kyberPublicKeyArray := [1568]byte(kyberPublicKeyBytes) + + timeKeysCreatedInt64, err := helpers.ConvertStringToInt64(timeKeysCreated) + if (err != nil) { + return false, [32]byte{}, [1568]byte{}, errors.New("Malformed myChatKeys map list: Item contains invalid CreatedTime: " + timeKeysCreated) + } + + if (index == 0 || newestChatKeysCreatedTime < timeKeysCreatedInt64){ + newestChatKeysCreatedTime = timeKeysCreatedInt64 + newestNaclPublicKey = naclPublicKeyArray + newestKyberPublicKey = kyberPublicKeyArray + } + } + + return true, newestNaclPublicKey, newestKyberPublicKey, nil + } + + // We must create new chat keys + + currentTime := time.Now().Unix() + currentTimeString := helpers.ConvertInt64ToString(currentTime) + + newNaclPublicKey, newNaclPrivateKey, err := nacl.GetNewRandomPublicPrivateNaclKeys() + if (err != nil) { return false, [32]byte{}, [1568]byte{}, err } + + newKyberPublicKey, newKyberPrivateKey, err := kyber.GetNewRandomPublicPrivateKyberKeys() + if (err != nil) { return false, [32]byte{}, [1568]byte{}, err } + + newNaclPublicKeyString := encoding.EncodeBytesToBase64String(newNaclPublicKey[:]) + newNaclPrivateKeyString := encoding.EncodeBytesToHexString(newNaclPrivateKey[:]) + + newKyberPublicKeyString := encoding.EncodeBytesToBase64String(newKyberPublicKey[:]) + newKyberPrivateKeyString := encoding.EncodeBytesToHexString(newKyberPrivateKey[:]) + + newChatKeysSetMap := map[string]string{ + "MyIdentityHash": myIdentityHashString, + "NetworkType": networkTypeString, + "CreatedTime": currentTimeString, + "NaclPublicKey": newNaclPublicKeyString, + "NaclPrivateKey": newNaclPrivateKeyString, + "KyberPublicKey": newKyberPublicKeyString, + "KyberPrivateKey": newKyberPrivateKeyString, + } + + err = myChatKeysMapListDatastore.AddMapListItem(newChatKeysSetMap) + if (err != nil) { return false, [32]byte{}, [1568]byte{}, err } + + return true, newNaclPublicKey, newKyberPublicKey, nil +} + +// This function is only called when a new profile is added to myBroadcasts +func SetMyChatKeysLatestUpdateTime(myIdentityHash [16]byte, networkType byte, newLatestUpdateTime int64)error{ + + isMine, myIdentityType, err := myIdentity.CheckIfIdentityHashIsMine(myIdentityHash) + if (err != nil) { return err } + if (isMine == false){ + return errors.New("SetMyChatKeysLatestUpdateTime called with identity that is not mine.") + } + if (myIdentityType != "Mate" && myIdentityType != "Moderator"){ + return errors.New("SetMyChatKeysLatestUpdateTime called with Host identity.") + } + + isValid := helpers.VerifyNetworkType(networkType) + if (isValid == false){ + networkTypeString := helpers.ConvertByteToString(networkType) + return errors.New("SetMyChatKeysLatestUpdateTime called with invalid networkType: " + networkTypeString) + } + + updatingMyChatKeysLatestUpdateTimeMutex.Lock() + defer updatingMyChatKeysLatestUpdateTimeMutex.Unlock() + + exists, existingLatestUpdateTime, err := GetMyChatKeysLatestUpdateTime(myIdentityHash, networkType) + if (err != nil) { return err } + if (exists == true){ + if (existingLatestUpdateTime > newLatestUpdateTime) { + // This would only happen if our system clock was invalid, and became valid + // We will log this, but allow our latestUpdateTime to be updated + err := logger.AddLogEntry("General", "Existing latest chat keys update time is later than new update time.") + if (err != nil) { return err } + + } else if (existingLatestUpdateTime == newLatestUpdateTime){ + // No update is needed. + return nil + } + } + + myIdentityHashString, _, err := identity.EncodeIdentityHashBytesToString(myIdentityHash) + if (err != nil){ + myIdentityHashHex := encoding.EncodeBytesToHexString(myIdentityHash[:]) + return errors.New("CheckIfIdentityHashIsMine not verifying identity hash: " + myIdentityHashHex) + } + + networkTypeString := helpers.ConvertByteToString(networkType) + + mapEntryKey := myIdentityHashString + "@" + networkTypeString + + newLatestUpdateTimeString := helpers.ConvertInt64ToString(newLatestUpdateTime) + + err = myChatKeysLatestUpdateTimeMapDatastore.SetMapEntry(mapEntryKey, newLatestUpdateTimeString) + if (err != nil) { return err } + + return nil +} + + +//Outputs: +// -bool: Update time exists (wont exist if profile has never been broadcast, or existing profile is not imported yet) +// -int64: Current latest update time +// -error +func GetMyChatKeysLatestUpdateTime(myIdentityHash [16]byte, networkType byte)(bool, int64, error){ + + isMine, myIdentityType, err := myIdentity.CheckIfIdentityHashIsMine(myIdentityHash) + if (err != nil) { return false, 0, err } + if (isMine == false){ + return false, 0, errors.New("GetMyChatKeysLatestUpdateTime called with identity that is not mine.") + } + if (myIdentityType != "Mate" && myIdentityType != "Moderator"){ + return false, 0, errors.New("GetMyChatKeysLatestUpdateTime called with Host identity.") + } + + isValid := helpers.VerifyNetworkType(networkType) + if (isValid == false){ + networkTypeString := helpers.ConvertByteToString(networkType) + return false, 0, errors.New("GetMyChatKeysLatestUpdateTime called with invalid networkType: " + networkTypeString) + } + + myIdentityHashString, _, err := identity.EncodeIdentityHashBytesToString(myIdentityHash) + if (err != nil){ + myIdentityHashHex := encoding.EncodeBytesToHexString(myIdentityHash[:]) + return false, 0, errors.New("CheckIfIdentityHashIsMine not verifying my identity hash: " + myIdentityHashHex) + } + + networkTypeString := helpers.ConvertByteToString(networkType) + + mapEntryKey := myIdentityHashString + "@" + networkTypeString + + exists, latestUpdateTimeString, err := myChatKeysLatestUpdateTimeMapDatastore.GetMapEntry(mapEntryKey) + if (err != nil) { return false, 0, err } + if (exists == false) { + return false, 0, nil + } + + latestUpdateTimeInt64, err := helpers.ConvertStringToInt64(latestUpdateTimeString) + if (err != nil) { + return false, 0, errors.New("myChatKeysLatestUpdateTimeMapDatastore is corrupt: Contains invalid latestUpdateTime: " + latestUpdateTimeString) + } + + return true, latestUpdateTimeInt64, nil +} + + +// This function is used to keep track of the user's newest broadcasted chat keys +// We need to do this because we need to be able to tell if the profile we are broadcasting contains new keys +// This is stored seperately from myBroadcasts +// This datastore can prevent users from having to update their latestChatKeysUpdateTime when moving to a new device, if they import their keys +func SetMyNewestBroadcastPublicChatKeys(myIdentityHash [16]byte, networkType byte, naclKey [32]byte, kyberKey [1568]byte)error{ + + isMine, myIdentityType, err := myIdentity.CheckIfIdentityHashIsMine(myIdentityHash) + if (err != nil) { return err } + if (isMine == false){ + return errors.New("SetMyNewestBroadcastPublicChatKeys called with identity that is not mine.") + } + if (myIdentityType != "Mate" && myIdentityType != "Moderator"){ + return errors.New("SetMyNewestBroadcastPublicChatKeys called with Host identity.") + } + + isValid := helpers.VerifyNetworkType(networkType) + if (isValid == false){ + networkTypeString := helpers.ConvertByteToString(networkType) + return errors.New("SetMyNewestBroadcastPublicChatKeys called with invalid networkType: " + networkTypeString) + } + + myIdentityHashString, _, err := identity.EncodeIdentityHashBytesToString(myIdentityHash) + if (err != nil){ + myIdentityHashHex := encoding.EncodeBytesToHexString(myIdentityHash[:]) + return errors.New("CheckIfIdentityHashIsMine not verifying my identity hash: " + myIdentityHashHex) + } + + networkTypeString := helpers.ConvertByteToString(networkType) + + mapEntryKey := myIdentityHashString + "@" + networkTypeString + + naclKeyEncoded := encoding.EncodeBytesToBase64String(naclKey[:]) + kyberKeyEncoded := encoding.EncodeBytesToBase64String(kyberKey[:]) + + newEntryValue := naclKeyEncoded + "+" + kyberKeyEncoded + + err = myNewestBroadcastPublicChatKeysMapDatastore.SetMapEntry(mapEntryKey, newEntryValue) + if (err != nil) { return err } + + return nil +} + +//Outputs: +// -bool: My identity found +// -bool: Any keys found +// -[32]byte: Newest broadcast Nacl key (Encoded base64) +// -[1568]byte: Newest broadcast Kyber key (Encoded base64) +// -error +func GetMyNewestBroadcastPublicChatKeys(myIdentityHash [16]byte, networkType byte)(bool, bool, [32]byte, [1568]byte, error){ + + isMine, myIdentityType, err := myIdentity.CheckIfIdentityHashIsMine(myIdentityHash) + if (err != nil) { return false, false, [32]byte{}, [1568]byte{}, err } + if (isMine == false){ + return false, false, [32]byte{}, [1568]byte{}, nil + } + if (myIdentityType != "Mate" && myIdentityType != "Moderator"){ + return false, false, [32]byte{}, [1568]byte{}, errors.New("GetMyNewestBroadcastPublicChatKeys called with Host identity.") + } + + isValid := helpers.VerifyNetworkType(networkType) + if (isValid == false){ + networkTypeString := helpers.ConvertByteToString(networkType) + return false, false, [32]byte{}, [1568]byte{}, errors.New("GetMyNewestBroadcastPublicChatKeys called with invalid networkType: " + networkTypeString) + } + + myIdentityHashString, _, err := identity.EncodeIdentityHashBytesToString(myIdentityHash) + if (err != nil){ + myIdentityHashHex := encoding.EncodeBytesToHexString(myIdentityHash[:]) + return false, false, [32]byte{}, [1568]byte{}, errors.New("CheckIfIdentityHashIsMine not verifying my identity hash: " + myIdentityHashHex) + } + + networkTypeString := helpers.ConvertByteToString(networkType) + + mapEntryKey := myIdentityHashString + "@" + networkTypeString + + exists, mapItemValue, err := myNewestBroadcastPublicChatKeysMapDatastore.GetMapEntry(mapEntryKey) + if (err != nil) { return false, false, [32]byte{}, [1568]byte{}, err } + if (exists == false){ + return true, false, [32]byte{}, [1568]byte{}, nil + } + + naclKeyString, kyberKeyString, delimiterFound := strings.Cut(mapItemValue, "+") + if (delimiterFound == false){ + return false, false, [32]byte{}, [1568]byte{}, errors.New("Malformed myNewestBroadcastPublicChatKeysMapDatastore item value: " + mapItemValue) + } + + naclKeyBytes, err := encoding.DecodeBase64StringToBytes(naclKeyString) + if (err != nil){ + return false, false, [32]byte{}, [1568]byte{}, errors.New("Malformed myNewestBroadcastPublicChatKeysMapDatastore item value: Invalid NaclKey: Not Base64: " + naclKeyString) + } + + if (len(naclKeyBytes) != 32){ + return false, false, [32]byte{}, [1568]byte{}, errors.New("Malformed myNewestBroadcastPublicChatKeysMapDatastore item value: Invalid NaclKey: Invalid length: " + naclKeyString) + } + + naclKey := [32]byte(naclKeyBytes) + + kyberKeyBytes, err := encoding.DecodeBase64StringToBytes(kyberKeyString) + if (err != nil){ + return false, false, [32]byte{}, [1568]byte{}, errors.New("Malformed myNewestBroadcastPublicChatKeysMapDatastore item value: Invalid KyberKey: Not Base64: " + kyberKeyString) + } + + if (len(kyberKeyBytes) != 1568){ + return false, false, [32]byte{}, [1568]byte{}, errors.New("Malformed myNewestBroadcastPublicChatKeysMapDatastore item value: Invalid KyberKey: Invalid length: " + kyberKeyString) + } + + kyberKey := [1568]byte(kyberKeyBytes) + + return true, true, naclKey, kyberKey, nil +} + + + diff --git a/internal/messaging/myChatMessages/myChatMessages.go b/internal/messaging/myChatMessages/myChatMessages.go new file mode 100644 index 0000000..8a1e09a --- /dev/null +++ b/internal/messaging/myChatMessages/myChatMessages.go @@ -0,0 +1,1358 @@ + +// myChatMessages provides functions to manage a user's sent and received chat messages. +// Raw messages are decrypted and then stored in an unencrypted form. + +package myChatMessages + +//TODO: Prune deleted/undecryptable message hashes lists +// Message hashes should be deleted from this lists once the message expiration time is reached + +import "seekia/internal/allowedText" +import "seekia/internal/badgerDatabase" +import "seekia/internal/encoding" +import "seekia/internal/helpers" +import "seekia/internal/identity" +import "seekia/internal/messaging/myChatKeys" +import "seekia/internal/messaging/myCipherKeys" +import "seekia/internal/messaging/myInbox" +import "seekia/internal/messaging/myReadStatus" +import "seekia/internal/messaging/peerChatKeys" +import "seekia/internal/messaging/peerDevices" +import "seekia/internal/messaging/peerSecretInboxes" +import "seekia/internal/messaging/readMessages" +import "seekia/internal/myBlockedUsers" +import "seekia/internal/myDatastores/myMap" +import "seekia/internal/myDatastores/myMapList" +import "seekia/internal/myIdentity" +import "seekia/internal/mySettings" +import "seekia/internal/parameters/getParameters" + +import "slices" +import "bytes" +import "sync" +import "errors" +import "time" + +// This will be locked whenever the chat messages map lists are being updated +var updatingChatMessagesMutex sync.Mutex + +var myMateChatMessagesMapListDatastore *myMapList.MyMapList +var myModeratorChatMessagesMapListDatastore *myMapList.MyMapList + +// This map stores deleted message hashes +// This map exists to prevent importing/decrypting messages that a user has deleted (or were maliciously crafted). +var myDeletedMessagesMapDatastore *myMap.MyMap + +// This map stores unreadable message hashes +// This map exists to prevent attempting to import messages which we cannot decrypt more than once +var myUndecryptableMessagesMapDatastore *myMap.MyMap + +func getMyChatMessagesMapListDatastore(myIdentityType string)(*myMapList.MyMapList, error){ + + if (myIdentityType == "Mate"){ + return myMateChatMessagesMapListDatastore, nil + } + if (myIdentityType == "Moderator"){ + return myModeratorChatMessagesMapListDatastore, nil + } + return nil, errors.New("getMyChatMessagesMapListDatastore called with invalid myIdentityType: " + myIdentityType) +} + +// This function must be called whenever an app user signs in +func InitializeMyChatMessageDatastores()error{ + + updatingChatMessagesMutex.Lock() + defer updatingChatMessagesMutex.Unlock() + + newMyMateChatMessagesMapListDatastore, err := myMapList.CreateNewMapList("MyMateChatMessages") + if (err != nil) { return err } + + newMyModeratorChatMessagesMapListDatastore, err := myMapList.CreateNewMapList("MyModeratorChatMessages") + if (err != nil) { return err } + + newMyDeletedMessagesMapDatastore, err := myMap.CreateNewMap("MyDeletedMessages") + if (err != nil) { return err } + + newMyUndecryptableMessagesMapDatastore, err := myMap.CreateNewMap("MyUndecryptableMessages") + if (err != nil) { return err } + + myMateChatMessagesMapListDatastore = newMyMateChatMessagesMapListDatastore + myModeratorChatMessagesMapListDatastore = newMyModeratorChatMessagesMapListDatastore + + myDeletedMessagesMapDatastore = newMyDeletedMessagesMapDatastore + myUndecryptableMessagesMapDatastore = newMyUndecryptableMessagesMapDatastore + + return nil +} + +// This function is called when a user deletes their identity +func DeleteMyChatMessagesMapList(myIdentityType string)error{ + + if (myIdentityType != "Mate" && myIdentityType != "Moderator"){ + return errors.New("DeleteMyChatMessagesMapList called with invalid myIdentityType: " + myIdentityType) + } + + updatingChatMessagesMutex.Lock() + defer updatingChatMessagesMutex.Unlock() + + myChatMessagesMapListDatastore, err := getMyChatMessagesMapListDatastore(myIdentityType) + if (err != nil) { return err } + + err = myChatMessagesMapListDatastore.DeleteMapList() + if (err != nil) { return err } + + return nil +} + + +// This function checks to see if chat messages are ready +// Messages become not ready every time a new message is downloaded for one of the user's inboxes +func GetMyChatMessagesReadyStatus(myIdentityType string)(bool, error){ + + if (myIdentityType != "Mate" && myIdentityType != "Moderator"){ + return false, errors.New("GetMyChatMessagesReadyStatus called with invalid myIdentityType: " + myIdentityType) + } + + exists, readyStatus, err := mySettings.GetSetting(myIdentityType + "ChatMessagesReadyStatus") + if (err != nil) { return false, err } + if (exists == false || readyStatus != "Yes"){ + return false, nil + } + + return true, nil +} + + +// This function returns an updated MyChatMessages map list +// This map list contains decrypted chat messages sent to the user +// The function will attempt to decrypt messages sent to a user's inboxes +// This function will also prune the map list of any different networkType messages +// +// Inputs: +// -string: My Identity Type ("Mate"/"Moderator") +// -byte: Network type (1 == Mainnet, 2 == Testnet 1) +// -func(int)error: Update progress function (will call the function to update between 0-100%) +//Outputs: +// -[]map[string]string: Updated myChatMessages map list +// -error +func GetUpdatedMyChatMessagesMapList(myIdentityType string, networkType byte, updateProgressFunction func(int)error)([]map[string]string, error){ + + isValid := helpers.VerifyNetworkType(networkType) + if (isValid == false){ + networkTypeString := helpers.ConvertByteToString(networkType) + return nil, errors.New("GetUpdatedMyChatMessagesMapList called with invalid networkType: " + networkTypeString) + } + + defer updateProgressFunction(100) + + myChatMessagesMapListDatastore, err := getMyChatMessagesMapListDatastore(myIdentityType) + if (err != nil) { return nil, err } + + messagesReadyStatus, err := GetMyChatMessagesReadyStatus(myIdentityType) + if (err != nil) { return nil, err } + if (messagesReadyStatus == false) { + + // We update our chat messages + + updateMyChatMessages := func()error{ + + parametersExist, err := getParameters.CheckIfSecretInboxEpochParametersExist(networkType) + if (err != nil){ return err } + if (parametersExist == false){ + // We need these parameters to import user secret inbox information + // We will not update chat messages until we download these parameters + return nil + } + + updatingChatMessagesMutex.Lock() + defer updatingChatMessagesMutex.Unlock() + + myIdentityFound, myIdentityHash, err := myIdentity.GetMyIdentityHash(myIdentityType) + if (err != nil) { return err } + if (myIdentityFound == false){ + // No chat messages can exist. + + err = myChatMessagesMapListDatastore.DeleteMapList() + if (err != nil) { return err } + + return nil + } + + myInboxesList, err := myInbox.GetAllMyInboxes(myIdentityHash, networkType) + if (err != nil) { return err } + + err = updateProgressFunction(5) + if (err != nil) { return err } + + // This map will store raw encrypted message hashes for messages in our inboxes + // Map structure: Message Hash -> Message inbox + myRawInboxMessageHashesMap := make(map[[26]byte][10]byte) + + for _, inbox := range myInboxesList{ + + exists, inboxMessageHashesList, err := badgerDatabase.GetChatInboxMessageHashesList(inbox) + if (err != nil) { return err } + if (exists == false) { + continue + } + + for _, messageHash := range inboxMessageHashesList{ + myRawInboxMessageHashesMap[messageHash] = inbox + } + } + + err = updateProgressFunction(10) + if (err != nil) { return err } + + if (len(myRawInboxMessageHashesMap) == 0){ + + // No messages to import, nothing left to do. + + return nil + } + + myIdentityExists, anyChatKeysExist, myChatDecryptionKeySetsList, err := myChatKeys.GetMyChatDecryptionKeySetsList(myIdentityHash, networkType) + if (err != nil) { return err } + if (myIdentityExists == false) { + return errors.New("My identity not found after already being found.") + } + if (anyChatKeysExist == false){ + // The user has never broadcast their chat keys, or has deleted them. + // All messages are undecryptable, unless user imports decryption keys from their last device/profile + + return nil + } + + err = updateProgressFunction(15) + if (err != nil) { return err } + + myExistingMessagesMapList, err := myChatMessagesMapListDatastore.GetMapList() + if (err != nil) { return err } + + // This will store the message hashes for messages we already have decrypted and imported + existingMessageHashesMap := make(map[[26]byte]struct{}) + + for _, messageMap := range myExistingMessagesMapList{ + + messageIAmSender, exists := messageMap["IAmSender"] + if (exists == false){ + return errors.New("Malformed message map: Missing IAmSender") + } + if (messageIAmSender == "Yes"){ + continue + } + + // Message was not send by user, so message status must be Sent + // Thus, messageHash will exist for all of these messages + + currentMessageHashString, exists := messageMap["MessageHash"] + if (exists == false) { + return errors.New("Malformed message map: Missing MessageHash.") + } + + currentMessageHash, err := readMessages.ReadMessageHashHex(currentMessageHashString) + if (err != nil){ + return errors.New("Malformed message map: contains invalid MessageHash: " + currentMessageHashString) + } + + existingMessageHashesMap[currentMessageHash] = struct{}{} + } + + err = updateProgressFunction(20) + if (err != nil) { return err } + + newMyChatMessagesMapList := make([]map[string]string, 0) + + index := 0 + maximumIndex := len(myRawInboxMessageHashesMap) + + for messageHash, messageInbox := range myRawInboxMessageHashesMap{ + + newPercentageProgress, err := helpers.ScaleNumberProportionally(true, index, 0, maximumIndex, 20, 100) + if (err != nil){ return err } + + err = updateProgressFunction(newPercentageProgress) + if (err != nil) { return err } + + index += 1 + + _, messageIsAlreadyAdded := existingMessageHashesMap[messageHash] + if (messageIsAlreadyAdded == true) { + continue + } + + messageHashHex := encoding.EncodeBytesToHexString(messageHash[:]) + + hasBeenDeleted, _, err := myDeletedMessagesMapDatastore.GetMapEntry(messageHashHex) + if (err != nil) { return err } + if (hasBeenDeleted == true) { + continue + } + + isUndecryptable, _, err := myUndecryptableMessagesMapDatastore.GetMapEntry(messageHashHex) + if (err != nil) { return err } + if (isUndecryptable == true) { + // These are messages which we have already tried to import but were not able to + // This could be due to missing chat keys or a malicious sender + + // If user ever imports new chat keys from a different device, + // the undecryptable messages map is deleted, so we can try again for all messages that haven't been deleted + + continue + } + + inboxFound, inboxMyIdentityHash, inboxDoubleSealedKeysSealerKey, inboxIsSecret, inboxNetworkType, conversationRecipient, err := myInbox.GetMyInboxInfo(messageInbox) + if (err != nil) { return err } + if (inboxFound == false){ + // We know that the inbox is not a public inbox. + // We do not have the sealer key for this inbox. We cannot decrypt the message. + + currentTime := time.Now().Unix() + currentTimeString := helpers.ConvertInt64ToString(currentTime) + + err := myUndecryptableMessagesMapDatastore.SetMapEntry(messageHashHex, currentTimeString) + if (err != nil) { return err } + + continue + } + if (inboxMyIdentityHash != myIdentityHash){ + return errors.New("GetAllMyInboxes is returning inbox for a different identity hash.") + } + if (inboxIsSecret == true && inboxNetworkType != networkType){ + return errors.New("GetAllMyInboxes is returning a secret inbox which belongs to a different network type.") + } + + // Now we attempt to decrypt the message + + messageExists, messageBytes, err := badgerDatabase.GetChatMessage(messageHash) + if (err != nil) { return err } + if (messageExists == false){ + // Database inbox messages list is outdated. It will be updated automatically. + continue + } + + ableToRead, _, messageNetworkType, messageInbox_Received, _, _, _, _, _, _, err := readMessages.ReadChatMessagePublicData(false, messageBytes) + if (err != nil) { return err } + if (ableToRead == false){ + return errors.New("Database corrupt: Contains message with unreadable public data.") + } + if (messageInbox != messageInbox_Received){ + return errors.New("myRawInboxMessageHashesMap contains mismatched message inbox value.") + } + if (messageNetworkType != networkType){ + // This message was sent to us on a different network type + // If the inbox is public, this is fine and expected + // If the inbox is secret, then the sender must be malicious (or our client is acting improperly) + + if (inboxIsSecret == true){ + currentTime := time.Now().Unix() + currentTimeString := helpers.ConvertInt64ToString(currentTime) + + err := myDeletedMessagesMapDatastore.SetMapEntry(messageHashHex, currentTimeString) + if (err != nil) { return err } + + //TODO: Log this + } + + continue + } + + ableToRead, _, _, _, messageCipherKey, senderIdentityHash, recipientIdentityHash, messageSentTimeUnix, messageCommunication, senderCurrentSecretInboxSeed, senderNextSecretInboxSeed, senderDeviceIdentifier, senderLatestChatKeysUpdateTime, err := readMessages.ReadChatMessage(messageBytes, inboxDoubleSealedKeysSealerKey, myChatDecryptionKeySetsList) + if (err != nil) { return err } + if (ableToRead == false) { + // Either keys are lost/deleted or sender is forming malicious messages + // Skip this message. + + //TODO: ReadChatMessage should return 3 bools: outer is malformed, Undecryptable, inner is malformed + // If the inner message is malformed, sender is malicious and we should add the message to the deletedMessagesMapDatastore + + currentTime := time.Now().Unix() + currentTimeString := helpers.ConvertInt64ToString(currentTime) + + err := myUndecryptableMessagesMapDatastore.SetMapEntry(messageHashHex, currentTimeString) + if (err != nil) { return err } + + continue + } + if (recipientIdentityHash != myIdentityHash){ + // Message is sent to my inbox, but not my identity + // Sender is either malicious, or sender is being tricked by another user who is using my chat keys and sending secret inboxes that are my inboxes. + // Either way, ignore message and delete it, never try to import it again. + + currentTime := time.Now().Unix() + currentTimeString := helpers.ConvertInt64ToString(currentTime) + + err := myDeletedMessagesMapDatastore.SetMapEntry(messageHashHex, currentTimeString) + if (err != nil) { return err } + + continue + } + + senderIsBlocked, _, _, _, err := myBlockedUsers.CheckIfUserIsBlocked(senderIdentityHash) + if (err != nil) { return err } + if (senderIsBlocked == true){ + + // We will not add messages sent from users we have blocked + + continue + } + + if (inboxIsSecret == true){ + + // Secret inboxes are reserved for a specific recipient + + if (senderIdentityHash != conversationRecipient){ + + // Sender is either malicious, or sender is being tricked by + // another user who is using my chat keys and sending secret inboxes that are my inboxes. + // Either way, ignore message and delete it, never import it again + + currentTime := time.Now().Unix() + currentTimeString := helpers.ConvertInt64ToString(currentTime) + + err := myDeletedMessagesMapDatastore.SetMapEntry(messageHashHex, currentTimeString) + if (err != nil) { return err } + + continue + } + } + + err = peerChatKeys.SavePeerMessageLatestChatKeysUpdateTime(senderIdentityHash, networkType, senderLatestChatKeysUpdateTime, messageSentTimeUnix) + if (err != nil) { return err } + + parametersExist, err = peerSecretInboxes.AddPeerConversationSecretInboxSeeds(myIdentityHash, senderIdentityHash, messageNetworkType, messageSentTimeUnix, senderCurrentSecretInboxSeed, senderNextSecretInboxSeed) + if (err != nil) { return err } + if (parametersExist == false){ + // This means we cannot add the sender's secret inboxes to our storage + // This means we will send all messages to their public inbox, reducing privacy + // This should not happen, because we already checked for parameters before starting this + // We will skip all the rest of the messages and try again when we have the parameters + break + } + + err = myCipherKeys.SaveMessageCipherKey(messageHash, messageCipherKey) + if (err != nil) { return err } + + err = peerDevices.AddPeerDeviceIdentifierFromMessage(senderIdentityHash, messageNetworkType, senderDeviceIdentifier, messageSentTimeUnix) + if (err != nil) { return err } + + //TODO: Verify communication. For example, verify that image is valid, greet/reject is valid, etc. + + err = myReadStatus.SetConversationReadUnreadStatus(myIdentityHash, senderIdentityHash, networkType, "Unread") + if (err != nil) { return err } + + // We add message to the MyChatMessages map list + + messageContentMap, err := getNewMessageMap("Sent", messageHash, [20]byte{}, myIdentityHash, senderIdentityHash, messageNetworkType, false, messageSentTimeUnix, messageCommunication) + if (err != nil) { return err } + + newMyChatMessagesMapList = append(newMyChatMessagesMapList, messageContentMap) + } + + if (len(newMyChatMessagesMapList) != 0){ + + // Some messages were successfully decrypted. + // We add them to our chat messages map list. + + allMyChatMessagesMapList := slices.Concat(myExistingMessagesMapList, newMyChatMessagesMapList) + + err = myChatMessagesMapListDatastore.OverwriteMapList(allMyChatMessagesMapList) + if (err != nil) { return err } + } + + return nil + } + + err := updateMyChatMessages() + if (err != nil) { return nil, err } + + err = mySettings.SetSetting(myIdentityType + "ChatMessagesReadyStatus", "Yes") + if (err != nil) { return nil, err } + } + + networkTypeString := helpers.ConvertByteToString(networkType) + + lookupMap := map[string]string{ + "NetworkType": networkTypeString, + } + + anyExist, updatedMapList, err := myChatMessagesMapListDatastore.GetMapListItems(lookupMap) + if (err != nil) { return nil, err } + if (anyExist == false){ + emptyMapList := make([]map[string]string, 0) + return emptyMapList, nil + } + + return updatedMapList, nil +} + + +func CheckIfUserHasMessagedMe(theirIdentityHash [16]byte, networkType byte)(bool, error){ + + theirIdentityHashString, userIdentityType, err := identity.EncodeIdentityHashBytesToString(theirIdentityHash) + if (err != nil) { + theirIdentityHashHex := encoding.EncodeBytesToHexString(theirIdentityHash[:]) + return false, errors.New("CheckIfUserHasMessagedMe called with invalid theirIdentityHash: " + theirIdentityHashHex) + } + if (userIdentityType == "Host"){ + return false, errors.New("CheckIfUserHasMessagedMe called with host identity.") + } + + isValid := helpers.VerifyNetworkType(networkType) + if (isValid == false){ + networkTypeString := helpers.ConvertByteToString(networkType) + return false, errors.New("CheckIfUserHasMessagedMe called with invalid networkType: " + networkTypeString) + } + + updateProgressFunction := func(_ int)error{ + return nil + } + + myChatMessagesMapList, err := GetUpdatedMyChatMessagesMapList(userIdentityType, networkType, updateProgressFunction) + if (err != nil) { return false, err } + + for _, messageMap := range myChatMessagesMapList{ + + messageTheirIdentityHash, exists := messageMap["TheirIdentityHash"] + if (exists == false) { + return false, errors.New("Malformed chat messages map list: missing theirIdentityHash") + } + + if (messageTheirIdentityHash != theirIdentityHashString){ + continue + } + + iAmSender, exists := messageMap["IAmSender"] + if (exists == false) { + return false, errors.New("Malformed myChatMessagesMapList: Missing IAmSender") + } + + if (iAmSender == "No"){ + return true, nil + } + } + + return false, nil +} + + +func CheckIfIHaveMessagedUser(theirIdentityHash [16]byte, networkType byte)(bool, error){ + + theirIdentityHashString, userIdentityType, err := identity.EncodeIdentityHashBytesToString(theirIdentityHash) + if (err != nil) { + theirIdentityHashHex := encoding.EncodeBytesToHexString(theirIdentityHash[:]) + return false, errors.New("CheckIfIHaveMessagedUser called with invalid theirIdentityHash: " + theirIdentityHashHex) + } + if (userIdentityType == "Host"){ + return false, errors.New("CheckIfIHaveMessagedUser called with host identity") + } + + isValid := helpers.VerifyNetworkType(networkType) + if (isValid == false){ + networkTypeString := helpers.ConvertByteToString(networkType) + return false, errors.New("CheckIfIHaveMessagedUser called with invalid networkType: " + networkTypeString) + } + + updateProgressFunction := func(_ int)error{ + return nil + } + + myChatMessagesMapList, err := GetUpdatedMyChatMessagesMapList(userIdentityType, networkType, updateProgressFunction) + if (err != nil) { return false, err } + + for _, messageMap := range myChatMessagesMapList{ + + messageTheirIdentityHash, exists := messageMap["TheirIdentityHash"] + if (exists == false) { + return false, errors.New("Malformed chat messages map list: missing TheirIdentityHash") + } + + if (messageTheirIdentityHash != theirIdentityHashString){ + continue + } + + iAmSender, exists := messageMap["IAmSender"] + if (exists == false) { + return false, errors.New("Malformed myChatMessagesMapList: Missing IAmSender") + } + + if (iAmSender == "Yes"){ + return true, nil + } + } + + return false, nil +} + +// This function retrieves the number of messages that exist between the user and another user +// We use this to warn the user before blocking a user about how many messages will be deleted +func GetNumberOfMyConversationMessages(theirIdentityHash [16]byte, networkType byte)(int, error){ + + theirIdentityHashString, theirIdentityType, err := identity.EncodeIdentityHashBytesToString(theirIdentityHash) + if (err != nil) { + theirIdentityHashHex := encoding.EncodeBytesToHexString(theirIdentityHash[:]) + return 0, errors.New("GetNumberOfMyConversationMessages called with invalid theirIdentityHash: " + theirIdentityHashHex) + } + if (theirIdentityType == "Host"){ + return 0, errors.New("GetNumberOfMyConversationMessages called with host identity.") + } + + isValid := helpers.VerifyNetworkType(networkType) + if (isValid == false){ + networkTypeString := helpers.ConvertByteToString(networkType) + return 0, errors.New("GetNumberOfMyConversationMessages called with invalid networkType: " + networkTypeString) + } + + updateProgressFunction := func(_ int)error{ + return nil + } + + chatMessagesMapList, err := GetUpdatedMyChatMessagesMapList(theirIdentityType, networkType, updateProgressFunction) + if (err != nil) { return 0, err } + + numberOfMessages := 0 + + for _, messageMap := range chatMessagesMapList{ + + currentTheirIdentityHash, exists := messageMap["TheirIdentityHash"] + if (exists == false) { + return 0, errors.New("Malformed chat messages map list: missing TheirIdentityHash") + } + + if (currentTheirIdentityHash == theirIdentityHashString){ + numberOfMessages += 1 + } + } + + return numberOfMessages, nil +} + +func GetNumberOfMessagesInMyInbox(myIdentityType string, networkType byte)(int, error){ + + identityExists, myIdentityHash, err := myIdentity.GetMyIdentityHash(myIdentityType) + if (err != nil) { return 0, err } + if (identityExists == false) { + return 0, nil + } + + isValid := helpers.VerifyNetworkType(networkType) + if (isValid == false){ + networkTypeString := helpers.ConvertByteToString(networkType) + return 0, errors.New("GetNumberOfMessagesInMyInbox called with invalid networkType: " + networkTypeString) + } + + updateProgressFunction := func(_ int)error{ + return nil + } + + myChatMessagesMapList, err := GetUpdatedMyChatMessagesMapList(myIdentityType, networkType, updateProgressFunction) + if (err != nil) { return 0, err } + + numberOfInboxMessages := 0 + + for _, messageMap := range myChatMessagesMapList{ + + messageMyIdentityHashString, exists := messageMap["MyIdentityHash"] + if (exists == false) { + return 0, errors.New("Malformed my chat messages map list: Item missing MyIdentityHash") + } + + messageMyIdentityHash, _, err := identity.ReadIdentityHashString(messageMyIdentityHashString) + if (err != nil){ + return 0, errors.New("Malformed myChatMessages map list: Item contains invalid MyIdentityHash: " + messageMyIdentityHashString) + } + + if (messageMyIdentityHash != myIdentityHash){ + return 0, errors.New("Malformed myChatMessages map list: Contains message that does not belong to my identity.") + } + + iAmSender, exists := messageMap["IAmSender"] + if (exists == false) { + return 0, errors.New("Malformed myChatMessagesMapList: Item missing IAmSender") + } + + if (iAmSender == "No"){ + numberOfInboxMessages += 1 + } + } + + return numberOfInboxMessages, nil +} + + +func CheckIfMessageIsDeleted(messageHash [26]byte)(bool, error){ + + messageHashHex := encoding.EncodeBytesToHexString(messageHash[:]) + + hasBeenDeleted, _, err := myDeletedMessagesMapDatastore.GetMapEntry(messageHashHex) + if (err != nil) { return false, err } + + return hasBeenDeleted, nil +} + +func CheckIfMessageIsUndecryptable(messageHash [26]byte)(bool, error){ + + messageHashHex := encoding.EncodeBytesToHexString(messageHash[:]) + + isUndecryptable, _, err := myUndecryptableMessagesMapDatastore.GetMapEntry(messageHashHex) + if (err != nil) { return false, err } + + return isUndecryptable, nil +} + +func CheckIfMessageIsImported(messageHash [26]byte, identityType string, messageNetworkType byte)(bool, error){ + + if (identityType != "Mate" && identityType != "Moderator"){ + return false, errors.New("CheckIfMessageIsImported called with invalid identityType: " + identityType) + } + + isValid := helpers.VerifyNetworkType(messageNetworkType) + if (isValid == false){ + messageNetworkTypeString := helpers.ConvertByteToString(messageNetworkType) + return false, errors.New("CheckIfMessageIsImported called with invalid messageNetworkType: " + messageNetworkTypeString) + } + + messageHashHex := encoding.EncodeBytesToHexString(messageHash[:]) + + updateProgressFunction := func(_ int)error{ + return nil + } + + myChatMessagesMapList, err := GetUpdatedMyChatMessagesMapList(identityType, messageNetworkType, updateProgressFunction) + if (err != nil) { return false, err } + + for _, messageMap := range myChatMessagesMapList{ + + currentMessageHash, exists := messageMap["MessageHash"] + if (exists == true){ + if (messageHashHex == currentMessageHash){ + return true, nil + } + } + } + + return false, nil +} + +// We use this type to represent messages returned from the GetMyConversationInfoAndSortedMessagesList function +type ConversationMessage struct{ + + // "Queued"/"Sent"/"Failed" + MessageStatus string + + // Sent == Message has been constructed and is in broadcast queue/has been broadcast + // If message is sent, then MessageHash exists. + MessageIsSent bool + + // If MessageHash exists, then MessageIdentifier is empty + // If MessageIdentifier exists, then MessageHash is empty + MessageHash [26]byte + MessageIdentifier [20]byte + + // Unix time of message sent time + TimeSent int64 + + // True if I am message sender, false if not. + IAmSender bool + + // The contents of the message communication + Communication string +} + +//This function returns all messages for a conversation and metadata about the conversation +//Inputs: +// -[16]byte: My Identity Hash +// -[16]byte: Their Identity Hash +// -byte: Network type of conversation +//Outputs: +// -bool: My identity found +// -bool: Conversation found (any messages found = true, no messages found = false) +// -[]ConversationMessage: Messages list (sorted by time sent) +// -int64: Conversation latest message sent Unix message time +// -bool: I have contacted them +// -bool: I have rejected them (if myIdentityType == Mate) +// -int64: Time of my most recent greet/rejection to them +// -bool: They have contacted me +// -bool: They have rejected me (if myIdentityType == Mate) +// -int64: Time of their most recent greet/rejection to me +// -error +func GetMyConversationInfoAndSortedMessagesList(myIdentityHash [16]byte, theirIdentityHash [16]byte, networkType byte) (bool, bool, []ConversationMessage, int64, bool, bool, int64, bool, bool, int64, error){ + + myIdentityFound, myIdentityType, err := myIdentity.CheckIfIdentityHashIsMine(myIdentityHash) + if (err != nil) { return false, false, nil, 0, false, false, 0, false, false, 0, err } + if (myIdentityFound == false){ + return false, false, nil, 0, false, false, 0, false, false, 0, nil + } + + theirIdentityType, err := identity.GetIdentityTypeFromIdentityHash(theirIdentityHash) + if (err != nil) { + theirIdentityHashHex := encoding.EncodeBytesToHexString(theirIdentityHash[:]) + return false, false, nil, 0, false, false, 0, false, false, 0, errors.New("GetMyConversationInfoAndSortedMessagesList called with invalid theirIdentityHash: " + theirIdentityHashHex) + } + if (theirIdentityType != myIdentityType){ + return false, false, nil, 0, false, false, 0, false, false, 0, errors.New("GetMyConversationInfoAndSortedMessagesList called with mismatched identityType identities.") + } + + isValid := helpers.VerifyNetworkType(networkType) + if (isValid == false){ + networkTypeString := helpers.ConvertByteToString(networkType) + return false, false, nil, 0, false, false, 0, false, false, 0, errors.New("GetMyConversationInfoAndSortedMessagesList called with invalid networkType: " + networkTypeString) + } + + updateProgressFunction := func(_ int)error{ + return nil + } + + myChatMessagesList, err := GetUpdatedMyChatMessagesMapList(myIdentityType, networkType, updateProgressFunction) + if (err != nil) { return false, false, nil, 0, false, false, 0, false, false, 0, err } + + if (len(myChatMessagesList) == 0) { + + return true, false, nil, 0, false, false, 0, false, false, 0, nil + } + + conversationMessagesList := make([]ConversationMessage, 0) + + lastMessageSentTimeUnix := int64(0) + + for _, messageMap := range myChatMessagesList { + + messageMyIdentityHashString, exists := messageMap["MyIdentityHash"] + if (exists == false) { + return false, false, nil, 0, false, false, 0, false, false, 0, errors.New("Malformed message map: Missing MyIdentityHash") + } + messageTheirIdentityHashString, exists := messageMap["TheirIdentityHash"] + if (exists == false) { + return false, false, nil, 0, false, false, 0, false, false, 0, errors.New("Malformed message map: Missing TheirIdentityHash") + } + + messageMyIdentityHash, _, err := identity.ReadIdentityHashString(messageMyIdentityHashString) + if (err != nil){ + return false, false, nil, 0, false, false, 0, false, false, 0, errors.New("Malformed message map: Contains invalid MyIdentityHash: " + messageMyIdentityHashString) + } + + messageTheirIdentityHash, _, err := identity.ReadIdentityHashString(messageTheirIdentityHashString) + if (err != nil){ + return false, false, nil, 0, false, false, 0, false, false, 0, errors.New("Malformed message map: Contains invalid TheirIdentityHash: " + messageTheirIdentityHashString) + } + + if (messageMyIdentityHash != myIdentityHash){ + // This must be a message from an earlier identity which we deleted + // This should not happen, because all of our identity's messages should be deleted when we delete our identity + return false, false, nil, 0, false, false, 0, false, false, 0, errors.New("Malformed message map: Contains message for identity which is not our current identity.") + } + if (messageTheirIdentityHash != theirIdentityHash) { + continue + } + + messageStatus, exists := messageMap["MessageStatus"] + if (exists == false){ + return false, false, nil, 0, false, false, 0, false, false, 0, errors.New("Malformed message map: Missing MessageStatus.") + } + + iAmSenderString, exists := messageMap["IAmSender"] + if (exists == false) { + return false, false, nil, 0, false, false, 0, false, false, 0, errors.New("Malformed message map: Missing IAmSender.") + } + + timeSentUnixString, exists := messageMap["TimeSent"] + if (exists == false) { + return false, false, nil, 0, false, false, 0, false, false, 0, errors.New("Malformed message map: Missing TimeSent.") + } + + communicationString, exists := messageMap["Communication"] + if (exists == false) { + return false, false, nil, 0, false, false, 0, false, false, 0, errors.New("Malformed message map: Missing Communication") + } + + iAmSender, err := helpers.ConvertYesOrNoStringToBool(iAmSenderString) + if (err != nil){ + return false, false, nil, 0, false, false, 0, false, false, 0, errors.New("Malformed message map: Contains invalid IAmSender: " + iAmSenderString) + } + + timeSentUnix, err := helpers.ConvertStringToInt64(timeSentUnixString) + if (err != nil) { + return false, false, nil, 0, false, false, 0, false, false, 0, errors.New("Malformed message map: Contains invalid TimeSent: " + timeSentUnixString) + } + + if (lastMessageSentTimeUnix < timeSentUnix){ + lastMessageSentTimeUnix = timeSentUnix + } + + newMessageObject := ConversationMessage{} + + newMessageObject.MessageStatus = messageStatus + + if (messageStatus == "Sent"){ + + newMessageObject.MessageIsSent = true + + messageHashString, exists := messageMap["MessageHash"] + if (exists == false) { + return false, false, nil, 0, false, false, 0, false, false, 0, errors.New("Malformed message map: Missing MessageHash.") + } + + messageHashArray, err := readMessages.ReadMessageHashHex(messageHashString) + if (err != nil) { + return false, false, nil, 0, false, false, 0, false, false, 0, errors.New("Malformed message map: Contains invalid MessageHash: " + messageHashString) + } + + newMessageObject.MessageHash = messageHashArray + } else { + + newMessageObject.MessageIsSent = false + + messageIdentifierString, exists := messageMap["MessageIdentifier"] + if (exists == false){ + return false, false, nil, 0, false, false, 0, false, false, 0, errors.New("Malformed message map: Missing MessageIdentifier.") + } + + messageIdentifierBytes, err := encoding.DecodeHexStringToBytes(messageIdentifierString) + if (err != nil){ + return false, false, nil, 0, false, false, 0, false, false, 0, errors.New("Malformed message map: Contains invalid MessageIdentifier: Not Hex: " + messageIdentifierString) + } + + if (len(messageIdentifierBytes) != 20){ + return false, false, nil, 0, false, false, 0, false, false, 0, errors.New("Malformed message map: Contains invalid MessageIdentifier: Invalid length: " + messageIdentifierString) + } + + messageIdentifierArray := [20]byte(messageIdentifierBytes) + + newMessageObject.MessageIdentifier = messageIdentifierArray + } + + newMessageObject.TimeSent = timeSentUnix + newMessageObject.IAmSender = iAmSender + newMessageObject.Communication = communicationString + + conversationMessagesList = append(conversationMessagesList, newMessageObject) + } + + if (len(conversationMessagesList) == 0){ + return true, false, nil, 0, false, false, 0, false, false, 0, nil + } + + // Now we sort messages oldest to newest + + compareMessagesFunction := func(messageA ConversationMessage, messageB ConversationMessage)int { + + aTimeSent := messageA.TimeSent + bTimeSent := messageB.TimeSent + + if (aTimeSent == bTimeSent){ + + // We sort messages so they always appear in the same order + + aIAmSender := messageA.IAmSender + bIAmSender := messageB.IAmSender + + if (aIAmSender == bIAmSender){ + + aCommunication := messageA.Communication + bCommunication := messageB.Communication + + if (aCommunication == bCommunication){ + return 0 + } + if (aCommunication < bCommunication){ + return -1 + } + return 1 + } + if (aIAmSender == false){ + return -1 + } + return 1 + } + + if (aTimeSent < bTimeSent){ + return -1 + } + + return 1 + } + + slices.SortFunc(conversationMessagesList, compareMessagesFunction) + + // Now we get conversation greet or reject status + + // A user has greeted another user if they have sent them any message. It does not have to be a Greet message + // A rejection can only be undone by a greet + // Any messages sent after a rejection do not cancel the rejection, unless they are Greet messages + + // Moderators cannot send greet/reject messages. TODO: Deal with this reality. + + iHaveContactedThem := false + iHaveRejectedThem := false + timeOfMyMostRecentGreetOrReject := int64(0) + theyHaveContactedMe := false + theyHaveRejectedMe := false + timeOfTheirMostRecentGreetOrReject := int64(0) + + // We iterate through list from oldest to newest message + + for _, currentMessageObject := range conversationMessagesList{ + + iAmSender := currentMessageObject.IAmSender + + if (iAmSender == true){ + iHaveContactedThem = true + } else { + theyHaveContactedMe = true + } + + messageTimeSent := currentMessageObject.TimeSent + messageCommunication := currentMessageObject.Communication + + if (messageCommunication == ">!>Greet"){ + if (iAmSender == true){ + iHaveRejectedThem = false + timeOfMyMostRecentGreetOrReject = messageTimeSent + } else { + theyHaveRejectedMe = false + timeOfTheirMostRecentGreetOrReject = messageTimeSent + } + } else if (messageCommunication == ">!>Reject"){ + if (iAmSender == true){ + iHaveRejectedThem = true + timeOfMyMostRecentGreetOrReject = messageTimeSent + } else { + theyHaveRejectedMe = true + timeOfTheirMostRecentGreetOrReject = messageTimeSent + } + } else { + if (iAmSender == true){ + if (iHaveRejectedThem == false){ + timeOfMyMostRecentGreetOrReject = messageTimeSent + } + } else{ + if (theyHaveRejectedMe == false){ + timeOfTheirMostRecentGreetOrReject = messageTimeSent + } + } + } + } + + return true, true, conversationMessagesList, lastMessageSentTimeUnix, iHaveContactedThem, iHaveRejectedThem, timeOfMyMostRecentGreetOrReject, theyHaveContactedMe, theyHaveRejectedMe, timeOfTheirMostRecentGreetOrReject, nil +} + + +func DeleteAllPeerConversationMessages(theirIdentityHash [16]byte)error{ + + theirIdentityHashString, theirIdentityType, err := identity.EncodeIdentityHashBytesToString(theirIdentityHash) + if (err != nil){ + theirIdentityHashHex := encoding.EncodeBytesToHexString(theirIdentityHash[:]) + return errors.New("DeleteAllPeerConversationMessages called with invalid theirIdentityHash: " + theirIdentityHashHex) + } + if (theirIdentityType == "Host"){ + return errors.New("DeleteAllPeerConversationMessages called with host identity.") + } + + myIdentityExists, myIdentityHash, err := myIdentity.GetMyIdentityHash(theirIdentityType) + if (err != nil) { return err } + if (myIdentityExists == false){ + // No messages should exist from this user. + return nil + } + + updatingChatMessagesMutex.Lock() + defer updatingChatMessagesMutex.Unlock() + + myChatMessagesMapListDatastore, err := getMyChatMessagesMapListDatastore(theirIdentityType) + if (err != nil) { return err } + + myChatMessagesMapList, err := myChatMessagesMapListDatastore.GetMapList() + if (err != nil) { return err } + + newMyChatMessagesMapList := make([]map[string]string, 0) + + for _, messageMap := range myChatMessagesMapList{ + + messageMyIdentityHashString, exists := messageMap["MyIdentityHash"] + if (exists == false) { + return errors.New("Malformed chat messages map list: missing MyIdentityHash") + } + + messageMyIdentityHash, _, err := identity.ReadIdentityHashString(messageMyIdentityHashString) + if (err != nil){ + return errors.New("Malformed chat messages map list: Contains invalid MyIdentityHash: " + messageMyIdentityHashString) + } + + if (messageMyIdentityHash != myIdentityHash){ + // A message exists that does not belong to our identity + // This should not happen, because when we delete our identity, we delete all of our chat messages + return errors.New("MyChatMessagesMapList contains message with identity that is not ours.") + } + + messageTheirIdentityHash, exists := messageMap["TheirIdentityHash"] + if (exists == false) { + return errors.New("Malformed myChatMessagesMapList: Item missing TheirIdentityHash") + } + + if (messageTheirIdentityHash != theirIdentityHashString){ + newMyChatMessagesMapList = append(newMyChatMessagesMapList, messageMap) + } + } + + err = myChatMessagesMapListDatastore.OverwriteMapList(newMyChatMessagesMapList) + if (err != nil) { return err } + + return nil +} + + +//Outputs: +// -bool: My identity exists +// -[][16]byte: List of all chat recipients that the user has not blocked +// -error +func GetAllMyNonBlockedChatRecipients(myIdentityType string, networkType byte)(bool, [][16]byte, error){ + + if (myIdentityType != "Mate" && myIdentityType != "Moderator"){ + return false, nil, errors.New("GetAllMyNonBlockedChatRecipients called with invalid identity type: " + myIdentityType) + } + + identityExists, myIdentityHash, err := myIdentity.GetMyIdentityHash(myIdentityType) + if (err != nil) { return false, nil, err } + if (identityExists == false){ + return false, nil, nil + } + + updateProgressFunction := func(_ int)error{ + return nil + } + + myChatMessagesMapList, err := GetUpdatedMyChatMessagesMapList(myIdentityType, networkType, updateProgressFunction) + if (err != nil) { return false, nil, err } + + if (len(myChatMessagesMapList) == 0){ + + emptyList := make([][16]byte, 0) + return true, emptyList, nil + } + + // We use a map to avoid duplicates + + // Map Structure: Recipient Identity Hash -> Nothing + chatRecipientsMap := make(map[[16]byte]struct{}) + + for _, messageMap := range myChatMessagesMapList{ + + messageMyIdentityHashString, exists := messageMap["MyIdentityHash"] + if (exists == false) { + return false, nil, errors.New("Malformed myChatMessagesMapList: Item missing MyIdentityHash") + } + + messageMyIdentityHash, _, err := identity.ReadIdentityHashString(messageMyIdentityHashString) + if (err != nil){ + return false, nil, errors.New("Malformed myChatMessagesMapList: Item contains invalid MyIdentityHash: " + messageMyIdentityHashString) + } + + if (messageMyIdentityHash != myIdentityHash){ + return false, nil, errors.New("myChatMessagesMapList contains message that does not belong to my identity.") + } + + messageTheirIdentityHashString, exists := messageMap["TheirIdentityHash"] + if (exists == false) { + return false, nil, errors.New("Malformed myChatMessagesMapList: Item missing TheirIdentityHash") + } + + messageTheirIdentityHash, _, err := identity.ReadIdentityHashString(messageTheirIdentityHashString) + if (err != nil){ + return false, nil, errors.New("Malformed myChatMessagesMapList: Item contains invalid TheirIdentityHash: " + messageTheirIdentityHashString) + } + + chatRecipientsMap[messageTheirIdentityHash] = struct{}{} + } + + nonBlockedChatRecipientsList := make([][16]byte, 0) + + for userIdentityHash, _ := range chatRecipientsMap{ + + isBlocked, _, _, _, err := myBlockedUsers.CheckIfUserIsBlocked(userIdentityHash) + if (err != nil) { return false, nil, err } + if (isBlocked == false){ + nonBlockedChatRecipientsList = append(nonBlockedChatRecipientsList, userIdentityHash) + } + } + + return true, nonBlockedChatRecipientsList, nil +} + + +// This function adds a new user sent/failed/queued message to myChatMessages map list where user is sender +// We also remove the queued message map if message is sent/failed +func AddMyNewMessageToMyChatMessages( + messageStatus string, + messageHash [26]byte, + messageIdentifier [20]byte, + myIdentityHash [16]byte, + theirIdentityHash [16]byte, + messageNetworkType byte, + timeSentUnix int64, + communication string)error{ + + identityIsMine, myIdentityType, err := myIdentity.CheckIfIdentityHashIsMine(myIdentityHash) + if (err != nil) { return err } + if (identityIsMine == false){ + return errors.New("AddMyNewMessageToMyChatMessages called with identity that is not mine.") + } + + if (myIdentityType != "Mate" && myIdentityType != "Moderator"){ + return errors.New("AddMyNewMessageToMyChatMessages called with host identity.") + } + + newMessageMap, err := getNewMessageMap(messageStatus, messageHash, messageIdentifier, myIdentityHash, theirIdentityHash, messageNetworkType, true, timeSentUnix, communication) + if (err != nil) { return err } + + updatingChatMessagesMutex.Lock() + defer updatingChatMessagesMutex.Unlock() + + myChatMessagesMapListDatastore, err := getMyChatMessagesMapListDatastore(myIdentityType) + if (err != nil) { return err } + + existingMessagesMapList, err := myChatMessagesMapListDatastore.GetMapList() + if (err != nil) { return err } + + getNewChatMessagesMapList := func()([]map[string]string, error){ + + if (messageStatus == "Queued"){ + + newChatMessagesMapList := append(existingMessagesMapList, newMessageMap) + return newChatMessagesMapList, nil + } + + // Chat message status is either Sent or Failed + // This means it will replace an existing queued message + // We remove that message from the existing map list + + newChatMessagesMapList := make([]map[string]string, 0) + + for _, messageMap := range existingMessagesMapList{ + + currentMessageIdentifierString, exists := messageMap["MessageIdentifier"] + if (exists == false){ + return nil, errors.New("MyChatMessages map list item missing MessageIdentifier") + } + + currentMessageIdentifier, err := encoding.DecodeHexStringToBytes(currentMessageIdentifierString) + if (err != nil){ + return nil, errors.New("MyChatMessages map list item contains invalid MessageIdentifier: " + currentMessageIdentifierString) + } + + if (len(currentMessageIdentifier) != 20){ + return nil, errors.New("MyChatMessages map list item contains invalid MessageIdentifier: " + currentMessageIdentifierString) + } + + areEqual := bytes.Equal(currentMessageIdentifier, messageIdentifier[:]) + if (areEqual == false){ + newChatMessagesMapList = append(newChatMessagesMapList, messageMap) + continue + } + + currentMessageStatus, exists := messageMap["MessageStatus"] + if (exists == false){ + return nil, errors.New("MyChatMessages map list item missing MessageStatus") + } + if (currentMessageStatus != "Queued"){ + return nil, errors.New("AddMyNewMessageToMyChatMessages called to replace existing message that is not queued: " + currentMessageStatus) + } + + // This is reached if the message is the one which we want to remove + // We do not add the message + } + + newChatMessagesMapList = append(newChatMessagesMapList, newMessageMap) + + return newChatMessagesMapList, nil + } + + newChatMessagesMapList, err := getNewChatMessagesMapList() + if (err != nil) { return err } + + err = myChatMessagesMapListDatastore.OverwriteMapList(newChatMessagesMapList) + if (err != nil) { return err } + + return nil +} + +// MessageHash should be empty ("") if messageStatus == "Queued" or "Failed" +// messageIdentifier should be empty if messageStatus == "Sent" +func getNewMessageMap(messageStatus string, messageHash [26]byte, messageIdentifier [20]byte, myIdentityHash [16]byte, theirIdentityHash [16]byte, messageNetworkType byte, iAmSender bool, timeSentUnix int64, communication string)(map[string]string, error){ + + if (messageStatus != "Sent" && messageStatus != "Queued" && messageStatus != "Failed"){ + return nil, errors.New("getNewMessageMap called with invalid messageStatus: " + messageStatus) + } + + myIdentityHashString, myIdentityType, err := identity.EncodeIdentityHashBytesToString(myIdentityHash) + if (err != nil) { + myIdentityHashString := encoding.EncodeBytesToHexString(myIdentityHash[:]) + return nil, errors.New("getNewMessageMap called with invalid my identity hash: " + myIdentityHashString) + } + + theirIdentityHashString, theirIdentityType, err := identity.EncodeIdentityHashBytesToString(theirIdentityHash) + if (err != nil) { + theirIdentityHashString := encoding.EncodeBytesToHexString(theirIdentityHash[:]) + return nil, errors.New("getNewMessageMap called with invalid theirIdentityHash: " + theirIdentityHashString) + } + + if (myIdentityType != theirIdentityType){ + return nil, errors.New("Trying to create message content map between different identityTypes") + } + if (myIdentityType == "Host"){ + return nil, errors.New("Trying to create message content map from Host identityType") + } + + isValid := helpers.VerifyNetworkType(messageNetworkType) + if (isValid == false){ + messageNetworkTypeString := helpers.ConvertByteToString(messageNetworkType) + return nil, errors.New("getNewMessageMap called with invalid messageNetworkType: " + messageNetworkTypeString) + } + + messageNetworkTypeString := helpers.ConvertByteToString(messageNetworkType) + + iAmSenderString := helpers.ConvertBoolToYesOrNoString(iAmSender) + + if (timeSentUnix > time.Now().Unix() + 10){ + return nil, errors.New("getNewMessageMap called with invalid time sent unix: is in the future.") + } + + isAllowed := allowedText.VerifyStringIsAllowed(communication) + if (isAllowed == false){ + return nil, errors.New("getNewMessageMap called with communication containing unallowed text.") + } + + timeSentUnixString := helpers.ConvertInt64ToString(timeSentUnix) + + messageContentMap := map[string]string{ + "MessageStatus": messageStatus, + "MyIdentityHash": myIdentityHashString, + "TheirIdentityHash": theirIdentityHashString, + "NetworkType": messageNetworkTypeString, + "IAmSender": iAmSenderString, + "TimeSent": timeSentUnixString, + "Communication": communication, + } + + if (messageStatus == "Sent"){ + + messageHashHex := encoding.EncodeBytesToHexString(messageHash[:]) + + messageContentMap["MessageHash"] = messageHashHex + } else { + + // messageStatus == "Queued" || messageStatus == "Failed" + + // Queued messages are messages that are queued to be sent, but are not sent yet + // They are displayed differently in the GUI + // They do not have a messageHash, because the raw encrypted message has not been created yet + // Failed messages are messages for which we were not able to broadcast, so they don't have a message hash either. + + messageIdentifierHex := encoding.EncodeBytesToHexString(messageIdentifier[:]) + + messageContentMap["MessageIdentifier"] = messageIdentifierHex + } + + return messageContentMap, nil +} + + diff --git a/internal/messaging/myCipherKeys/myCipherKeys.go b/internal/messaging/myCipherKeys/myCipherKeys.go new file mode 100644 index 0000000..9543d3a --- /dev/null +++ b/internal/messaging/myCipherKeys/myCipherKeys.go @@ -0,0 +1,64 @@ + +// myCipherKeys provides functions to save a user's received message cipher keys +// The user stores these in case they want to report a message +// They share the message's cipher key in their report, enabling moderators to decrypt and review the message + +package myCipherKeys + +//TODO: Add a function to prune old cipher keys for deleted and expired messages + +import "seekia/internal/encoding" +import "seekia/internal/myDatastores/myMap" +import "seekia/internal/messaging/readMessages" + +import "errors" + +var messageCipherKeysMapDatastore *myMap.MyMap + +// This function must be called whenever an app user signs in +func InitializeMyMessageCipherKeysDatastore()error{ + + newMessagesCipherKeysMapDatastore, err := myMap.CreateNewMap("MyMessageCipherKeys") + if (err != nil) { return err } + + messageCipherKeysMapDatastore = newMessagesCipherKeysMapDatastore + + return nil +} + +func SaveMessageCipherKey(messageHash [26]byte, messageCipherKey [32]byte)error{ + + messageHashString := encoding.EncodeBytesToHexString(messageHash[:]) + messageCipherKeyString := encoding.EncodeBytesToHexString(messageCipherKey[:]) + + err := messageCipherKeysMapDatastore.SetMapEntry(messageHashString, messageCipherKeyString) + if (err != nil) { return err } + + return nil +} + + +//Outputs: +// -bool: Message Cipher key found +// -[32]byte: Message cipher key +// -error +func GetMessageCipherKey(messageHash [26]byte)(bool, [32]byte, error){ + + messageHashHex := encoding.EncodeBytesToHexString(messageHash[:]) + + exists, messageCipherKeyHex, err := messageCipherKeysMapDatastore.GetMapEntry(messageHashHex) + if (err != nil) { return false, [32]byte{}, err } + if (exists == false) { + return false, [32]byte{}, nil + } + + messageCipherKey, err := readMessages.ReadMessageCipherKeyHex(messageCipherKeyHex) + if (err != nil){ + return false, [32]byte{}, errors.New("messageCipherKeysMapDatastore contains invalid messageCipherKey: " + err.Error()) + } + + return true, messageCipherKey, nil +} + + + diff --git a/internal/messaging/myConversationIndexes/myConversationIndexes.go b/internal/messaging/myConversationIndexes/myConversationIndexes.go new file mode 100644 index 0000000..da6e46f --- /dev/null +++ b/internal/messaging/myConversationIndexes/myConversationIndexes.go @@ -0,0 +1,121 @@ + +// myConversationIndexes provides functions to keep track of the viewed message index of conversations +// Each index describes the page of messages that the user is viewing for each conversation + +package myConversationIndexes + +//TODO: Create function to prune indexes for conversations that no longer exist + +import "seekia/internal/encoding" +import "seekia/internal/helpers" +import "seekia/internal/identity" +import "seekia/internal/myDatastores/myMap" + +import "errors" + +var myConversationViewIndexesMapDatastore *myMap.MyMap + +// This function must be called whenever an app user signs in +func InitializeMyConversationIndexesDatastore()error{ + + newMyConversationViewIndexesMapDatastore, err := myMap.CreateNewMap("MyConversationViewIndexes") + if (err != nil) { return err } + + myConversationViewIndexesMapDatastore = newMyConversationViewIndexesMapDatastore + + return nil +} + + +func SetConversationMessageViewIndex(myIdentityHash [16]byte, theirIdentityHash [16]byte, networkType byte, viewIndex int)error{ + + myIdentityHashString, myIdentityType, err := identity.EncodeIdentityHashBytesToString(myIdentityHash) + if (err != nil){ + myIdentityHashHex := encoding.EncodeBytesToHexString(myIdentityHash[:]) + return errors.New("SetConversationMessageViewIndex called with invalid myIdentityHash: " + myIdentityHashHex) + } + + theirIdentityHashString, theirIdentityType, err := identity.EncodeIdentityHashBytesToString(theirIdentityHash) + if (err != nil){ + theirIdentityHashHex := encoding.EncodeBytesToHexString(theirIdentityHash[:]) + return errors.New("SetConversationMessageViewIndex called with invalid theirIdentityHash: " + theirIdentityHashHex) + } + + if (myIdentityType != theirIdentityType){ + return errors.New("SetConversationMessageViewIndex called with mismatched my/their identityTypes.") + } + if (myIdentityType == "Host"){ + return errors.New("SetConversationMessageViewIndex called with Host identityTypes.") + } + + isValid := helpers.VerifyNetworkType(networkType) + if (isValid == false){ + networkTypeString := helpers.ConvertByteToString(networkType) + return errors.New("GetConversationMessageViewIndex called with invalid networkType: " + networkTypeString) + } + + networkTypeString := helpers.ConvertByteToString(networkType) + + conversationKey := myIdentityHashString + "+" + theirIdentityHashString + "@" + networkTypeString + + mapEntryValue := helpers.ConvertIntToString(viewIndex) + + err = myConversationViewIndexesMapDatastore.SetMapEntry(conversationKey, mapEntryValue) + if (err != nil) { return err } + + return nil +} + +//Outputs: +// -bool: Index exists +// -int: Conversation index +// -error +func GetConversationMessageViewIndex(myIdentityHash [16]byte, theirIdentityHash [16]byte, networkType byte)(bool, int, error){ + + myIdentityHashString, myIdentityType, err := identity.EncodeIdentityHashBytesToString(myIdentityHash) + if (err != nil){ + myIdentityHashHex := encoding.EncodeBytesToHexString(myIdentityHash[:]) + return false, 0, errors.New("GetConversationMessageViewIndex called with invalid myIdentityHash: " + myIdentityHashHex) + } + + theirIdentityHashString, theirIdentityType, err := identity.EncodeIdentityHashBytesToString(theirIdentityHash) + if (err != nil){ + theirIdentityHashHex := encoding.EncodeBytesToHexString(theirIdentityHash[:]) + return false, 0, errors.New("GetConversationMessageViewIndex called with invalid theirIdentityHash: " + theirIdentityHashHex) + } + + if (myIdentityType != theirIdentityType){ + return false, 0, errors.New("GetConversationMessageViewIndex called with mismatched my/their identityTypes.") + } + + if (myIdentityType == "Host"){ + return false, 0, errors.New("GetConversationMessageViewIndex called with Host identityTypes.") + } + + isValid := helpers.VerifyNetworkType(networkType) + if (isValid == false){ + networkTypeString := helpers.ConvertByteToString(networkType) + return false, 0, errors.New("GetConversationMessageViewIndex called with invalid networkType: " + networkTypeString) + } + + networkTypeString := helpers.ConvertByteToString(networkType) + + conversationKey := myIdentityHashString + "+" + theirIdentityHashString + "@" + networkTypeString + + exists, viewIndexString, err := myConversationViewIndexesMapDatastore.GetMapEntry(conversationKey) + if (err != nil) { return false, 0, err } + if (exists == false) { + return false, 0, nil + } + + viewIndex, err := helpers.ConvertStringToInt(viewIndexString) + if (err != nil) { + return false, 0, errors.New("myConversationViewIndexesMapDatastore malformed: Contains invalid view index: " + viewIndexString) + } + + return true, viewIndex, nil +} + + + + diff --git a/internal/messaging/myInbox/myInbox.go b/internal/messaging/myInbox/myInbox.go new file mode 100644 index 0000000..8eb9045 --- /dev/null +++ b/internal/messaging/myInbox/myInbox.go @@ -0,0 +1,149 @@ + +// myInbox provides functions to get a user's public and secret inboxes + +package myInbox + +import "seekia/internal/encoding" +import "seekia/internal/helpers" +import "seekia/internal/myIdentity" +import "seekia/internal/messaging/inbox" +import "seekia/internal/messaging/mySecretInboxes" + +import "errors" + + +// Returns all inboxes that we want to download messages from +// It will omit inboxes that are old enough that they have no messages, or ones we have checked sufficiently +func GetAllMyActiveInboxes(myIdentityHash [16]byte, networkType byte)([][10]byte, error){ + + myPublicInbox, err := inbox.GetPublicInboxFromIdentityHash(myIdentityHash) + if (err != nil) { + myIdentityHashHex := encoding.EncodeBytesToHexString(myIdentityHash[:]) + return nil, errors.New("GetAllMyActiveInboxes called with invalid identity hash: " + myIdentityHashHex) + } + + isValid := helpers.VerifyNetworkType(networkType) + if (isValid == false){ + networkTypeString := helpers.ConvertByteToString(networkType) + return nil, errors.New("GetAllMyActiveInboxes called with invalid networkType: " + networkTypeString) + } + + myInboxesList := [][10]byte{myPublicInbox} + + myActiveSecretInboxesList, err := mySecretInboxes.GetAllMyActiveSecretInboxes(myIdentityHash, networkType) + if (err != nil) { return nil, err } + + myInboxesList = append(myInboxesList, myActiveSecretInboxesList...) + + return myInboxesList, nil +} + +// Returns all inboxes, even ones we have already sufficiently checked +// Will not return inboxes for users I have blocked +func GetAllMyInboxes(myIdentityHash [16]byte, networkType byte)([][10]byte, error){ + + myPublicInbox, err := inbox.GetPublicInboxFromIdentityHash(myIdentityHash) + if (err != nil) { + myIdentityHashHex := encoding.EncodeBytesToHexString(myIdentityHash[:]) + return nil, errors.New("GetAllMyInboxes called with invalid identity hash: " + myIdentityHashHex) + } + + isValid := helpers.VerifyNetworkType(networkType) + if (isValid == false){ + networkTypeString := helpers.ConvertByteToString(networkType) + return nil, errors.New("GetAllMyInboxes called with invalid networkType: " + networkTypeString) + } + + myInboxesList := [][10]byte{myPublicInbox} + + mySecretInboxesList, err := mySecretInboxes.GetAllMySecretInboxes(myIdentityHash, networkType) + if (err != nil) { return nil, err } + + myInboxesList = append(myInboxesList, mySecretInboxesList...) + + return myInboxesList, nil +} + + +//Outputs: +// -bool: Inbox is mine +// -string: Identity type of inbox +// -error +func CheckIfInboxIsMine(inputInbox [10]byte, networkType byte)(bool, string, error){ + + isValid := helpers.VerifyNetworkType(networkType) + if (isValid == false){ + networkTypeString := helpers.ConvertByteToString(networkType) + return false, "", errors.New("CheckIfInboxIsMine called with invalid networkType: " + networkTypeString) + } + + chatIdentityTypesList := []string{"Mate", "Moderator"} + + for _, identityType := range chatIdentityTypesList{ + + myIdentityFound, myIdentityHash, err := myIdentity.GetMyIdentityHash(identityType) + if (err != nil) { return false, "", err } + if (myIdentityFound == false){ + continue + } + + myInboxesList, err := GetAllMyInboxes(myIdentityHash, networkType) + if (err != nil) { return false, "", err } + + for _, inbox := range myInboxesList{ + + if (inbox == inputInbox){ + return true, identityType, nil + } + } + } + + return false, "", nil +} + + +//Outputs: +// -bool: Inbox found +// -[16]byte: Inbox My Identity Hash +// -[32]byte: inbox doubledSealedKeys sealer key +// -bool: Inbox is secret +// -byte: Inbox network type (if inbox is secret) +// -[16]byte: Conversation recipient (if inbox is secret) +// -error +func GetMyInboxInfo(inputInbox [10]byte)(bool, [16]byte, [32]byte, bool, byte, [16]byte, error){ + + chatIdentityTypesList := []string{"Mate", "Moderator"} + + for _, identityType := range chatIdentityTypesList{ + + myIdentityExists, myIdentityHash, err := myIdentity.GetMyIdentityHash(identityType) + if (err != nil) { return false, [16]byte{}, [32]byte{}, false, 0, [16]byte{}, err } + if (myIdentityExists == false){ + continue + } + + myPublicInbox, err := inbox.GetPublicInboxFromIdentityHash(myIdentityHash) + if (err != nil) { return false, [16]byte{}, [32]byte{}, false, 0, [16]byte{}, err } + + if (inputInbox == myPublicInbox){ + + doubleSealedKeysSealerKey, err := inbox.GetPublicInboxSealerKeyFromIdentityHash(myIdentityHash) + if (err != nil) { return false, [16]byte{}, [32]byte{}, false, 0, [16]byte{}, err } + + return true, myIdentityHash, doubleSealedKeysSealerKey, false, 0, [16]byte{}, nil + } + } + + inboxFound, inboxMyIdentityHash, conversationRecipientIdentityHash, inboxNetworkType, secretInboxSealerKey, _, _, err := mySecretInboxes.GetMySecretInboxInfo(inputInbox) + if (err != nil){ return false, [16]byte{}, [32]byte{}, false, 0, [16]byte{}, err } + if (inboxFound == true){ + return true, inboxMyIdentityHash, secretInboxSealerKey, true, inboxNetworkType, conversationRecipientIdentityHash, nil + } + + return false, [16]byte{}, [32]byte{}, false, 0, [16]byte{}, nil +} + + + + + diff --git a/internal/messaging/myMessageQueue/myMessageQueue.go b/internal/messaging/myMessageQueue/myMessageQueue.go new file mode 100644 index 0000000..e8f9319 --- /dev/null +++ b/internal/messaging/myMessageQueue/myMessageQueue.go @@ -0,0 +1,349 @@ + +// myMessageQueue provides functions to add messages we want to send to the message queue +// All messages are added to the queue before being sent +// This is useful for 2 reasons. +// 1. If the account credit server is down/unreachable, we can automatically retry funding the message later +// 2. We can queue messages to send to users whose chat keys we do not yet have (and may not exist on the network at all) + +package myMessageQueue + +// For users whose keys are missing, we will wait for their chat keys to download in the background +// If we can't download their chat keys after trying for a certain time, we will discard the message + +//TODO: If we disable our profile or delete our identity, clear our chat message queue +//TODO: A way to view the message queue through the GUI + +import "seekia/internal/encoding" +import "seekia/internal/helpers" +import "seekia/internal/identity" +import "seekia/internal/messaging/myChatMessages" +import "seekia/internal/messaging/peerChatKeys" +import "seekia/internal/messaging/sendMessages" +import "seekia/internal/myDatastores/myMapList" +import "seekia/internal/myIdentity" +import "seekia/internal/network/myAccountCredit" +import "seekia/internal/parameters/getParameters" +import "seekia/internal/unixTime" + +import "errors" +import "time" +import "sync" + +// This will be locked whenever we are updating the message queue map list +var updatingMessageQueueMapListMutex sync.Mutex + +var myMessageQueueMapListDatastore *myMapList.MyMapList + +// This function must be called whenever an app user signs in +func InitializeMyMessageQueueDatastore()error{ + + updatingMessageQueueMapListMutex.Lock() + defer updatingMessageQueueMapListMutex.Unlock() + + newMyMessageQueueMapListDatastore, err := myMapList.CreateNewMapList("MyMessageQueue") + if (err != nil) { return err } + + myMessageQueueMapListDatastore = newMyMessageQueueMapListDatastore + + return nil +} + + +//Outputs: +// -bool: Parameters exist +// -bool: Sufficient credits exist +// -error +func AddMessageToMyMessageQueue(myIdentityHash [16]byte, recipientIdentityHash [16]byte, messageNetworkType byte, messageDuration int, messageCommunication string)(bool, bool, error){ + + // We keep track of our identity to make sure if we don't clear the queue properly between changing user/importing new identity, messages will not be sent by the wrong identity + + myIdentityExists, myIdentityType, err := myIdentity.CheckIfIdentityHashIsMine(myIdentityHash) + if (err != nil) { return false, false, err } + if (myIdentityExists == false){ + return false, false, errors.New("AddMessageToMyMessageQueue called with an unknown identity.") + } + + myIdentityHashString, _, err := identity.EncodeIdentityHashBytesToString(myIdentityHash) + if (err != nil) { + myIdentityHashHex := encoding.EncodeBytesToHexString(myIdentityHash[:]) + return false, false, errors.New("CheckIfIdentityHashIsMine returning invalid identity hash: " + myIdentityHashHex) + } + + recipientIdentityHashString, recipientIdentityType, err := identity.EncodeIdentityHashBytesToString(recipientIdentityHash) + if (err != nil) { + recipientIdentityHashHex := encoding.EncodeBytesToHexString(recipientIdentityHash[:]) + return false, false, errors.New("AddMessageToMyMessageQueue called with invalid recipientIdentityHash: " + recipientIdentityHashHex) + } + + if (myIdentityType != recipientIdentityType){ + return false, false, errors.New("AddMessageToMyMessageQueue called with mismatched my/recipient identityTypes.") + } + + isValid := helpers.VerifyNetworkType(messageNetworkType) + if (isValid == false){ + messageNetworkTypeString := helpers.ConvertByteToString(messageNetworkType) + return false, false, errors.New("AddMessageToMyMessageQueue called with invalid messageNetworkType: " + messageNetworkTypeString) + } + + messageNetworkTypeString := helpers.ConvertByteToString(messageNetworkType) + + messageDurationString := helpers.ConvertIntToString(messageDuration) + + messageIdentifierBytes, err := helpers.GetNewRandomBytes(20) + if (err != nil) { return false, false, err } + + messageIdentifier := [20]byte(messageIdentifierBytes) + messageIdentifierString := encoding.EncodeBytesToHexString(messageIdentifierBytes) + + // TimeAdded will be used as the messageSentTime when we send the message + currentTime := time.Now().Unix() + currentTimeString := helpers.ConvertInt64ToString(currentTime) + + newMessageMap := map[string]string{ + "MessageIdentifier": messageIdentifierString, + "MyIdentityHash": myIdentityHashString, + "RecipientIdentityHash": recipientIdentityHashString, + "NetworkType": messageNetworkTypeString, + "MessageCommunication": messageCommunication, + "MessageDuration": messageDurationString, + "TimeAdded": currentTimeString, + } + + estimatedMessageSize, err := sendMessages.GetEstimatedMessageSize(recipientIdentityHash, messageCommunication) + if (err != nil) { return false, false, err } + + updatingMessageQueueMapListMutex.Lock() + defer updatingMessageQueueMapListMutex.Unlock() + + parametersExist, sufficientCreditsExist, err := myAccountCredit.FreezeCreditForMessage(messageIdentifier, messageNetworkType, messageDuration, estimatedMessageSize) + if (err != nil) { return false, false, err } + if (parametersExist == false){ + return false, false, nil + } + if (sufficientCreditsExist == false){ + return true, false, nil + } + + err = myMessageQueueMapListDatastore.AddMapListItem(newMessageMap) + if (err != nil) { return false, false, err } + + // Now we add queued message map to myChatMessages so the user will see a queued message in their GUI + + err = myChatMessages.AddMyNewMessageToMyChatMessages("Queued", [26]byte{}, messageIdentifier, myIdentityHash, recipientIdentityHash, messageNetworkType, currentTime, messageCommunication) + if (err != nil) { return false, false, err } + + return true, true, nil +} + +func PruneMessageQueue()error{ + + //TODO: Delete messages that have already been sent + + // Use sendMessages.CheckIfMessageIsSent() + + return nil +} + +// This function should be run automatically on a continual period +// It will attempt to send a random selection of messages from the queue +// It will contact 1 host per message +func AttemptToSendMessagesInQueue(identityType string, networkType byte, maximumMessagesToSend int)error{ + + if (identityType != "Mate" && identityType != "Moderator"){ + return errors.New("AttemptToSendMessagesInQueue called with invalid identityType: " + identityType) + } + + isValid := helpers.VerifyNetworkType(networkType) + if (isValid == false){ + networkTypeString := helpers.ConvertByteToString(networkType) + return errors.New("AttemptToSendMessagesInQueue called with invalid networkType: " + networkTypeString) + } + + myIdentityExists, myIdentityHash, err := myIdentity.GetMyIdentityHash(identityType) + if (err != nil) { return err } + if (myIdentityExists == false){ + return nil + } + + parametersExist, err := getParameters.CheckIfChatParametersExist(networkType) + if (err != nil) { return err } + if (parametersExist == false){ + // We need these parameters to send messages. We wait for them to download. + return nil + } + + updatingMessageQueueMapListMutex.Lock() + defer updatingMessageQueueMapListMutex.Unlock() + + currentMessageQueueMapList, err := myMessageQueueMapListDatastore.GetMapList() + if (err != nil) { return err } + + // We randomize the map list order, so we are sending a random subset of the messages + + helpers.RandomizeListOrder(currentMessageQueueMapList) + + // We use this to keep track of how many message sends we have succesfully started + sentMessagesCount := 0 + + messageMapsToDeleteList := make([]map[string]string, 0) + + for _, messageQueueMessageMap := range currentMessageQueueMapList{ + + messageMyIdentityHashString, exists := messageQueueMessageMap["MyIdentityHash"] + if (exists == false) { + return errors.New("MessageQueue mapList malformed: Item missing MyIdentityHash") + } + + messageMyIdentityHash, _, err := identity.ReadIdentityHashString(messageMyIdentityHashString) + if (err != nil){ + return errors.New("MessageQueue mapList malformed: Item contains invalid MyIdentityHash: " + messageMyIdentityHashString) + } + + if (messageMyIdentityHash != myIdentityHash){ + // Must be identity for different identityType + continue + } + + messageNetworkTypeString, exists := messageQueueMessageMap["NetworkType"] + if (exists == false){ + return errors.New("MessageQueue mapList malformed: Item missing NetworkType.") + } + + messageNetworkType, err := helpers.ConvertNetworkTypeStringToByte(messageNetworkTypeString) + if (err != nil){ + return errors.New("MessageQueue mapList malformed: Item contains invalid NetworkType: " + messageNetworkTypeString) + } + if (messageNetworkType != networkType){ + // Message belongs to a different networkType. + continue + } + + messageIdentifierString, exists := messageQueueMessageMap["MessageIdentifier"] + if (exists == false) { + return errors.New("MessageQueue mapList malformed: Item missing MessageIdentifier") + } + + messageIdentifierBytes, err := encoding.DecodeHexStringToBytes(messageIdentifierString) + if (err != nil){ + return errors.New("MessageQueue mapList malformed: Item contains invalid messageIdentifier: Not Hex: " + messageIdentifierString) + } + + if (len(messageIdentifierBytes) != 20){ + return errors.New("MessageQueue mapList malformed: Item contains invalid messageIdentifier: Invalid length: " + messageIdentifierString) + } + + messageIdentifier := [20]byte(messageIdentifierBytes) + + messageRecipientString, exists := messageQueueMessageMap["RecipientIdentityHash"] + if (exists == false) { + return errors.New("MessageQueue mapList malformed: item missing RecipientIdentityHash") + } + + messageRecipient, _, err := identity.ReadIdentityHashString(messageRecipientString) + if (err != nil){ + return errors.New("MessageQueue mapList malformed: Item contains invalid RecipientIdentityHash: " + messageRecipientString) + } + + messageTimeAdded, exists := messageQueueMessageMap["TimeAdded"] + if (exists == false) { + return errors.New("MessageQueue mapList malformed: Item missing TimeAdded") + } + + messageTimeAddedInt64, err := helpers.ConvertBroadcastTimeStringToInt64(messageTimeAdded) + if (err != nil) { + return errors.New("MessageQueue mapList malformed: Item contains invalid TimeAdded: " + messageTimeAdded) + } + + messageCommunication, exists := messageQueueMessageMap["MessageCommunication"] + if (exists == false) { + return errors.New("MessageQueue mapList malformed: item missing MessageCommunication") + } + + // Now we check if recipient keys exist + + peerIsDisabled, peerChatKeysExist, _, _, err := peerChatKeys.GetPeerNewestActiveChatKeys(messageRecipient, networkType) + if (err != nil) { return err } + if (peerIsDisabled == true || peerChatKeysExist == false){ + + // We see if message entry is more than 1 week old + // If it is, we will delete it + + currentTime := time.Now().Unix() + + elapsedTime := currentTime - messageTimeAddedInt64 + + maximumTime := unixTime.GetWeekUnix() + + if (elapsedTime > maximumTime){ + + creditsFound, err := myAccountCredit.ReleaseFrozenCreditForMessage(messageIdentifier) + if (err != nil){ return err } + if (creditsFound == false){ + // This should not happen + // If the user manually released credits, the queued message that the credits were held for should have been deleted from queue + return errors.New("MessageQueue message frozen credits not found.") + } + + // We update the existing message status inside myChatMessages to "Failed" + err = myChatMessages.AddMyNewMessageToMyChatMessages("Failed", [26]byte{}, messageIdentifier, messageMyIdentityHash, messageRecipient, messageNetworkType, messageTimeAddedInt64, messageCommunication) + if (err != nil) { return err } + + messageMapsToDeleteList = append(messageMapsToDeleteList, messageQueueMessageMap) + } + + // Peer chat keys were not found. + // We will keep trying to download them. We skip this message for now. + + continue + } + + // We initiate message send + + messageDurationString, exists := messageQueueMessageMap["MessageDuration"] + if (exists == false) { + return errors.New("MessageQueue map list malformed: item missing MessageDuration") + } + + messageDurationToFund, err := helpers.ConvertStringToInt(messageDurationString) + if (err != nil) { + return errors.New("MessageQueue mapList malformed: Item contains invalid MessageDuration: " + messageDurationString) + } + + messageIsPendingOrSent, successfullyStartedSend, err := sendMessages.StartSendingMessage(messageIdentifier, myIdentityHash, messageRecipient, messageNetworkType, messageTimeAddedInt64, messageCommunication, messageDurationToFund) + if (err != nil) { return err } + if (messageIsPendingOrSent == true){ + // We are already trying to send the message, or the message has already been sent. + continue + } + if (successfullyStartedSend == false){ + + // We don't have the required data + // We are probably missing parameters + // We continue looking for messages to send + + continue + } + + // We successfully started sending the message. + // It will be deleted from the message queue automatically if the send is successful + + sentMessagesCount += 1 + + if (sentMessagesCount >= maximumMessagesToSend){ + // We have sent the number of messages we wanted to + break + } + } + + for _, messageMapToDelete := range messageMapsToDeleteList{ + + err := myMessageQueueMapListDatastore.DeleteMapListItems(messageMapToDelete) + if (err != nil) { return err } + } + + return nil +} + + + + diff --git a/internal/messaging/myReadStatus/myReadStatus.go b/internal/messaging/myReadStatus/myReadStatus.go new file mode 100644 index 0000000..8b60eeb --- /dev/null +++ b/internal/messaging/myReadStatus/myReadStatus.go @@ -0,0 +1,112 @@ + +// myReadStatus provides functions to manage the read/unread status of chat conversations + +package myReadStatus + +//TODO: Add function to prune statuses for conversations with no messages + +import "seekia/internal/encoding" +import "seekia/internal/helpers" +import "seekia/internal/identity" +import "seekia/internal/myDatastores/myMap" + +import "errors" + +var myConversationReadUnreadStatusesMapDatastore *myMap.MyMap + +// This function must be called whenever an app user signs in +func InitializeMyReadStatusDatastore()error{ + + newMyConversationReadUnreadStatusesMapDatastore, err := myMap.CreateNewMap("ConversationReadUnreadStatuses") + if (err != nil) { return err } + + myConversationReadUnreadStatusesMapDatastore = newMyConversationReadUnreadStatusesMapDatastore + + return nil +} + + +func SetConversationReadUnreadStatus(myIdentityHash [16]byte, theirIdentityHash [16]byte, networkType byte, readOrUnread string)error{ + + myIdentityHashString, myIdentityType, err := identity.EncodeIdentityHashBytesToString(myIdentityHash) + if (err != nil){ + myIdentityHashHex := encoding.EncodeBytesToHexString(myIdentityHash[:]) + return errors.New("SetConversationReadUnreadStatus called with invalid myIdentityHash: " + myIdentityHashHex) + } + theirIdentityHashString, theirIdentityType, err := identity.EncodeIdentityHashBytesToString(theirIdentityHash) + if (err != nil){ + theirIdentityHashHex := encoding.EncodeBytesToHexString(theirIdentityHash[:]) + return errors.New("SetConversationReadUnreadStatus called with invalid theirIdentityHash: " + theirIdentityHashHex) + } + + if (myIdentityType != theirIdentityType){ + return errors.New("SetConversationReadUnreadStatus called with mismatched my/their identityTypes.") + } + if (myIdentityType == "Host"){ + return errors.New("SetConversationReadUnreadStatus called with Host identityTypes.") + } + + isValid := helpers.VerifyNetworkType(networkType) + if (isValid == false){ + networkTypeString := helpers.ConvertByteToString(networkType) + return errors.New("SetConversationReadUnreadStatus called with invalid networkType: " + networkTypeString) + } + + if (readOrUnread != "Read" && readOrUnread != "Unread") { + return errors.New("SetConversationReadUnreadStatus called with invalid read/unread status: " + readOrUnread) + } + + networkTypeString := helpers.ConvertByteToString(networkType) + + conversationKey := myIdentityHashString + "+" + theirIdentityHashString + "@" + networkTypeString + + err = myConversationReadUnreadStatusesMapDatastore.SetMapEntry(conversationKey, readOrUnread) + if (err != nil) { return err } + + return nil +} + +func GetConversationReadUnreadStatus(myIdentityHash [16]byte, theirIdentityHash [16]byte, networkType byte)(string, error){ + + myIdentityHashString, myIdentityType, err := identity.EncodeIdentityHashBytesToString(myIdentityHash) + if (err != nil){ + myIdentityHashHex := encoding.EncodeBytesToHexString(myIdentityHash[:]) + return "", errors.New("GetConversationReadUnreadStatus called with invalid myIdentityHash: " + myIdentityHashHex) + } + theirIdentityHashString, theirIdentityType, err := identity.EncodeIdentityHashBytesToString(theirIdentityHash) + if (err != nil){ + theirIdentityHashHex := encoding.EncodeBytesToHexString(theirIdentityHash[:]) + return "", errors.New("GetConversationReadUnreadStatus called with invalid theirIdentityHash: " + theirIdentityHashHex) + } + + if (myIdentityType != theirIdentityType){ + return "", errors.New("GetConversationReadUnreadStatus called with mismatched my/their identityTypes.") + } + if (myIdentityType == "Host"){ + return "", errors.New("GetConversationReadUnreadStatus called with Host identityTypes.") + } + + isValid := helpers.VerifyNetworkType(networkType) + if (isValid == false){ + networkTypeString := helpers.ConvertByteToString(networkType) + return "", errors.New("GetConversationReadUnreadStatus called with invalid networkType: " + networkTypeString) + } + + networkTypeString := helpers.ConvertByteToString(networkType) + + conversationKey := myIdentityHashString + "+" + theirIdentityHashString + "@" + networkTypeString + + exists, readOrUnread, err := myConversationReadUnreadStatusesMapDatastore.GetMapEntry(conversationKey) + if (err != nil) { return "", err } + if (exists == false) { + return "Unread", nil + } + + if (readOrUnread != "Read" && readOrUnread != "Unread"){ + return "", errors.New("myConversationReadUnreadStatusesMapDatastore malformed: Item contains invalid ReadOrUnread: " + readOrUnread) + } + + return readOrUnread, nil +} + + diff --git a/internal/messaging/mySecretInboxes/mySecretInboxes.go b/internal/messaging/mySecretInboxes/mySecretInboxes.go new file mode 100644 index 0000000..da2f6a8 --- /dev/null +++ b/internal/messaging/mySecretInboxes/mySecretInboxes.go @@ -0,0 +1,575 @@ + +// mySecretInboxes provides functions to manage a user's secret inboxes +// User sends these inboxes out in messages to users +// They must check them for messages until they expire + +package mySecretInboxes + +//TODO: Prune expired inboxes we have checked sufficiently + +import "seekia/internal/encoding" +import "seekia/internal/helpers" +import "seekia/internal/identity" +import "seekia/internal/messaging/inbox" +import "seekia/internal/messaging/secretInboxEpoch" +import "seekia/internal/myBlockedUsers" +import "seekia/internal/myDatastores/myMapList" + +import "bytes" +import "sync" +import "errors" + +// This will be locked whenever we update the map list +var updatingMySecretInboxesMapListMutex sync.Mutex + +var mySecretInboxesMapListDatastore *myMapList.MyMapList + +// This function must be called whenever an app user signs in +func InitializeMySecretInboxesDatastore()error{ + + updatingMySecretInboxesMapListMutex.Lock() + defer updatingMySecretInboxesMapListMutex.Unlock() + + newMySecretInboxesMapListDatastore, err := myMapList.CreateNewMapList("MySecretInboxes") + if (err != nil) { return err } + + mySecretInboxesMapListDatastore = newMySecretInboxesMapListDatastore + + return nil +} + + +// This function will omit inboxes that we no longer need to download messages for +// This would be because they have been sufficiently checked and old enough that no new messages are being received +func GetAllMyActiveSecretInboxes(myIdentityHash [16]byte, networkType byte)([][10]byte, error){ + + isValid, err := identity.VerifyIdentityHash(myIdentityHash, false, "") + if (err != nil) { return nil, err } + if (isValid == false){ + myIdentityHashHex := encoding.EncodeBytesToHexString(myIdentityHash[:]) + return nil, errors.New("GetAllMyActiveSecretInboxes called with invalid myIdentityHash: " + myIdentityHashHex) + } + + isValid = helpers.VerifyNetworkType(networkType) + if (isValid == false){ + networkTypeString := helpers.ConvertByteToString(networkType) + return nil, errors.New("GetAllMyActiveSecretInboxes called with invalid networkType: " + networkTypeString) + } + + mySecretInboxesList, err := GetAllMySecretInboxes(myIdentityHash, networkType) + if (err != nil){ return nil, err } + + //TODO: We need code that keeps track of how many times we have checked a particular inbox after it has expired + // Then we will omit those inboxes + + return mySecretInboxesList, nil +} + +// This function will not return inboxes for users whom the user has blocked +func GetAllMySecretInboxes(myIdentityHash [16]byte, networkType byte)([][10]byte, error){ + + isValid, err := identity.VerifyIdentityHash(myIdentityHash, false, "") + if (err != nil) { return nil, err } + if (isValid == false){ + myIdentityHashHex := encoding.EncodeBytesToHexString(myIdentityHash[:]) + return nil, errors.New("GetAllMySecretInboxes called with invalid myIdentityHash: " + myIdentityHashHex) + } + + isValid = helpers.VerifyNetworkType(networkType) + if (isValid == false){ + networkTypeString := helpers.ConvertByteToString(networkType) + return nil, errors.New("GetAllMySecretInboxes called with invalid networkType: " + networkTypeString) + } + + mySecretInboxesMapList, err := mySecretInboxesMapListDatastore.GetMapList() + if (err != nil) { return nil, err } + + allMySecretInboxesList := make([][10]byte, 0) + + for _, inboxMap := range mySecretInboxesMapList{ + + conversationMyIdentityHashString, exists := inboxMap["MyIdentityHash"] + if (exists == false){ + return nil, errors.New("mySecretInboxesMapList is malformed: Item missing MyIdentityHash.") + } + + conversationMyIdentityHash, _, err := identity.ReadIdentityHashString(conversationMyIdentityHashString) + if (err != nil){ + return nil, errors.New("mySecretInboxesMapList is malformed: Item contains invalid MyIdentityHash: " + conversationMyIdentityHashString) + } + + if (conversationMyIdentityHash != myIdentityHash){ + continue + } + + recipientIdentityHashString, exists := inboxMap["TheirIdentityHash"] + if (exists == false){ + return nil, errors.New("mySecretInboxesMapList is malformed: Item missing TheirIdentityHash") + } + + recipientIdentityHash, _, err := identity.ReadIdentityHashString(recipientIdentityHashString) + if (err != nil){ + return nil, errors.New("mySecretInboxesMapList is malformed: Item contains invalid TheirIdentityHash: " + recipientIdentityHashString) + } + + recipientIsBlocked, _, _, _, err := myBlockedUsers.CheckIfUserIsBlocked(recipientIdentityHash) + if (err != nil){ return nil, err } + if (recipientIsBlocked == true){ + continue + } + + inboxNetworkTypeString, exists := inboxMap["NetworkType"] + if (exists == false){ + return nil, errors.New("mySecretInboxesMapList is malformed: Item missing NetworkType.") + } + + inboxNetworkType, err := helpers.ConvertNetworkTypeStringToByte(inboxNetworkTypeString) + if (err != nil){ + return nil, errors.New("mySecretInboxesMapList is malformed: Item contains invalid NetworkType: " + inboxNetworkTypeString) + } + + if (inboxNetworkType != networkType){ + continue + } + + secretInboxSeedHex, exists := inboxMap["SecretInboxSeed"] + if (exists == false){ + return nil, errors.New("mySecretInboxesMapList is malformed: Item missing SecretInboxSeed.") + } + + secretInboxSeed, err := encoding.DecodeHexStringToBytes(secretInboxSeedHex) + if (err != nil){ + return nil, errors.New("mySecretInboxesMapList is malformed: Contains invalid secretInboxSeed: Not Hex.") + } + + if (len(secretInboxSeed) != 22){ + return nil, errors.New("mySecretInboxesMapList is malformed: Contains invalid secretInboxSeed: Invalid length.") + } + + secretInboxSeedArray := [22]byte(secretInboxSeed) + + currentSecretInbox, _, err := inbox.GetSecretInboxAndSealerKeyFromSecretInboxSeed(secretInboxSeedArray) + if (err != nil){ return nil, err } + + allMySecretInboxesList = append(allMySecretInboxesList, currentSecretInbox) + } + + return allMySecretInboxesList, nil +} + +//Outputs: +// -bool: My secret inbox found +// -[16]byte: Conversation my identity hash +// -[16]byte: Conversation recipient identity hash +// -byte: Secret inbox network type +// -[32]byte: Secret inbox sealer key +// -int64: Secret inbox start time +// -int64: Secret inbox expiration time +// -error +func GetMySecretInboxInfo(secretInbox [10]byte)(bool, [16]byte, [16]byte, byte, [32]byte, int64, int64, error){ + + mySecretInboxesMapList, err := mySecretInboxesMapListDatastore.GetMapList() + if (err != nil) { return false, [16]byte{}, [16]byte{}, 0, [32]byte{}, 0, 0, err } + + for _, inboxMap := range mySecretInboxesMapList{ + + secretInboxSeedHex, exists := inboxMap["SecretInboxSeed"] + if (exists == false){ + return false, [16]byte{}, [16]byte{}, 0, [32]byte{}, 0, 0, errors.New("MySecretInboxesMapList is malformed: Inbox map missing SecretInboxSeed") + } + + secretInboxSeed, err := encoding.DecodeHexStringToBytes(secretInboxSeedHex) + if (err != nil){ + return false, [16]byte{}, [16]byte{}, 0, [32]byte{}, 0, 0, errors.New("MySecretInboxes map list malformed: Contains invalid secretInboxSeed: Not Hex.") + } + + if (len(secretInboxSeed) != 22){ + return false, [16]byte{}, [16]byte{}, 0, [32]byte{}, 0, 0, errors.New("MySecretInboxes map list malformed: Contains invalid secretInboxSeed: Invalid length.") + } + + secretInboxSeedArray := [22]byte(secretInboxSeed) + + currentSecretInbox, currentSecretInboxSealerKey, err := inbox.GetSecretInboxAndSealerKeyFromSecretInboxSeed(secretInboxSeedArray) + if (err != nil){ + return false, [16]byte{}, [16]byte{}, 0, [32]byte{}, 0, 0, errors.New("MySecretInboxes map list malformed: Contains invalid secretInboxSeed.") + } + + if (currentSecretInbox != secretInbox){ + continue + } + + conversationMyIdentityHashString, exists := inboxMap["MyIdentityHash"] + if (exists == false){ + return false, [16]byte{}, [16]byte{}, 0, [32]byte{}, 0, 0, errors.New("MySecretInboxesMapList is malformed: Inbox map missing MyIdentityHash") + } + + conversationMyIdentityHash, _, err := identity.ReadIdentityHashString(conversationMyIdentityHashString) + if (err != nil){ + return false, [16]byte{}, [16]byte{}, 0, [32]byte{}, 0, 0, errors.New("MySecretInboxesMapList is malformed: Inbox map contains invalid MyIdentityHash: " + conversationMyIdentityHashString) + } + + conversationTheirIdentityHashString, exists := inboxMap["TheirIdentityHash"] + if (exists == false){ + return false, [16]byte{}, [16]byte{}, 0, [32]byte{}, 0, 0, errors.New("MySecretInboxesMapList is malformed: Inbox map missing TheirIdentityHash") + } + + conversationTheirIdentityHash, _, err := identity.ReadIdentityHashString(conversationTheirIdentityHashString) + if (err != nil){ + return false, [16]byte{}, [16]byte{}, 0, [32]byte{}, 0, 0, errors.New("MySecretInboxesMapList is malformed: Inbox map contains invalid TheirIdentityHash: " + conversationTheirIdentityHashString) + } + + inboxNetworkTypeString, exists := inboxMap["NetworkType"] + if (exists == false){ + return false, [16]byte{}, [16]byte{}, 0, [32]byte{}, 0, 0, errors.New("MySecretInboxesMapList is malformed: Inbox map missing NetworkType") + } + + inboxNetworkType, err := helpers.ConvertNetworkTypeStringToByte(inboxNetworkTypeString) + if (err != nil){ + return false, [16]byte{}, [16]byte{}, 0, [32]byte{}, 0, 0, errors.New("MySecretInboxesMapList is malformed: Inbox map contains invalid NetworkType: " + inboxNetworkTypeString) + } + + inboxStartTimeString, exists := inboxMap["InboxStartTime"] + if (exists == false){ + return false, [16]byte{}, [16]byte{}, 0, [32]byte{}, 0, 0, errors.New("MySecretInboxesMapList is malformed: Inbox map missing InboxStartTime") + } + + inboxStartTimeInt64, err := helpers.ConvertStringToInt64(inboxStartTimeString) + if (err != nil) { + return false, [16]byte{}, [16]byte{}, 0, [32]byte{}, 0, 0, errors.New("Malformed mySecretInboxesMapList: Invalid secret inbox start time: " + inboxStartTimeString) + } + + inboxEndTimeString, exists := inboxMap["InboxEndTime"] + if (exists == false){ + return false, [16]byte{}, [16]byte{}, 0, [32]byte{}, 0, 0, errors.New("MySecretInboxesMapList is malformed: Inbox map missing InboxEndTime") + } + + inboxEndTimeInt64, err := helpers.ConvertStringToInt64(inboxEndTimeString) + if (err != nil) { + return false, [16]byte{}, [16]byte{}, 0, [32]byte{}, 0, 0, errors.New("Malformed mySecretInboxesMapList: Invalid secret inbox end time: " + inboxEndTimeString) + } + + return true, conversationMyIdentityHash, conversationTheirIdentityHash, inboxNetworkType, currentSecretInboxSealerKey, inboxStartTimeInt64, inboxEndTimeInt64, nil + } + + return false, [16]byte{}, [16]byte{}, 0, [32]byte{}, 0, 0, nil +} + + +//Outputs: +// -bool: Parameters exist +// -bool: My current epoch secret inbox seed exists +// -[22]byte: My current epoch secret inbox seed +// -bool: My next epoch secret inbox seed exists +// -[22]byte: My next epoch secret inbox seed +// -error +func GetMySecretInboxSeedsForMessage(myIdentityHash [16]byte, recipientIdentityHash [16]byte, networkType byte, messageSentTimeUnix int64)(bool, bool, [22]byte, bool, [22]byte, error){ + + isValid, err := identity.VerifyIdentityHash(myIdentityHash, false, "") + if (err != nil){ return false, false, [22]byte{}, false, [22]byte{}, err } + if (isValid == false){ + myIdentityHashHex := encoding.EncodeBytesToHexString(myIdentityHash[:]) + return false, false, [22]byte{}, false, [22]byte{}, errors.New("GetMySecretInboxSeedsForMessage called with invalid myIdentityHash: " + myIdentityHashHex) + } + + isValid = helpers.VerifyNetworkType(networkType) + if (isValid == false){ + networkTypeString := helpers.ConvertByteToString(networkType) + return false, false, [22]byte{}, false, [22]byte{}, errors.New("GetMySecretInboxSeedsForMessage called with invalid networkType: " + networkTypeString) + } + + parametersExist, currentEpochStartTime, currentEpochEndTime, nextEpochStartTime, nextEpochEndTime, err := secretInboxEpoch.GetSecretInboxEpochStartAndEndTimes(networkType, messageSentTimeUnix) + if (err != nil) { return false, false, [22]byte{}, false, [22]byte{}, err } + if (parametersExist == false){ + return false, false, [22]byte{}, false, [22]byte{}, nil + } + + myIdentityHashString, _, err := identity.EncodeIdentityHashBytesToString(myIdentityHash) + if (err != nil){ + myIdentityHashHex := encoding.EncodeBytesToHexString(myIdentityHash[:]) + return false, false, [22]byte{}, false, [22]byte{}, errors.New("VerifyIdentityHash failed to verify identity hash: " + myIdentityHashHex) + } + + recipientIdentityHashString, _, err := identity.EncodeIdentityHashBytesToString(recipientIdentityHash) + if (err != nil){ + recipientIdentityHashHex := encoding.EncodeBytesToHexString(recipientIdentityHash[:]) + return false, false, [22]byte{}, false, [22]byte{}, errors.New("GetMySecretInboxSeedsForMessage called with invalid recipient identity hash: " + recipientIdentityHashHex) + } + + // We retrieve all secret inboxes for this conversation, and find an inbox that exists for the entire desired epoch duration + + networkTypeString := helpers.ConvertByteToString(networkType) + + lookupMap := map[string]string{ + "MyIdentityHash": myIdentityHashString, + "TheirIdentityHash": recipientIdentityHashString, + "NetworkType": networkTypeString, + } + + anyItemsFound, foundItemsMapList, err := mySecretInboxesMapListDatastore.GetMapListItems(lookupMap) + if (err != nil) { return false, false, [22]byte{}, false, [22]byte{}, err } + if (anyItemsFound == false){ + return true, false, [22]byte{}, false, [22]byte{}, nil + } + + //Outputs: + // -bool: Inbox found + // -[22]byte: Secret inbox seed + // -error + getSecretInboxSeedForEpoch := func(epochStartTime int64, epochEndTime int64)(bool, [22]byte, error){ + + for _, inboxMap := range foundItemsMapList{ + + inboxStartTimeString, exists := inboxMap["InboxStartTime"] + if (exists == false){ + return false, [22]byte{}, errors.New("MySecretInboxesMapList is malformed: Item missing InboxStartTime") + } + + inboxStartTimeInt64, err := helpers.ConvertStringToInt64(inboxStartTimeString) + if (err != nil) { + return false, [22]byte{}, errors.New("MySecretInboxesMapList is malformed: Invalid secret inbox start time: " + inboxStartTimeString) + } + + inboxEndTimeString, exists := inboxMap["InboxEndTime"] + if (exists == false){ + return false, [22]byte{}, errors.New("MySecretInboxesMapList is malformed: Item missing InboxEndTime") + } + + inboxEndTimeInt64, err := helpers.ConvertStringToInt64(inboxEndTimeString) + if (err != nil) { + return false, [22]byte{}, errors.New("MySecretInboxesMapList is malformed: Invalid secret inbox end time: " + inboxEndTimeString) + } + + if (inboxStartTimeInt64 > epochStartTime || inboxEndTimeInt64 < epochEndTime){ + continue + } + + secretInboxSeedHex, exists := inboxMap["SecretInboxSeed"] + if (exists == false){ + return false, [22]byte{}, errors.New("MySecretInboxesMapList is malformed: Item missing SecretInboxSeed") + } + secretInboxSeed, err := encoding.DecodeHexStringToBytes(secretInboxSeedHex) + if (err != nil){ + return false, [22]byte{}, errors.New("MySecretInboxesMapList is malformed: Invalid secret inbox seed: " + secretInboxSeedHex) + } + + if (len(secretInboxSeed) != 22){ + return false, [22]byte{}, errors.New("MySecretInboxesMapList is malformed: Invalid secret inbox seed: " + secretInboxSeedHex) + } + + secretInboxSeedArray := [22]byte(secretInboxSeed) + + return true, secretInboxSeedArray, nil + } + + return false, [22]byte{}, nil + } + + foundCurrentEpochInboxSeed, currentEpochInboxSeed, err := getSecretInboxSeedForEpoch(currentEpochStartTime, currentEpochEndTime) + if (err != nil) { return false, false, [22]byte{}, false, [22]byte{}, err } + + foundNextEpochInboxSeed, nextEpochInboxSeed, err := getSecretInboxSeedForEpoch(nextEpochStartTime, nextEpochEndTime) + if (err != nil) { return false, false, [22]byte{}, false, [22]byte{}, err } + + return true, foundCurrentEpochInboxSeed, currentEpochInboxSeed, foundNextEpochInboxSeed, nextEpochInboxSeed, nil +} + + +//Outputs: +// -bool: Parameters exist +// -error +func AddMySecretInboxSeeds(myIdentityHash [16]byte, theirIdentityHash [16]byte, networkType byte, messageSentTime int64, currentSecretInboxSeed [22]byte, nextSecretInboxSeed [22]byte)(bool, error){ + + isValid, err := identity.VerifyIdentityHash(myIdentityHash, false, "") + if (err != nil) { return false, err } + if (isValid == false){ + myIdentityHashHex := encoding.EncodeBytesToHexString(myIdentityHash[:]) + return false, errors.New("AddMySecretInboxSeeds called with invalid myIdentityHash: " + myIdentityHashHex) + } + isValid, err = identity.VerifyIdentityHash(theirIdentityHash, false, "") + if (err != nil) { return false, err } + if (isValid == false){ + theirIdentityHashHex := encoding.EncodeBytesToHexString(theirIdentityHash[:]) + return false, errors.New("AddMySecretInboxSeeds called with invalid theirIdentityHash: " + theirIdentityHashHex) + } + + isValid = helpers.VerifyNetworkType(networkType) + if (isValid == false){ + networkTypeString := helpers.ConvertByteToString(networkType) + return false, errors.New("AddMySecretInboxSeeds called with invalid networkType: " + networkTypeString) + } + + updatingMySecretInboxesMapListMutex.Lock() + defer updatingMySecretInboxesMapListMutex.Unlock() + + parametersExist, currentSecretInboxStartTime, currentSecretInboxEndTime, nextSecretInboxStartTime, nextSecretInboxEndTime, err := secretInboxEpoch.GetSecretInboxEpochStartAndEndTimes(networkType, messageSentTime) + if (err != nil) { return false, err } + if (parametersExist == false){ + return false, nil + } + + mySecretInboxesMapList, err := mySecretInboxesMapListDatastore.GetMapList() + if (err != nil) { return false, err } + + addSecretInboxSeedToMapList := func(secretInboxSeed [22]byte, inboxStartTime int64, inboxEndTime int64)error{ + + // We see if an entry already exists for this inbox + + for _, inboxMap := range mySecretInboxesMapList{ + + currentSecretInboxSeedString, exists := inboxMap["SecretInboxSeed"] + if (exists == false){ + return errors.New("mySecretInboxesMapListDatastore is malformed: Item missing SecretInboxSeed") + } + + currentSecretInboxSeedBytes, err := encoding.DecodeHexStringToBytes(currentSecretInboxSeedString) + if (err != nil){ + return errors.New("mySecretInboxesMapListDatastore is malformed: Item contains invalid SecretInboxSeed: Not Hex.") + } + + if (len(currentSecretInboxSeedBytes) != 22){ + return errors.New("mySecretInboxesMapListDatastore is malformed: Item contains invalid SecretInboxSeed: Invalid length.") + } + + areEqual := bytes.Equal(currentSecretInboxSeedBytes, secretInboxSeed[:]) + if (areEqual == false){ + continue + } + + conversationMyIdentityHashString, exists := inboxMap["MyIdentityHash"] + if (exists == false){ + return errors.New("Malformed mySecretInboxesMapListDatastore: Item missing MyIdentityHash") + } + + conversationMyIdentityHash, _, err := identity.ReadIdentityHashString(conversationMyIdentityHashString) + if (err != nil){ + return errors.New("Malformed mySecretInboxesMapListDatastore: Item contains invalid MyIdentityHash: " + conversationMyIdentityHashString) + } + + if (conversationMyIdentityHash != myIdentityHash){ + // This should never happen. Secret inbox seeds are generated randomly. + return errors.New("Trying to add a secret inbox seed which already exists for a different identity.") + } + + conversationTheirIdentityHashString, exists := inboxMap["TheirIdentityHash"] + if (exists == false) { + return errors.New("Malformed mySecretInboxesMapListDatastore: Item missing TheirIdentityHash") + } + + conversationTheirIdentityHash, _, err := identity.ReadIdentityHashString(conversationTheirIdentityHashString) + if (err != nil){ + return errors.New("Malformed mySecretInboxesMapListDatastore: Item contains invalid TheirIdentityHash: " + conversationTheirIdentityHashString) + } + + if (conversationTheirIdentityHash != theirIdentityHash){ + // This should never happen. Secret inbox seeds are generated randomly. + return errors.New("Trying to add a secret inbox seed which already exists for a different recipient identity.") + } + + inboxNetworkTypeString, exists := inboxMap["NetworkType"] + if (exists == false){ + return errors.New("Malformed mySecretInboxesMapListDatastore: Item missing NetworkType") + } + + inboxNetworkType, err := helpers.ConvertNetworkTypeStringToByte(inboxNetworkTypeString) + if (err != nil){ + return errors.New("Malformed mySecretInboxesMapListDatastore: Item contains invalid NetworkType: " + inboxNetworkTypeString) + } + if (inboxNetworkType != networkType){ + // This should never happen. Secret inbox seeds are generated randomly. + return errors.New("Trying to add a secret inbox seed that already exists with a different networkType.") + } + + existingInboxStartTimeString, exists := inboxMap["InboxStartTime"] + if (exists == false){ + return errors.New("mySecretInboxesMapListDatastore is malformed: Item missing InboxStartTime") + } + + existingInboxStartTime, err := helpers.ConvertStringToInt64(existingInboxStartTimeString) + if (err != nil) { + return errors.New("Malformed mySecretInboxesMapList: Invalid secret inbox start time: " + existingInboxStartTimeString) + } + + existingInboxEndTimeString, exists := inboxMap["InboxEndTime"] + if (exists == false){ + return errors.New("mySecretInboxesMapListDatastore is malformed: Item missing InboxEndTime") + } + + existingInboxEndTime, err := helpers.ConvertStringToInt64(existingInboxEndTimeString) + if (err != nil) { + return errors.New("Malformed mySecretInboxesMapList: Invalid secret inbox end time: " + existingInboxEndTimeString) + } + + // We see if the existing inbox start/end times are earlier/later than what we have recorded + // This would only happen if secret inbox epoch duration was changed + + if (inboxStartTime >= existingInboxStartTime && inboxEndTime <= existingInboxEndTime){ + // Existing inbox range bounds are valid. + // Nothing to change + return nil + } + + // Inbox range must be expanded + // Secret inbox epoch duration must have changed (or sender is malicious) + + newInboxStartTime := min(existingInboxStartTime, inboxStartTime) + + newInboxEndTime := max(existingInboxEndTime, inboxEndTime) + + newSecretInboxStartTimeString := helpers.ConvertInt64ToString(newInboxStartTime) + newSecretInboxEndTimeString := helpers.ConvertInt64ToString(newInboxEndTime) + + inboxMap["InboxStartTime"] = newSecretInboxStartTimeString + inboxMap["InboxEndTime"] = newSecretInboxEndTimeString + + return nil + } + + // This is only reached if inbox map does not exist + // We create and append a new inbox map + + myIdentityHashString, _, err := identity.EncodeIdentityHashBytesToString(myIdentityHash) + if (err != nil) { + myIdentityHashHex := encoding.EncodeBytesToHexString(myIdentityHash[:]) + return errors.New("VerifyIdentityHash failed to verify myIdentityHash: " + myIdentityHashHex) + } + + theirIdentityHashString, _, err := identity.EncodeIdentityHashBytesToString(theirIdentityHash) + if (err != nil) { + theirIdentityHashHex := encoding.EncodeBytesToHexString(theirIdentityHash[:]) + return errors.New("VerifyIdentityHash failed to verify theirIdentityHash: " + theirIdentityHashHex) + } + + secretInboxSeedHex := encoding.EncodeBytesToHexString(secretInboxSeed[:]) + networkTypeString := helpers.ConvertByteToString(networkType) + + secretInboxStartTimeString := helpers.ConvertInt64ToString(currentSecretInboxStartTime) + secretInboxEndTimeString := helpers.ConvertInt64ToString(currentSecretInboxEndTime) + + newInboxMap := map[string]string{ + "MyIdentityHash": myIdentityHashString, + "TheirIdentityHash": theirIdentityHashString, + "SecretInboxSeed": secretInboxSeedHex, + "NetworkType": networkTypeString, + "InboxStartTime": secretInboxStartTimeString, + "InboxEndTime": secretInboxEndTimeString, + } + + mySecretInboxesMapList = append(mySecretInboxesMapList, newInboxMap) + + return nil + } + + err = addSecretInboxSeedToMapList(currentSecretInboxSeed, currentSecretInboxStartTime, currentSecretInboxEndTime) + if (err != nil) { return false, err } + + err = addSecretInboxSeedToMapList(nextSecretInboxSeed, nextSecretInboxStartTime, nextSecretInboxEndTime) + if (err != nil) { return false, err } + + err = mySecretInboxesMapListDatastore.OverwriteMapList(mySecretInboxesMapList) + if (err != nil) { return false, err } + + return true, nil +} + + + diff --git a/internal/messaging/peerChatKeys/peerChatKeys.go b/internal/messaging/peerChatKeys/peerChatKeys.go new file mode 100644 index 0000000..cc51a16 --- /dev/null +++ b/internal/messaging/peerChatKeys/peerChatKeys.go @@ -0,0 +1,512 @@ + +// peerChatKeys provides functions to store user chat keys + +package peerChatKeys + +// We need to keep their keys in a seperate location, as opposed to reading user chat keys from the database +// We do this because if a user's profile expires/is banned, the person chatting with them may want to continue talking to them. +// If we prevented deleting their profile from the database, that could pose a fingerprinting risk when contacting hosts +// Hosts would know that the requestor had been chatting with the profiles which were expired, because they would normally be deleted +// Therefore, we store the user's chat keys in peerChatKeys, so we can delete their expired profile and still keep their chat keys +// Their profile will be deleted from the database when the identity expires/is banned. + +// Active keys are keys that are a maximum of 1 day older than any any known peer latestChatKeysUpdateTime + +// TODO: create updateStoredChatKeys to refresh all chat keys for all saved identities +// TODO: Delete chat keys for users who are not chatting partners + +import "seekia/internal/encoding" +import "seekia/internal/helpers" +import "seekia/internal/identity" +import "seekia/internal/messaging/peerDevices" +import "seekia/internal/myDatastores/myMap" +import "seekia/internal/myDatastores/myMapList" +import "seekia/internal/profiles/profileStorage" +import "seekia/internal/profiles/readProfiles" +import "seekia/internal/unixTime" + +import "strings" +import "sync" +import "errors" + +// This mutex will be locked whenever we are updating the peer chat keys MyMapList +var updatingPeerChatKeysMutex sync.Mutex + +// This mutex will be locked whenever we are updating the peer chat keys latest update time MyMap +var updatingPeerChatKeysLatestUpdateTimeMutex sync.Mutex + +var peerChatKeysMapListDatastore *myMapList.MyMapList + +// Map Structure: Peer Identity Hash + "@" + networkType -> Latest update unix time +var peerChatKeysLatestUpdateTimeMapDatastore *myMap.MyMap + +// This function must be called whenever an app user signs in +func InitializePeerChatKeysDatastores()error{ + + updatingPeerChatKeysMutex.Lock() + defer updatingPeerChatKeysMutex.Unlock() + + updatingPeerChatKeysLatestUpdateTimeMutex.Lock() + defer updatingPeerChatKeysLatestUpdateTimeMutex.Unlock() + + newPeerChatKeysMapListDatastore, err := myMapList.CreateNewMapList("PeerChatKeys") + if (err != nil) { return err } + + newPeerChatKeysLatestUpdateTimeMapDatastore, err := myMap.CreateNewMap("PeerChatKeysLatestUpdateTime") + if (err != nil) { return err } + + peerChatKeysMapListDatastore = newPeerChatKeysMapListDatastore + peerChatKeysLatestUpdateTimeMapDatastore = newPeerChatKeysLatestUpdateTimeMapDatastore + + return nil +} + + +// This function will return true if we need to download a user's newest chat keys +// It determines this by comparing a user's saved chat keys to their latest chat keys update time +//Outputs: +// -bool: User's chat keys are missing (will return false if their newest profile is disabled) +// -error +func CheckIfUsersChatKeysAreMissing(userIdentityHash [16]byte, networkType byte)(bool, error){ + + isValid := helpers.VerifyNetworkType(networkType) + if (isValid == false){ + networkTypeString := helpers.ConvertByteToString(networkType) + return false, errors.New("CheckIfUsersChatKeysAreMissing called with invalid networkType: " + networkTypeString) + } + + profileIsDisabled, chatKeysExist, _, _, err := GetPeerNewestActiveChatKeys(userIdentityHash, networkType) + if (err != nil) { return false, err } + if (profileIsDisabled == true){ + // We treat this as the chat keys not being missing + return false, nil + } + if (chatKeysExist == true){ + return false, nil + } + + return true, nil +} + +// This function saves a user's chat key latest update time from a message +// It is used to keep track of when peer updates their key set, so the app knows to retrieve new chat keys +// This information could be wrong, if newer keys have been broadcast since the user sent their message +func SavePeerMessageLatestChatKeysUpdateTime(peerIdentityHash [16]byte, networkType byte, theirLatestChatKeysUpdateTime int64, theirMessageSentTime int64)error{ + + updatingPeerChatKeysLatestUpdateTimeMutex.Lock() + defer updatingPeerChatKeysLatestUpdateTimeMutex.Unlock() + + peerIdentityHashString, _, err := identity.EncodeIdentityHashBytesToString(peerIdentityHash) + if (err != nil){ + peerIdentityHashHex := encoding.EncodeBytesToHexString(peerIdentityHash[:]) + return errors.New("SavePeerMessageLatestChatKeysUpdateTime called with invalid peerIdentityHash: " + peerIdentityHashHex) + } + + isValid := helpers.VerifyNetworkType(networkType) + if (isValid == false){ + networkTypeString := helpers.ConvertByteToString(networkType) + return errors.New("SavePeerMessageLatestChatKeysUpdateTime called with invalid networkType: " + networkTypeString) + } + + existingLatestUpdateTimeExists, existingLatestUpdateTime, existingLatestUpdateTimeSentTime, err := getSavedPeerMessageLatestChatKeysUpdateTime(peerIdentityHash, networkType) + if (err != nil){ return err } + if (existingLatestUpdateTimeExists == true){ + + if (existingLatestUpdateTimeSentTime > theirMessageSentTime){ + // We already have a latest update time was sent at later time + // Nothing to do + return nil + } + + // We have a latest update time that was sent earler + // We see if the newer sent latest update time is actually newer + if (existingLatestUpdateTime > theirLatestChatKeysUpdateTime){ + // They are sending a latestUpdateTime that is older than one they have previously sent + // The user has probably moved to a different device, and then moved back to their previous device + // This should not happen + // To avoid this happening we should make sure there is an option in the GUI to reinitialize the application as if it is a new device + // This will require broadcasting a new LatestChatKeysUpdateTime in our profile + // + // We will delete our stored chatKeys and update our datastore latestChatKeysUpdateTime + // This will allow their older chat keys to be used if and when they broadcast a profile from their device. + + networkTypeString := helpers.ConvertByteToString(networkType) + + mapToDelete := map[string]string{ + "PeerIdentityHash": peerIdentityHashString, + "NetworkType": networkTypeString, + } + + err := peerChatKeysMapListDatastore.DeleteMapListItems(mapToDelete) + if (err != nil) { return err } + } + } + + theirLatestChatKeysUpdateTimeString := helpers.ConvertInt64ToString(theirLatestChatKeysUpdateTime) + theirMessageSentTimeString := helpers.ConvertInt64ToString(theirMessageSentTime) + + networkTypeString := helpers.ConvertByteToString(networkType) + + newMapKey := peerIdentityHashString + "@" + networkTypeString + newMapValue := theirLatestChatKeysUpdateTimeString + "$" + theirMessageSentTimeString + + err = peerChatKeysLatestUpdateTimeMapDatastore.SetMapEntry(newMapKey, newMapValue) + if (err != nil) { return err } + + return nil +} + + +// This function retrieves our saved peer latest chat keys update time that we received in a message +//Outputs: +// -bool: Latest update time info exists +// -int64: Latest Update Time +// -int64: Latest update time broadcasted time (the time when the user shared this latestUpdateTime) +// -error +func getSavedPeerMessageLatestChatKeysUpdateTime(peerIdentityHash [16]byte, networkType byte)(bool, int64, int64, error){ + + peerIdentityHashString, _, err := identity.EncodeIdentityHashBytesToString(peerIdentityHash) + if (err != nil){ + peerIdentityHashHex := encoding.EncodeBytesToHexString(peerIdentityHash[:]) + return false, 0, 0, errors.New("getSavedPeerMessageLatestChatKeysUpdateTime called with invalid peerIdentityHash: " + peerIdentityHashHex) + } + + isValid := helpers.VerifyNetworkType(networkType) + if (isValid == false){ + networkTypeString := helpers.ConvertByteToString(networkType) + return false, 0, 0, errors.New("getSavedPeerMessageLatestChatKeysUpdateTime called with invalid networkType: " + networkTypeString) + } + + networkTypeString := helpers.ConvertByteToString(networkType) + + mapKey := peerIdentityHashString + "@" + networkTypeString + + entryExists, mapValue, err := peerChatKeysLatestUpdateTimeMapDatastore.GetMapEntry(mapKey) + if (err != nil) { return false, 0, 0, err } + if (entryExists == false){ + return false, 0, 0, nil + } + + latestUpdateTime, latestUpdateTimeBroadcastTime, delimiterFound := strings.Cut(mapValue, "$") + if (delimiterFound == false){ + return false, 0, 0, errors.New("peerChatKeysLatestUpdateTimeMapDatastore entry is malformed: Map value missing $: " + mapValue) + } + + latestUpdateTimeInt64, err := helpers.ConvertStringToInt64(latestUpdateTime) + if (err != nil){ + return false, 0, 0, errors.New("peerChatKeysLatestUpdateTimeMapDatastore entry value is malformed: Invalid latestUpdateTime: " + latestUpdateTime) + } + + latestUpdateTimeBroadcastTimeInt64, err := helpers.ConvertStringToInt64(latestUpdateTimeBroadcastTime) + if (err != nil){ + return false, 0, 0, errors.New("peerChatKeysLatestUpdateTimeMapDatastore entry value is malformed: Invalid latestUpdateTimeBroadcastTime: " + latestUpdateTimeBroadcastTime) + } + + return true, latestUpdateTimeInt64, latestUpdateTimeBroadcastTimeInt64, nil +} + + +// This function returns the newest saved device chat keys list for a user +// It also checks saved profiles to update saved keys. +//Outputs: +// -bool: Peer newest profile is known to be disabled +// -bool: Peer active chat keys found +// -[32]byte: Nacl Key +// -[1568]byte: Kyber key +// -error +func GetPeerNewestActiveChatKeys(peerIdentityHash [16]byte, networkType byte)(bool, bool, [32]byte, [1568]byte, error){ + + peerIdentityHashString, userIdentityType, err := identity.EncodeIdentityHashBytesToString(peerIdentityHash) + if (err != nil) { + peerIdentityHashHex := encoding.EncodeBytesToHexString(peerIdentityHash[:]) + return false, false, [32]byte{}, [1568]byte{}, errors.New("GetPeerNewestActiveChatKeys called with invalid peerIdentityHash: " + peerIdentityHashHex) + } + + if (userIdentityType != "Mate" && userIdentityType != "Moderator"){ + return false, false, [32]byte{}, [1568]byte{}, errors.New("Trying to get peer chat keys for host identity") + } + + isValid := helpers.VerifyNetworkType(networkType) + if (isValid == false){ + networkTypeString := helpers.ConvertByteToString(networkType) + return false, false, [32]byte{}, [1568]byte{}, errors.New("GetPeerNewestActiveChatKeys called with invalid networkType: " + networkTypeString) + } + + updatingPeerChatKeysMutex.Lock() + defer updatingPeerChatKeysMutex.Unlock() + + //Outputs: + // -bool: Peer newest profile is known to be disabled + // -bool: Any keys found + // -[32]byte: User Nacl key + // -[1568]byte: User Kyber key + // -int64: Time keys were broadcast + // -error + getPeerNewestKnownChatKeys := func()(bool, bool, [32]byte, [1568]byte, int64, error){ + + // We get newest known user chat keys + // We check database and storage, and return whichever are newer + + // Database = Profiles within the badger database + // Storage = myMapList datastore + + //Outputs: + // -bool: Profile found + // -bool: Profile is disabled + // -int64: Profile broadcast time + // -[32]byte: Peer Nacl key + // -[1568]byte: Peer Kyber key + // -error + getPeerChatKeysMapFromDatabase := func()(bool, bool, int64, [32]byte, [1568]byte, error){ + + profileExists, _, _, _, profileBroadcastTime, rawProfileMap, err := profileStorage.GetNewestUserProfile(peerIdentityHash, networkType) + if (err != nil) { return false, false, 0, [32]byte{}, [1568]byte{}, err } + if (profileExists == false) { + return false, false, 0, [32]byte{}, [1568]byte{}, nil + } + + profileIsDisabled, _, err := readProfiles.GetFormattedProfileAttributeFromRawProfileMap(rawProfileMap, "Disabled") + if (err != nil) { return false, false, 0, [32]byte{}, [1568]byte{}, err } + if (profileIsDisabled == true){ + return true, true, profileBroadcastTime, [32]byte{}, [1568]byte{}, nil + } + + exists, peerNaclKey, err := readProfiles.GetFormattedProfileAttributeFromRawProfileMap(rawProfileMap, "NaclKey") + if (err != nil) { return false, false, 0, [32]byte{}, [1568]byte{}, err } + if (exists == false){ + return false, false, 0, [32]byte{}, [1568]byte{}, errors.New("Database corrupt: Contains Mate/Moderator profile missing NaclKey") + } + + exists, peerKyberKey, err := readProfiles.GetFormattedProfileAttributeFromRawProfileMap(rawProfileMap, "KyberKey") + if (err != nil) { return false, false, 0, [32]byte{}, [1568]byte{}, err } + if (exists == false){ + return false, false, 0, [32]byte{}, [1568]byte{}, errors.New("Database corrupt: Contains Mate/Moderator profile missing KyberKey") + } + + peerNaclKeyBytes, err := encoding.DecodeBase64StringToBytes(peerNaclKey) + if (err != nil){ + return false, false, 0, [32]byte{}, [1568]byte{}, errors.New("Database corrupt: Contains Mate/Moderator profile with invalid NaclKey: Not Base64: " + peerNaclKey) + } + + if (len(peerNaclKeyBytes) != 32){ + return false, false, 0, [32]byte{}, [1568]byte{}, errors.New("Database corrupt: Contains Mate/Moderator profile with invalid NaclKey: Invalid length: " + peerNaclKey) + } + + peerNaclKeyArray := [32]byte(peerNaclKeyBytes) + + peerKyberKeyBytes, err := encoding.DecodeBase64StringToBytes(peerKyberKey) + if (err != nil){ + return false, false, 0, [32]byte{}, [1568]byte{}, errors.New("Database corrupt: Contains Mate/Moderator profile with invalid KyberKey: Not Base64: " + peerKyberKey) + } + + if (len(peerKyberKeyBytes) != 1568){ + return false, false, 0, [32]byte{}, [1568]byte{}, errors.New("Database corrupt: Contains Mate/Moderator profile with invalid KyberKey: Invalid length: " + peerKyberKey) + } + + peerKyberKeyArray := [1568]byte(peerKyberKeyBytes) + + return true, false, profileBroadcastTime, peerNaclKeyArray, peerKyberKeyArray, nil + } + + //Outputs: + // -bool: Peer stored keys found + // -bool: Peer is disabled + // -int64: Broadcast time of these keys/disabled status + // -[32]byte: Peer Nacl Key + // -[1568]byte: Peer Kyber key + // -error + getPeerChatKeysMapListFromMyMapListStorage := func()(bool, bool, int64, [32]byte, [1568]byte, error){ + + networkTypeString := helpers.ConvertByteToString(networkType) + + lookupMap := map[string]string{ + "PeerIdentityHash": peerIdentityHashString, + "NetworkType": networkTypeString, + } + + exists, retrievedMapListItems, err := peerChatKeysMapListDatastore.GetMapListItems(lookupMap) + if (err != nil) { return false, false, 0, [32]byte{}, [1568]byte{}, err } + if (exists == false){ + return false, false, 0, [32]byte{}, [1568]byte{}, nil + } + + if (len(retrievedMapListItems) != 1) { + return false, false, 0, [32]byte{}, [1568]byte{}, errors.New("Malformed peer chat keys maplist: multiple entries found.") + } + + peerChatKeysMap := retrievedMapListItems[0] + + keysBroadcastTime, exists := peerChatKeysMap["BroadcastedTime"] + if (exists == false) { + return false, false, 0, [32]byte{}, [1568]byte{}, errors.New("Malformed peer chat keys maplist: Item missing BroadcastedTime") + } + + keysBroadcastTimeInt64, err := helpers.ConvertBroadcastTimeStringToInt64(keysBroadcastTime) + if (err != nil) { + return false, false, 0, [32]byte{}, [1568]byte{}, errors.New("Malformed peer chat keys maplist: Item contains invalid BroadcastedTime: " + keysBroadcastTime) + } + + peerIsDisabled, exists := peerChatKeysMap["PeerIsDisabled"] + if (exists == true && peerIsDisabled == "Yes"){ + return true, true, keysBroadcastTimeInt64, [32]byte{}, [1568]byte{}, nil + } + + peerNaclKey, exists := peerChatKeysMap["NaclKey"] + if (exists == false) { + return false, false, 0, [32]byte{}, [1568]byte{}, errors.New("Malformed peer chat keys maplist: Item missing NaclKey") + } + + peerKyberKey, exists := peerChatKeysMap["KyberKey"] + if (exists == false) { + return false, false, 0, [32]byte{}, [1568]byte{}, errors.New("Malformed peer chat keys maplist: Item missing KyberKey") + } + + peerNaclKeyBytes, err := encoding.DecodeBase64StringToBytes(peerNaclKey) + if (err != nil){ + return false, false, 0, [32]byte{}, [1568]byte{}, errors.New("Malformed peer chat keys maplist: Item contains invalid NaclKey: Not Base64: " + peerNaclKey) + } + + if (len(peerNaclKeyBytes) != 32){ + return false, false, 0, [32]byte{}, [1568]byte{}, errors.New("Malformed peer chat keys maplist: Item contains invalid NaclKey: Invalid length: " + peerNaclKey) + } + + peerNaclKeyArray := [32]byte(peerNaclKeyBytes) + + peerKyberKeyBytes, err := encoding.DecodeBase64StringToBytes(peerKyberKey) + if (err != nil){ + return false, false, 0, [32]byte{}, [1568]byte{}, errors.New("Malformed peer chat keys maplist: Item contains invalid KyberKey: Not Base64: " + peerKyberKey) + } + + if (len(peerKyberKeyBytes) != 1568){ + return false, false, 0, [32]byte{}, [1568]byte{}, errors.New("Malformed peer chat keys maplist: Item contains invalid KyberKey: Invalid length: " + peerKyberKey) + } + + peerKyberKeyArray := [1568]byte(peerKyberKeyBytes) + + return true, false, keysBroadcastTimeInt64, peerNaclKeyArray, peerKyberKeyArray, nil + } + + databaseUserProfileExists, databaseProfileIsDisabled, databaseUserProfileBroadcastTime, databaseUserNaclKey, databaseUserKyberKey, err := getPeerChatKeysMapFromDatabase() + if (err != nil) { return false, false, [32]byte{}, [1568]byte{}, 0, err } + + storageUserChatKeysInfoExists, storageProfileIsDisabled, storageUserProfileBroadcastTime, storageUserNaclKey, storageUserKyberKey, err := getPeerChatKeysMapListFromMyMapListStorage() + if (err != nil) { return false, false, [32]byte{}, [1568]byte{}, 0, err } + + if (databaseUserProfileExists == false){ + + if (storageUserChatKeysInfoExists == false){ + return false, false, [32]byte{}, [1568]byte{}, 0, nil + } + + if (storageProfileIsDisabled == true){ + return true, false, [32]byte{}, [1568]byte{}, 0, nil + } + + return false, true, storageUserNaclKey, storageUserKyberKey, storageUserProfileBroadcastTime, nil + } + // Database chat keys exist + + if (storageUserChatKeysInfoExists == true){ + + // Both the database profile and the datastore info exist + // We see which contains newer information + + if (databaseUserProfileBroadcastTime <= storageUserProfileBroadcastTime){ + // myMapList storage is up to date, or even more recent than the stored profile + // The latter would occur if a newer profile was deleted at some point, and an older profile was downloaded + // We return the myMapList stored data + + if (storageProfileIsDisabled == true){ + return true, false, [32]byte{}, [1568]byte{}, 0, nil + } + return false, true, storageUserNaclKey, storageUserKyberKey, storageUserProfileBroadcastTime, nil + } + } + + // MyMapList chat keys entry is either missing or out of date. + // Update them with database profile keys, and return the database profile chat keys + // Note that just because the broadcasted profile is newer does not mean the chat keys are any different. + + networkTypeString := helpers.ConvertByteToString(networkType) + + deleteLookupMap := map[string]string{ + "PeerIdentityHash": peerIdentityHashString, + "NetworkType": networkTypeString, + } + + err = peerChatKeysMapListDatastore.DeleteMapListItems(deleteLookupMap) + if (err != nil) { return false, false, [32]byte{}, [1568]byte{}, 0, err } + + broadcastedTimeString := helpers.ConvertInt64ToString(databaseUserProfileBroadcastTime) + + newChatKeysMap := map[string]string{ + "PeerIdentityHash": peerIdentityHashString, + "NetworkType": networkTypeString, + "BroadcastedTime": broadcastedTimeString, + } + + if (databaseProfileIsDisabled == true){ + newChatKeysMap["PeerIsDisabled"] = "Yes" + } else { + + databaseUserNaclKeyString := encoding.EncodeBytesToBase64String(databaseUserNaclKey[:]) + databaseUserKyberKeyString := encoding.EncodeBytesToBase64String(databaseUserKyberKey[:]) + + newChatKeysMap["NaclKey"] = databaseUserNaclKeyString + newChatKeysMap["KyberKey"] = databaseUserKyberKeyString + } + + err = peerChatKeysMapListDatastore.AddMapListItem(newChatKeysMap) + if (err != nil) { return false, false, [32]byte{}, [1568]byte{}, 0, err } + + if (databaseProfileIsDisabled == true){ + return true, false, [32]byte{}, [1568]byte{}, 0, nil + } + + return false, true, databaseUserNaclKey, databaseUserKyberKey, databaseUserProfileBroadcastTime, nil + } + + peerProfileIsDisabled, peerChatKeysExist, peerNewestKnownNaclKey, peerNewestKnownKyberKey, peerNewestKnownKeysBroadcastTime, err := getPeerNewestKnownChatKeys() + if (err != nil) { return false, false, [32]byte{}, [1568]byte{}, err } + if (peerProfileIsDisabled == true){ + return true, false, [32]byte{}, [1568]byte{}, nil + } + if (peerChatKeysExist == false){ + return false, false, [32]byte{}, [1568]byte{}, nil + } + + // We check to see if user has updated their device since they sent/broadcast these keys + + deviceInfoFound, _, deviceFirstSeenTime, err := peerDevices.GetPeerNewestDeviceInfo(peerIdentityHash, networkType) + if (err != nil) { return false, false, [32]byte{}, [1568]byte{}, err } + if (deviceInfoFound == true){ + if (deviceFirstSeenTime > peerNewestKnownKeysBroadcastTime){ + // User has changed their device since they sent/broadcast these keys + // We do not have access to the user's chat keys + return false, false, [32]byte{}, [1568]byte{}, nil + } + } + + // Now we check to see if user has sent a newer ChatKeysLatestUpdateTime + // We allow a 1 day grace period where old keys will still work + + newestUpdateTimeIsKnown, newestUpdateTime, _, err := getSavedPeerMessageLatestChatKeysUpdateTime(peerIdentityHash, networkType) + if (err != nil) { return false, false, [32]byte{}, [1568]byte{}, err } + if (newestUpdateTimeIsKnown == true){ + + dayUnix := unixTime.GetDayUnix() + + keysExpirationTime := newestUpdateTime - dayUnix + + if (peerNewestKnownKeysBroadcastTime < keysExpirationTime){ + return false, false, [32]byte{}, [1568]byte{}, nil + } + } + + return false, true, peerNewestKnownNaclKey, peerNewestKnownKyberKey, nil +} + + + + + diff --git a/internal/messaging/peerDevices/peerDevices.go b/internal/messaging/peerDevices/peerDevices.go new file mode 100644 index 0000000..142b45a --- /dev/null +++ b/internal/messaging/peerDevices/peerDevices.go @@ -0,0 +1,285 @@ + +// peerDevices provides functions to keep track of peer device identifiers +// When a user changes their device identifier, we discard their old secret inboxes and chat keys +// We use this package to keep track of when a user has moved to a new device + +package peerDevices + +import "seekia/internal/encoding" +import "seekia/internal/helpers" +import "seekia/internal/identity" +import "seekia/internal/myDatastores/myMap" +import "seekia/internal/profiles/profileStorage" +import "seekia/internal/profiles/readProfiles" +import "seekia/internal/unixTime" + +import "time" +import "strings" +import "sync" +import "errors" + +// We lock this whenever we are updating the map +var updatingPeerDevicesMapMutex sync.Mutex + +// Map Structure: Peer Identity Hash + "@" + Network Type -> Device Identifier + "$" + Device First Seen Time +var peerDevicesMapDatastore *myMap.MyMap + +// This function must be called whenever an app user signs in +func InitializePeerDevicesDatastore()error{ + + updatingPeerDevicesMapMutex.Lock() + defer updatingPeerDevicesMapMutex.Unlock() + + newPeerDevicesMapDatastore, err := myMap.CreateNewMap("PeerDevices") + if (err != nil) { return err } + + peerDevicesMapDatastore = newPeerDevicesMapDatastore + + return nil +} + +//Outputs: +// -bool: Device info found +// -[11]byte: Newest device identifier +// -int64: Time that this device was first seen +// -We discard all chat keys/secret inboxes that were sent before this point in time +// -error +func GetPeerNewestDeviceInfo(peerIdentityHash [16]byte, networkType byte)(bool, [11]byte, int64, error){ + + identityType, err := identity.GetIdentityTypeFromIdentityHash(peerIdentityHash) + if (err != nil){ + peerIdentityHashHex := encoding.EncodeBytesToHexString(peerIdentityHash[:]) + return false, [11]byte{}, 0, errors.New("GetPeerNewestDeviceInfo called with invalid identityHash: " + peerIdentityHashHex) + } + if (identityType == "Host"){ + return false, [11]byte{}, 0, errors.New("GetPeerNewestDeviceInfo called with Host identity.") + } + + isValid := helpers.VerifyNetworkType(networkType) + if (isValid == false){ + networkTypeString := helpers.ConvertByteToString(networkType) + return false, [11]byte{}, 0, errors.New("GetPeerNewestDeviceInfo called with invalid networkType: " + networkTypeString) + } + + // We use this lock because we are comparing the map time with the newest stored profile broadcast time + // If we added a newer time from a message during this process, without a lock, we could overwrite the newer time from the message + updatingPeerDevicesMapMutex.Lock() + defer updatingPeerDevicesMapMutex.Unlock() + + //Outputs: + // -bool: Peer profile found + // -string: Peer profile device identifier (encoded Hex) + // -[11]byte: Peer profile device identifier + // -int64: Broadcast time of profile + // -error + getPeerDeviceIdentifierFromNewestProfile := func()(bool, string, [11]byte, int64, error){ + + profileExists, _, _, _, profileBroadcastTime, rawProfileMap, err := profileStorage.GetNewestUserProfile(peerIdentityHash, networkType) + if (err != nil) { return false, "", [11]byte{}, 0, err } + if (profileExists == false){ + return false, "", [11]byte{}, 0, nil + } + + attributeExists, deviceIdentifierHex, err := readProfiles.GetFormattedProfileAttributeFromRawProfileMap(rawProfileMap, "DeviceIdentifier") + if (err != nil) { return false, "", [11]byte{}, 0, err } + if (attributeExists == false){ + return false, "", [11]byte{}, 0, errors.New("Database corrupt: Contains Mate/Moderator profile missing DeviceIdentifier.") + } + + deviceIdentifierBytes, err := encoding.DecodeHexStringToBytes(deviceIdentifierHex) + if (err != nil){ + return false, "", [11]byte{}, 0, errors.New("Database corrupt: Contains profile with invalid DeviceIdentifier: " + deviceIdentifierHex) + } + + if (len(deviceIdentifierBytes) != 11){ + return false, "", [11]byte{}, 0, errors.New("Database corrupt: Contains profile with invalid DeviceIdentifier: " + deviceIdentifierHex) + } + + deviceIdentifierArray := [11]byte(deviceIdentifierBytes) + + return true, deviceIdentifierHex, deviceIdentifierArray, profileBroadcastTime, nil + } + + peerProfileFound, peerProfileDeviceIdentifierHex, peerProfileDeviceIdentifier, peerProfileBroadcastTime, err := getPeerDeviceIdentifierFromNewestProfile() + if (err != nil) { return false, [11]byte{}, 0, err } + + peerIdentityHashString, _, err := identity.EncodeIdentityHashBytesToString(peerIdentityHash) + if (err != nil) { + peerIdentityHashHex := encoding.EncodeBytesToHexString(peerIdentityHash[:]) + return false, [11]byte{}, 0, errors.New("GetIdentityTypeFromIdentityHash not verifying identity hash: " + peerIdentityHashHex) + } + + networkTypeString := helpers.ConvertByteToString(networkType) + + peerDevicesMapDatastoreKey := peerIdentityHashString + "@" + networkTypeString + + mapEntryExists, mapValue, err := peerDevicesMapDatastore.GetMapEntry(peerDevicesMapDatastoreKey) + if (err != nil) { return false, [11]byte{}, 0, err } + if (mapEntryExists == false){ + + if (peerProfileFound == false){ + return false, [11]byte{}, 0, nil + } + + // peerProfileFound == true + + peerProfileBroadcastTimeString := helpers.ConvertInt64ToString(peerProfileBroadcastTime) + + newMapValue := peerProfileDeviceIdentifierHex + "$" + peerProfileBroadcastTimeString + + err := peerDevicesMapDatastore.SetMapEntry(peerDevicesMapDatastoreKey, newMapValue) + if (err != nil) { return false, [11]byte{}, 0, err } + + return true, peerProfileDeviceIdentifier, peerProfileBroadcastTime, nil + } + + // mapEntryExists == true + + mapDeviceIdentifier, mapDeviceFirstSeenTimeString, delimiterFound := strings.Cut(mapValue, "$") + if (delimiterFound == false){ + return false, [11]byte{}, 0, errors.New("peerDevicesMapDatastore is malformed: Invalid value: " + mapValue) + } + + mapDeviceIdentifierBytes, err := encoding.DecodeHexStringToBytes(mapDeviceIdentifier) + if (err != nil){ + return false, [11]byte{}, 0, errors.New("peerDevicesMapDatastore is malformed: Invalid DeviceIdentifier: " + mapDeviceIdentifier) + } + + if (len(mapDeviceIdentifierBytes) != 11){ + return false, [11]byte{}, 0, errors.New("peerDevicesMapDatastore is malformed: Invalid DeviceIdentifier: " + mapDeviceIdentifier) + } + + mapDeviceIdentifierArray := [11]byte(mapDeviceIdentifierBytes) + + mapDeviceFirstSeenTime, err := helpers.ConvertBroadcastTimeStringToInt64(mapDeviceFirstSeenTimeString) + if (err != nil){ + return false, [11]byte{}, 0, errors.New("peerDevicesMapDatastore is malformed: Invalid device first seen time: " + mapDeviceFirstSeenTimeString) + } + + if (peerProfileFound == false){ + + return true, mapDeviceIdentifierArray, mapDeviceFirstSeenTime, nil + } + + // Both a map entry and a stored profile exist + // We see if the profile is newer/older than what we have stored in the map + // If it is, we update the map + + if (peerProfileDeviceIdentifierHex == mapDeviceIdentifier){ + + if (mapDeviceFirstSeenTime <= peerProfileBroadcastTime){ + // Map device first seen time is earlier than or equal to the profile + // Nothing to update + return true, mapDeviceIdentifierArray, mapDeviceFirstSeenTime, nil + } + + // Profile has a device first seen time that is earlier + // We will update the map with the profile time + } else { + + if (mapDeviceFirstSeenTime >= peerProfileBroadcastTime){ + // Map device first seen time is newer than or equal to the profile + // Nothing to update + return true, mapDeviceIdentifierArray, mapDeviceFirstSeenTime, nil + } + } + + peerProfileBroadcastTimeString := helpers.ConvertInt64ToString(peerProfileBroadcastTime) + + newMapValue := peerProfileDeviceIdentifierHex + "$" + peerProfileBroadcastTimeString + + err = peerDevicesMapDatastore.SetMapEntry(peerDevicesMapDatastoreKey, newMapValue) + if (err != nil) { return false, [11]byte{}, 0, err } + + return true, peerProfileDeviceIdentifier, peerProfileBroadcastTime, nil +} + + +func AddPeerDeviceIdentifierFromMessage(peerIdentityHash [16]byte, networkType byte, deviceIdentifierBytes [11]byte, messageTimeSentUnix int64)error{ + + peerIdentityHashString, identityType, err := identity.EncodeIdentityHashBytesToString(peerIdentityHash) + if (err != nil) { + peerIdentityHashHex := encoding.EncodeBytesToHexString(peerIdentityHash[:]) + return errors.New("AddPeerDeviceIdentifierFromMessage called with invalid identityHash: " + peerIdentityHashHex) + } + if (identityType == "Host"){ + return errors.New("AddPeerDeviceIdentifierFromMessage called with Host identity.") + } + + isValid := helpers.VerifyNetworkType(networkType) + if (isValid == false){ + networkTypeString := helpers.ConvertByteToString(networkType) + return errors.New("AddPeerDeviceIdentifierFromMessage called with invalid networkType: " + networkTypeString) + } + + deviceIdentifier := encoding.EncodeBytesToHexString(deviceIdentifierBytes[:]) + + currentTime := time.Now().Unix() + + dayUnix := unixTime.GetDayUnix() + + latestAllowedTime := currentTime + dayUnix + + if (messageTimeSentUnix > latestAllowedTime){ + // Either sender's or our own computer clock must be inaccurate + // We don't want to add the device identifier because it could prevent us from ever being able to contact the user + // For example, if the user's message sent time is accidentally 1 year in the future, we would not be able to contact them for 1 year + // We would mistakenly reject all of their new profile chat keys as being out of date + return nil + } + + networkTypeString := helpers.ConvertByteToString(networkType) + + peerDevicesMapDatastoreKey := peerIdentityHashString + "@" + networkTypeString + + updatingPeerDevicesMapMutex.Lock() + defer updatingPeerDevicesMapMutex.Unlock() + + mapEntryExists, mapValue, err := peerDevicesMapDatastore.GetMapEntry(peerDevicesMapDatastoreKey) + if (err != nil) { return err } + if (mapEntryExists == true){ + + // We see if the map entry is newer than the device identifier sent in the message + // If it is newer, then we will not add the message device identifier + + mapDeviceIdentifier, mapDeviceFirstSeenTimeString, delimiterFound := strings.Cut(mapValue, "$") + if (delimiterFound == false){ + return errors.New("peerDevicesMapDatastore is malformed: Invalid value: " + mapValue) + } + + mapDeviceFirstSeenTime, err := helpers.ConvertBroadcastTimeStringToInt64(mapDeviceFirstSeenTimeString) + if (err != nil){ + return errors.New("peerDevicesMapDatastore is malformed: Invalid device first seen time: " + mapDeviceFirstSeenTimeString) + } + + if (mapDeviceIdentifier == deviceIdentifier){ + + if (mapDeviceFirstSeenTime <= messageTimeSentUnix){ + // Our map device first seen time is earlier than or equal to the message + // Nothing to do + return nil + } + // We will update our map device first seen time to be earlier + } else { + + if (mapDeviceFirstSeenTime >= messageTimeSentUnix){ + // Our map device first seen time is newer than or equal to the input message. + // Nothing to do + return nil + } + } + } + + messageTimeSentUnixString := helpers.ConvertInt64ToString(messageTimeSentUnix) + + newMapValue := deviceIdentifier + "$" + messageTimeSentUnixString + + err = peerDevicesMapDatastore.SetMapEntry(peerDevicesMapDatastoreKey, newMapValue) + if (err != nil) { return err } + + return nil +} + + + + diff --git a/internal/messaging/peerSecretInboxes/peerSecretInboxes.go b/internal/messaging/peerSecretInboxes/peerSecretInboxes.go new file mode 100644 index 0000000..37bf926 --- /dev/null +++ b/internal/messaging/peerSecretInboxes/peerSecretInboxes.go @@ -0,0 +1,355 @@ + +// peerSecretInboxes provides functions to manage secret inboxes of users that we are chatting with + +package peerSecretInboxes + +// Secret inboxes are inboxes that users send to us in messages +// They are used to obscure the identity of the recipient, unlike public inboxes. +// Messages should only be sent to them during their epoch. +// After a peer's secret inbox epoch passes, the inbox is expired, and can be deleted. +// This package keeps track of the peer secret inboxes we should be sending our messages to + +import "seekia/internal/encoding" +import "seekia/internal/helpers" +import "seekia/internal/identity" +import "seekia/internal/messaging/inbox" +import "seekia/internal/messaging/peerDevices" +import "seekia/internal/messaging/secretInboxEpoch" +import "seekia/internal/myDatastores/myMapList" + +import "errors" +import "sync" +import "time" + +// This will be locked whenever we update the map list +var updatingPeerSecretInboxesMapListMutex sync.Mutex + +var peerSecretInboxesMapListDatastore *myMapList.MyMapList + +// This function must be called whenever an app user signs in +func InitializePeerSecretInboxesDatastore()error{ + + updatingPeerSecretInboxesMapListMutex.Lock() + defer updatingPeerSecretInboxesMapListMutex.Unlock() + + newPeerSecretInboxesMapListDatastore, err := myMapList.CreateNewMapList("PeerSecretInboxes") + if (err != nil) { return err } + + peerSecretInboxesMapListDatastore = newPeerSecretInboxesMapListDatastore + + return nil +} + +//Outputs: +// -bool: Parameters exist +// -error +func AddPeerConversationSecretInboxSeeds(myIdentityHash [16]byte, peerIdentityHash [16]byte, networkType byte, messageSentTime int64, peerCurrentSecretInboxSeed [22]byte, peerNextSecretInboxSeed [22]byte)(bool, error){ + + myIdentityHashString, myIdentityType, err := identity.EncodeIdentityHashBytesToString(myIdentityHash) + if (err != nil){ + myIdentityHashHex := encoding.EncodeBytesToHexString(myIdentityHash[:]) + return false, errors.New("AddPeerConversationSecretInboxSeeds called with invalid myIdentityHash: " + myIdentityHashHex) + } + peerIdentityHashString, peerIdentityType, err := identity.EncodeIdentityHashBytesToString(peerIdentityHash) + if (err != nil) { + peerIdentityHashHex := encoding.EncodeBytesToHexString(peerIdentityHash[:]) + return false, errors.New("AddPeerConversationSecretInboxSeeds called with invalid peerIdentityHash: " + peerIdentityHashHex) + } + + if (myIdentityType != peerIdentityType){ + return false, errors.New("AddPeerConversationSecretInboxSeeds called with mismatched my/peer identityTypes.") + } + + networkTypeIsValid := helpers.VerifyNetworkType(networkType) + if (networkTypeIsValid == false){ + networkTypeString := helpers.ConvertByteToString(networkType) + return false, errors.New("AddPeerConversationSecretInboxSeeds called with invalid networkType: " + networkTypeString) + } + + parametersExist, currentSecretInboxStartTime, currentSecretInboxEndTime, nextSecretInboxStartTime, nextSecretInboxEndTime, err := secretInboxEpoch.GetSecretInboxEpochStartAndEndTimes(networkType, messageSentTime) + if (err != nil) { return false, err } + if (parametersExist == false){ + return false, nil + } + + updatingPeerSecretInboxesMapListMutex.Lock() + defer updatingPeerSecretInboxesMapListMutex.Unlock() + + peerSecretInboxesMapList, err := peerSecretInboxesMapListDatastore.GetMapList() + if (err != nil) { return false, err } + + addSecretInboxToPeerInboxesMapList := func(secretInboxSeed [22]byte, inboxStartTime int64, inboxEndTime int64)error{ + + secretInboxSeedHex := encoding.EncodeBytesToHexString(secretInboxSeed[:]) + + for _, inboxMap := range peerSecretInboxesMapList{ + + conversationMyIdentityHash, exists := inboxMap["MyIdentityHash"] + if (exists == false){ + return errors.New("Malformed peerSecretInboxesMapList: Item missing MyIdentityHash") + } + + if (conversationMyIdentityHash != myIdentityHashString){ + continue + } + + conversationTheirIdentityHash, exists := inboxMap["TheirIdentityHash"] + if (exists == false) { + return errors.New("Malformed peerSecretInboxesMapList: Item missing TheirIdentityHash") + } + + if (conversationTheirIdentityHash != peerIdentityHashString){ + continue + } + + conversationNetworkType, exists := inboxMap["NetworkType"] + if (exists == false){ + return errors.New("Malformed peerSecretInboxesMapList: Item missing NetworkType") + } + + conversationNetworkTypeByte, err := helpers.ConvertNetworkTypeStringToByte(conversationNetworkType) + if (err != nil){ + return errors.New("Malformed peerSecretInboxesMapList: Item contains invalid NetworkType: " + conversationNetworkType) + } + if (conversationNetworkTypeByte != networkType){ + continue + } + + currentSecretInboxSeed, exists := inboxMap["SecretInboxSeed"] + if (exists == false){ + return errors.New("Malformed peerSecretInboxesMapList: Item missing SecretInboxSeed") + } + + if (currentSecretInboxSeed != secretInboxSeedHex){ + continue + } + + // An entry already exists for this inbox + // We will update it if needed + + mostRecentSentTime, exists := inboxMap["MostRecentSentTime"] + if (exists == false){ + return errors.New("Malformed peerSecretInboxesMapList: Item missing MostRecentSentTime") + } + + mostRecentSentTimeInt64, err := helpers.ConvertStringToInt64(mostRecentSentTime) + if (err != nil){ + return errors.New("Malformed peerSecretInboxesMapList: Item contains invalid MostRecentSentTime: " + mostRecentSentTime) + } + + if (mostRecentSentTimeInt64 < messageSentTime){ + messageSentTimeString := helpers.ConvertInt64ToString(messageSentTime) + inboxMap["MostRecentSentTime"] = messageSentTimeString + } + + existingInboxStartTimeString, exists := inboxMap["InboxStartTime"] + if (exists == false){ + return errors.New("Malformed peerSecretInboxesMapList: Item missing InboxStartTime") + } + existingInboxStartTime, err := helpers.ConvertStringToInt64(existingInboxStartTimeString) + if (err != nil){ + return errors.New("Malformed peerSecretInboxesMapList: Item contains invalid InboxStartTime: " + existingInboxStartTimeString) + } + + existingInboxEndTimeString, exists := inboxMap["InboxEndTime"] + if (exists == false){ + return errors.New("Malformed peerSecretInboxesMapList: Item missing InboxEndTime") + } + existingInboxEndTime, err := helpers.ConvertStringToInt64(existingInboxEndTimeString) + if (err != nil){ + return errors.New("Malformed peerSecretInboxesMapList: Item contains invalid InboxEndTime: " + existingInboxEndTimeString) + } + + if (existingInboxStartTime <= inboxStartTime && existingInboxEndTime >= inboxEndTime){ + // Existing inbox start/end times are valid + // Nothing else to change + return nil + } + + // Start/end times must be expanded + + newInboxStartTime := min(existingInboxStartTime, inboxStartTime) + + newInboxEndTime := max(existingInboxEndTime, inboxEndTime) + + newInboxStartTimeString := helpers.ConvertInt64ToString(newInboxStartTime) + newInboxEndTimeString := helpers.ConvertInt64ToString(newInboxEndTime) + + inboxMap["InboxStartTime"] = newInboxStartTimeString + inboxMap["InboxEndTime"] = newInboxEndTimeString + + return nil + } + + // This is only reached if the inbox map does not exist. + + networkTypeString := helpers.ConvertByteToString(networkType) + messageSentTimeString := helpers.ConvertInt64ToString(messageSentTime) + inboxStartTimeString := helpers.ConvertInt64ToString(inboxStartTime) + inboxEndTimeString := helpers.ConvertInt64ToString(inboxEndTime) + + newInboxMap := map[string]string{ + "MyIdentityHash": myIdentityHashString, + "TheirIdentityHash": peerIdentityHashString, + "NetworkType": networkTypeString, + "SecretInboxSeed": secretInboxSeedHex, + "MostRecentSentTime": messageSentTimeString, + "InboxStartTime": inboxStartTimeString, + "InboxEndTime": inboxEndTimeString, + } + + peerSecretInboxesMapList = append(peerSecretInboxesMapList, newInboxMap) + + return nil + } + + err = addSecretInboxToPeerInboxesMapList(peerCurrentSecretInboxSeed, currentSecretInboxStartTime, currentSecretInboxEndTime) + if (err != nil) { return false, err } + + err = addSecretInboxToPeerInboxesMapList(peerNextSecretInboxSeed, nextSecretInboxStartTime, nextSecretInboxEndTime) + if (err != nil) { return false, err } + + err = peerSecretInboxesMapListDatastore.OverwriteMapList(peerSecretInboxesMapList) + if (err != nil) { return false, err } + + return true, nil +} + + +//Outputs: +// -bool: Secret inbox found +// -[10]byte: Current Secret inbox +// -[32]byte: Current secret inbox sealer key +// -error +func GetPeerConversationCurrentSecretInbox(myIdentityHash [16]byte, peerIdentityHash [16]byte, networkType byte)(bool, [10]byte, [32]byte, error){ + + myIdentityHashString, myIdentityType, err := identity.EncodeIdentityHashBytesToString(myIdentityHash) + if (err != nil){ + myIdentityHashHex := encoding.EncodeBytesToHexString(myIdentityHash[:]) + return false, [10]byte{}, [32]byte{}, errors.New("GetPeerConversationCurrentSecretInbox called with invalid myIdentityHash: " + myIdentityHashHex) + } + peerIdentityHashString, peerIdentityType, err := identity.EncodeIdentityHashBytesToString(peerIdentityHash) + if (err != nil) { + peerIdentityHashHex := encoding.EncodeBytesToHexString(peerIdentityHash[:]) + return false, [10]byte{}, [32]byte{}, errors.New("GetPeerConversationCurrentSecretInbox called with invalid peerIdentityHash: " + peerIdentityHashHex) + } + + if (myIdentityType != peerIdentityType){ + return false, [10]byte{}, [32]byte{}, errors.New("GetPeerConversationCurrentSecretInbox called with mismatched my/peer identityTypes.") + } + + networkTypeIsValid := helpers.VerifyNetworkType(networkType) + if (networkTypeIsValid == false){ + networkTypeString := helpers.ConvertByteToString(networkType) + return false, [10]byte{}, [32]byte{}, errors.New("GetPeerConversationCurrentSecretInbox called with invalid networkType: " + networkTypeString) + } + + // We find the newest available peer secret inbox seed for this conversation + // We first retrieve all peer secret inboxes for this conversation that exist on this network + + networkTypeString := helpers.ConvertByteToString(networkType) + + lookupMap := map[string]string{ + "MyIdentityHash": myIdentityHashString, + "TheirIdentityHash": peerIdentityHashString, + "NetworkType": networkTypeString, + } + + anyExist, peerSecretInboxesMapList, err := peerSecretInboxesMapListDatastore.GetMapListItems(lookupMap) + if (err != nil) { return false, [10]byte{}, [32]byte{}, err } + if (anyExist == false){ + return false, [10]byte{}, [32]byte{}, nil + } + + currentTime := time.Now().Unix() + + anyValidInboxFound := false + var newestSecretInboxSeed [22]byte + newestSecretInboxMostRecentSentTime := int64(0) + + for _, inboxMap := range peerSecretInboxesMapList{ + + inboxStartTimeString, exists := inboxMap["InboxStartTime"] + if (exists == false){ + return false, [10]byte{}, [32]byte{}, errors.New("Malformed peerSecretInboxesMapList: Item missing InboxStartTime") + } + + inboxStartTime, err := helpers.ConvertStringToInt64(inboxStartTimeString) + if (err != nil){ + return false, [10]byte{}, [32]byte{}, errors.New("Malformed peerSecretInboxesMapList: Item contains invalid inboxStartTime: " + inboxStartTimeString) + } + + inboxEndTimeString, exists := inboxMap["InboxEndTime"] + if (exists == false){ + return false, [10]byte{}, [32]byte{}, errors.New("Malformed peerSecretInboxesMapList: Item missing InboxEndTime") + } + + inboxEndTime, err := helpers.ConvertStringToInt64(inboxEndTimeString) + if (err != nil){ + return false, [10]byte{}, [32]byte{}, errors.New("Malformed peerSecretInboxesMapList: Item contains invalid inboxEndTime: " + inboxEndTimeString) + } + + if (inboxStartTime > currentTime || inboxEndTime < currentTime){ + // Inbox is not valid right now. Skip it. + continue + } + + secretInboxSeedString, exists := inboxMap["SecretInboxSeed"] + if (exists == false){ + return false, [10]byte{}, [32]byte{}, errors.New("Malformed peerSecretInboxesMapList: Item missing SecretInboxSeed") + } + + secretInboxSeedBytes, err := encoding.DecodeHexStringToBytes(secretInboxSeedString) + if (err != nil){ + return false, [10]byte{}, [32]byte{}, errors.New("Malformed peerSecretInboxesMapList: Item contains invalid SecretInboxSeed: Not Hex.") + } + + if (len(secretInboxSeedBytes) != 22){ + return false, [10]byte{}, [32]byte{}, errors.New("Malformed peerSecretInboxesMapList: Item contains invalid SecretInboxSeed: Invalid length.") + } + + secretInboxSeed := [22]byte(secretInboxSeedBytes) + + inboxMostRecentSentTimeString, exists := inboxMap["MostRecentSentTime"] + if (exists == false){ + return false, [10]byte{}, [32]byte{}, errors.New("Malformed peerSecretInboxesMapList: Item missing MostRecentSentTime") + } + + inboxMostRecentSentTime, err := helpers.ConvertStringToInt64(inboxMostRecentSentTimeString) + if (err != nil){ + return false, [10]byte{}, [32]byte{}, errors.New("Malformed peerSecretInboxesMapList: Item contains invalid MostRecentSentTime: " + inboxMostRecentSentTimeString) + } + + if (anyValidInboxFound == false || newestSecretInboxMostRecentSentTime < inboxMostRecentSentTime){ + + anyValidInboxFound = true + newestSecretInboxMostRecentSentTime = inboxMostRecentSentTime + newestSecretInboxSeed = secretInboxSeed + } + } + + if (anyValidInboxFound == false){ + return false, [10]byte{}, [32]byte{}, nil + } + + // Now we check if their device identifier has changed after we received this secret inbox + + deviceInfoFound, _, newestDeviceFirstSeenTime, err := peerDevices.GetPeerNewestDeviceInfo(peerIdentityHash, networkType) + if (err != nil) { return false, [10]byte{}, [32]byte{}, err } + if (deviceInfoFound == true && newestDeviceFirstSeenTime > newestSecretInboxMostRecentSentTime){ + + // The peer has changed their device after they sent us this secret inbox + // We assume this inbox was not carried over to the new device, and we say that no secret inbox exists + + return false, [10]byte{}, [32]byte{}, nil + } + + newestSecretInbox, currentSecretInboxSealerKey, err := inbox.GetSecretInboxAndSealerKeyFromSecretInboxSeed(newestSecretInboxSeed) + if (err != nil){ + return false, [10]byte{}, [32]byte{}, errors.New("Malformed peerSecretInboxesMapList: Item contains invalid SecretInboxSeed.") + } + + return true, newestSecretInbox, currentSecretInboxSealerKey, nil +} + + diff --git a/internal/messaging/readMessages/readMessages.go b/internal/messaging/readMessages/readMessages.go new file mode 100644 index 0000000..6fff302 --- /dev/null +++ b/internal/messaging/readMessages/readMessages.go @@ -0,0 +1,613 @@ + +// readMessages provides functions to read Seekia chat messages +// The chat message structure will eventually be documented in /documentation/Specification.md + +package readMessages + +//TODO: Add an upper limit on bytes, and an upper limit for size of communication. +//TODO: Verify all message values and function inputs. + +import "seekia/internal/cryptography/blake3" +import "seekia/internal/cryptography/chaPolyShrink" +import "seekia/internal/cryptography/edwardsKeys" +import "seekia/internal/cryptography/kyber" +import "seekia/internal/cryptography/nacl" +import "seekia/internal/encoding" +import "seekia/internal/helpers" +import "seekia/internal/identity" + +import messagepack "github.com/vmihailenco/msgpack/v5" + +import "bytes" +import "errors" + +func VerifyMessageHashHex(inputHash string)bool{ + + isValid := helpers.VerifyHexString(26, inputHash) + + return isValid +} + +func VerifyMessageCipherKeyHex(inputCipherKey string)bool{ + + isValid := helpers.VerifyHexString(32, inputCipherKey) + + return isValid +} + +func ReadMessageHashHex(inputMessageHash string)([26]byte, error){ + + messageHashBytes, err := encoding.DecodeHexStringToBytes(inputMessageHash) + if (err != nil){ + return [26]byte{}, errors.New("ReadMessageHashHex called with invalid MessageHash: Not Hex: " + inputMessageHash) + } + + if (len(messageHashBytes) != 26){ + return [26]byte{}, errors.New("ReadMessageHashHex called with invalid MessageHash: Invalid length: " + inputMessageHash) + } + + messageHashArray := [26]byte(messageHashBytes) + + return messageHashArray, nil +} + +func ReadMessageCipherKeyHex(inputCipherKey string)([32]byte, error){ + + cipherKeyBytes, err := encoding.DecodeHexStringToBytes(inputCipherKey) + if (err != nil){ + return [32]byte{}, errors.New("ReadMessageCipherKeyHex called with invalid inputCipherKey: Not Hex: " + inputCipherKey) + } + + if (len(cipherKeyBytes) != 32){ + return [32]byte{}, errors.New("ReadMessageCipherKeyHex called with invalid inputCipherKey: Invalid length: " + inputCipherKey) + } + + cipherKeyArray := [32]byte(cipherKeyBytes) + + return cipherKeyArray, nil +} + +func VerifyMessageCipherKeyHashHex(inputCipherKeyHash string)bool{ + + isValid := helpers.VerifyHexString(25, inputCipherKeyHash) + + return isValid +} + +func ConvertMessageCipherKeyToCipherKeyHash(inputMessageCipherKey [32]byte)([25]byte, error){ + + cipherKeyHash, err := blake3.GetBlake3HashAsBytes(25, inputMessageCipherKey[:]) + if (err != nil) { return [25]byte{}, err } + + cipherKeyHashArray := [25]byte(cipherKeyHash) + + return cipherKeyHashArray, nil +} + + +// This function reads a chat message's public data +// It also computes and returns the message hash +// It does not decrypt the message. +// Outputs: +// -bool: Able to read +// -[26]byte: Message Hash +// -int: Message version +// -byte: Network type (1 == Mainnet, 2 == Testnet) +// -[10]byte: Message Inbox +// -messagepack.RawMessage: Message noncipheredSection bytes (needed to decrypt the cipheredMessage) +// -[24]byte: DoubleSealedKeysNonce +// -[]byte: DoubleSealedKeys +// -[25]byte: CipherKeyHash +// -[24]byte: CipheredMessageNonce +// -[]byte: CipheredMessage +// -error (will return err if there is a bug) +func ReadChatMessagePublicDataAndHash(verifyData bool, inputMessage []byte)(bool, [26]byte, int, byte, [10]byte, messagepack.RawMessage, [24]byte, []byte, [25]byte, [24]byte, []byte, error){ + + ableToRead, messageVersion, messageNetworkType, messageInbox, messageNoncipheredSectionBytes, messageDoubleSealedKeysNonce, messageDoubleSealedKeys, messageCipherKeyHash, messageCipheredMessageNonce, messageCipheredMessage, err := ReadChatMessagePublicData(verifyData, inputMessage) + if (err != nil) { return false, [26]byte{}, 0, 0, [10]byte{}, nil, [24]byte{}, nil, [25]byte{}, [24]byte{}, nil, err } + if (ableToRead == false){ + return false, [26]byte{}, 0, 0, [10]byte{}, nil, [24]byte{}, nil, [25]byte{}, [24]byte{}, nil, nil + } + + messageHashSlice, err := blake3.GetBlake3HashAsBytes(26, inputMessage) + if (err != nil) { return false, [26]byte{}, 0, 0, [10]byte{}, nil, [24]byte{}, nil, [25]byte{}, [24]byte{}, nil, err } + + messageHash := [26]byte(messageHashSlice) + + return true, messageHash, messageVersion, messageNetworkType, messageInbox, messageNoncipheredSectionBytes, messageDoubleSealedKeysNonce, messageDoubleSealedKeys, messageCipherKeyHash, messageCipheredMessageNonce, messageCipheredMessage, nil +} + +// This function reads a chat message's public data, and optionally verifies the message's public data. +// It does not compute the message hash, thus it is faster +// It does not decrypt the message. +// Outputs: +// -bool: Able to read +// -int: Message version +// -byte: Network type (1 == Mainnet, 2 == Testnet) +// -[10]byte: Message Inbox +// -messagepack.RawMessage: Message noncipheredSection bytes (needed to decrypt the cipheredMessage) +// -[24]byte: DoubleSealedKeysNonce +// -[]byte: DoubleSealedKeys +// -[25]byte: CipherKeyHash +// -[24]byte: CipheredMessageNonce +// -[]byte: CipheredMessage +// -error (will return err if there is a bug) +func ReadChatMessagePublicData(verifyData bool, inputMessage []byte)(bool, int, byte, [10]byte, messagepack.RawMessage, [24]byte, []byte, [25]byte, [24]byte, []byte, error){ + + var messageSlice []messagepack.RawMessage + + err := encoding.DecodeMessagePackBytes(false, inputMessage, &messageSlice) + if (err != nil){ + // Cannot read message: Invalid messagepack + return false, 0, 0, [10]byte{}, nil, [24]byte{}, nil, [25]byte{}, [24]byte{}, nil, nil + } + + if (len(messageSlice) != 2){ + // Cannot read message: Invalid messagepack + return false, 0, 0, [10]byte{}, nil, [24]byte{}, nil, [25]byte{}, [24]byte{}, nil, nil + } + + messageNoncipheredSectionEncoded := messageSlice[0] + cipheredMessageEncoded := messageSlice[1] + + var noncipheredSectionSlice []messagepack.RawMessage + + err = encoding.DecodeMessagePackBytes(false, messageNoncipheredSectionEncoded, &noncipheredSectionSlice) + if (err != nil){ + // Malformed message: Invalid message content + return false, 0, 0, [10]byte{}, nil, [24]byte{}, nil, [25]byte{}, [24]byte{}, nil, nil + } + + if (len(noncipheredSectionSlice) != 7){ + // Malformed message: Invalid message noncipheredSection + return false, 0, 0, [10]byte{}, nil, [24]byte{}, nil, [25]byte{}, [24]byte{}, nil, nil + } + + messageVersionEncoded := noncipheredSectionSlice[0] + networkTypeEncoded := noncipheredSectionSlice[1] + recipientInboxEncoded := noncipheredSectionSlice[2] + doubleSealedKeysNonceEncoded := noncipheredSectionSlice[3] + doubleSealedKeysEncoded := noncipheredSectionSlice[4] + cipherKeyHashEncoded := noncipheredSectionSlice[5] + cipheredMessageNonceEncoded := noncipheredSectionSlice[6] + + messageVersion, err := encoding.DecodeRawMessagePackToInt(messageVersionEncoded) + if (err != nil){ + // Malformed message: Invalid message version + return false, 0, 0, [10]byte{}, nil, [24]byte{}, nil, [25]byte{}, [24]byte{}, nil, nil + } + + if (messageVersion != 1){ + // We cannot read the message. It is created for a newer version of Seekia + return false, 0, 0, [10]byte{}, nil, [24]byte{}, nil, [25]byte{}, [24]byte{}, nil, nil + } + + messageNetworkType, err := encoding.DecodeRawMessagePackToByte(networkTypeEncoded) + if (err != nil){ + // Malformed message: Invalid networkType + return false, 0, 0, [10]byte{}, nil, [24]byte{}, nil, [25]byte{}, [24]byte{}, nil, nil + } + + isValid := helpers.VerifyNetworkType(messageNetworkType) + if (isValid == false){ + // Malformed message: Invalid networkType + return false, 0, 0, [10]byte{}, nil, [24]byte{}, nil, [25]byte{}, [24]byte{}, nil, nil + } + + recipientInbox, err := encoding.DecodeRawMessagePackTo10ByteArray(recipientInboxEncoded) + if (err != nil){ + // Malformed message: Invalid recipient inbox + return false, 0, 0, [10]byte{}, nil, [24]byte{}, nil, [25]byte{}, [24]byte{}, nil, nil + } + + doubleSealedKeysNonce, err := encoding.DecodeRawMessagePackTo24ByteArray(doubleSealedKeysNonceEncoded) + if (err != nil){ + // Malformed message: Invalid doubleSealedKeysNonce + return false, 0, 0, [10]byte{}, nil, [24]byte{}, nil, [25]byte{}, [24]byte{}, nil, nil + } + + doubleSealedKeys, err := encoding.DecodeRawMessagePackToBytes(doubleSealedKeysEncoded) + if (err != nil){ + // Malformed message: Invalid doubleSealedKeys + return false, 0, 0, [10]byte{}, nil, [24]byte{}, nil, [25]byte{}, [24]byte{}, nil, nil + } + + cipherKeyHash, err := encoding.DecodeRawMessagePackTo25ByteArray(cipherKeyHashEncoded) + if (err != nil){ + // Malformed message: Invalid cipherKeyHash + return false, 0, 0, [10]byte{}, nil, [24]byte{}, nil, [25]byte{}, [24]byte{}, nil, nil + } + + cipheredMessageNonce, err := encoding.DecodeRawMessagePackTo24ByteArray(cipheredMessageNonceEncoded) + if (err != nil){ + // Malformed message: Invalid cipheredMessageNonce + return false, 0, 0, [10]byte{}, nil, [24]byte{}, nil, [25]byte{}, [24]byte{}, nil, nil + } + + cipheredMessage, err := encoding.DecodeRawMessagePackToBytes(cipheredMessageEncoded) + if (err != nil){ + // Malformed message: Invalid cipheredMessage + return false, 0, 0, [10]byte{}, nil, [24]byte{}, nil, [25]byte{}, [24]byte{}, nil, nil + } + + if (verifyData == true){ + + if (len(doubleSealedKeys) != 1684){ + // Invalid doubleSealedKeys: Invalid length + return false, 0, 0, [10]byte{}, nil, [24]byte{}, nil, [25]byte{}, [24]byte{}, nil, nil + } + + if (len(cipheredMessage) < 3){ + // Invalid Message: Invalid cipheredMessage + return false, 0, 0, [10]byte{}, nil, [24]byte{}, nil, [25]byte{}, [24]byte{}, nil, nil + } + } + + return true, messageVersion, messageNetworkType, recipientInbox, messageNoncipheredSectionEncoded, doubleSealedKeysNonce, doubleSealedKeys, cipherKeyHash, cipheredMessageNonce, cipheredMessage, nil +} + +// This struct defines a chat key set which is used to decrypt a chat message +// Each user may have multiple key sets, if they have broadcast newer keys more recently. +// They keep their old keys so they can decrypt messages sent to their older keys. +type ChatKeySet struct{ + + NaclPublicKey [32]byte + NaclPrivateKey [32]byte + KyberPrivateKey [1536]byte +} + +// This function will decrypt a chat message with a recipient device chat keys list +// This is used by users who are decrypting messages sent to them +//Inputs: +// -[]byte: InputMessage +// -[32]byte: Double Sealed Keys decryption key +// -[]ChatKeySet: RecipientChatDecryptionKeysList (contains decryption keys for each recipient chat key set) +//Outputs: +// -bool: Able to read message (could be false if message is malformed or unable to decrypt) +// -[26]byte: Message Hash +// -int: Message Version +// -byte: Message network type (1 == Mainnet, 2 == Testnet1) +// -[32]byte: Message Cipher Key +// -[16]byte: Sender Identity Hash +// -[16]byte: Recipient Identity Hash +// -int64: Message sent time (unix) +// -string: Communication +// -[22]byte: Sender Current secret inbox seed +// -[22]byte: Sender Next secret inbox seed +// -[11]byte: Sender Device Identifier +// -int64: Sender Latest chat keys update time +// -error (will return err if non-message inputs are malformed, or there is a bug) +func ReadChatMessage(inputMessage []byte, doubleSealedKeysDecryptionKey [32]byte, recipientDecryptionKeySetsList []ChatKeySet)(bool, [26]byte, int, byte, [32]byte, [16]byte, [16]byte, int64, string, [22]byte, [22]byte, [11]byte, int64, error){ + + ableToRead, messageHash, messageVersion, messageNetworkType, _, messageNoncipheredSectionBytes, doubleSealedKeysNonce, doubleSealedKeys, expectedCipherKeyHash, cipheredMessageNonce, cipheredMessage, err := ReadChatMessagePublicDataAndHash(true, inputMessage) + if (err != nil) { return false, [26]byte{}, 0, 0, [32]byte{}, [16]byte{}, [16]byte{}, 0, "", [22]byte{}, [22]byte{}, [11]byte{}, 0, err } + if (ableToRead == false){ + // Invalid message format + return false, [26]byte{}, 0, 0, [32]byte{}, [16]byte{}, [16]byte{}, 0, "", [22]byte{}, [22]byte{}, [11]byte{}, 0, nil + } + + ableToDecrypt, decryptedBytes, err := chaPolyShrink.DecryptChaPolyShrink(doubleSealedKeys, doubleSealedKeysDecryptionKey, doubleSealedKeysNonce, false, [32]byte{}) + if (err != nil) { return false, [26]byte{}, 0, 0, [32]byte{}, [16]byte{}, [16]byte{}, 0, "", [22]byte{}, [22]byte{}, [11]byte{}, 0, err } + if (ableToDecrypt == false){ + // doubleSealedKeys cannot be decrypted + // Sender must be malicious + return false, [26]byte{}, 0, 0, [32]byte{}, [16]byte{}, [16]byte{}, 0, "", [22]byte{}, [22]byte{}, [11]byte{}, 0, nil + } + + if (len(decryptedBytes) != 1648){ + // Sender must be malicious. + return false, [26]byte{}, 0, 0, [32]byte{}, [16]byte{}, [16]byte{}, 0, "", [22]byte{}, [22]byte{}, [11]byte{}, 0, nil + } + + naclEncryptedKeyPieceA := [80]byte(decryptedBytes[:80]) + kyberEncryptedKeyPieceB := [1568]byte(decryptedBytes[80:]) + + //Outputs: + // -bool: Able to decrypt + // -[32]byte: Basaldata decryption key + // -error (will be error if inputs are invalid) + getBasaldataDecryptionKey := func()(bool, [32]byte, error){ + + // This cycles through all chat decryption keys the user has + // They broadcast new keys on their profile, and delete old keys after a certain time period + + for _, keySetObject := range recipientDecryptionKeySetsList{ + + recipientNaclPublicKey := keySetObject.NaclPublicKey + recipientNaclPrivateKey := keySetObject.NaclPrivateKey + recipientKyberPrivateKey := keySetObject.KyberPrivateKey + + ableToDecrypt, keyPieceA, err := nacl.DecryptNaclEncryptedKey(naclEncryptedKeyPieceA, recipientNaclPublicKey, recipientNaclPrivateKey) + if (err != nil) { return false, [32]byte{}, err } + if (ableToDecrypt == false){ + // Keys are not the correct decryption keys + // Skip to next keys + continue + } + + keyPieceB, err := kyber.DecryptKyberEncryptedKey(kyberEncryptedKeyPieceB, recipientKyberPrivateKey) + if (err != nil) { return false, [32]byte{}, err } + + basaldataDecryptionKey := helpers.XORTwo32ByteArrays(keyPieceA, keyPieceB) + + return true, basaldataDecryptionKey, nil + } + + // We don't have the keys needed to decrypt the message. + return false, [32]byte{}, nil + } + + ableToDecrypt, basaldataDecryptionKey, err := getBasaldataDecryptionKey() + if (err != nil) { return false, [26]byte{}, 0, 0, [32]byte{}, [16]byte{}, [16]byte{}, 0, "", [22]byte{}, [22]byte{}, [11]byte{}, 0, err } + if (ableToDecrypt == false){ + // We don't have the keys to decrypt, or message is invalid + return false, [26]byte{}, 0, 0, [32]byte{}, [16]byte{}, [16]byte{}, 0, "", [22]byte{}, [22]byte{}, [11]byte{}, 0, nil + } + + // CipheredMessageKey is a hash of the Basaldata decryption key + // This allows users to report messages and reveal only the cipheredMessage + // This way, only the innerMessage contents are revealed, and the basaldata remains encrypted + + messageCipherKey, err := blake3.GetBlake3HashAsBytes(32, basaldataDecryptionKey[:]) + if (err != nil) { return false, [26]byte{}, 0, 0, [32]byte{}, [16]byte{}, [16]byte{}, 0, "", [22]byte{}, [22]byte{}, [11]byte{}, 0, err } + + messageCipherKeyArray := [32]byte(messageCipherKey) + + messageCipherKeyHash, err := blake3.GetBlake3HashAsBytes(25, messageCipherKey) + if (err != nil) { return false, [26]byte{}, 0, 0, [32]byte{}, [16]byte{}, [16]byte{}, 0, "", [22]byte{}, [22]byte{}, [11]byte{}, 0, err } + + areEqual := bytes.Equal(messageCipherKeyHash, expectedCipherKeyHash[:]) + if (areEqual == false){ + // Cipher key hash does not match expected cipher key hash + return false, [26]byte{}, 0, 0, [32]byte{}, [16]byte{}, [16]byte{}, 0, "", [22]byte{}, [22]byte{}, [11]byte{}, 0, nil + } + + ableToRead, senderIdentityHash, senderIdentityType, encryptedBasaldata, basaldataNonce, messageCommunication, err := readCipheredMessage(1, cipheredMessage, messageCipherKeyArray, cipheredMessageNonce, messageNoncipheredSectionBytes) + if (ableToRead == false){ + // Invalid ciphered message + return false, [26]byte{}, 0, 0, [32]byte{}, [16]byte{}, [16]byte{}, 0, "", [22]byte{}, [22]byte{}, [11]byte{}, 0, nil + } + + // Now we decrypt basaldata + + ableToDecrypt, decryptedBasaldataBytes, err := chaPolyShrink.DecryptChaPolyShrink(encryptedBasaldata, basaldataDecryptionKey, basaldataNonce, false, [32]byte{}) + if (err != nil) { return false, [26]byte{}, 0, 0, [32]byte{}, [16]byte{}, [16]byte{}, 0, "", [22]byte{}, [22]byte{}, [11]byte{}, 0, err } + if (ableToDecrypt == false) { + // Unable to decrypt. Sender is malicious. + return false, [26]byte{}, 0, 0, [32]byte{}, [16]byte{}, [16]byte{}, 0, "", [22]byte{}, [22]byte{}, [11]byte{}, 0, nil + } + + var basaldataSlice []messagepack.RawMessage + + err = encoding.DecodeMessagePackBytes(false, decryptedBasaldataBytes, &basaldataSlice) + if (err != nil) { + // Message basaldata messagepack is invalid + return false, [26]byte{}, 0, 0, [32]byte{}, [16]byte{}, [16]byte{}, 0, "", [22]byte{}, [22]byte{}, [11]byte{}, 0, nil + } + + if (len(basaldataSlice) != 6){ + // Message basaldata is invalid + return false, [26]byte{}, 0, 0, [32]byte{}, [16]byte{}, [16]byte{}, 0, "", [22]byte{}, [22]byte{}, [11]byte{}, 0, nil + } + + recipientIdentityHashEncoded := basaldataSlice[0] + messageSentTimeUnixEncoded := basaldataSlice[1] + senderCurrentSecretInboxSeedEncoded := basaldataSlice[2] + senderNextSecretInboxSeedEncoded := basaldataSlice[3] + senderDeviceIdentifierEncoded := basaldataSlice[4] + senderLatestChatKeysUpdateTimeEncoded := basaldataSlice[5] + + recipientIdentityHash, err := encoding.DecodeRawMessagePackTo16ByteArray(recipientIdentityHashEncoded) + if (err != nil) { + // Malformed message basaldata: Invalid RecipientIdentityHash + return false, [26]byte{}, 0, 0, [32]byte{}, [16]byte{}, [16]byte{}, 0, "", [22]byte{}, [22]byte{}, [11]byte{}, 0, nil + } + + messageSentTimeUnix, err := encoding.DecodeRawMessagePackToInt64(messageSentTimeUnixEncoded) + if (err != nil) { + // Malformed message basaldata: Invalid SentTime + return false, [26]byte{}, 0, 0, [32]byte{}, [16]byte{}, [16]byte{}, 0, "", [22]byte{}, [22]byte{}, [11]byte{}, 0, nil + } + + senderCurrentSecretInboxSeed, err := encoding.DecodeRawMessagePackTo22ByteArray(senderCurrentSecretInboxSeedEncoded) + if (err != nil) { + // Malformed message basaldata: Invalid SenderCurrentSecretInboxSeed + return false, [26]byte{}, 0, 0, [32]byte{}, [16]byte{}, [16]byte{}, 0, "", [22]byte{}, [22]byte{}, [11]byte{}, 0, nil + } + + senderNextSecretInboxSeed, err := encoding.DecodeRawMessagePackTo22ByteArray(senderNextSecretInboxSeedEncoded) + if (err != nil) { + // Malformed message basaldata: Invalid SenderNextSecretInboxSeed + return false, [26]byte{}, 0, 0, [32]byte{}, [16]byte{}, [16]byte{}, 0, "", [22]byte{}, [22]byte{}, [11]byte{}, 0, nil + } + + senderDeviceIdentifier, err := encoding.DecodeRawMessagePackTo11ByteArray(senderDeviceIdentifierEncoded) + if (err != nil) { + // Malformed message basaldata: Invalid SenderDeviceIdentifier + return false, [26]byte{}, 0, 0, [32]byte{}, [16]byte{}, [16]byte{}, 0, "", [22]byte{}, [22]byte{}, [11]byte{}, 0, nil + } + + senderLatestChatKeysUpdateTime, err := encoding.DecodeRawMessagePackToInt64(senderLatestChatKeysUpdateTimeEncoded) + if (err != nil) { + // Malformed message basaldata: Invalid SenderLatestChatKeysUpdateTime + return false, [26]byte{}, 0, 0, [32]byte{}, [16]byte{}, [16]byte{}, 0, "", [22]byte{}, [22]byte{}, [11]byte{}, 0, nil + } + + recipientIdentityType, err := identity.GetIdentityTypeFromIdentityHash(recipientIdentityHash) + if (err != nil) { + // Malformed message basaldata: Invalid RecipientIdentityHash + return false, [26]byte{}, 0, 0, [32]byte{}, [16]byte{}, [16]byte{}, 0, "", [22]byte{}, [22]byte{}, [11]byte{}, 0, nil + } + + if (senderIdentityType != recipientIdentityType){ + // Malformed message basaldata: Recipient and sender identity types do not match. + return false, [26]byte{}, 0, 0, [32]byte{}, [16]byte{}, [16]byte{}, 0, "", [22]byte{}, [22]byte{}, [11]byte{}, 0, nil + } + + isValid := helpers.VerifyBroadcastTime(messageSentTimeUnix) + if (isValid == false){ + // Malformed message basaldata: Contains invalid unixSentTime + return false, [26]byte{}, 0, 0, [32]byte{}, [16]byte{}, [16]byte{}, 0, "", [22]byte{}, [22]byte{}, [11]byte{}, 0, nil + } + + return true, messageHash, messageVersion, messageNetworkType, messageCipherKeyArray, senderIdentityHash, recipientIdentityHash, messageSentTimeUnix, messageCommunication, senderCurrentSecretInboxSeed, senderNextSecretInboxSeed, senderDeviceIdentifier, senderLatestChatKeysUpdateTime, nil +} + +// This is used by moderators to decrypt a chat message +// This does not decrypt the basaldata +//Outputs: +// -bool: Able to read message (could be false if message is malformed or unable to decrypt) +// -[26]byte: Message Hash +// -int: Message Version +// -byte: Message network type (1 == Mainnet, 2 == Testnet1) +// -[16]byte: Sender Identity Hash +// -string: Communication +// -error (will return err if non-message inputs are malformed, or there is a bug) +func ReadChatMessageWithCipherKey(inputMessage []byte, messageCipherKey [32]byte)(bool, [26]byte, int, byte, [16]byte, string, error){ + + ableToRead, messageHash, messageVersion, messageNetworkType, _, messageNoncipheredSectionBytes, _, _, expectedCipherKeyHash, cipheredMessageNonce, cipheredMessage, err := ReadChatMessagePublicDataAndHash(true, inputMessage) + if (err != nil) { return false, [26]byte{}, 0, 0, [16]byte{}, "", err } + if (ableToRead == false){ + // Invalid message format + return false, [26]byte{}, 0, 0, [16]byte{}, "", nil + } + + cipherKeyHash, err := blake3.GetBlake3HashAsBytes(25, messageCipherKey[:]) + if (err != nil) { return false, [26]byte{}, 0, 0, [16]byte{}, "", err } + + areEqual := bytes.Equal(cipherKeyHash, expectedCipherKeyHash[:]) + if (areEqual == false){ + // Invalid message: Cipher key hash does not match + return false, [26]byte{}, 0, 0, [16]byte{}, "", nil + } + + ableToRead, senderIdentityHash, _, _, _, messageCommunication, err := readCipheredMessage(messageVersion, cipheredMessage, messageCipherKey, cipheredMessageNonce, messageNoncipheredSectionBytes) + if (err != nil) { return false, [26]byte{}, 0, 0, [16]byte{}, "", err } + if (ableToRead == false){ + return false, [26]byte{}, 0, 0, [16]byte{}, "", nil + } + + return true, messageHash, messageVersion, messageNetworkType, senderIdentityHash, messageCommunication, nil +} + +//Outputs: +// -bool: Able to read (message is decryptable and well formed) +// -[16]byte: Sender Identity Hash +// -string: Sender Identity Type +// -[]byte: Encrypted Basaldata +// -[24]byte: Basaldata nonce +// -string: Communication +// -error (this will return err if there is a bug in the function) +func readCipheredMessage(messageVersion int, cipheredMessage []byte, cipheredMessageKey [32]byte, cipheredMessageNonce [24]byte, noncipheredSectionBytes messagepack.RawMessage)(bool, [16]byte, string, []byte, [24]byte, string, error){ + + if (messageVersion != 1){ + return false, [16]byte{}, "", nil, [24]byte{}, "", errors.New("readCipheredMessage called with invalid messageVersion") + } + + noncipheredSectionHash, err := blake3.Get32ByteBlake3Hash(noncipheredSectionBytes) + if (err != nil) { return false, [16]byte{}, "", nil, [24]byte{}, "", err } + + ableToDecrypt, decryptedBytes, err := chaPolyShrink.DecryptChaPolyShrink(cipheredMessage, cipheredMessageKey, cipheredMessageNonce, true, noncipheredSectionHash) + if (err != nil) { return false, [16]byte{}, "", nil, [24]byte{}, "", err } + if (ableToDecrypt == false) { + // Unable to decrypt. Sender is malicious. + return false, [16]byte{}, "", nil, [24]byte{}, "", nil + } + + // Now we read inner message + + var innerMessageSlice []messagepack.RawMessage + + err = encoding.DecodeMessagePackBytes(false, decryptedBytes, &innerMessageSlice) + if (err != nil) { + // Inner message messagepack is invalid + return false, [16]byte{}, "", nil, [24]byte{}, "", nil + } + + if (len(innerMessageSlice) != 2){ + // Inner message messagepack is invalid + return false, [16]byte{}, "", nil, [24]byte{}, "", nil + } + + innerMessageSignatureEncoded := innerMessageSlice[0] + innerMessageRawMessagepack := innerMessageSlice[1] + + innerMessageSignature, err := encoding.DecodeRawMessagePackTo64ByteArray(innerMessageSignatureEncoded) + if (err != nil){ + // Malformed inner message: Invalid innerMessageSignature + return false, [16]byte{}, "", nil, [24]byte{}, "", nil + } + + var innerMessageContentSlice []messagepack.RawMessage + + err = encoding.DecodeMessagePackBytes(false, innerMessageRawMessagepack, &innerMessageContentSlice) + if (err != nil) { + // Message Inner plaintext messagepack is invalid + return false, [16]byte{}, "", nil, [24]byte{}, "", nil + } + + if (len(innerMessageContentSlice) != 5){ + // Malformed inner message: Invalid inner message content + return false, [16]byte{}, "", nil, [24]byte{}, "", nil + } + + senderIdentityKeyEncoded := innerMessageContentSlice[0] + senderIdentityTypeEncoded := innerMessageContentSlice[1] + basaldataNonceEncoded := innerMessageContentSlice[2] + encryptedBasaldataEncoded := innerMessageContentSlice[3] + communicationEncoded := innerMessageContentSlice[4] + + senderIdentityKey, err := encoding.DecodeRawMessagePackTo32ByteArray(senderIdentityKeyEncoded) + if (err != nil){ + // Malformed inner message: Sender identity key is invalid. + return false, [16]byte{}, "", nil, [24]byte{}, "", nil + } + + senderIdentityType, err := encoding.DecodeRawMessagePackToString(senderIdentityTypeEncoded) + if (err != nil){ + // Malformed inner message: Sender identity type is invalid. + return false, [16]byte{}, "", nil, [24]byte{}, "", nil + } + + basaldataNonce, err := encoding.DecodeRawMessagePackTo24ByteArray(basaldataNonceEncoded) + if (err != nil){ + // Malformed inner message: Basaldata nonce is invalid. + return false, [16]byte{}, "", nil, [24]byte{}, "", nil + } + + encryptedBasaldata, err := encoding.DecodeRawMessagePackToBytes(encryptedBasaldataEncoded) + if (err != nil){ + // Malformed inner message: Encrypted basaldata is invalid. + return false, [16]byte{}, "", nil, [24]byte{}, "", nil + } + + messageCommunication, err := encoding.DecodeRawMessagePackToString(communicationEncoded) + if (err != nil){ + // Malformed inner message: Message communication is invalid. + return false, [16]byte{}, "", nil, [24]byte{}, "", nil + } + + //TODO: Verify communication is valid + + if (senderIdentityType != "Mate" && senderIdentityType != "Moderator"){ + // Malformed inner message: Invalid senderIdentityType. + return false, [16]byte{}, "", nil, [24]byte{}, "", nil + } + + innerContentHash, err := blake3.Get32ByteBlake3Hash(innerMessageRawMessagepack) + if (err != nil) { return false, [16]byte{}, "", nil, [24]byte{}, "", err } + + isValid := edwardsKeys.VerifySignature(senderIdentityKey, innerMessageSignature, innerContentHash) + if (isValid == false) { + // Malformed inner message: signature is invalid. + return false, [16]byte{}, "", nil, [24]byte{}, "", nil + } + + senderIdentityHash, err := identity.ConvertIdentityKeyToIdentityHash(senderIdentityKey, senderIdentityType) + if (err != nil) { return false, [16]byte{}, "", nil, [24]byte{}, "", err } + + return true, senderIdentityHash, senderIdentityType, encryptedBasaldata, basaldataNonce, messageCommunication, nil +} + + + diff --git a/internal/messaging/secretInboxEpoch/secretInboxEpoch.go b/internal/messaging/secretInboxEpoch/secretInboxEpoch.go new file mode 100644 index 0000000..d03852e --- /dev/null +++ b/internal/messaging/secretInboxEpoch/secretInboxEpoch.go @@ -0,0 +1,70 @@ + +// secretInboxEpoch provides a function to calculate start and end times of secret inbox epochs. +// A secret inbox epoch is the time period during which a secret inbox is active. + +package secretInboxEpoch + +// When a secret inbox is active, the sender should send their messages to the inbox. +// Once the epoch has passed, the recipient will check the inbox to a sufficient degree and then stop checking it. + +// Here is an example of how the epoch time works +// Example epoch origin time = 100 +// Example Epoch Duration = 10 +// Example epochs: +// 1st epoch start time: 100 +// 1st epoch end time: 110 +// 2nd epoch start time: 111 +// 2nd epoch end time: 121 +// 3rd epoch start time: 122 +// 3rd epoch end time: 132 +// ... + +import "seekia/internal/helpers" +import "seekia/internal/parameters/getParameters" + +import "errors" + +// This is a unix time value from which the secret inbox epochs are calculated. +// This value should never change (once the Seekia network goes live) +const secretInboxEpochOriginTime int64 = 1670000000 + + +// This function returns the epoch start and end times at the time of the provided messageSentTime +//Outputs: +// -bool: Parameters exist +// -int64: Current epoch start time +// -int64: Current epoch end time +// -int64: Next epoch start time +// -int64: Next epoch start time +// -error +func GetSecretInboxEpochStartAndEndTimes(networkType byte, messageSentTime int64)(bool, int64, int64, int64, int64, error){ + + isValid := helpers.VerifyNetworkType(networkType) + if (isValid == false){ + networkTypeString := helpers.ConvertByteToString(networkType) + return false, 0, 0, 0, 0, errors.New("GetSecretInboxEpochStartAndEndTimes called with invalid networkType: " + networkTypeString) + } + + if (messageSentTime < secretInboxEpochOriginTime){ + return false, 0, 0, 0, 0, errors.New("GetSecretInboxEpochStartAndEndTimes called with invalid messageSentTime.") + } + + parametersExist, epochDuration, err := getParameters.GetSecretInboxEpochDuration(networkType, messageSentTime) + if (err != nil) { return false, 0, 0, 0, 0, err } + if (parametersExist == false){ + return false, 0, 0, 0, 0, nil + } + + remainder := (messageSentTime - secretInboxEpochOriginTime) % (epochDuration + 1) + + currentEpochStartTime := messageSentTime - remainder + currentEpochEndTime := currentEpochStartTime + epochDuration + + nextEpochStartTime := currentEpochEndTime + 1 + nextEpochEndTime := nextEpochStartTime + epochDuration + + return true, currentEpochStartTime, currentEpochEndTime, nextEpochStartTime, nextEpochEndTime, nil +} + + + diff --git a/internal/messaging/sendMessages/sendMessages.go b/internal/messaging/sendMessages/sendMessages.go new file mode 100644 index 0000000..4b9ec3d --- /dev/null +++ b/internal/messaging/sendMessages/sendMessages.go @@ -0,0 +1,420 @@ + +// sendMessages provides functions to create and send messages + +package sendMessages + +//TODO: Add function to prune mySentMessagesMapDatastore + +import "seekia/internal/convertCurrencies" +import "seekia/internal/cryptography/kyber" +import "seekia/internal/cryptography/nacl" +import "seekia/internal/encoding" +import "seekia/internal/helpers" +import "seekia/internal/identity" +import "seekia/internal/logger" +import "seekia/internal/messaging/createMessages" +import "seekia/internal/messaging/inbox" +import "seekia/internal/messaging/myChatKeys" +import "seekia/internal/messaging/myChatMessages" +import "seekia/internal/messaging/mySecretInboxes" +import "seekia/internal/messaging/peerChatKeys" +import "seekia/internal/messaging/peerSecretInboxes" +import "seekia/internal/messaging/readMessages" +import "seekia/internal/myDatastores/myMap" +import "seekia/internal/myDevice" +import "seekia/internal/myIdentity" +import "seekia/internal/network/myBroadcasts" +import "seekia/internal/network/spendCredit" +import "seekia/internal/parameters/getParameters" +import "seekia/internal/profiles/myProfileStatus" + +import "time" +import "errors" +import "sync" + +// This mutex will be locked whenever we are adding a new message to send +var addingMessageMutex sync.Mutex + +// This list host a map of messages which we have already sent +// Map Structure: Message Identifier -> Message Hash +var mySentMessagesMapDatastore *myMap.MyMap + +// This function must be called whenever an app user signs in +func InitializeSentMessagesDatastore()error{ + + addingMessageMutex.Lock() + defer addingMessageMutex.Unlock() + + newMySentMessagesMapDatastore, err := myMap.CreateNewMap("SentMessages") + if (err != nil) { return err } + + mySentMessagesMapDatastore = newMySentMessagesMapDatastore + + return nil +} + +func WaitForPendingSendsToComplete(){ + + pendingMessagesWaitgroup.Wait() +} + +// This waitgroup will be used to wait for all pending messages to be sent +var pendingMessagesWaitgroup sync.WaitGroup + +var pendingMessagesMapMutex sync.RWMutex + +// This list contains a map of message identifiers which we are currently attempting to send +var pendingMessagesMap map[[20]byte]struct{} = make(map[[20]byte]struct{}) + +func addMessageToPendingMessagesMap(messageIdentifier [20]byte){ + pendingMessagesMapMutex.Lock() + pendingMessagesMap[messageIdentifier] = struct{}{} + pendingMessagesMapMutex.Unlock() +} + +func deleteMessageFromPendingMessagesMap(messageIdentifier [20]byte){ + + pendingMessagesMapMutex.Lock() + delete(pendingMessagesMap, messageIdentifier) + pendingMessagesMapMutex.Unlock() +} + +func checkIfMessageIsPending(messageIdentifier [20]byte)bool{ + + pendingMessagesMapMutex.RLock() + _, exists := pendingMessagesMap[messageIdentifier] + pendingMessagesMapMutex.RUnlock() + if (exists == false){ + return false + } + + return true +} + +// This function will tell us if a message has been successfully sent +//Outputs: +// -bool: Message is sent +// -[26]byte: Message hash +// -error +func CheckIfMessageIsSent(messageIdentifier [20]byte)(bool, [26]byte, error){ + + messageIdentifierHex := encoding.EncodeBytesToHexString(messageIdentifier[:]) + + exists, messageHashString, err := mySentMessagesMapDatastore.GetMapEntry(messageIdentifierHex) + if (err != nil) { return false, [26]byte{}, err } + if (exists == false){ + return false, [26]byte{}, nil + } + + messageHash, err := readMessages.ReadMessageHashHex(messageHashString) + if (err != nil){ + return false, [26]byte{}, errors.New("mySentMessagesMapDatastore contains invalid map value: Not hex: " + messageHashString) + } + + return true, messageHash, nil +} + +//Outputs: +// -bool: Message is pending or sent (we are already trying to send the message, or we already sent it) +// -bool: Successfully started send (required information exists) +// -error +func StartSendingMessage(messageIdentifier [20]byte, myIdentityHash [16]byte, recipientIdentityHash [16]byte, messageNetworkType byte, messageTimeSent int64, messageCommunication string, messageDuration int)(bool, bool, error){ + + isValid := helpers.VerifyNetworkType(messageNetworkType) + if (isValid == false){ + messageNetworkTypeString := helpers.ConvertByteToString(messageNetworkType) + return false, false, errors.New("StartSendingMessage called with invalid messageNetworkType: " + messageNetworkTypeString) + } + + addingMessageMutex.Lock() + defer addingMessageMutex.Unlock() + + messageIsPending := checkIfMessageIsPending(messageIdentifier) + if (messageIsPending == true){ + // We are already trying to send this message. + return true, false, nil + } + + messageIsSent, _, err := CheckIfMessageIsSent(messageIdentifier) + if (err != nil) { return false, false, err } + if (messageIsSent == true){ + // We already sent the message + return true, false, nil + } + + identityExists, myIdentityPublicKey, myIdentityPrivateKey, err := myIdentity.GetMyPublicPrivateIdentityKeysFromIdentityHash(myIdentityHash) + if (err != nil) { return false, false, err } + if (identityExists == false) { + return false, false, errors.New("StartSendingMessage called when my identity does not exist.") + } + + myIdentityExists, isActiveStatus, err := myProfileStatus.GetMyProfileIsActiveStatus(myIdentityHash, messageNetworkType) + if (err != nil) { return false, false, err } + if (myIdentityExists == false) { + return false, false, errors.New("My identity not found after being found already.") + } + if (isActiveStatus == false){ + return false, false, nil + } + + exists, myChatKeysLatestUpdateTime, err := myChatKeys.GetMyChatKeysLatestUpdateTime(myIdentityHash, messageNetworkType) + if (err != nil) { return false, false, err } + if (exists == false){ + // We have not broadcast a profile yet. + return false, false, nil + } + + //Outputs: + // -[10]byte: Recipient Inbox + // -[32]byte: Recipient inbox sealer key + // -error + getRecipientInbox := func()([10]byte, [32]byte, error){ + + secretInboxFound, theirSecretInbox, theirSecretInboxSealerKey, err := peerSecretInboxes.GetPeerConversationCurrentSecretInbox(myIdentityHash, recipientIdentityHash, messageNetworkType) + if (err != nil) { return [10]byte{}, [32]byte{}, err } + if (secretInboxFound == true){ + return theirSecretInbox, theirSecretInboxSealerKey, nil + } + + recipientPublicInbox, err := inbox.GetPublicInboxFromIdentityHash(recipientIdentityHash) + if (err != nil) { return [10]byte{}, [32]byte{}, err } + + recipientPublicInboxSealerKey, err := inbox.GetPublicInboxSealerKeyFromIdentityHash(recipientIdentityHash) + if (err != nil) { return [10]byte{}, [32]byte{}, err } + + return recipientPublicInbox, recipientPublicInboxSealerKey, nil + } + + recipientInbox, recipientInboxSealerKey, err := getRecipientInbox() + if (err != nil) { return false, false, err } + + if (len(recipientInboxSealerKey) != 32){ + return false, false, errors.New("getRecipientInbox returning invalid recipientInboxSealerKey: Invalid length.") + } + + recipientInboxSealerKeyArray := [32]byte(recipientInboxSealerKey) + + peerIsDisabled, peerChatKeysExist, recipientNaclKey, recipientKyberKey, err := peerChatKeys.GetPeerNewestActiveChatKeys(recipientIdentityHash, messageNetworkType) + if (err != nil) { return false, false, err } + if (peerIsDisabled == true || peerChatKeysExist == false) { + return false, false, nil + } + + // The secret inboxes returned from the function below may not exist yet in our storage. + // If they are missing, we will create new secret inbox(es) + // We will add our new secret inboxes to our mySecretInboxes storage only if the message send is successful + + parametersExist, myCurrentSecretInboxExists, myExistingCurrentSecretInboxSeed, myNextSecretInboxExists, myExistingNextSecretInboxSeed, err := mySecretInboxes.GetMySecretInboxSeedsForMessage(myIdentityHash, recipientIdentityHash, messageNetworkType, messageTimeSent) + if (err != nil) { return false, false, err } + if (parametersExist == false){ + // We cannot determine the current epoch without the epoch parameters + return false, false, nil + } + + getMyCurrentSecretInboxSeed := func()([22]byte, error){ + + if (myCurrentSecretInboxExists == true){ + return myExistingCurrentSecretInboxSeed, nil + } + + newCurrentSecretInboxSeed, err := inbox.GetNewRandomSecretInboxSeed() + if (err != nil) { return [22]byte{}, err } + + return newCurrentSecretInboxSeed, nil + } + + getMyNextSecretInboxSeed := func()([22]byte, error){ + + if (myNextSecretInboxExists == true){ + return myExistingNextSecretInboxSeed, nil + } + + newNextSecretInboxSeed, err := inbox.GetNewRandomSecretInboxSeed() + if (err != nil) { return [22]byte{}, err } + + return newNextSecretInboxSeed, nil + } + + myCurrentSecretInboxSeed, err := getMyCurrentSecretInboxSeed() + if (err != nil) { return false, false, err } + + myNextSecretInboxSeed, err := getMyNextSecretInboxSeed() + if (err != nil) { return false, false, err } + + myIdentityFound, myDeviceIdentifier, err := myDevice.GetMyDeviceIdentifier(myIdentityHash, messageNetworkType) + if (err != nil) { return false, false, err } + if (myIdentityFound == false){ + return false, false, errors.New("My identity not found after being found already.") + } + + chatParametersExist, err := getParameters.CheckIfChatParametersExist(messageNetworkType) + if (err != nil) { return false, false, err } + if (chatParametersExist == false){ + return false, false, nil + } + + // We have everything we need to send the message + // It will only fail in 2 ways: + // 1. We cannot connect to the account credit server(s) + // 2. The cost of sending messages has increased since we froze the funds, and we don't have enough funds to send the message anymore + + finalMessage, messageHash, err := createMessages.CreateChatMessage(messageNetworkType, myIdentityHash, myIdentityPublicKey, myIdentityPrivateKey, messageTimeSent, myCurrentSecretInboxSeed, myNextSecretInboxSeed, myDeviceIdentifier, myChatKeysLatestUpdateTime, messageCommunication, recipientIdentityHash, recipientInbox, recipientNaclKey, recipientKyberKey, recipientInboxSealerKeyArray) + if (err != nil) { return false, false, err } + + sendMessageFunction := func(){ + + attemptToSendMessage := func()error{ + + messageSize := len(finalMessage) + + sufficientFundsExist, successfullyFunded, err := spendCredit.FundMyMessageWithServer(messageIdentifier, messageHash, messageNetworkType, messageSize, messageDuration) + if (err != nil) { return err } + if (sufficientFundsExist == false){ + // Message send is not successful because we ran out of funds. + // We will try to send message again in the future, when hopefully user has added more funds + // This should only happen if the cost of sending the message has gone up since user froze funds + return nil + } + if (successfullyFunded == false){ + // We could not connect to the server + return nil + } + + // Message was successfully funded + // We add it to our broadcasts to be automatically broadcast + // We also add the secret inbox seeds and the message map to our chat messages + + parametersExist, err := mySecretInboxes.AddMySecretInboxSeeds(myIdentityHash, recipientIdentityHash, messageNetworkType, messageTimeSent, myCurrentSecretInboxSeed, myNextSecretInboxSeed) + if (err != nil) { return err } + if (parametersExist == false){ + // This is very unlikely, because we just checked for parameters + // We cannot determine the secret inbox epoch for the message we have funded + // We won't be able to download message replies to our sent secret inboxes + // As long as the user can pass this at least once within the current secret inbox epoch with a message sent + // to this recipient, they should be able to add their secret inbox seeds to the mySecretInboxes datastore + // TODO: Use a fallback value so this doesn't happen + } + + err = myBroadcasts.BroadcastMyMessage(finalMessage) + if (err != nil) { return err } + + err = myChatMessages.AddMyNewMessageToMyChatMessages("Sent", messageHash, messageIdentifier, myIdentityHash, recipientIdentityHash, messageNetworkType, messageTimeSent, messageCommunication) + if (err != nil) { return err } + + messageIdentifierHex := encoding.EncodeBytesToHexString(messageIdentifier[:]) + messageHashHex := encoding.EncodeBytesToHexString(messageHash[:]) + + err = mySentMessagesMapDatastore.SetMapEntry(messageIdentifierHex, messageHashHex) + if (err != nil){ return err } + + return nil + } + + err := attemptToSendMessage() + if (err != nil){ + logger.AddLogError("General", err) + } + + deleteMessageFromPendingMessagesMap(messageIdentifier) + pendingMessagesWaitgroup.Done() + } + + addMessageToPendingMessagesMap(messageIdentifier) + pendingMessagesWaitgroup.Add(1) + + go sendMessageFunction() + + return false, true, nil +} + +// This function calculates current network cost for a provided message +// This is used to show the user approximately how much a message will cost +//Outputs: +// -bool: Parameters exist +// -float64: Cost in provided currency +// -error +func GetMessageNetworkCurrencyCostForProvidedDuration(messageNetworkType byte, messageSizeBytes int, messageDurationToFund int, currencyCode string)(bool, float64, error){ + + isValid := helpers.VerifyNetworkType(messageNetworkType) + if (isValid == false){ + messageNetworkTypeString := helpers.ConvertByteToString(messageNetworkType) + return false, 0, errors.New("GetMessageNetworkCurrencyCostForProvidedDuration called with invalid messageNetworkType: " + messageNetworkTypeString) + } + + messageKilobytes := messageSizeBytes/1000 + + currentTime := time.Now().Unix() + + parametersExist, currentCostPerDay, err := getParameters.GetMessageKilobyteGoldCostPerDay(messageNetworkType, currentTime) + if (err != nil) { return false, 0, err } + if (parametersExist == false) { + return false, 0, nil + } + + messageDays := messageDurationToFund/86400 + + // GetMessageKilobyteGoldCostPerDay returns grams. We convert to kilograms. + kilogramsCostPerDay := currentCostPerDay/1000 + + costInKilogramsOfGold := float64(messageDays) * float64(messageKilobytes) * kilogramsCostPerDay + + exchangeRatesFound, currencyCost, err := convertCurrencies.ConvertKilogramsOfGoldToAnyCurrency(messageNetworkType, costInKilogramsOfGold, currencyCode) + if (err != nil) { return false, 0, err } + if (exchangeRatesFound == false){ + return false, 0, nil + } + + return true, currencyCost, nil +} + +// We use this function to estimate the size of a message we have not created yet +// This enables us to show the user how much the message will cost, and for us to freeze the required funds +func GetEstimatedMessageSize(recipientIdentityHash [16]byte, messageCommunication string)(int, error){ + + recipientIdentityType, err := identity.GetIdentityTypeFromIdentityHash(recipientIdentityHash) + if (err != nil) { + recipientIdentityHashHex := encoding.EncodeBytesToHexString(recipientIdentityHash[:]) + return 0, errors.New("GetEstimatedMessageSize called with invalid recipientIdentityHash: " + recipientIdentityHashHex) + } + + senderIdentityPublicKey, senderIdentityPrivateKey, err := identity.GetNewRandomPublicPrivateIdentityKeys() + if (err != nil) { return 0, err } + + senderIdentityHash, err := identity.ConvertIdentityKeyToIdentityHash(senderIdentityPublicKey, recipientIdentityType) + if (err != nil) { return 0, err } + + senderCurrentSecretInboxSeed, err := inbox.GetNewRandomSecretInboxSeed() + if (err != nil) { return 0, err } + + senderNextSecretInboxSeed, err := inbox.GetNewRandomSecretInboxSeed() + if (err != nil) { return 0, err } + + recipientInbox, err := inbox.GetPublicInboxFromIdentityHash(recipientIdentityHash) + if (err != nil) { return 0, err } + + doubleSealedKeysSealerKey, err := helpers.GetNewRandom32ByteArray() + if (err != nil) { return 0, err } + + senderLatestChatKeysUpdateTime := time.Now().UnixNano() + + recipientNaclPublicKey, err := nacl.GetNewRandomPublicNaclKey() + if (err != nil) { return 0, err } + + recipientKyberPublicKey, err := kyber.GetNewRandomPublicKyberKey() + if (err != nil) { return 0, err } + + messageSentTime := time.Now().Unix() + + senderDeviceIdentifier, err := helpers.GetNewRandomDeviceIdentifier() + if (err != nil) { return 0, err } + + finalMessage, _, err := createMessages.CreateChatMessage(1, senderIdentityHash, senderIdentityPublicKey, senderIdentityPrivateKey, messageSentTime, senderCurrentSecretInboxSeed, senderNextSecretInboxSeed, senderDeviceIdentifier, senderLatestChatKeysUpdateTime, messageCommunication, recipientIdentityHash, recipientInbox, recipientNaclPublicKey, recipientKyberPublicKey, doubleSealedKeysSealerKey) + if (err != nil) { return 0, err } + + messageSize := len(finalMessage) + + return messageSize, nil +} + + + diff --git a/internal/moderation/bannedModeratorConsensus/bannedModeratorConsensus.go b/internal/moderation/bannedModeratorConsensus/bannedModeratorConsensus.go new file mode 100644 index 0000000..b19e816 --- /dev/null +++ b/internal/moderation/bannedModeratorConsensus/bannedModeratorConsensus.go @@ -0,0 +1,244 @@ + +// bannedModeratorConsensus provides functions to determine which moderators are banned + +package bannedModeratorConsensus + +import "seekia/internal/badgerDatabase" +import "seekia/internal/encoding" +import "seekia/internal/helpers" +import "seekia/internal/identity" +import "seekia/internal/moderation/moderatorRanking" +import "seekia/internal/moderation/moderatorScores" +import "seekia/internal/moderation/readReviews" +import "seekia/internal/parameters/getParameters" + +import "slices" +import "sync" +import "errors" + +var bannedModeratorsListMutex sync.RWMutex + +// This list stores all banned moderators +// It will not necessarily be up to date +var bannedModeratorsList [][16]byte + +// This variable keeps track of the networkType of the bannedModeratorsList +var bannedModeratorsListNetworkType byte + + +// This function should be called whenever the app network type is changed. +func UpdateBannedModeratorsList(networkType byte)error{ + + isValid := helpers.VerifyNetworkType(networkType) + if (isValid == false){ + networkTypeString := helpers.ConvertByteToString(networkType) + return errors.New("UpdateBannedModeratorsList called with invalid networkType: " + networkTypeString) + } + + bannedModeratorsListMutex.Lock() + bannedModeratorsList = nil + bannedModeratorsListNetworkType = 0 + bannedModeratorsListMutex.Unlock() + + _, _, _, err := GetBannedModeratorsList(false, networkType) + if (err != nil) { return err } + + return nil +} + + +//Outputs: +// -bool: Moderator reviews and profiles are being downloaded (required to check if a moderator is banned) +// -bool: Parameters exist +// -bool: Moderator is banned +// -error +func GetModeratorIsBannedStatus(allowCache bool, identityHash [16]byte, networkType byte)(bool, bool, bool, error){ + + isValid, err := identity.VerifyIdentityHash(identityHash, true, "Moderator") + if (err != nil) { return false, false, false, err } + if (isValid == false){ + identityHashHex := encoding.EncodeBytesToHexString(identityHash[:]) + return false, false, false, errors.New("GetModeratorIsBannedStatus called with invalid identity hash: " + identityHashHex) + } + + //TODO: Make it possible to retrieve the isBanned status without copying the cache list + + downloadingRequiredData, parametersExist, bannedModeratorsList, err := GetBannedModeratorsList(allowCache, networkType) + if (err != nil) { return false, false, false, err } + if (downloadingRequiredData == false){ + return false, false, false, nil + } + if (parametersExist == false){ + return true, false, false, nil + } + + isBanned := slices.Contains(bannedModeratorsList, identityHash) + + return true, true, isBanned, nil +} + +//Outputs: +// -bool: Moderator reviews and profiles are being downloaded (required to calculate the banned moderators list) +// -bool: Parameters exist +// -[][16]byte: List of banned moderators +// -error +func GetBannedModeratorsList(allowCache bool, networkType byte)(bool, bool, [][16]byte, error){ + + //TODO: Check if parameters exist and moderator reviews/profiles are being downloaded + + isValid := helpers.VerifyNetworkType(networkType) + if (isValid == false){ + networkTypeString := helpers.ConvertByteToString(networkType) + return false, false, nil, errors.New("GetBannedModeratorsList called with invalid networkType: " + networkTypeString) + } + + if (allowCache == true){ + + bannedModeratorsListMutex.RLock() + + if (bannedModeratorsList != nil && bannedModeratorsListNetworkType == networkType){ + + // Cache list has been generated and belongs to the specified network type. + + listCopy := slices.Clone(bannedModeratorsList) + + bannedModeratorsListMutex.RUnlock() + + return true, true, listCopy, nil + } + + bannedModeratorsListMutex.RUnlock() + } + + // We must generate a new banned moderators cache list + + rankedModeratorsList, err := moderatorRanking.GetRankedModeratorsList(networkType) + if (err != nil) { return false, false, nil, err } + + // We start with the highest ranked moderator, and ban moderators as we traverse the list + // If an identity is found to be banned, all of its reviews are ignored + + // Supermoderators are all-powerful moderators who can only be banned by higher-ranked supermoderators + // They are appointed by the admin(s) + + parametersExist, supermoderatorsList, err := getParameters.GetSupermoderatorIdentityHashesList(networkType) + if (err != nil) { return false, false, nil, err } + if (parametersExist == false){ + return true, false, nil, nil + } + + // Supermoderators are added to the beginning of the ranked moderators list + rankedModeratorsList = append(supermoderatorsList, rankedModeratorsList...) + + // Approved Moderators are the moderators who were not banned by anyone of a higher rank + // Map structure: Approved Moderator Identity Hash -> Nothing + approvedModeratorsMap := make(map[[16]byte]struct{}) + + // This map stores all banned identities + // Map structure: Banned Moderator Identity Hash -> Nothing + bannedModeratorsMap := make(map[[16]byte]struct{}) + + for _, moderatorIdentityHash := range rankedModeratorsList{ + + _, exists := bannedModeratorsMap[moderatorIdentityHash] + if (exists == true){ + // This moderator has been banned, none of their reviews are counted. Skip them. + continue + } + + // We make sure moderator is eligible + + moderatorIsSupermoderator := slices.Contains(supermoderatorsList, moderatorIdentityHash) + if (moderatorIsSupermoderator == false){ + + // Moderator is not a supermoderator, so we make sure they are funded enough to be able to ban other moderators + + scoreIsKnown, _, scoreIsSufficient, canBanModerators, _, err := moderatorScores.GetModeratorIdentityScore(moderatorIdentityHash) + if (err != nil) { return false, false, nil, err } + if (scoreIsKnown == false || scoreIsSufficient == false || canBanModerators == false){ + // This moderator is not funded, or cannot ban moderators + continue + } + } + + // This moderator is approved (not banned by any moderators with a higher rank) + approvedModeratorsMap[moderatorIdentityHash] = struct{}{} + + // We create a list of all identity reviews created by this moderator + + exists, reviewHashesList, err := badgerDatabase.GetReviewerReviewHashesList(moderatorIdentityHash, "Identity") + if (err != nil) { return false, false, nil, err } + if (exists == false){ + // This moderator has not authored any reviews + continue + } + + moderatorReviewsList := make([]readReviews.ReviewWithHash, 0) + + for _, reviewHash := range reviewHashesList{ + + exists, reviewBytes, err := badgerDatabase.GetReview(reviewHash) + if (err != nil) { return false, false, nil, err } + if (exists == false){ + // Review has been deleted. The missing entry will be removed automatically in the background. + continue + } + + reviewObject := readReviews.ReviewWithHash{ + ReviewHash: reviewHash, + ReviewBytes: reviewBytes, + } + + moderatorReviewsList = append(moderatorReviewsList, reviewObject) + } + + moderatorReviewsMap, err := readReviews.GetNewestModeratorReviewsMapFromReviewsList(moderatorReviewsList, moderatorIdentityHash, networkType, true, "Identity") + if (err != nil){ return false, false, nil, err } + + for reviewedIdentityHashString, _ := range moderatorReviewsMap{ + + reviewedIdentityHashBytes := []byte(reviewedIdentityHashString) + + if (len(reviewedIdentityHashBytes) != 16){ + reviewedIdentityHashHex := encoding.EncodeBytesToHexString(reviewedIdentityHashBytes) + return false, false, nil, errors.New("GetNewestModeratorReviewsMapFromReviewsList returning non-identity reviewedHash: " + reviewedIdentityHashHex) + } + + reviewedIdentityHash := [16]byte(reviewedIdentityHashBytes) + + // Each reviewedIdentityHash is an identity this moderator has banned + + identityType, err := identity.GetIdentityTypeFromIdentityHash(reviewedIdentityHash) + if (err != nil) { + reviewedIdentityHashHex := encoding.EncodeBytesToHexString(reviewedIdentityHash[:]) + return false, false, nil, errors.New("GetNewestModeratorReviewsMapFromReviewsList returning non-identity reviewedHash: " + reviewedIdentityHashHex) + } + if (identityType != "Moderator"){ + continue + } + + _, exists = approvedModeratorsMap[reviewedIdentityHash] + if (exists == true){ + //The current moderator cannot ban this moderator, because they are of a lower rank. + continue + } + bannedModeratorsMap[reviewedIdentityHash] = struct{}{} + } + } + + // bannedModeratorsMap is a map whose keys are the identity hashes of banned moderators + newBannedModeratorsList := helpers.GetListOfMapKeys(bannedModeratorsMap) + + bannedModeratorsListMutex.Lock() + + bannedModeratorsList = newBannedModeratorsList + bannedModeratorsListNetworkType = networkType + + listCopy := slices.Clone(bannedModeratorsList) + + bannedModeratorsListMutex.Unlock() + + return true, true, listCopy, nil +} + + diff --git a/internal/moderation/contentControversy/contentControversy.go b/internal/moderation/contentControversy/contentControversy.go new file mode 100644 index 0000000..a672360 --- /dev/null +++ b/internal/moderation/contentControversy/contentControversy.go @@ -0,0 +1,248 @@ + +// contentControversy provides functions to calculate the controversy of a piece of reviewed content +// Controversy is a numeric rating of how much disagreement exists among moderators. +// Moderators can view the most controversial content to find moderators to ban or debate with + +package contentControversy + +// The content controversy calculation can be adjusted using several options: + +// 1. Multiply moderator identity scores +// This will multiply the controversy by the sum of all reviewer identity scores, rather than by the number of moderators +// Toggling this on and off is useful to find controversial moderators who do and do not have high identity scores +// 2. Use enabled only or include banned moderators +// -This is useful to find moderators who have already been banned +// -We want as many moderators as possible to ban unruleful moderators, even if they have been banned already +// -This defends against an unruleful moderator increasing their score to evade a ban +// -It also defends against a powerful moderator disabling their profile, resulting in the unbanning of many unruleful moderators +// 3. TODO: Time range +// -This will enable only showing controversial content that was created during a certain time range +// -This will make it possible to only view content that was created recently or long ago + +//TODO: Add filters +// Example: +// -Hide content that I have already reviewed + +import "seekia/internal/contentMetadata" +import "seekia/internal/encoding" +import "seekia/internal/helpers" +import "seekia/internal/moderation/bannedModeratorConsensus" +import "seekia/internal/moderation/enabledModerators" +import "seekia/internal/moderation/moderatorScores" +import "seekia/internal/moderation/reviewStorage" +import "seekia/internal/network/appNetworkType/getAppNetworkType" +import "seekia/internal/network/backgroundDownloads" + +import "errors" + +//Outputs: +// -bool: Controversy is known (content metadata exists, required reviews are being downloaded, parameters exist) +// -int64: Content controversy rating +// -error +func GetContentControversyRating(contentHash []byte)(bool, int64, error){ + + contentType, err := helpers.GetContentTypeFromContentHash(contentHash) + if (err != nil){ + contentHashHex := encoding.EncodeBytesToHexString(contentHash) + return false, 0, errors.New("GetContentControversyRating called with invalid contentHash: " + contentHashHex) + } + if (contentType != "Profile" && contentType != "Message"){ + contentHashHex := encoding.EncodeBytesToHexString(contentHash) + return false, 0, errors.New("GetContentControversyRating called with invalid contentHash: Not profile/message: " + contentHashHex) + } + + appNetworkType, err := getAppNetworkType.GetAppNetworkType() + if (err != nil) { return false, 0, err } + + //TODO: Check if parameters exist + + //TODO: Fix below to retrieve from settings + multiplyIdentityScores := true + includeBannedModerators := true + + //Outputs: + // -bool: Required data exists (Metadata exists and downloading required reviews/moderator profiles) + // -map[[16]byte]int64: Approve advocates map (identity hash -> Time of approval) + // -map[[16]byte]int64: Ban advocates map (identity hash -> Time of ban) + // -error + getApproveAndBanAdvocatesMaps := func()(bool, map[[16]byte]int64, map[[16]byte]int64, error){ + + if (contentType == "Message"){ + + if (len(contentHash) != 26){ + contentHashHex := encoding.EncodeBytesToHexString(contentHash) + return false, nil, nil, errors.New("GetContentTypeFromContentHash returning Message for different length contentHash: " + contentHashHex) + } + + messageHash := [26]byte(contentHash) + + metadataExists, _, messageNetworkType, _, messageInbox, messageCipherKeyHash, err := contentMetadata.GetMessageMetadata(messageHash) + if (err != nil) { return false, nil, nil, err } + if (metadataExists == false){ + return false, nil, nil, nil + } + if (messageNetworkType != appNetworkType){ + // We cannot determine controversy for content from a different networkType + return false, nil, nil, nil + } + + downloadingRequiredContent, err := backgroundDownloads.CheckIfAppCanDetermineMessageVerdict(messageNetworkType, messageInbox, true, messageHash) + if (err != nil) { return false, nil, nil, err } + if (downloadingRequiredContent == false){ + return true, nil, nil, nil + } + + approveAdvocatesMap, banAdvocatesMap, err := reviewStorage.GetMessageVerdictMaps(messageHash, messageNetworkType, messageCipherKeyHash) + if (err != nil) { return false, nil, nil, err } + + return true, approveAdvocatesMap, banAdvocatesMap, nil + } + + // contentType is Profile + + if (len(contentHash) != 28){ + + contentHashHex := encoding.EncodeBytesToHexString(contentHash) + return false, nil, nil, errors.New("GetContentTypeFromContentHash returning Profile for different length contentHash: " + contentHashHex) + } + + profileHash := [28]byte(contentHash) + + profileMetadataExists, _, profileNetworkType, profileIdentityHash, _, profileIsDisabled, _, profileAttributeHashesMap, err := contentMetadata.GetProfileMetadata(profileHash) + if (err != nil) { return false, nil, nil, err } + if (profileMetadataExists == false){ + return false, nil, nil, nil + } + if (profileNetworkType != appNetworkType){ + // We cannot determine controversy for content from a different networkType. + return false, nil, nil, nil + } + if (profileIsDisabled == true){ + // Profile cannot be reviewed, content controversy is not applicable + return false, nil, nil, nil + } + + downloadingRequiredReviews, err := backgroundDownloads.CheckIfAppCanDetermineIdentityVerdicts(profileIdentityHash) + if (err != nil){ return false, nil, nil, err } + if (downloadingRequiredReviews == false){ + return false, nil, nil, nil + } + + // We have to check for any moderators who banned an attribute after approving the full profile + // This would negate their full profile approval + + attributeProfileHashesList := helpers.GetListOfMapValues(profileAttributeHashesMap) + + approveAdvocatesMap, banAdvocatesMap, err := reviewStorage.GetProfileVerdictMaps(profileHash, profileNetworkType, true, attributeProfileHashesList) + if (err != nil) { return false, nil, nil, err } + + return true, approveAdvocatesMap, banAdvocatesMap, nil + } + + requiredDataExists, approveAdvocatesMap, banAdvocatesMap, err := getApproveAndBanAdvocatesMaps() + if (err != nil) { return false, 0, err } + if (requiredDataExists == false){ + return false, 0, nil + } + + if (len(approveAdvocatesMap) == 0 && len(banAdvocatesMap) == 0){ + // No eligible reviewers exist. + // Thus, no controversy exists. + return true, 0, nil + } + + //Outputs: + // -bool: Parameters and required data exists to determine moderator is banned status + // -float64: Moderators value (either number of moderators or a sum of identity scores) + // -error + getModeratorsValue := func(inputModeratorsMap map[[16]byte]int64)(bool, float64, error){ + + moderatorsValue := float64(0) + + for identityHash, _ := range inputModeratorsMap{ + + moderatorIsEnabled, err := enabledModerators.CheckIfModeratorIsEnabled(true, identityHash, appNetworkType) + if (err != nil){ return false, 0, err } + if (moderatorIsEnabled == false){ + continue + } + if (includeBannedModerators == false){ + + requiredDataExists, parametersExist, moderatorIsBanned, err := bannedModeratorConsensus.GetModeratorIsBannedStatus(true, identityHash, appNetworkType) + if (err != nil){ return false, 0, err } + if (requiredDataExists == false || parametersExist == false){ + return false, 0, nil + } + if (moderatorIsBanned == true){ + continue + } + } + + if (multiplyIdentityScores == false){ + moderatorsValue += 1 + continue + } + + scoreIsKnown, moderatorScore, scoreIsSufficient, _, _, err := moderatorScores.GetModeratorIdentityScore(identityHash) + if (err != nil) { return false, 0, err } + if (scoreIsKnown == false){ + // Client has not downloaded moderator score, will do so automatically, skip for now. + continue + } + if (scoreIsSufficient == false){ + // Moderator is not funded enough to make reviews + continue + } + moderatorsValue += moderatorScore + } + + return true, moderatorsValue, nil + } + + requiredDataExists, approveValue, err := getModeratorsValue(approveAdvocatesMap) + if (err != nil) { return false, 0, err } + if (requiredDataExists == false){ + // We cannot determine content controversy + // Client should automatically download parameters in the background, and then content controversy will be calculable + return false, 0, nil + } + + requiredDataExists, banValue, err := getModeratorsValue(banAdvocatesMap) + if (err != nil) { return false, 0, err } + if (requiredDataExists == false){ + // We cannot determine content controversy + // Client should automatically download parameters in the background, and then content controversy will be calculable + return false, 0, nil + } + + getValuesRatio := func()float64{ + + if (approveValue == banValue){ + // Highest possible controversy + return 1 + } + if (approveValue == 0 || banValue == 0){ + // Lowest possible controversy + return 0 + } + if (approveValue > banValue){ + ratio := banValue/approveValue + return ratio + } + + ratio := approveValue/banValue + return ratio + } + + valuesRatio := getValuesRatio() + + contentControversyFloat := valuesRatio * (approveValue + banValue) + + contentControversyInt, err := helpers.FloorFloat64ToInt64(contentControversyFloat) + if (err != nil) { return false, 0, err } + + return true, contentControversyInt, nil +} + + + diff --git a/internal/moderation/createReports/createReports.go b/internal/moderation/createReports/createReports.go new file mode 100644 index 0000000..81d5892 --- /dev/null +++ b/internal/moderation/createReports/createReports.go @@ -0,0 +1,116 @@ + +// createReports provides a function to create reports. + +package createReports + +// We encode each attribute as an integer in MessagePack: +// ReportVersion = 1 +// BroadcastTime = 3 +// ReportedHash = 4 +// Reason = 5 +// MessageCipherKey = 6 + +//TODO: Encode ReportType value as a number to save space + +import "seekia/internal/appValues" +import "seekia/internal/helpers" +import "seekia/internal/moderation/readReports" +import "seekia/internal/encoding" + +import messagepack "github.com/vmihailenco/msgpack/v5" + +import "time" +import "errors" + +//Outputs: +// -[]byte: Report bytes +// -error +func CreateReport(reportMap map[string]string)([]byte, error){ + + reportVersion := appValues.GetReportVersion() + + reportNetworkTypeString, exists := reportMap["NetworkType"] + if (exists == false){ + return nil, errors.New("CreateReport called with reportMap missing NetworkType") + } + + reportNetworkType, err := helpers.ConvertNetworkTypeStringToByte(reportNetworkTypeString) + if (err != nil){ + return nil, errors.New("CreateReport called with reportMap containing invalid NetworkType: " + reportNetworkTypeString) + } + + broadcastTime := time.Now().Unix() + + reportedHashString, exists := reportMap["ReportedHash"] + if (exists == false){ + return nil, errors.New("CreateReport called with reportMap missing reportedHash") + } + + reportedHash, reportType, err := helpers.ReadReportedHashString(reportedHashString) + if (err != nil){ + return nil, errors.New("CreateReport called with reportMap containing invalid ReportedHash: " + reportedHashString + ". Reason: " + err.Error()) + } + + reportVersionEncoded, err := encoding.EncodeMessagePackBytes(reportVersion) + if (err != nil){ return nil, err } + + reportNetworkTypeEncoded, err := encoding.EncodeMessagePackBytes(reportNetworkType) + if (err != nil){ return nil, err } + + broadcastTimeEncoded, err := encoding.EncodeMessagePackBytes(broadcastTime) + if (err != nil){ return nil, err } + + reportedHashEncoded, err := encoding.EncodeMessagePackBytes(reportedHash) + if (err != nil){ return nil, err } + + reportContentMap := map[int]messagepack.RawMessage{ + 1: reportVersionEncoded, + 2: reportNetworkTypeEncoded, + 3: broadcastTimeEncoded, + 4: reportedHashEncoded, + } + + reason, exists := reportMap["Reason"] + if (exists == true){ + + reasonEncoded, err := encoding.EncodeMessagePackBytes(reason) + if (err != nil){ return nil, err } + + reportContentMap[5] = reasonEncoded + } + + if (reportType == "Message"){ + + messageCipherKeyString, exists := reportMap["MessageCipherKey"] + if (exists == false){ + return nil, errors.New("CreateReport called with message reportMap missing MessageCipherKey") + } + messageCipherKey, err := encoding.DecodeHexStringToBytes(messageCipherKeyString) + if (err != nil){ + return nil, errors.New("CreateReport called with reportMap containing invalid messageCipherKey: Not Hex: " + messageCipherKeyString) + } + + if (len(messageCipherKey) != 32){ + return nil, errors.New("CreateReport called with reportMap containing invalid messageCipherKey: Invalid length: " + messageCipherKeyString) + } + + messageCipherKeyEncoded, err := encoding.EncodeMessagePackBytes(messageCipherKey) + if (err != nil){ return nil, err } + + reportContentMap[6] = messageCipherKeyEncoded + } + + reportBytes, err := encoding.EncodeMessagePackBytes(reportContentMap) + if (err != nil) { return nil, err } + + isValid, err := readReports.VerifyReport(reportBytes) + if (err != nil) { return nil, err } + if (isValid == false){ + return nil, errors.New("Failed to create report: Invalid result.") + } + + return reportBytes, nil +} + + + diff --git a/internal/moderation/createReviews/createReviews.go b/internal/moderation/createReviews/createReviews.go new file mode 100644 index 0000000..dd07e03 --- /dev/null +++ b/internal/moderation/createReviews/createReviews.go @@ -0,0 +1,152 @@ + +// createReviews provides a function to create moderator reviews + +package createReviews + +// We encode each attribute as an integer in MessagePack: + +// ReviewVersion = 1 +// NetworkType = 2 +// IdentityKey = 3 +// BroadcastTime = 4 +// ReviewedHash = 5 +// Verdict = 6 +// Reason = 7 +// MessageCipherKey = 8 +// ErrantProfiles = 9 +// ErrantMessages = 10 +// ErrantReviews = 11 +// ErrantAttributes = 12 + +import "seekia/internal/appValues" +import "seekia/internal/cryptography/blake3" +import "seekia/internal/cryptography/edwardsKeys" +import "seekia/internal/encoding" +import "seekia/internal/helpers" +import "seekia/internal/moderation/readReviews" + +import messagepack "github.com/vmihailenco/msgpack/v5" + +import "errors" +import "time" + +func CreateReview(identityPublicKey [32]byte, identityPrivateKey [64]byte, reviewMap map[string]string)([]byte, error){ + + reviewVersion := appValues.GetReviewVersion() + + networkTypeString, exists := reviewMap["NetworkType"] + if (exists == false){ + return nil, errors.New("CreateReview called with reviewMap missing NetworkType") + } + + networkType, err := helpers.ConvertNetworkTypeStringToByte(networkTypeString) + if (err != nil){ + return nil, errors.New("CreateReview called with reviewMap containing invalid NetworkType: " + networkTypeString) + } + + broadcastTime := time.Now().Unix() + + reviewedHashString, exists := reviewMap["ReviewedHash"] + if (exists == false){ + return nil, errors.New("CreateReview called with reviewMap missing ReviewedHash") + } + + reviewVerdict, exists := reviewMap["Verdict"] + if (exists == false){ + return nil, errors.New("CreateReview called with reviewMap missing Verdict") + } + + reviewedHashBytes, reviewType, err := helpers.ReadReviewedHashString(reviewedHashString) + if (err != nil) { + return nil, errors.New("CreateReview called with reviewMap containing invalid reviewedHash: " + reviewedHashString) + } + + reviewVersionEncoded, err := encoding.EncodeMessagePackBytes(reviewVersion) + if (err != nil) { return nil, err } + + networkTypeEncoded, err := encoding.EncodeMessagePackBytes(networkType) + if (err != nil) { return nil, err } + + identityKeyEncoded, err := encoding.EncodeMessagePackBytes(identityPublicKey) + if (err != nil) { return nil, err } + + broadcastTimeEncoded, err := encoding.EncodeMessagePackBytes(broadcastTime) + if (err != nil) { return nil, err } + + reviewedHashEncoded, err := encoding.EncodeMessagePackBytes(reviewedHashBytes) + if (err != nil) { return nil, err } + + reviewVerdictEncoded, err := encoding.EncodeMessagePackBytes(reviewVerdict) + if (err != nil) { return nil, err } + + reviewContentMap := map[int]messagepack.RawMessage{ + 1: reviewVersionEncoded, + 2: networkTypeEncoded, + 3: identityKeyEncoded, + 4: broadcastTimeEncoded, + 5: reviewedHashEncoded, + 6: reviewVerdictEncoded, + } + + reviewReason, exists := reviewMap["Reason"] + if (exists == true){ + + reviewReasonEncoded, err := encoding.EncodeMessagePackBytes(reviewReason) + if (err != nil) { return nil, err } + + reviewContentMap[7] = reviewReasonEncoded + } + + if (reviewType == "Message"){ + + messageCipherKeyString, exists := reviewMap["MessageCipherKey"] + if (exists == false){ + return nil, errors.New("CreateReview called with message reviewMap missing MessageCipherKey") + } + messageCipherKey, err := encoding.DecodeHexStringToBytes(messageCipherKeyString) + if (err != nil){ + return nil, errors.New("CreateReview called with reviewMap containing invalid messageCipherKey: Not Hex: " + messageCipherKeyString) + } + + messageCipherKeyEncoded, err := encoding.EncodeMessagePackBytes(messageCipherKey) + if (err != nil){ return nil, err } + + reviewContentMap[8] = messageCipherKeyEncoded + } + + //TODO: ErrantProfiles, ErrantMessages, ErrantReviews, ErrantAttributes + + reviewContentMessagepack, err := encoding.EncodeMessagePackBytes(reviewContentMap) + if (err != nil) { + return nil, errors.New("Failed to create review: " + err.Error()) + } + + contentHash, err := blake3.Get32ByteBlake3Hash(reviewContentMessagepack) + if (err != nil) { + return nil, errors.New("Failed to create review: " + err.Error()) + } + + reviewSignature := edwardsKeys.CreateSignature(identityPrivateKey, contentHash) + + signatureEncoded, err := encoding.EncodeMessagePackBytes(reviewSignature) + if (err != nil) { return nil, err } + + reviewSlice := []messagepack.RawMessage{signatureEncoded, reviewContentMessagepack} + + reviewBytes, err := encoding.EncodeMessagePackBytes(reviewSlice) + if (err != nil) { + return nil, errors.New("Failed to create review: " + err.Error()) + } + + isValid, err := readReviews.VerifyReview(reviewBytes) + if (err != nil) { return nil, err } + if (isValid == false){ + return nil, errors.New("Cannot create review: Resulting review is invalid.") + } + + return reviewBytes, nil +} + + + + diff --git a/internal/moderation/enabledModerators/enabledModerators.go b/internal/moderation/enabledModerators/enabledModerators.go new file mode 100644 index 0000000..6564dce --- /dev/null +++ b/internal/moderation/enabledModerators/enabledModerators.go @@ -0,0 +1,153 @@ + +// enabledModerators provides functions to retrieve and refresh a list of all enabled moderators +// An enabled moderator is a moderator whose profile is not disabled + +package enabledModerators + +import "seekia/internal/badgerDatabase" +import "seekia/internal/helpers" +import "seekia/internal/profiles/profileStorage" +import "seekia/internal/profiles/readProfiles" + +import "slices" +import "sync" +import "errors" + +var enabledModeratorsListMutex sync.RWMutex + +// This list stores all enabled moderators +// It will not necessarily be up to date +var enabledModeratorsList [][16]byte + +// This variable stores the network type that the enabledModerators list belongs to +var enabledModeratorsListNetworkType byte + +// This function must be called upon application startup, and then again on a regular automatic interval +// It must also be called whenever the application network type is changed +func UpdateEnabledModeratorsList(networkType byte)error{ + + isValid := helpers.VerifyNetworkType(networkType) + if (isValid == false){ + networkTypeString := helpers.ConvertByteToString(networkType) + return errors.New("UpdateEnabledModeratorsList called with invalid networkType: " + networkTypeString) + } + + enabledModeratorsListMutex.Lock() + enabledModeratorsList = nil + enabledModeratorsListNetworkType = 0 + enabledModeratorsListMutex.Unlock() + + _, err := GetEnabledModeratorsList(false, networkType) + if (err != nil) { return err } + + return nil +} + +//Outputs: +// -int: Number of enabled moderators +// -error +func GetNumberOfEnabledModerators(allowCache bool, networkType byte)(int, error){ + + isValid := helpers.VerifyNetworkType(networkType) + if (isValid == false){ + networkTypeString := helpers.ConvertByteToString(networkType) + return 0, errors.New("GetNumberOfEnabledModerators called with invalid networkType: " + networkTypeString) + } + + currentEnabledModeratorsList, err := GetEnabledModeratorsList(allowCache, networkType) + if (err != nil) { return 0, err } + + numberOfModerators := len(currentEnabledModeratorsList) + + return numberOfModerators, nil +} + +//Outputs: +// -bool: Moderator is enabled +// -error +func CheckIfModeratorIsEnabled(allowCache bool, moderatorIdentityHash [16]byte, networkType byte)(bool, error){ + + isValid := helpers.VerifyNetworkType(networkType) + if (isValid == false){ + networkTypeString := helpers.ConvertByteToString(networkType) + return false, errors.New("CheckIfModeratorIsEnabled called with invalid networkType: " + networkTypeString) + } + + currentEnabledModeratorsList, err := GetEnabledModeratorsList(allowCache, networkType) + if (err != nil) { return false, err } + + isEnabled := slices.Contains(currentEnabledModeratorsList, moderatorIdentityHash) + + return isEnabled, nil +} + + +// This function returns enabled moderators list for current app network type +//Outputs: +// -[][16]byte: List of enabled moderator identity hashes +// -error +func GetEnabledModeratorsList(allowCache bool, networkType byte)([][16]byte, error){ + + isValid := helpers.VerifyNetworkType(networkType) + if (isValid == false){ + networkTypeString := helpers.ConvertByteToString(networkType) + return nil, errors.New("GetEnabledModeratorsList called with invalid networkType: " + networkTypeString) + } + + if (allowCache == true){ + + enabledModeratorsListMutex.RLock() + + if (enabledModeratorsList != nil && enabledModeratorsListNetworkType == networkType){ + + // enabledModeratorsList is generated and belongs to the requested networkType + + listCopy := slices.Clone(enabledModeratorsList) + + enabledModeratorsListMutex.RUnlock() + + return listCopy, nil + } + + enabledModeratorsListMutex.RUnlock() + } + + // We must generate a new enabled moderators cache list + + moderatorIdentityHashesList, err := badgerDatabase.GetAllProfileIdentityHashes("Moderator") + if (err != nil) { return nil, err } + + newEnabledModeratorsList := make([][16]byte, 0) + + for _, identityHash := range moderatorIdentityHashesList{ + + profileExists, _, _, _, _, newestRawProfileMap, err := profileStorage.GetNewestUserProfile(identityHash, networkType) + if (err != nil) { return nil, err } + if (profileExists == false){ + // Profile does not exist anymore. + // Missing entries from Identity Profiles list will be removed automatically by backgroundJobs + continue + } + + profileIsDisabled, _, err := readProfiles.GetFormattedProfileAttributeFromRawProfileMap(newestRawProfileMap, "Disabled") + if (err != nil) { return nil, err } + if (profileIsDisabled == true){ + continue + } + + newEnabledModeratorsList = append(newEnabledModeratorsList, identityHash) + } + + enabledModeratorsListMutex.Lock() + + enabledModeratorsList = newEnabledModeratorsList + enabledModeratorsListNetworkType = networkType + + listCopy := slices.Clone(enabledModeratorsList) + + enabledModeratorsListMutex.Unlock() + + return listCopy, nil +} + + diff --git a/internal/moderation/moderatorControversy/moderatorControversy.go b/internal/moderation/moderatorControversy/moderatorControversy.go new file mode 100644 index 0000000..a2f65b9 --- /dev/null +++ b/internal/moderation/moderatorControversy/moderatorControversy.go @@ -0,0 +1,390 @@ + +// moderatorControversy provides functions for calculating a moderator's controversy rating +// Controversy ratings are used by moderators to find controversial moderators +// These are moderators who might be worthy of being banned or debated with + +package moderatorControversy + +// Controversy calculation can be tuned in several ways: +// +// 1. Weight controversy by moderator scores +// -If we turn off, we use number of moderators. +// -If we turn on, we use combined score of moderators +// -When toggled on, controversy is lower if disagreement is among low-score moderators +// 2. Exclude banned moderators in calculation +// -When enabled, banned moderator verdicts will not be included +// 3. TODO: Only include from a specific time range (Time min- Time Max) +// -This is useful to see moderators who only recently became controversial +// 4. Omit agree with consensus in calculation +// -When enabled, moderator controversy will only count when moderators disagree with the majority +// -This prevents moderators from making their controversy scores seem lower by creating many agreement reviews +// -When enabled, the controversy score is a measure of how much the moderator disagrees, only when they dissent from the majority + +//TODO: Reduce the calculation to a random sampling to make it less expensive + +import "seekia/internal/badgerDatabase" +import "seekia/internal/contentMetadata" +import "seekia/internal/encoding" +import "seekia/internal/helpers" +import "seekia/internal/identity" +import "seekia/internal/moderation/bannedModeratorConsensus" +import "seekia/internal/moderation/enabledModerators" +import "seekia/internal/moderation/moderatorScores" +import "seekia/internal/moderation/readReviews" +import "seekia/internal/moderation/reviewStorage" + +import "bytes" +import "errors" + +//Outputs: +// -bool: Score is known (parameters exist, downloading any reviews) +// -int64: Moderator controversy score +// -error +func GetModeratorControversyRating(moderatorIdentityHash [16]byte, networkType byte)(bool, int64, error){ + + isValid, err := identity.VerifyIdentityHash(moderatorIdentityHash, true, "Moderator") + if (err != nil) { return false, 0, err } + if (isValid == false){ + moderatorIdentityHashHex := encoding.EncodeBytesToHexString(moderatorIdentityHash[:]) + return false, 0, errors.New("GetModeratorControversyRating called with invalid moderatorIdentityHash: " + moderatorIdentityHashHex) + } + + isValid = helpers.VerifyNetworkType(networkType) + if (isValid == false){ + networkTypeString := helpers.ConvertByteToString(networkType) + return false, 0, errors.New("GetModeratorControversyRating called with invalid networkType: " + networkTypeString) + } + + //TODO: Make sure we are actually downloading any reviews (moderator mode/host mode is enabled) + // We may only be hosting a small range of total reviews. This is fine. + // Controversy will be calculated for reviews of content within the range + + // There is no controversy for identity reviews + // This is because there can be no identity approve reviews, only ban reviews. + reviewTypesList := []string{"Profile", "Attribute", "Message"} + + numberOfRatingsIncluded := 0 + controversyRatingsSum := float64(0) + + for _, reviewType := range reviewTypesList{ + + // These are all reviews created by this moderator + exists, reviewHashesList, err := badgerDatabase.GetReviewerReviewHashesList(moderatorIdentityHash, reviewType) + if (err != nil) { return false, 0, err } + if (exists == false){ + // No reviews exist by this moderator + // No controversy exists + return true, 0, nil + } + + reviewsList := make([]readReviews.ReviewWithHash, 0) + + for _, reviewHash := range reviewHashesList{ + + exists, reviewBytes, err := badgerDatabase.GetReview(reviewHash) + if (err != nil) { return false, 0, err } + if (exists == false){ + // Review must have been deleted, backgroundJobs will prune reviewHashesList + continue + } + + reviewObject := readReviews.ReviewWithHash{ + ReviewHash: reviewHash, + ReviewBytes: reviewBytes, + } + + reviewsList = append(reviewsList, reviewObject) + } + + // This returns a map of the newest reviews for each reviewedHash in the list + newestReviewsMap, err := readReviews.GetNewestModeratorReviewsMapFromReviewsList(reviewsList, moderatorIdentityHash, networkType, true, reviewType) + if (err != nil) { return false, 0, err } + + for reviewedHashString, reviewBytes := range newestReviewsMap{ + + reviewedHash := []byte(reviewedHashString) + + ableToRead, _, reviewNetworkType, reviewerIdentityHash, _, currentReviewType, currentReviewedHash, reviewVerdict, reviewMap, err := readReviews.ReadReview(false, reviewBytes) + if (err != nil) { return false, 0, err } + if (ableToRead == false){ + return false, 0, errors.New("Database corrupt: Contains invalid review.") + } + if (reviewNetworkType != networkType){ + return false, 0, errors.New("GetNewestModeratorReviewsMapFromReviewsList returning different networkType review.") + } + if (reviewerIdentityHash != moderatorIdentityHash){ + return false, 0, errors.New("GetNewestModeratorReviewsMapFromReviewsList returning review by different reviewer.") + } + areEqual := bytes.Equal(reviewedHash, currentReviewedHash) + if (areEqual == false){ + return false, 0, errors.New("GetNewestModeratorReviewsMapFromReviewsList returning different reviewedHash review") + } + if (currentReviewType != reviewType){ + return false, 0, errors.New("GetNewestModeratorReviewsMapFromReviewsList returning review of different reviewType.") + } + if (reviewVerdict == "None"){ + return false, 0, errors.New("GetNewestModeratorReviewsMapFromReviewsList returning review with None verdict.") + } + + //Outputs: + // -bool: Required data exists + // -map[[16]byte]int64: Content Approve advocates map map (identity hash -> Time of approval) + // -map[[16]byte]int64: Content Ban advocates map (identity hash -> Time of ban) + // -error + getApproveAndBanAdvocateMaps := func()(bool, map[[16]byte]int64, map[[16]byte]int64, error){ + + if (reviewType == "Profile"){ + + if (len(reviewedHash) != 28){ + reviewedHashHex := encoding.EncodeBytesToHexString(reviewedHash) + return false, nil, nil, errors.New("ReadReview returning invalid reviewed profileHash: " + reviewedHashHex) + } + + reviewedProfileHash := [28]byte(reviewedHash) + + metadataExists, _, profileNetworkType, _, _, profileIsDisabled, _, profileAttributeHashesMap, err := contentMetadata.GetProfileMetadata(reviewedProfileHash) + if (err != nil) { return false, nil, nil, err } + if (metadataExists == false){ + // We can't verify attribute reviews + + return false, nil, nil, nil + } + if (profileNetworkType != reviewNetworkType){ + // This moderator has reviewed a profile from a different network as the review network + // This moderator must be malicious + // The Seekia app should automatically ban these moderators + // We will not count this profile in the calculation of this moderator's controversy + + return false, nil, nil, nil + } + if (profileIsDisabled == true){ + // Disabled profiles can't be reviewed. + // The review is invalid. This moderator must be malicious. + // The moderator should automatically ban them in the background. + + return false, nil, nil, nil + } + + // We will find all moderators who have approved/banned the profile + // This requires finding users who have banned any attribute within the profile + // This could override their full profile approval, if the attribute ban was submitted after the full profile approval + + // profileAttributeHashesMap is a map whose values are the attribute hashes of this profile + attributeHashesList := helpers.GetListOfMapValues(profileAttributeHashesMap) + + approveAdvocatesMap, banAdvocatesMap, err := reviewStorage.GetProfileVerdictMaps(reviewedProfileHash, profileNetworkType, true, attributeHashesList) + if (err != nil) { return false, nil, nil, err } + + return true, approveAdvocatesMap, banAdvocatesMap, nil + } + if (reviewType == "Attribute"){ + + // We want to find all moderators who have approved/banned this particular attribute + // This requires checking for full profile approvals for all profiles which contain this attribute + + if (len(reviewedHash) != 27){ + reviewedHashHex := encoding.EncodeBytesToHexString(reviewedHash) + return false, nil, nil, errors.New("ReadReview returning invalid reviewed attributeHash: " + reviewedHashHex) + } + + reviewedAttributeHash := [27]byte(reviewedHash) + + getAttributeProfileHashesList := func()([][28]byte, error){ + + anyExist, attributeProfilesList, err := badgerDatabase.GetAttributeProfilesList(reviewedAttributeHash) + if (err != nil) { return nil, err } + if (anyExist == false){ + emptyList := make([][28]byte, 0) + return emptyList, nil + } + return attributeProfilesList, nil + } + + attributeProfileHashesList, err := getAttributeProfileHashesList() + if (err != nil) { return false, nil, nil, err } + + approveAdvocatesMap, banAdvocatesMap, err := reviewStorage.GetProfileAttributeVerdictMaps(reviewedAttributeHash, networkType, true, attributeProfileHashesList) + if (err != nil) { return false, nil, nil, err } + + return true, approveAdvocatesMap, banAdvocatesMap, nil + } + if (reviewType == "Message"){ + + if (len(reviewedHash) != 26){ + reviewedHashHex := encoding.EncodeBytesToHexString(reviewedHash) + return false, nil, nil, errors.New("ReadReview returning invalid reviewed messageHash: " + reviewedHashHex) + } + + reviewedMessageHash := [26]byte(reviewedHash) + + metadataExists, _, messageNetworkType, _, _, messageCipherKeyHash, err := contentMetadata.GetMessageMetadata(reviewedMessageHash) + if (err != nil){ return false, nil, nil, err } + if (metadataExists == false){ + // We cannot verify reviews, so no approve or ban advocates can be determined + + return false, nil, nil, nil + } + if (messageNetworkType != reviewNetworkType){ + + // Review author must be malicious + // They created a review for a message on a different network + // The app should ban them automatically in the background + + return false, nil, nil, nil + } + + reviewIsValid, err := readReviews.VerifyMessageReviewCipherKey(reviewMap, messageCipherKeyHash) + if (err != nil){ return false, nil, nil, err } + if (reviewIsValid == false){ + // Moderator must be malicious. + // They should be banned automatically in the background + + return false, nil, nil, nil + } + + approveAdvocatesMap, banAdvocatesMap, err := reviewStorage.GetMessageVerdictMaps(reviewedMessageHash, messageNetworkType, messageCipherKeyHash) + if (err != nil) { return false, nil, nil, err } + + return true, approveAdvocatesMap, banAdvocatesMap, nil + } + + return false, nil, nil, errors.New("reviewTypesList contains invalid reviewType: " + reviewType) + } + + requiredDataExists, approveAdvocatesMap, banAdvocatesMap, err := getApproveAndBanAdvocateMaps() + if (err != nil) { return false, 0, err } + if (requiredDataExists == false){ + continue + } + if (len(approveAdvocatesMap) == 0 && len(banAdvocatesMap) == 0){ + continue + } + + //TODO: Fix below to retrieve from settings + includeBannedModerators := true + multiplyIdentityScores := true + excludeConsensusAgreement := true + + // Outputs: + // -bool: Required data exists + // -float64: Moderators value + // -error + getModeratorsValue := func(inputModeratorsMap map[[16]byte]int64)(bool, float64, error){ + + moderatorsValue := float64(0) + + for identityHash, _ := range inputModeratorsMap{ + + if (identityHash == reviewerIdentityHash){ + // Exclude current reviewer + continue + } + + moderatorIsEnabled, err := enabledModerators.CheckIfModeratorIsEnabled(true, identityHash, networkType) + if (err != nil) { return false, 0, err } + if (moderatorIsEnabled == false){ + // Skip this moderator + continue + } + + if (includeBannedModerators == false){ + + requiredDataBeingDownloaded, parametersExist, isBanned, err := bannedModeratorConsensus.GetModeratorIsBannedStatus(true, identityHash, networkType) + if (err != nil){ return false, 0, err } + if (requiredDataBeingDownloaded == false || parametersExist == false){ + return false, 0, nil + } + if (isBanned == true){ + continue + } + } + + if (multiplyIdentityScores == false){ + moderatorsValue += 1 + continue + } + + statusIsKnown, moderatorScore, scoreIsSufficient, _, _, err := moderatorScores.GetModeratorIdentityScore(identityHash) + if (err != nil) { return false, 0, err } + if (statusIsKnown == false){ + // Client has not downloaded moderator score. It will do so automatically. + continue + } + if (scoreIsSufficient == false){ + // Moderator cannot participate in consensus + continue + } + moderatorsValue += moderatorScore + } + + return true, moderatorsValue, nil + } + + requiredDataExists, approveModeratorsValue, err := getModeratorsValue(approveAdvocatesMap) + if (requiredDataExists == false){ + // We need the parameters and required data to calculate if moderators are banned + // This is required for all moderators, and is required to calculate controversy + // Thus, we cannot calculate controversy for this moderator + + return false, 0, nil + } + + requiredDataExists, banModeratorsValue, err := getModeratorsValue(banAdvocatesMap) + if (requiredDataExists == false){ + return false, 0, nil + } + + if (approveModeratorsValue == 0 && banModeratorsValue == 0){ + continue + } + + getAgreeDisagreeValues := func()(float64, float64, error){ + + if (reviewVerdict == "Approve"){ + return approveModeratorsValue, banModeratorsValue, nil + } + + return banModeratorsValue, approveModeratorsValue, nil + } + + agreeValue, disagreeValue, err := getAgreeDisagreeValues() + if (err != nil) { return false, 0, err } + + if (excludeConsensusAgreement == true){ + + if (agreeValue > disagreeValue){ + continue + } + } + + getCurrentControversyRating := func()float64{ + if (disagreeValue == 0){ + return 0 + } + currentControversyRating := (agreeValue/disagreeValue) * disagreeValue + + return currentControversyRating + } + + currentControversyRating := getCurrentControversyRating() + + controversyRatingsSum += currentControversyRating + numberOfRatingsIncluded += 1 + } + } + + if (numberOfRatingsIncluded == 0){ + // There is no controversy + return true, 0, nil + } + + moderatorControversyRating := controversyRatingsSum/float64(numberOfRatingsIncluded) + + scoreInt, err := helpers.FloorFloat64ToInt64(moderatorControversyRating) + if (err != nil) { return false, 0, err } + + return true, scoreInt, nil +} + + + diff --git a/internal/moderation/moderatorRanking/moderatorRanking.go b/internal/moderation/moderatorRanking/moderatorRanking.go new file mode 100644 index 0000000..203cd3e --- /dev/null +++ b/internal/moderation/moderatorRanking/moderatorRanking.go @@ -0,0 +1,187 @@ + +// moderatorRanking provides functions to calculate a moderator's rank, and to get a list of ranked moderators +// A moderator's rank is determined by ranking all moderators by their identity scores. + +package moderatorRanking + +//TODO: Add a bool to both functions to exclude banned moderators +// When a moderator is viewing another moderator's rank, they should be able to view both rankings: Banned excluded and banned included +// This might actually be a bad idea, because it could encourage moderators to ban other moderators so they can surpass them in rank without having to burn more funds. + +import "seekia/internal/encoding" +import "seekia/internal/helpers" +import "seekia/internal/identity" +import "seekia/internal/moderation/enabledModerators" +import "seekia/internal/moderation/moderatorScores" + +import "slices" +import "errors" + +// This function returns the rank of a particular moderator +// This is faster than ranking all of the moderators, because we don't have to sort +//Outputs: +// -bool: Moderator ranking known +// -int: Moderator rank +// -int: Total number of moderators +// -error +func GetModeratorRanking(moderatorIdentityHash [16]byte, networkType byte)(bool, int, int, error){ + + isValid := helpers.VerifyNetworkType(networkType) + if (isValid == false){ + networkTypeString := helpers.ConvertByteToString(networkType) + return false, 0, 0, errors.New("GetModeratorRanking called with invalid networkType: " + networkTypeString) + } + + moderatorScoresMap, err := getEnabledModeratorScoresMap(networkType) + if (err != nil) { return false, 0, 0, err } + + moderatorScore, exists := moderatorScoresMap[moderatorIdentityHash] + if (exists == false){ + // We may not have downloaded their profile or their identity score + // Their profile may also be disabled + // We cannot determine their ranking + return false, 0, 0, nil + } + + moderatorIdentityHashString, _, err := identity.EncodeIdentityHashBytesToString(moderatorIdentityHash) + if (err != nil) { + moderatorIdentityHashHex := encoding.EncodeBytesToHexString(moderatorIdentityHash[:]) + return false, 0, 0, errors.New("moderatorScoresMap contains invalid moderator identity hash: " + moderatorIdentityHashHex) + } + + totalNumberOfModerators := len(moderatorScoresMap) + + moderatorsWithLowerScoreCounter := 0 + + for currentModeratorIdentityHash, currentModeratorScore := range moderatorScoresMap{ + + if (currentModeratorIdentityHash == moderatorIdentityHash){ + continue + } + + if (moderatorScore == currentModeratorScore){ + + currentModeratorIdentityHashString, _, err := identity.EncodeIdentityHashBytesToString(currentModeratorIdentityHash) + if (err != nil) { + moderatorIdentityHashHex := encoding.EncodeBytesToHexString(currentModeratorIdentityHash[:]) + return false, 0, 0, errors.New("moderatorScoresMap contains invalid moderator identity hash: " + moderatorIdentityHashHex) + } + + // We sort moderators with the same identity score in alphabetical order + + if (moderatorIdentityHashString < currentModeratorIdentityHashString){ + moderatorsWithLowerScoreCounter += 1 + } + continue + } + + if (moderatorScore > currentModeratorScore){ + moderatorsWithLowerScoreCounter += 1 + } + } + + moderatorRank := totalNumberOfModerators - moderatorsWithLowerScoreCounter + + return true, moderatorRank, totalNumberOfModerators, nil +} + + +// This function returns a list of all moderators ranked, starting from highest to lowest rank +func GetRankedModeratorsList(networkType byte)([][16]byte, error){ + + isValid := helpers.VerifyNetworkType(networkType) + if (isValid == false){ + networkTypeString := helpers.ConvertByteToString(networkType) + return nil, errors.New("GetRankedModeratorsList called with invalid networkType: " + networkTypeString) + } + + moderatorScoresMap, err := getEnabledModeratorScoresMap(networkType) + if (err != nil) { return nil, err } + + moderatorIdentitiesList := helpers.GetListOfMapKeys(moderatorScoresMap) + + compareIdentitiesFunction := func(moderatorIdentityA [16]byte, moderatorIdentityB [16]byte)int { + + if (moderatorIdentityA == moderatorIdentityB){ + panic("compareIdentitiesFunction called with identical moderator identities.") + } + + moderatorAScore, exists := moderatorScoresMap[moderatorIdentityA] + if (exists == false) { + panic("Moderator score not found in moderatorScoresMap") + } + + moderatorBScore, exists := moderatorScoresMap[moderatorIdentityB] + if (exists == false) { + panic("Moderator score not found in moderatorScoresMap") + } + + if (moderatorAScore == moderatorBScore){ + + // This will sort the moderators in unicode order + // This is to make sure that everyone calculates moderator order the same way + + moderatorIdentityAString, _, err := identity.EncodeIdentityHashBytesToString(moderatorIdentityA) + if (err != nil) { + moderatorAIdentityHashHex := encoding.EncodeBytesToHexString(moderatorIdentityA[:]) + panic("moderatorScoresMap contains invalid moderator identity hash: " + moderatorAIdentityHashHex) + } + + moderatorIdentityBString, _, err := identity.EncodeIdentityHashBytesToString(moderatorIdentityB) + if (err != nil) { + moderatorBIdentityHashHex := encoding.EncodeBytesToHexString(moderatorIdentityB[:]) + panic("moderatorScoresMap contains invalid moderator identity hash: " + moderatorBIdentityHashHex) + } + + if (moderatorIdentityAString < moderatorIdentityBString){ + return -1 + } + + return 1 + } + + if (moderatorAScore < moderatorBScore){ + return 1 + } + + return -1 + } + + slices.SortFunc(moderatorIdentitiesList, compareIdentitiesFunction) + + return moderatorIdentitiesList, nil +} + + +// This function returns a map of all enabled (not disabled) moderators with their identity scores +// It will omit any moderators whose scores have not been downloaded or are not sufficient + +//Outputs: +// -map[[16]byte]float64: Identity Hash -> Moderator Score +// -error +func getEnabledModeratorScoresMap(networkType byte)(map[[16]byte]float64, error){ + + enabledModeratorsList, err := enabledModerators.GetEnabledModeratorsList(true, networkType) + if (err != nil) { return nil, err } + + moderatorScoresMap := make(map[[16]byte]float64) + + for _, moderatorIdentityHash := range enabledModeratorsList{ + + scoreIsKnown, moderatorScore, scoreIsSufficient, _, _, err := moderatorScores.GetModeratorIdentityScore(moderatorIdentityHash) + if (err != nil) { return nil, err } + if (scoreIsKnown == false){ + continue + } + if (scoreIsSufficient == false){ + // Moderator has not funded their identity sufficiently. + continue + } + + moderatorScoresMap[moderatorIdentityHash] = moderatorScore + } + + return moderatorScoresMap, nil +} + + diff --git a/internal/moderation/moderatorScores/moderatorScores.go b/internal/moderation/moderatorScores/moderatorScores.go new file mode 100644 index 0000000..1ea632a --- /dev/null +++ b/internal/moderation/moderatorScores/moderatorScores.go @@ -0,0 +1,118 @@ + +// moderatorScores provides functions to retrieve Moderator identity scores. +// A moderator's identity score determines a moderator's rank and ability to ban other moderators. +// It is a measure of how much money has been sent to their identity cryptocurrency address(es), represented as the value in gold at time sent. + +package moderatorScores + +//TODO: Build package +// Moderator scores are constructed from identity deposits. +// Identity deposits are queried from blockchain hosts, or from a locally hosted blockchain node. +// We will use the verifiedAddressDeposits package for cryptocurrencies for which the user is running their own blockchain node +// We will use the trustedAddressDeposits package for cryptocurrencies for which the user is not running their own blockchain node + +// We calculate the score by multiplying each deposit amount by the gold exchange rate at the time of the deposit and summing all gold amounts +// The exchange rates are downloaded from the parameters, so all hosts and moderators will agree on everyone's score + +// Number of reviews describes how many reviews this moderator can create +// This limit prevents moderators from spamming reviews +// It is calculated by multiplying the moderator score by a constant provided by the network parameters +// There should also be an upper limit for all moderators, regardless of their score + +import "seekia/internal/badgerDatabase" +import "seekia/internal/cryptocurrency/cardanoAddress" +import "seekia/internal/cryptocurrency/ethereumAddress" +import "seekia/internal/encoding" +import "seekia/internal/identity" +import "seekia/internal/moderation/trustedAddressDeposits" +import "seekia/internal/moderation/verifiedAddressDeposits" + +import "errors" + +//Outputs: +// -bool: Score is known (Deposits have been downloaded from hosts, and parameters exist) +// -float64: Moderator score +// -bool: Moderator score is sufficient (this is required for reviews to be hosted and to have an effect on the moderation consensus) +// -bool: Moderator is able to ban other moderators +// -int: Number of reviews allowed +// -error +func GetModeratorIdentityScore(identityHash [16]byte)(bool, float64, bool, bool, int, error){ + + isValid, err := identity.VerifyIdentityHash(identityHash, true, "Moderator") + if (err != nil) { return false, 0, false, false, 0, err } + if (isValid == false){ + identityHashHex := encoding.EncodeBytesToHexString(identityHash[:]) + return false, 0, false, false, 0, errors.New("GetModeratorIdentityScore called with invalid identityHash: " + identityHashHex) + } + + //TODO + + return true, 100, true, true, 100, nil +} + + +//Outputs: +// -bool: Local blockchain node is being hoste (all addresses are known) +// -[]string: List of addresses whose address deposits (whose deposits we do not know, or we do know) +// -error +func GetModeratorCryptoDepositAddressesList(cryptocurrency string, knownOrUnknown string)(bool, []string, error){ + + if (cryptocurrency != "Ethereum" && cryptocurrency != "Cardano"){ + return false, nil, errors.New("GetModeratorCryptoAddressesList called with invalid cryptocurrency: " + cryptocurrency) + } + + if (knownOrUnknown != "Known" && knownOrUnknown != "Unknown"){ + return false, nil, errors.New("GetModeratorCryptoAddressesList called with invalid knownOrUnknown: " + knownOrUnknown) + } + + nodeIsHosted, err := verifiedAddressDeposits.CheckIfCryptocurrencyNodeIsBeingHosted(cryptocurrency) + if (err != nil) { return false, nil, err } + if (nodeIsHosted == true){ + return true, nil, nil + } + + allModeratorIdentityHashesList, err := badgerDatabase.GetAllProfileIdentityHashes("Moderator") + if (err != nil) { return false, nil, err } + + depositAddressesList := make([]string, 0) + + for _, moderatorIdentityHash := range allModeratorIdentityHashesList{ + + getModeratorDepositAddress := func()(string, error){ + + if (cryptocurrency == "Ethereum"){ + + depositAddress, err := ethereumAddress.GetIdentityScoreEthereumAddressFromIdentityHash(moderatorIdentityHash) + if (err != nil) { return "", err } + + return depositAddress, nil + } + + // cryptocurrency == "Cardano" + + depositAddress, err := cardanoAddress.GetIdentityScoreCardanoAddressFromIdentityHash(moderatorIdentityHash) + if (err != nil) { return "", err } + + return depositAddress, nil + } + + depositAddress, err := getModeratorDepositAddress() + if (err != nil) { return false, nil, err } + + depositsAreKnown, err := trustedAddressDeposits.CheckIfAddressDepositsAreKnown(cryptocurrency, depositAddress) + if (err != nil) { return false, nil, err } + if (depositsAreKnown == false){ + if (knownOrUnknown == "Unknown"){ + depositAddressesList = append(depositAddressesList, depositAddress) + } + } else { + if (knownOrUnknown == "Known"){ + depositAddressesList = append(depositAddressesList, depositAddress) + } + } + } + + return false, depositAddressesList, nil +} + + diff --git a/internal/moderation/myHiddenContent/myHiddenContent.go b/internal/moderation/myHiddenContent/myHiddenContent.go new file mode 100644 index 0000000..5782e1b --- /dev/null +++ b/internal/moderation/myHiddenContent/myHiddenContent.go @@ -0,0 +1,399 @@ + +// myHiddenContent provides functions to manage a moderator's hidden content +// Hidden content is content that the moderator does not want to review +// An example is content written in a language that the moderator does not understand + +package myHiddenContent + +//TODO: Add job to prune hidden content when we don't need it anymore + +import "seekia/internal/encoding" +import "seekia/internal/helpers" +import "seekia/internal/identity" +import "seekia/internal/myDatastores/myMap" +import "seekia/internal/profiles/readProfiles" + +import "strings" +import "time" +import "errors" + + +// Map Structure: Profile Hash -> Added Time + "$" + Author Identity Hash +var myHiddenProfilesMapObject *myMap.MyMap + +// Map Structure: Attribute Hash -> Added Time + "$" + Author Identity Hash +var myHiddenAttributesMapObject *myMap.MyMap + +// Map Structure: Message Hash -> Added Time + "$" + Author Identity Hash +var myHiddenMessagesMapObject *myMap.MyMap + +func InitializeMyHiddenContentDatastores()error{ + + newMyHiddenProfilesMapObject, err := myMap.CreateNewMap("MyHiddenProfiles") + if (err != nil) { return err } + + newMyHiddenAttributesMapObject, err := myMap.CreateNewMap("MyHiddenAttributes") + if (err != nil) { return err } + + newMyHiddenMessagesMapObject, err := myMap.CreateNewMap("MyHiddenMessages") + if (err != nil) { return err } + + myHiddenProfilesMapObject = newMyHiddenProfilesMapObject + myHiddenAttributesMapObject = newMyHiddenAttributesMapObject + myHiddenMessagesMapObject = newMyHiddenMessagesMapObject + + return nil +} + + +func AddProfileToMyHiddenProfilesMap(profileHash [28]byte, authorIdentityHash [16]byte)error{ + + isValid, err := readProfiles.VerifyProfileHash(profileHash, false, "", false, false) + if (err != nil) { return err } + if (isValid == false){ + profileHashHex := encoding.EncodeBytesToHexString(profileHash[:]) + return errors.New("AddProfileToMyHiddenProfilesMap called with invalid profile hash: " + profileHashHex) + } + + profileHashString := encoding.EncodeBytesToHexString(profileHash[:]) + + authorIdentityHashString, _, err := identity.EncodeIdentityHashBytesToString(authorIdentityHash) + if (err != nil) { + authorIdentityHashHex := encoding.EncodeBytesToHexString(authorIdentityHash[:]) + return errors.New("AddProfileToMyHiddenProfilesMap called with invalid authorIdentityHash: " + authorIdentityHashHex) + } + + addedTime := time.Now().Unix() + addedTimeString := helpers.ConvertInt64ToString(addedTime) + + entryValue := addedTimeString + "$" + authorIdentityHashString + + err = myHiddenProfilesMapObject.SetMapEntry(profileHashString, entryValue) + if (err != nil) { return err } + + return nil +} + + +func DeleteProfileFromMyHiddenProfilesMap(profileHash [28]byte)error{ + + isValid, err := readProfiles.VerifyProfileHash(profileHash, false, "", false, false) + if (err != nil) { return err } + if (isValid == false){ + profileHashHex := encoding.EncodeBytesToHexString(profileHash[:]) + return errors.New("DeleteProfileFromMyHiddenProfilesMap called with invalid profile hash: " + profileHashHex) + } + + profileHashString := encoding.EncodeBytesToHexString(profileHash[:]) + + err = myHiddenProfilesMapObject.DeleteMapEntry(profileHashString) + if (err != nil) { return err } + + return nil +} + +//Outputs: +// -bool: Profile is hidden +// -int64: Profile added time +// -[16]byte: Profile author identity hash +// -error +func CheckIfProfileIsHidden(profileHash [28]byte)(bool, int64, [16]byte, error){ + + isValid, err := readProfiles.VerifyProfileHash(profileHash, false, "", false, false) + if (err != nil) { return false, 0, [16]byte{}, err } + if (isValid == false){ + profileHashHex := encoding.EncodeBytesToHexString(profileHash[:]) + return false, 0, [16]byte{}, errors.New("CheckIfProfileIsHidden called with invalid profile hash: " + profileHashHex) + } + + profileHashHex := encoding.EncodeBytesToHexString(profileHash[:]) + + exists, entryValue, err := myHiddenProfilesMapObject.GetMapEntry(profileHashHex) + if (err != nil) { return false, 0, [16]byte{}, err } + if (exists == false){ + return false, 0, [16]byte{}, nil + } + + addedTimeString, authorIdentityHashString, delimiterFound := strings.Cut(entryValue, "$") + if (delimiterFound == false){ + return false, 0, [16]byte{}, errors.New("myHiddenProfilesMap is malformed: Contains invalid entryValue: " + entryValue) + } + + addedTimeInt64, err := helpers.ConvertStringToInt64(addedTimeString) + if (err != nil){ + return false, 0, [16]byte{}, errors.New("myHiddenProfilesMap is malformed: Contains invalid addedTime: " + addedTimeString) + } + + authorIdentityHash, _, err := identity.ReadIdentityHashString(authorIdentityHashString) + if (err != nil){ + return false, 0, [16]byte{}, errors.New("myHiddenProfilesMap is malformed: Contains invalid authorIdentityHash: " + authorIdentityHashString) + } + + return true, addedTimeInt64, authorIdentityHash, nil +} + + +func GetMyHiddenProfilesMap()(map[[28]byte]int64, error){ + + rawHiddenProfilesMap, err := myHiddenProfilesMapObject.GetMap() + if (err != nil) { return nil, err } + + hiddenProfilesMap := make(map[[28]byte]int64) + + for profileHashString, entryValue := range rawHiddenProfilesMap{ + + profileHashBytes, err := encoding.DecodeHexStringToBytes(profileHashString) + if (err != nil){ + return nil, errors.New("myHiddenProfilesMap is malformed: Contains invalid profileHash: " + profileHashString) + } + + if (len(profileHashBytes) != 28){ + return nil, errors.New("myHiddenProfilesMap is malformed: Contains invalid length profileHash: " + profileHashString) + } + + profileHash := [28]byte(profileHashBytes) + + addedTimeString, _, delimiterFound := strings.Cut(entryValue, "$") + if (delimiterFound == false){ + return nil, errors.New("myHiddenProfilesMap is malformed: Contains invalid entryValue: " + entryValue) + } + + addedTimeInt64, err := helpers.ConvertStringToInt64(addedTimeString) + if (err != nil){ + return nil, errors.New("myHiddenProfilesMap is malformed: Contains invalid addedTime: " + addedTimeString) + } + + hiddenProfilesMap[profileHash] = addedTimeInt64 + } + + return hiddenProfilesMap, nil +} + + + +func AddAttributeToMyHiddenAttributesMap(attributeHash [27]byte, authorIdentityHash [16]byte)error{ + + isValid, err := readProfiles.VerifyAttributeHash(attributeHash, false, "", false, false) + if (err != nil) { return err } + if (isValid == false){ + attributeHashHex := encoding.EncodeBytesToHexString(attributeHash[:]) + return errors.New("AddAttributeToMyHiddenAttributesMap called with invalid attribute hash: " + attributeHashHex) + } + + attributeHashString := encoding.EncodeBytesToHexString(attributeHash[:]) + + authorIdentityHashString, _, err := identity.EncodeIdentityHashBytesToString(authorIdentityHash) + if (err != nil) { + authorIdentityHashHex := encoding.EncodeBytesToHexString(authorIdentityHash[:]) + return errors.New("AddAttributeToMyHiddenAttributesMap called with invalid authorIdentityHash: " + authorIdentityHashHex) + } + + addedTime := time.Now().Unix() + addedTimeString := helpers.ConvertInt64ToString(addedTime) + + entryValue := addedTimeString + "$" + authorIdentityHashString + + err = myHiddenAttributesMapObject.SetMapEntry(attributeHashString, entryValue) + if (err != nil) { return err } + + return nil +} + + +func DeleteAttributeFromMyHiddenAttributesMap(attributeHash [27]byte)error{ + + isValid, err := readProfiles.VerifyAttributeHash(attributeHash, false, "", false, false) + if (err != nil) { return err } + if (isValid == false){ + attributeHashHex := encoding.EncodeBytesToHexString(attributeHash[:]) + return errors.New("DeleteAttributeFromMyHiddenAttributesMap called with invalid attribute hash: " + attributeHashHex) + } + + attributeHashString := encoding.EncodeBytesToHexString(attributeHash[:]) + + err = myHiddenAttributesMapObject.DeleteMapEntry(attributeHashString) + if (err != nil) { return err } + + return nil +} + +//Outputs: +// -bool: Attribute is hidden +// -int64: Attribute added time +// -[16]byte: Attribute author identity hash +// -error +func CheckIfAttributeIsHidden(attributeHash [27]byte)(bool, int64, [16]byte, error){ + + isValid, err := readProfiles.VerifyAttributeHash(attributeHash, false, "", false, false) + if (err != nil) { return false, 0, [16]byte{}, err } + if (isValid == false){ + attributeHashHex := encoding.EncodeBytesToHexString(attributeHash[:]) + return false, 0, [16]byte{}, errors.New("CheckIfAttributeIsHidden called with invalid attribute hash: " + attributeHashHex) + } + + attributeHashHex := encoding.EncodeBytesToHexString(attributeHash[:]) + + exists, entryValue, err := myHiddenAttributesMapObject.GetMapEntry(attributeHashHex) + if (err != nil) { return false, 0, [16]byte{}, err } + if (exists == false){ + return false, 0, [16]byte{}, nil + } + + addedTimeString, authorIdentityHashString, delimiterFound := strings.Cut(entryValue, "$") + if (delimiterFound == false){ + return false, 0, [16]byte{}, errors.New("myHiddenAttributesMap is malformed: Contains invalid entryValue: " + entryValue) + } + + addedTimeInt64, err := helpers.ConvertStringToInt64(addedTimeString) + if (err != nil){ + return false, 0, [16]byte{}, errors.New("myHiddenAttributesMap is malformed: Contains invalid addedTime: " + addedTimeString) + } + + authorIdentityHash, _, err := identity.ReadIdentityHashString(authorIdentityHashString) + if (err != nil){ + return false, 0, [16]byte{}, errors.New("myHiddenAttributesMap is malformed: Contains invalid authorIdentityHash: " + authorIdentityHashString) + } + + return true, addedTimeInt64, authorIdentityHash, nil +} + + +func GetMyHiddenAttributesMap()(map[[27]byte]int64, error){ + + rawHiddenAttributesMap, err := myHiddenAttributesMapObject.GetMap() + if (err != nil) { return nil, err } + + hiddenAttributesMap := make(map[[27]byte]int64) + + for attributeHashString, entryValue := range rawHiddenAttributesMap{ + + attributeHashBytes, err := encoding.DecodeHexStringToBytes(attributeHashString) + if (err != nil){ + return nil, errors.New("myHiddenAttributesMap is malformed: Contains invalid attributeHash: " + attributeHashString) + } + + if (len(attributeHashBytes) != 27){ + return nil, errors.New("myHiddenAttributesMap is malformed: Contains invalid length attributeHash: " + attributeHashString) + } + + attributeHash := [27]byte(attributeHashBytes) + + addedTimeString, _, delimiterFound := strings.Cut(entryValue, "$") + if (delimiterFound == false){ + return nil, errors.New("myHiddenAttributesMap is malformed: Contains invalid entryValue: " + entryValue) + } + + addedTimeInt64, err := helpers.ConvertStringToInt64(addedTimeString) + if (err != nil){ + return nil, errors.New("myHiddenAttributesMap is malformed: Contains invalid addedTime: " + addedTimeString) + } + + hiddenAttributesMap[attributeHash] = addedTimeInt64 + } + + return hiddenAttributesMap, nil +} + +func AddMessageToMyHiddenMessagesMap(messageHash [26]byte, authorIdentityHash [16]byte)error{ + + messageHashString := encoding.EncodeBytesToHexString(messageHash[:]) + + authorIdentityHashString, _, err := identity.EncodeIdentityHashBytesToString(authorIdentityHash) + if (err != nil) { + authorIdentityHashHex := encoding.EncodeBytesToHexString(authorIdentityHash[:]) + return errors.New("AddMessageToMyHiddenMessagesMap called with invalid authorIdentityHash: " + authorIdentityHashHex) + } + + addedTime := time.Now().Unix() + addedTimeString := helpers.ConvertInt64ToString(addedTime) + + entryValue := addedTimeString + "$" + authorIdentityHashString + + err = myHiddenMessagesMapObject.SetMapEntry(messageHashString, entryValue) + if (err != nil) { return err } + + return nil +} + + +func DeleteMessageFromMyHiddenMessagesMap(messageHash [26]byte)error{ + + messageHashString := encoding.EncodeBytesToHexString(messageHash[:]) + + err := myHiddenMessagesMapObject.DeleteMapEntry(messageHashString) + if (err != nil) { return err } + + return nil +} + +//Outputs: +// -bool: Message is hidden +// -int64: Message added time +// -[16]byte: Message author identity hash +// -error +func CheckIfMessageIsHidden(messageHash [26]byte)(bool, int64, [16]byte, error){ + + messageHashHex := encoding.EncodeBytesToHexString(messageHash[:]) + + exists, entryValue, err := myHiddenMessagesMapObject.GetMapEntry(messageHashHex) + if (err != nil) { return false, 0, [16]byte{}, err } + if (exists == false){ + return false, 0, [16]byte{}, nil + } + + addedTimeString, authorIdentityHashString, delimiterFound := strings.Cut(entryValue, "$") + if (delimiterFound == false){ + return false, 0, [16]byte{}, errors.New("myHiddenMessagesMap is malformed: Contains invalid entryValue: " + entryValue) + } + + addedTimeInt64, err := helpers.ConvertStringToInt64(addedTimeString) + if (err != nil){ + return false, 0, [16]byte{}, errors.New("myHiddenMessagesMap is malformed: Contains invalid addedTime: " + addedTimeString) + } + + authorIdentityHash, _, err := identity.ReadIdentityHashString(authorIdentityHashString) + if (err != nil){ + return false, 0, [16]byte{}, errors.New("myHiddenMessagesMap is malformed: Contains invalid authorIdentityHash: " + authorIdentityHashString) + } + + return true, addedTimeInt64, authorIdentityHash, nil +} + + +func GetMyHiddenMessagesMap()(map[[26]byte]int64, error){ + + rawHiddenMessagesMap, err := myHiddenMessagesMapObject.GetMap() + if (err != nil) { return nil, err } + + hiddenMessagesMap := make(map[[26]byte]int64) + + for messageHashString, entryValue := range rawHiddenMessagesMap{ + + messageHashBytes, err := encoding.DecodeHexStringToBytes(messageHashString) + if (err != nil){ + return nil, errors.New("myHiddenMessagesMap is malformed: Contains invalid messageHash: " + messageHashString) + } + + if (len(messageHashBytes) != 26){ + return nil, errors.New("myHiddenMessagesMap is malformed: Contains invalid length messageHash: " + messageHashString) + } + + messageHash := [26]byte(messageHashBytes) + + addedTimeString, _, delimiterFound := strings.Cut(entryValue, "$") + if (delimiterFound == false){ + return nil, errors.New("myHiddenMessagesMap is malformed: Contains invalid entryValue: " + entryValue) + } + + addedTimeInt64, err := helpers.ConvertStringToInt64(addedTimeString) + if (err != nil){ + return nil, errors.New("myHiddenMessagesMap is malformed: Contains invalid addedTime: " + addedTimeString) + } + + hiddenMessagesMap[messageHash] = addedTimeInt64 + } + + return hiddenMessagesMap, nil +} + + diff --git a/internal/moderation/myIdentityScore/myIdentityScore.go b/internal/moderation/myIdentityScore/myIdentityScore.go new file mode 100644 index 0000000..0fe36bf --- /dev/null +++ b/internal/moderation/myIdentityScore/myIdentityScore.go @@ -0,0 +1,71 @@ + +// myIdentityScore provides functions to keep track of a user's moderator identity score + +package myIdentityScore + +import "seekia/internal/cryptocurrency/cardanoAddress" +import "seekia/internal/cryptocurrency/ethereumAddress" +import "seekia/internal/moderation/moderatorScores" +import "seekia/internal/myIdentity" + +import "errors" + +// This function returns the address that moderators can send funds to to increase their identity score +//Outputs: +// -bool: My moderator identity found +// -string: Identity receiving address for provided cryptocurrency +// -error +func GetMyIdentityScoreCryptocurrencyAddress(cryptocurrency string)(bool, string, error){ + + if (cryptocurrency != "Ethereum" && cryptocurrency != "Cardano"){ + return false, "", errors.New("GetMyIdentityScoreCryptocurrencyAddress called with invalid cryptocurrency: " + cryptocurrency) + } + + myIdentityExists, myIdentityHash, err := myIdentity.GetMyIdentityHash("Moderator") + if (err != nil) { return false, "", err } + if (myIdentityExists == false){ + return false, "", nil + } + + if (cryptocurrency == "Ethereum"){ + + address, err := ethereumAddress.GetIdentityScoreEthereumAddressFromIdentityHash(myIdentityHash) + if (err != nil) { return false, "", err } + + return true, address, nil + + } + + // cryptocurrency == "Cardano" + + address, err := cardanoAddress.GetIdentityScoreCardanoAddressFromIdentityHash(myIdentityHash) + if (err != nil) { return false, "", err } + + return true, address, nil +} + +// This function returns our identity score +// It is not retrieving this score from the web. +// The score is calculated from trusted or verified address deposits which we have already downloaded. +//Outputs: +// -bool: My Moderator identity found +// -float64: My Moderator identity score +// -bool: Score is sufficient +// -bool: Score is enough to ban moderators +// -int: Number of allowed reviews +// -error +func GetMyIdentityScore()(bool, float64, bool, bool, int, error){ + + myIdentityExists, myIdentityHash, err := myIdentity.GetMyIdentityHash("Moderator") + if (err != nil) { return false, 0, false, false, 0, err } + if (myIdentityExists == false){ + return false, 0, false, false, 0, nil + } + + identityScoreKnown, moderatorScore, scoreIsSufficient, moderatorCanBanModerators, numberOfReviewsAllowed, err := moderatorScores.GetModeratorIdentityScore(myIdentityHash) + if (err != nil) { return false, 0, false, false, 0, err } + + return identityScoreKnown, moderatorScore, scoreIsSufficient, moderatorCanBanModerators, numberOfReviewsAllowed, nil +} + + diff --git a/internal/moderation/myReviews/myReviews.go b/internal/moderation/myReviews/myReviews.go new file mode 100644 index 0000000..6c2b9a3 --- /dev/null +++ b/internal/moderation/myReviews/myReviews.go @@ -0,0 +1,827 @@ + +// myReviews provides functions to retrieve a user's moderator reviews +// This package retrieves our reviews from the database. +// myBroadcasts keeps a backup of a moderator's reviews that is retained if the database is deleted. + +package myReviews + +import "seekia/internal/badgerDatabase" +import "seekia/internal/contentMetadata" +import "seekia/internal/encoding" +import "seekia/internal/helpers" +import "seekia/internal/identity" +import "seekia/internal/moderation/createReviews" +import "seekia/internal/moderation/readReviews" +import "seekia/internal/myIdentity" +import "seekia/internal/network/myBroadcasts" +import "seekia/internal/profiles/profileStorage" +import "seekia/internal/profiles/readProfiles" + +import "slices" +import "bytes" +import "sync" +import "errors" + + +// This function will create and broadcast a new moderator review +//Outputs: +// -bool: My moderator identity exists +// -error +func CreateAndBroadcastMyReview(reviewMap map[string]string)(bool, error){ + + exists, myIdentityPublicKey, myIdentityPrivateKey, err := myIdentity.GetMyPublicPrivateIdentityKeys("Moderator") + if (err != nil) { return false, err } + if (exists == false){ + return false, nil + } + + newReview, err := createReviews.CreateReview(myIdentityPublicKey, myIdentityPrivateKey, reviewMap) + if (err != nil) { return false, err } + + err = myBroadcasts.BroadcastMyReview(newReview) + if (err != nil) { return false, err } + + ableToRead, _, _, _, _, newReviewType, _, _, _, err := readReviews.ReadReview(false, newReview) + if (err != nil) { return false, err } + if (ableToRead == false){ + return false, errors.New("CreateReview is returning an invalid review.") + } + + err = DeleteMyReviewsListCache(newReviewType) + if (err != nil) { return false, err } + + return true, nil +} + +// This function will return what a user's current verdict is for an identity +//Outputs: +// -bool: My Moderator Identity exists +// -bool: I have banned the identity +// -error +func GetMyNewestIdentityModerationVerdict(identityHash [16]byte, networkType byte)(bool, bool, error){ + + isValid := helpers.VerifyNetworkType(networkType) + if (isValid == false){ + networkTypeString := helpers.ConvertByteToString(networkType) + return false, false, errors.New("GetMyNewestIdentityModerationVerdict called with invalid networkType: " + networkTypeString) + } + + myIdentityExists, myIdentityHash, err := myIdentity.GetMyIdentityHash("Moderator") + if (err != nil) { return false, false, err } + if (myIdentityExists == false){ + return false, false, nil + } + + isValid, err = identity.VerifyIdentityHash(identityHash, false, "") + if (err != nil) { return false, false, err } + if (isValid == false){ + identityHashHex := encoding.EncodeBytesToHexString(identityHash[:]) + return false, false, errors.New("GetMyNewestIdentityModerationVerdict called with invalid identityHash: " + identityHashHex) + } + + myReviewsList, err := getMyReviewsList(myIdentityHash, "Identity") + if (err != nil) { return false, false, err } + + if (len(myReviewsList) == 0){ + // Moderator has no reviews yet + return true, false, nil + } + + reviewExists, _, _, err := readReviews.GetModeratorNewestIdentityReviewFromReviewsList(myReviewsList, myIdentityHash, identityHash, networkType) + if (err != nil){ return false, false, err } + if (reviewExists == true){ + // Review must be a ban review, because Identities can only be banned + return true, true, nil + } + + return true, false, nil +} + + +// This function will return what a user's current verdict is for a profile +// This will optionally check for profile attribute bans which would undo a full profile approval +// It will not check if we have banned the author identity +//Outputs: +// -bool: My Moderator identity exists +// -bool: Profile is disabled +// -bool: Profile metadata exists (needed to determine verdict) +// -bool: I have reviewed the profile (review exists and is not None) +// -string: Verdict ("Ban"/"Approve") +// -error +func GetMyNewestProfileModerationVerdict(profileHash [28]byte, integrateAttributeBans bool)(bool, bool, bool, bool, string, error){ + + myIdentityExists, myIdentityHash, err := myIdentity.GetMyIdentityHash("Moderator") + if (err != nil) { return false, false, false, false, "", err } + if (myIdentityExists == false){ + return false, false, false, false, "", nil + } + + _, profileIsDisabled, err := readProfiles.ReadProfileHashMetadata(profileHash) + if (err != nil){ + profileHashHex := encoding.EncodeBytesToHexString(profileHash[:]) + return false, false, false, false, "", errors.New("GetMyNewestProfileModerationVerdict called with invalid profile hash: " + profileHashHex) + } + + metadataExists, _, profileNetworkType, _, _, profileIsDisabledB, _, attributeHashesMap, err := contentMetadata.GetProfileMetadata(profileHash) + if (err != nil) { return false, false, false, false, "", err } + if (metadataExists == false){ + return true, profileIsDisabled, false, false, "", nil + } + if (profileIsDisabled != profileIsDisabledB){ + return false, false, false, false, "", errors.New("GetProfileMetadata returning different profileIsDisabled status than ReadProfileHashMetadata.") + } + if (profileIsDisabled == true){ + // Profile is disabled. It cannot be reviewed. + return true, true, true, false, "", nil + } + + myProfileReviewsList, err := getMyReviewsList(myIdentityHash, "Profile") + if (err != nil) { return false, false, false, false, "", err } + + fullProfileReviewExists, _, _, fullProfileVerdict, fullProfileVerdictTime, err := readReviews.GetModeratorNewestProfileReviewFromReviewsList(myProfileReviewsList, myIdentityHash, profileHash, profileNetworkType) + if (err != nil){ return false, false, false, false, "", err } + + if (integrateAttributeBans == false){ + if (fullProfileReviewExists == true){ + + return true, false, true, true, fullProfileVerdict, nil + } + + return true, false, true, false, "", nil + } + + if (fullProfileReviewExists == true && fullProfileVerdict == "Ban"){ + // We do not have to check if we have banned any profile attributes, because we have already banned the full profile + return true, false, true, true, "Ban", nil + } + + // Now we see if we have banned any attributes belonging to this profile + + for _, attributeHash := range attributeHashesMap{ + + myIdentityExists, attributeMetadataExists, reviewExists, myVerdict, myVerdictTime, err := GetMyNewestProfileAttributeModerationVerdict(attributeHash, false) + if (err != nil) { return false, false, false, false, "", err } + if (myIdentityExists == false){ + return false, false, false, false, "", errors.New("My identity not found after being found already.") + } + if (attributeMetadataExists == false){ + // We don't have knowledge of profiles belonging to this profile in the attributeProfileHashesList entry in the database + // This should not happen unless those entries were deleted + // We will interpret this the same as no other profiles existing which contain this attribute + continue + } + if (reviewExists == true && myVerdict == "Ban"){ + + if (fullProfileReviewExists == true && myVerdictTime < fullProfileVerdictTime){ + // We have approved the full profile after we banned this attribute + continue + } + // We have banned a profile attribute, thus, we have banned the profile. + + return true, false, true, true, "Ban", nil + } + } + + if (fullProfileReviewExists == true){ + // We could not find any attribute bans that undo the full profile approval + return true, false, true, true, "Approve", nil + } + + return true, false, true, false, "", nil +} + + +// This function will return a user's newest profile attribute verdict +// It will optionally check for full profile approvals, which it considers the same as approving all of the profile's attributes +//Outputs: +// -bool: My Moderator identity exists +// -bool: Attribute metadata exists (this is needed to determine verdict) +// -bool: I have reviewed the attribute (review exists and is not None) +// -string: Verdict ("Ban"/"Approve") +// -int64: Time of verdict +// -error +func GetMyNewestProfileAttributeModerationVerdict(attributeHash [27]byte, integrateFullProfileApprovals bool)(bool, bool, bool, string, int64, error){ + + myIdentityExists, myIdentityHash, err := myIdentity.GetMyIdentityHash("Moderator") + if (err != nil) { return false, false, false, "", 0, err } + if (myIdentityExists == false){ + return false, false, false, "", 0, nil + } + + isValid, err := readProfiles.VerifyAttributeHash(attributeHash, false, "", false, false) + if (err != nil) { return false, false, false, "", 0, err } + if (isValid == false){ + attributeHashHex := encoding.EncodeBytesToHexString(attributeHash[:]) + return false, false, false, "", 0, errors.New("GetMyNewestProfileAttributeModerationVerdict called with invalid attributeHash: " + attributeHashHex) + } + + attributeMetadataExists, _, _, attributeNetworkType, attributeProfileHashesList, err := profileStorage.GetProfileAttributeMetadata(attributeHash) + if (err != nil) { return false, false, false, "", 0, err } + if (attributeMetadataExists == false){ + // We cannot determine verdict. + return true, false, false, "", 0, nil + } + + myAttributeReviewsList, err := getMyReviewsList(myIdentityHash, "Attribute") + if (err != nil) { return false, false, false, "", 0, err } + + attributeReviewExists, _, _, attributeReviewVerdict, attributeReviewTime, err := readReviews.GetModeratorNewestProfileAttributeReviewFromReviewsList(myAttributeReviewsList, myIdentityHash, attributeHash, attributeNetworkType) + if (err != nil){ return false, false, false, "", 0, err } + + if (integrateFullProfileApprovals == false){ + + if (attributeReviewExists == false){ + return true, true, false, "", 0, nil + } + + return true, true, true, attributeReviewVerdict, attributeReviewTime, nil + } + + if (attributeReviewExists == true && attributeReviewVerdict == "Approve"){ + + // We approved the attribute + // We don't have to check for full profile approvals, because they would not change this verdict + + return true, true, true, "Approve", attributeReviewTime, nil + } + + // Now we check for full profile approvals + // A full profile approval is equivalent to an attribute approval for all attributes within the profile + // attributeProfileHashesList is a list of all profile hashes for profiles which contain the attribute + + myProfileReviewsList, err := getMyReviewsList(myIdentityHash, "Profile") + if (err != nil) { return false, false, false, "", 0, err } + + for _, profileHash := range attributeProfileHashesList{ + + // We are only checking for full profile approvals + // We don't need to check for profile attribute bans, because we are only concerned with the provided attribute's status + // A full profile approval approves all attributes. + // If we ban a full profile, it does not change the status of any of our attribute approvals of the profile + + fullProfileReviewExists, _, _, fullProfileVerdict, fullProfileVerdictTime, err := readReviews.GetModeratorNewestProfileReviewFromReviewsList(myProfileReviewsList, myIdentityHash, profileHash, attributeNetworkType) + if (err != nil){ return false, false, false, "", 0, err } + if (fullProfileReviewExists == true && fullProfileVerdict == "Approve"){ + + if (attributeReviewExists == true && fullProfileVerdictTime < attributeReviewTime){ + // We banned the attribute after approving the full profile + // The attribute ban is therefore disregarded. + continue + } + // We have approved the full profile after banning the attribute, thus, the attribute is approved + + return true, true, true, "Approve", fullProfileVerdictTime, nil + } + } + + if (attributeReviewExists == true){ + // We could not find any full profile approvals that undo the attribute ban + return true, true, true, "Ban", attributeReviewTime, nil + } + + return true, true, false, "", 0, nil +} + + +// This function will return what a user's current verdict is for a message +// -bool: My Moderator Identity exists +// -bool: Message metadata exists (this is needed to determine our verdict) +// -bool: I have reviewed the message (review exists and is not None) +// -string: Verdict ("Ban"/"Approve") +// -error +func GetMyNewestMessageModerationVerdict(messageHash [26]byte)(bool, bool, bool, string, error){ + + myIdentityExists, myIdentityHash, err := myIdentity.GetMyIdentityHash("Moderator") + if (err != nil) { return false, false, false, "", err } + if (myIdentityExists == false){ + return false, false, false, "", nil + } + + messageMetadataExists, _, messageNetworkType, _, _, messageCipherKeyHash, err := contentMetadata.GetMessageMetadata(messageHash) + if (err != nil){ return false, false, false, "", err } + if (messageMetadataExists == false){ + // We cannot determine our verdict without message metadata + return true, false, false, "", nil + } + + myReviewsList, err := getMyReviewsList(myIdentityHash, "Message") + if (err != nil) { return false, false, false, "", err } + + if (len(myReviewsList) == 0){ + // Moderator has no reviews yet + return true, true, false, "", nil + } + + reviewExists, _, _, reviewVerdict, err := readReviews.GetModeratorNewestMessageReviewFromReviewsList(myReviewsList, myIdentityHash, messageHash, messageNetworkType, messageCipherKeyHash) + if (err != nil){ return false, false, false, "", err } + if (reviewExists == false){ + return true, true, false, "", nil + } + + return true, true, true, reviewVerdict, nil +} + +// This function returns a list of all the identity hashes that the user has banned +//Outputs: +// -bool: My Moderator identity exists +// -[][16]byte: List of identity hashes user has banned +// -error +func GetMyBannedIdentityHashesList(networkType byte)(bool, [][16]byte, error){ + + isValid := helpers.VerifyNetworkType(networkType) + if (isValid == false){ + networkTypeString := helpers.ConvertByteToString(networkType) + return false, nil, errors.New("GetMyBannedIdentityHashesList called with invalid networkType: " + networkTypeString) + } + + myIdentityExists, myIdentityHash, err := myIdentity.GetMyIdentityHash("Moderator") + if (err != nil) { return false, nil, err } + if (myIdentityExists == false){ + return false, nil, nil + } + + myIdentityReviewsMap, err := getMyNewestReviewsMap(myIdentityHash, "Identity", networkType) + if (err != nil) { return false, nil, err } + + bannedIdentityHashesList := make([][16]byte, 0, len(myIdentityReviewsMap)) + + for reviewedIdentityHashString, _ := range myIdentityReviewsMap{ + + reviewedIdentityHashBytes := []byte(reviewedIdentityHashString) + + if (len(reviewedIdentityHashBytes) != 16){ + reviewedIdentityHashHex := encoding.EncodeBytesToHexString(reviewedIdentityHashBytes) + return false, nil, errors.New("getMyNewestReviewsMap returning invalid reviewedIdentityHash: " + reviewedIdentityHashHex) + } + + reviewedIdentityHash := [16]byte(reviewedIdentityHashBytes) + + bannedIdentityHashesList = append(bannedIdentityHashesList, reviewedIdentityHash) + } + + return true, bannedIdentityHashesList, nil +} + + +//Outputs: +// -bool: My Moderator identity exists +// -map[[28]byte]string: Map of profile hashes user has reviewed (Profile hash -> "Approve"/"Ban") +// -error +func GetMyReviewedProfileHashesMap(profileType string, networkType byte)(bool, map[[28]byte]string, error){ + + if (profileType != "Mate" && profileType != "Host" && profileType != "Moderator"){ + return false, nil, errors.New("GetMyReviewedProfileHashesMap called with invalid profile type: " + profileType) + } + + isValid := helpers.VerifyNetworkType(networkType) + if (isValid == false){ + networkTypeString := helpers.ConvertByteToString(networkType) + return false, nil, errors.New("GetMyReviewedProfileHashesMap called with invalid networkType: " + networkTypeString) + } + + myIdentityExists, _, err := myIdentity.GetMyIdentityHash("Moderator") + if (err != nil) { return false, nil, err } + if (myIdentityExists == false){ + return false, nil, nil + } + + allProfileHashesList, err := badgerDatabase.GetAllProfileHashes(profileType) + if (err != nil) { return false, nil, err } + + //Map structure: Profile hash -> My Verdict + myReviewedProfileHashesMap := make(map[[28]byte]string) + + for _, profileHash := range allProfileHashesList{ + + myIdentityExists, profileIsDisabled, profileMetadataExists, iHaveReviewed, myVerdict, err := GetMyNewestProfileModerationVerdict(profileHash, true) + if (err != nil) { return false, nil, err } + if (myIdentityExists == false){ + return false, nil, errors.New("My identity not found after being found already.") + } + if (profileIsDisabled == true){ + continue + } + if (profileMetadataExists == false){ + continue + } + + if (iHaveReviewed == true){ + myReviewedProfileHashesMap[profileHash] = myVerdict + } + } + + return true, myReviewedProfileHashesMap, nil +} + +//Outputs: +// -bool: My Moderator identity exists +// -map[[27]byte]string: Map Structure: Attribute Hash -> Verdict +// -Map of profile attribute hashes user has reviewed (approved or banned) +// -error +func GetMyReviewedProfileAttributeHashesMap(networkType byte)(bool, map[[27]byte]string, error){ + + isValid := helpers.VerifyNetworkType(networkType) + if (isValid == false){ + networkTypeString := helpers.ConvertByteToString(networkType) + return false, nil, errors.New("GetMyReviewedProfileAttributeHashesMap called with invalid networkType: " + networkTypeString) + } + + myIdentityExists, _, err := myIdentity.GetMyIdentityHash("Moderator") + if (err != nil) { return false, nil, err } + if (myIdentityExists == false){ + return false, nil, nil + } + + allProfileAttributeHashesList, err := badgerDatabase.GetAllReviewedProfileAttributeHashes() + if (err != nil) { return false, nil, err } + + //Map structure: Attribute hash -> My verdict + myAttributeVerdictsMap := make(map[[27]byte]string) + + for _, attributeHash := range allProfileAttributeHashesList{ + + myIdentityExists, attributeMetadataExists, myReviewExists, myVerdict, _, err := GetMyNewestProfileAttributeModerationVerdict(attributeHash, true) + if (err != nil) { return false, nil, err } + if (myIdentityExists == false){ + return false, nil, errors.New("My moderator identity not found after being found already.") + } + if (attributeMetadataExists == false){ + // We cannot determine our verdict for this attribute + continue + } + if (myReviewExists == true){ + myAttributeVerdictsMap[attributeHash] = myVerdict + } + } + + return true, myAttributeVerdictsMap, nil +} + + +//Outputs: +// -bool: My Moderator identity exists +// -[][26]byte: List of message hashes user has reviewed (approved or banned) +// -error +func GetMyReviewedMessageHashesList(networkType byte)(bool, [][26]byte, error){ + + isValid := helpers.VerifyNetworkType(networkType) + if (isValid == false){ + networkTypeString := helpers.ConvertByteToString(networkType) + return false, nil, errors.New("GetMyReviewedMessageHashesList called with invalid networkType: " + networkTypeString) + } + + myIdentityExists, myIdentityHash, err := myIdentity.GetMyIdentityHash("Moderator") + if (err != nil) { return false, nil, err } + if (myIdentityExists == false){ + return false, nil, nil + } + + myMessageReviewsMap, err := getMyNewestReviewsMap(myIdentityHash, "Message", networkType) + if (err != nil) { return false, nil, err } + + myReviewedMessageHashesList := make([][26]byte, 0, len(myMessageReviewsMap)) + + for messageHashString, reviewBytes := range myMessageReviewsMap{ + + messageHashBytes := []byte(messageHashString) + + if (len(messageHashBytes) != 26){ + messageHashHex := encoding.EncodeBytesToHexString(messageHashBytes) + return false, nil, errors.New("getMyNewestReviewsMap returning reviewsMap with invalid messageHash map key: " + messageHashHex) + } + + messageHashArray := [26]byte(messageHashBytes) + + ableToRead, _, reviewNetworkType, reviewerIdentityHash, _, reviewType, reviewedHash, _, _, err := readReviews.ReadReview(false, reviewBytes) + if (err != nil) { return false, nil, err } + if (ableToRead == false){ + return false, nil, errors.New("getMyNewestReviewsMap returning invalid review.") + } + if (reviewNetworkType != networkType){ + return false, nil, errors.New("getMyNewestReviewsMap returning review of different networkType.") + } + + if (reviewerIdentityHash != myIdentityHash){ + return false, nil, errors.New("getMyNewestReviewsMap list contains review by different reviewer") + } + + if (reviewType != "Message"){ + return false, nil, errors.New("getMyNewestReviewsMap returns different reviewType review: " + reviewType) + } + + areEqual := bytes.Equal(reviewedHash, messageHashBytes) + if (areEqual == false){ + return false, nil, errors.New("getMyNewestReviewsMap returns key with different review reviewedHash") + } + + myReviewedMessageHashesList = append(myReviewedMessageHashesList, messageHashArray) + } + + return true, myReviewedMessageHashesList, nil +} + + +//Outputs: +// -bool: User moderator identity found +// -[][]byte: List of all reviews created by user, sorted from newest to oldest (Reviews with None verdict not included) +// -error +func GetMyNewestReviewsListSorted(reviewType string, networkType byte)(bool, [][]byte, error){ + + if (reviewType != "Identity" && reviewType != "Profile" && reviewType != "Attribute" && reviewType != "Message"){ + return false, nil, errors.New("GetMyNewestReviewsListSorted called with invalid reviewType: " + reviewType) + } + + isValid := helpers.VerifyNetworkType(networkType) + if (isValid == false){ + networkTypeString := helpers.ConvertByteToString(networkType) + return false, nil, errors.New("GetMyNewestReviewsListSorted called with invalid networkType: " + networkTypeString) + } + + myIdentityExists, myIdentityHash, err := myIdentity.GetMyIdentityHash("Moderator") + if (err != nil) { return false, nil, err } + if (myIdentityExists == false){ + return false, nil, nil + } + + myNewestReviewsMap, err := getMyNewestReviewsMap(myIdentityHash, reviewType, networkType) + if (err != nil) { return false, nil, err } + + type ReviewObject struct{ + ReviewBroadcastTime int64 + ReviewBytes []byte + } + + newestReviewObjectsList := make([]ReviewObject, 0, len(myNewestReviewsMap)) + + for reviewedHashString, reviewBytes := range myNewestReviewsMap{ + + reviewedHashBytes := []byte(reviewedHashString) + + ableToRead, _, reviewNetworkType, reviewAuthorIdentityHash, reviewBroadcastTime, currentReviewType, currentReviewedHash, _, _, err := readReviews.ReadReview(false, reviewBytes) + if (err != nil) { return false, nil, err } + if (ableToRead == false){ + return false, nil, errors.New("getMyNewestReviewsMap returning invalid review") + } + if (reviewNetworkType != networkType){ + return false, nil, errors.New("getMyNewestReviewsMap returning review for different networkType.") + } + if (reviewAuthorIdentityHash != myIdentityHash){ + return false, nil, errors.New("getMyNewestReviewsMap returning review by different author.") + } + if (currentReviewType != reviewType){ + return false, nil, errors.New("getMyNewestReviewsMap returning review of a different reviewType.") + } + areEqual := bytes.Equal(reviewedHashBytes, currentReviewedHash) + if (areEqual == false){ + return false, nil, errors.New("getMyNewestReviewsMap returning map where reviewedHash key does not match reviewBytes's.") + } + + reviewObject := ReviewObject{ + ReviewBroadcastTime: reviewBroadcastTime, + ReviewBytes: reviewBytes, + } + + newestReviewObjectsList = append(newestReviewObjectsList, reviewObject) + } + + compareReviewsFunction := func(reviewA ReviewObject, reviewB ReviewObject)int{ + + reviewABroadcastTime := reviewA.ReviewBroadcastTime + reviewBBroadcastTime := reviewB.ReviewBroadcastTime + + if (reviewABroadcastTime == reviewBBroadcastTime){ + return 0 + } + + if (reviewABroadcastTime < reviewBBroadcastTime){ + return 1 + } + + return -1 + } + + slices.SortFunc(newestReviewObjectsList, compareReviewsFunction) + + newestReviewsList := make([][]byte, 0, len(newestReviewObjectsList)) + + for _, reviewObject := range newestReviewObjectsList{ + + reviewBytes := reviewObject.ReviewBytes + + newestReviewsList = append(newestReviewsList, reviewBytes) + } + + return true, newestReviewsList, nil +} + +//Outputs: +// -map[string]string: Reviewed Hash -> Newest Review Bytes +// -error +func getMyNewestReviewsMap(myIdentityHash [16]byte, reviewType string, networkType byte)(map[string][]byte, error){ + + if (reviewType != "Identity" && reviewType != "Profile" && reviewType != "Attribute" && reviewType != "Message"){ + return nil, errors.New("getMyNewestReviewsMap called with invalid reviewType: " + reviewType) + } + + reviewsList, err := getMyReviewsList(myIdentityHash, reviewType) + if (err != nil) { return nil, err } + + newestReviewsMap, err := readReviews.GetNewestModeratorReviewsMapFromReviewsList(reviewsList, myIdentityHash, networkType, true, reviewType) + if (err != nil) { return nil, err } + + return newestReviewsMap, nil +} + + +// We read our reviews into memory so we don't have to retrieve them from the database each time + +var myIdentityReviewsList []readReviews.ReviewWithHash +var myIdentityReviewsListMutex sync.RWMutex + +var myProfileReviewsList []readReviews.ReviewWithHash +var myProfileReviewsListMutex sync.RWMutex + +var myAttributeReviewsList []readReviews.ReviewWithHash +var myAttributeReviewsListMutex sync.RWMutex + +var myMessageReviewsList []readReviews.ReviewWithHash +var myMessageReviewsListMutex sync.RWMutex + +// We call this function whenever we add a new review to the database +func DeleteMyReviewsListCache(reviewType string)error{ + + if (reviewType == "Identity"){ + + myIdentityReviewsListMutex.Lock() + myIdentityReviewsList = nil + myIdentityReviewsListMutex.Unlock() + + return nil + + } else if (reviewType == "Profile"){ + + myProfileReviewsListMutex.Lock() + myProfileReviewsList = nil + myProfileReviewsListMutex.Unlock() + + return nil + + } else if (reviewType == "Attribute"){ + + myAttributeReviewsListMutex.Lock() + myAttributeReviewsList = nil + myAttributeReviewsListMutex.Unlock() + + return nil + + } else if (reviewType == "Message"){ + + myMessageReviewsListMutex.Lock() + myMessageReviewsList = nil + myMessageReviewsListMutex.Unlock() + + return nil + } + + return errors.New("DeleteMyReviewsListCache called with invalid reviewType: " + reviewType) +} + + +// This function returns our reviews for all networkTypes. +// It does not copy each review object's review bytes, so returned review bytes should never be edited +func getMyReviewsList(myIdentityHash [16]byte, reviewType string)([]readReviews.ReviewWithHash, error){ + + // We first attempt to return the list stored in memory + + if (reviewType == "Identity"){ + + myIdentityReviewsListMutex.RLock() + if (myIdentityReviewsList != nil){ + + listCopy := slices.Clone(myIdentityReviewsList) + myIdentityReviewsListMutex.RUnlock() + + return listCopy, nil + } + myIdentityReviewsListMutex.RUnlock() + + } else if (reviewType == "Profile"){ + + myProfileReviewsListMutex.RLock() + if (myProfileReviewsList != nil){ + + listCopy := slices.Clone(myProfileReviewsList) + myProfileReviewsListMutex.RUnlock() + + return listCopy, nil + } + myProfileReviewsListMutex.RUnlock() + + } else if (reviewType == "Attribute"){ + + myAttributeReviewsListMutex.RLock() + if (myAttributeReviewsList != nil){ + + listCopy := slices.Clone(myAttributeReviewsList) + myAttributeReviewsListMutex.RUnlock() + + return listCopy, nil + } + myAttributeReviewsListMutex.RUnlock() + + } else if (reviewType == "Message"){ + + myMessageReviewsListMutex.RLock() + if (myMessageReviewsList != nil){ + + listCopy := slices.Clone(myMessageReviewsList) + myMessageReviewsListMutex.RUnlock() + + return listCopy, nil + } + myMessageReviewsListMutex.RUnlock() + } else { + return nil, errors.New("getMyReviewsList called with invalid reviewType: " + reviewType) + } + + // The list does not exist in memory + // We will create a new list + + exists, myReviewHashesList, err := badgerDatabase.GetReviewerReviewHashesList(myIdentityHash, reviewType) + if (err != nil) { return nil, err } + if (exists == false){ + emptyList := make([]readReviews.ReviewWithHash, 0) + return emptyList, nil + } + + newReviewsList := make([]readReviews.ReviewWithHash, 0) + + for _, reviewHash := range myReviewHashesList{ + + exists, reviewBytes, err := badgerDatabase.GetReview(reviewHash) + if (err != nil) { return nil, err } + if (exists == false) { + // Reviewer reviews list is outdated, will be updated automatically + continue + } + + reviewObject := readReviews.ReviewWithHash{ + ReviewHash: reviewHash, + ReviewBytes: reviewBytes, + } + + newReviewsList = append(newReviewsList, reviewObject) + } + + if (reviewType == "Identity"){ + + myIdentityReviewsListMutex.Lock() + + myIdentityReviewsList = newReviewsList + + listCopy := slices.Clone(myIdentityReviewsList) + + myIdentityReviewsListMutex.Unlock() + + return listCopy, nil + + } else if (reviewType == "Profile"){ + + myProfileReviewsListMutex.Lock() + + myProfileReviewsList = newReviewsList + + listCopy := slices.Clone(myProfileReviewsList) + + myProfileReviewsListMutex.Unlock() + + return listCopy, nil + + } else if (reviewType == "Attribute"){ + + myAttributeReviewsListMutex.Lock() + + myAttributeReviewsList = newReviewsList + + listCopy := slices.Clone(myAttributeReviewsList) + + myAttributeReviewsListMutex.Unlock() + + return listCopy, nil + } + // reviewType == "Message"){ + + myMessageReviewsListMutex.Lock() + + myMessageReviewsList = newReviewsList + + listCopy := slices.Clone(myMessageReviewsList) + + myMessageReviewsListMutex.Unlock() + + return listCopy, nil +} + + + + diff --git a/internal/moderation/mySkippedContent/mySkippedContent.go b/internal/moderation/mySkippedContent/mySkippedContent.go new file mode 100644 index 0000000..845a1e9 --- /dev/null +++ b/internal/moderation/mySkippedContent/mySkippedContent.go @@ -0,0 +1,400 @@ + +// mySkippedContent provides functions to manage a moderator's skipped content +// Skipped content is content that the moderator has skipped while reviewing content +// Skipped content will be moved to the end of the myUnreviewed queue + +package mySkippedContent + +//TODO: Add job to prune skipped content when we don't need it anymore + +import "seekia/internal/encoding" +import "seekia/internal/helpers" +import "seekia/internal/identity" +import "seekia/internal/myDatastores/myMap" +import "seekia/internal/profiles/readProfiles" + +import "errors" +import "strings" +import "time" + +// Map Structure: Profile Hash -> Added Time + "$" + Author Identity Hash +var mySkippedProfilesMapObject *myMap.MyMap + +// Map Structure: Attribute Hash -> Added Time + "$" + Author Identity Hash +var mySkippedAttributesMapObject *myMap.MyMap + +// Map Structure: Message Hash -> Added Time + "$" + Author Identity Hash +var mySkippedMessagesMapObject *myMap.MyMap + +func InitializeMySkippedContentDatastores()error{ + + newMySkippedProfilesMapObject, err := myMap.CreateNewMap("MySkippedProfiles") + if (err != nil) { return err } + + newMySkippedAttributesMapObject, err := myMap.CreateNewMap("MySkippedAttributes") + if (err != nil) { return err } + + newMySkippedMessagesMapObject, err := myMap.CreateNewMap("MySkippedMessages") + if (err != nil) { return err } + + mySkippedProfilesMapObject = newMySkippedProfilesMapObject + mySkippedAttributesMapObject = newMySkippedAttributesMapObject + mySkippedMessagesMapObject = newMySkippedMessagesMapObject + + return nil +} + + +func AddProfileToMySkippedProfilesMap(profileHash [28]byte, authorIdentityHash [16]byte)error{ + + isValid, err := readProfiles.VerifyProfileHash(profileHash, false, "", false, false) + if (err != nil) { return err } + if (isValid == false){ + profileHashHex := encoding.EncodeBytesToHexString(profileHash[:]) + return errors.New("AddProfileToMySkippedProfilesMap called with invalid profile hash: " + profileHashHex) + } + + profileHashString := encoding.EncodeBytesToHexString(profileHash[:]) + + authorIdentityHashString, _, err := identity.EncodeIdentityHashBytesToString(authorIdentityHash) + if (err != nil) { + authorIdentityHashHex := encoding.EncodeBytesToHexString(authorIdentityHash[:]) + return errors.New("AddProfileToMySkippedProfilesMap called with invalid authorIdentityHash: " + authorIdentityHashHex) + } + + addedTime := time.Now().Unix() + addedTimeString := helpers.ConvertInt64ToString(addedTime) + + entryValue := addedTimeString + "$" + authorIdentityHashString + + err = mySkippedProfilesMapObject.SetMapEntry(profileHashString, entryValue) + if (err != nil) { return err } + + return nil +} + + +func DeleteProfileFromMySkippedProfilesMap(profileHash [28]byte)error{ + + isValid, err := readProfiles.VerifyProfileHash(profileHash, false, "", false, false) + if (err != nil) { return err } + if (isValid == false){ + profileHashHex := encoding.EncodeBytesToHexString(profileHash[:]) + return errors.New("DeleteProfileFromMySkippedProfilesMap called with invalid profile hash: " + profileHashHex) + } + + profileHashString := encoding.EncodeBytesToHexString(profileHash[:]) + + err = mySkippedProfilesMapObject.DeleteMapEntry(profileHashString) + if (err != nil) { return err } + + return nil +} + +//Outputs: +// -bool: Profile is skipped +// -int64: Profile added time +// -[16]byte: Profile author identity hash +// -error +func CheckIfProfileIsSkipped(profileHash [28]byte)(bool, int64, [16]byte, error){ + + isValid, err := readProfiles.VerifyProfileHash(profileHash, false, "", false, false) + if (err != nil) { return false, 0, [16]byte{}, err } + if (isValid == false){ + profileHashHex := encoding.EncodeBytesToHexString(profileHash[:]) + return false, 0, [16]byte{}, errors.New("CheckIfProfileIsSkipped called with invalid profile hash: " + profileHashHex) + } + + profileHashHex := encoding.EncodeBytesToHexString(profileHash[:]) + + exists, entryValue, err := mySkippedProfilesMapObject.GetMapEntry(profileHashHex) + if (err != nil) { return false, 0, [16]byte{}, err } + if (exists == false){ + return false, 0, [16]byte{}, nil + } + + addedTimeString, authorIdentityHashString, delimiterFound := strings.Cut(entryValue, "$") + if (delimiterFound == false){ + return false, 0, [16]byte{}, errors.New("mySkippedProfilesMap is malformed: Contains invalid entryValue: " + entryValue) + } + + addedTimeInt64, err := helpers.ConvertStringToInt64(addedTimeString) + if (err != nil){ + return false, 0, [16]byte{}, errors.New("mySkippedProfilesMap is malformed: Contains invalid addedTime: " + addedTimeString) + } + + authorIdentityHash, _, err := identity.ReadIdentityHashString(authorIdentityHashString) + if (err != nil){ + return false, 0, [16]byte{}, errors.New("mySkippedProfilesMap is malformed: Contains invalid authorIdentityHash: " + authorIdentityHashString) + } + + return true, addedTimeInt64, authorIdentityHash, nil +} + + +func GetMySkippedProfilesMap()(map[[28]byte]int64, error){ + + rawSkippedProfilesMap, err := mySkippedProfilesMapObject.GetMap() + if (err != nil) { return nil, err } + + skippedProfilesMap := make(map[[28]byte]int64) + + for profileHashString, entryValue := range rawSkippedProfilesMap{ + + profileHashBytes, err := encoding.DecodeHexStringToBytes(profileHashString) + if (err != nil){ + return nil, errors.New("mySkippedProfilesMap is malformed: Contains invalid profileHash: " + profileHashString) + } + + if (len(profileHashBytes) != 28){ + return nil, errors.New("mySkippedProfilesMap is malformed: Contains invalid length profileHash: " + profileHashString) + } + + profileHash := [28]byte(profileHashBytes) + + addedTimeString, _, delimiterFound := strings.Cut(entryValue, "$") + if (delimiterFound == false){ + return nil, errors.New("mySkippedProfilesMap is malformed: Contains invalid entryValue: " + entryValue) + } + + addedTimeInt64, err := helpers.ConvertStringToInt64(addedTimeString) + if (err != nil){ + return nil, errors.New("mySkippedProfilesMap is malformed: Contains invalid addedTime: " + addedTimeString) + } + + skippedProfilesMap[profileHash] = addedTimeInt64 + } + + return skippedProfilesMap, nil +} + + + +func AddAttributeToMySkippedAttributesMap(attributeHash [27]byte, authorIdentityHash [16]byte)error{ + + isValid, err := readProfiles.VerifyAttributeHash(attributeHash, false, "", false, false) + if (err != nil) { return err } + if (isValid == false){ + attributeHashHex := encoding.EncodeBytesToHexString(attributeHash[:]) + return errors.New("AddAttributeToMySkippedAttributesMap called with invalid attribute hash: " + attributeHashHex) + } + + attributeHashString := encoding.EncodeBytesToHexString(attributeHash[:]) + + authorIdentityHashString, _, err := identity.EncodeIdentityHashBytesToString(authorIdentityHash) + if (err != nil) { + authorIdentityHashHex := encoding.EncodeBytesToHexString(authorIdentityHash[:]) + return errors.New("AddAttributeToMySkippedAttributesMap called with invalid authorIdentityHash: " + authorIdentityHashHex) + } + + addedTime := time.Now().Unix() + addedTimeString := helpers.ConvertInt64ToString(addedTime) + + entryValue := addedTimeString + "$" + authorIdentityHashString + + err = mySkippedAttributesMapObject.SetMapEntry(attributeHashString, entryValue) + if (err != nil) { return err } + + return nil +} + + +func DeleteAttributeFromMySkippedAttributesMap(attributeHash [27]byte)error{ + + isValid, err := readProfiles.VerifyAttributeHash(attributeHash, false, "", false, false) + if (err != nil) { return err } + if (isValid == false){ + attributeHashHex := encoding.EncodeBytesToHexString(attributeHash[:]) + return errors.New("DeleteAttributeFromMySkippedAttributesMap called with invalid attribute hash: " + attributeHashHex) + } + + attributeHashString := encoding.EncodeBytesToHexString(attributeHash[:]) + + err = mySkippedAttributesMapObject.DeleteMapEntry(attributeHashString) + if (err != nil) { return err } + + return nil +} + +//Outputs: +// -bool: Attribute is skipped +// -int64: Attribute added time +// -[16]byte: Attribute author identity hash +// -error +func CheckIfAttributeIsSkipped(attributeHash [27]byte)(bool, int64, [16]byte, error){ + + isValid, err := readProfiles.VerifyAttributeHash(attributeHash, false, "", false, false) + if (err != nil) { return false, 0, [16]byte{}, err } + if (isValid == false){ + attributeHashHex := encoding.EncodeBytesToHexString(attributeHash[:]) + return false, 0, [16]byte{}, errors.New("CheckIfAttributeIsSkipped called with invalid attribute hash: " + attributeHashHex) + } + + attributeHashHex := encoding.EncodeBytesToHexString(attributeHash[:]) + + exists, entryValue, err := mySkippedAttributesMapObject.GetMapEntry(attributeHashHex) + if (err != nil) { return false, 0, [16]byte{}, err } + if (exists == false){ + return false, 0, [16]byte{}, nil + } + + addedTimeString, authorIdentityHashString, delimiterFound := strings.Cut(entryValue, "$") + if (delimiterFound == false){ + return false, 0, [16]byte{}, errors.New("mySkippedAttributesMap is malformed: Contains invalid entryValue: " + entryValue) + } + + addedTimeInt64, err := helpers.ConvertStringToInt64(addedTimeString) + if (err != nil){ + return false, 0, [16]byte{}, errors.New("mySkippedAttributesMap is malformed: Contains invalid addedTime: " + addedTimeString) + } + + authorIdentityHash, _, err := identity.ReadIdentityHashString(authorIdentityHashString) + if (err != nil){ + return false, 0, [16]byte{}, errors.New("mySkippedAttributesMap is malformed: Contains invalid authorIdentityHash: " + authorIdentityHashString) + } + + return true, addedTimeInt64, authorIdentityHash, nil +} + + +func GetMySkippedAttributesMap()(map[[27]byte]int64, error){ + + rawSkippedAttributesMap, err := mySkippedAttributesMapObject.GetMap() + if (err != nil) { return nil, err } + + skippedAttributesMap := make(map[[27]byte]int64) + + for attributeHashString, entryValue := range rawSkippedAttributesMap{ + + attributeHashBytes, err := encoding.DecodeHexStringToBytes(attributeHashString) + if (err != nil){ + return nil, errors.New("mySkippedAttributesMap is malformed: Contains invalid attributeHash: " + attributeHashString) + } + + if (len(attributeHashBytes) != 27){ + return nil, errors.New("mySkippedAttributesMap is malformed: Contains invalid length attributeHash: " + attributeHashString) + } + + attributeHash := [27]byte(attributeHashBytes) + + addedTimeString, _, delimiterFound := strings.Cut(entryValue, "$") + if (delimiterFound == false){ + return nil, errors.New("mySkippedAttributesMap is malformed: Contains invalid entryValue: " + entryValue) + } + + addedTimeInt64, err := helpers.ConvertStringToInt64(addedTimeString) + if (err != nil){ + return nil, errors.New("mySkippedAttributesMap is malformed: Contains invalid addedTime: " + addedTimeString) + } + + skippedAttributesMap[attributeHash] = addedTimeInt64 + } + + return skippedAttributesMap, nil +} + +func AddMessageToMySkippedMessagesMap(messageHash [26]byte, authorIdentityHash [16]byte)error{ + + messageHashString := encoding.EncodeBytesToHexString(messageHash[:]) + + authorIdentityHashString, _, err := identity.EncodeIdentityHashBytesToString(authorIdentityHash) + if (err != nil) { + authorIdentityHashHex := encoding.EncodeBytesToHexString(authorIdentityHash[:]) + return errors.New("AddMessageToMySkippedMessagesMap called with invalid authorIdentityHash: " + authorIdentityHashHex) + } + + addedTime := time.Now().Unix() + addedTimeString := helpers.ConvertInt64ToString(addedTime) + + entryValue := addedTimeString + "$" + authorIdentityHashString + + err = mySkippedMessagesMapObject.SetMapEntry(messageHashString, entryValue) + if (err != nil) { return err } + + return nil +} + + +func DeleteMessageFromMySkippedMessagesMap(messageHash [26]byte)error{ + + messageHashString := encoding.EncodeBytesToHexString(messageHash[:]) + + err := mySkippedMessagesMapObject.DeleteMapEntry(messageHashString) + if (err != nil) { return err } + + return nil +} + +//Outputs: +// -bool: Message is skipped +// -int64: Message added time +// -[16]byte: Message author identity hash +// -error +func CheckIfMessageIsSkipped(messageHash [26]byte)(bool, int64, [16]byte, error){ + + messageHashHex := encoding.EncodeBytesToHexString(messageHash[:]) + + exists, entryValue, err := mySkippedMessagesMapObject.GetMapEntry(messageHashHex) + if (err != nil) { return false, 0, [16]byte{}, err } + if (exists == false){ + return false, 0, [16]byte{}, nil + } + + addedTimeString, authorIdentityHashString, delimiterFound := strings.Cut(entryValue, "$") + if (delimiterFound == false){ + return false, 0, [16]byte{}, errors.New("mySkippedMessagesMap is malformed: Contains invalid entryValue: " + entryValue) + } + + addedTimeInt64, err := helpers.ConvertStringToInt64(addedTimeString) + if (err != nil){ + return false, 0, [16]byte{}, errors.New("mySkippedMessagesMap is malformed: Contains invalid addedTime: " + addedTimeString) + } + + authorIdentityHash, _, err := identity.ReadIdentityHashString(authorIdentityHashString) + if (err != nil){ + return false, 0, [16]byte{}, errors.New("mySkippedMessagesMap is malformed: Contains invalid authorIdentityHash: " + authorIdentityHashString) + } + + return true, addedTimeInt64, authorIdentityHash, nil +} + + +func GetMySkippedMessagesMap()(map[[26]byte]int64, error){ + + rawSkippedMessagesMap, err := mySkippedMessagesMapObject.GetMap() + if (err != nil) { return nil, err } + + skippedMessagesMap := make(map[[26]byte]int64) + + for messageHashString, entryValue := range rawSkippedMessagesMap{ + + messageHashBytes, err := encoding.DecodeHexStringToBytes(messageHashString) + if (err != nil){ + return nil, errors.New("mySkippedMessagesMap is malformed: Contains invalid messageHash: " + messageHashString) + } + + if (len(messageHashBytes) != 26){ + return nil, errors.New("mySkippedMessagesMap is malformed: Contains invalid length messageHash: " + messageHashString) + } + + messageHash := [26]byte(messageHashBytes) + + addedTimeString, _, delimiterFound := strings.Cut(entryValue, "$") + if (delimiterFound == false){ + return nil, errors.New("mySkippedMessagesMap is malformed: Contains invalid entryValue: " + entryValue) + } + + addedTimeInt64, err := helpers.ConvertStringToInt64(addedTimeString) + if (err != nil){ + return nil, errors.New("mySkippedMessagesMap is malformed: Contains invalid addedTime: " + addedTimeString) + } + + skippedMessagesMap[messageHash] = addedTimeInt64 + } + + return skippedMessagesMap, nil +} + + + + diff --git a/internal/moderation/myUnreviewed/myUnreviewed.go b/internal/moderation/myUnreviewed/myUnreviewed.go new file mode 100644 index 0000000..4da9d57 --- /dev/null +++ b/internal/moderation/myUnreviewed/myUnreviewed.go @@ -0,0 +1,660 @@ + +// myUnreviewed provides functions to get message, profile, and attribute hashes the user has not yet reviewed + +package myUnreviewed + +// Each content to review has a priority +// Content that has been reviewed by the fewest moderators will have the highest priority +// Content that has been sufficiently reviewed by other moderators will have a lower priority +// Unreviewed content will be retained by the client until it expires from the network + +//TODO: Add filter for ProfileLanguage, so user will only review content they understand +//TODO: Add higher priority for reported profiles? +//TODO: Add limit where if it has found enough options to review, break +// -This means we will only need to search through a maximum number of contents before we settle on one that is the highest priority +// -It may not be the highest priority out of all contents, but it will be the highest priority out of a subset of contents +// -This would make the retrieval faster +//TODO: Extract errantProfiles from identity reviews and treat them like profile reports +//TODO: Add a toggle to consider/not consider profiles/messages reviewed if we have banned the author +//TODO: Build a cache to store the highest priority unreviewed content hashes +// -This way we only have to repeat the process after the highest priority unreviewed content has been reviewed + +import "seekia/internal/moderation/myReviews" +import "seekia/internal/moderation/verifiedVerdict" +import "seekia/internal/moderation/mySkippedContent" +import "seekia/internal/moderation/myHiddenContent" +import "seekia/internal/profiles/profileStorage" +import "seekia/internal/profiles/readProfiles" +import "seekia/internal/messaging/chatMessageStorage" +import "seekia/internal/myRanges" +import "seekia/internal/badgerDatabase" +import "seekia/internal/helpers" + +import "strings" +import "errors" +import "slices" + +//Outputs: +// -bool: Any unreviewed message hash exists +// -[26]byte: Message Hash +// -error +func GetMyHighestPriorityUnreviewedMessageHash(networkType byte, imageOrText string)(bool, [26]byte, error){ + + isValid := helpers.VerifyNetworkType(networkType) + if (isValid == false){ + networkTypeString := helpers.ConvertByteToString(networkType) + return false, [26]byte{}, errors.New("GetMyHighestPriorityUnreviewedMessageHash called with invalid networkType: " + networkTypeString) + } + + if (imageOrText != "Image" && imageOrText != "Text"){ + return false, [26]byte{}, errors.New("GetMyHighestPriorityUnreviewedMessageHash called with invalid imageOrText: " + imageOrText) + } + + allReviewedMessageHashesList, err := badgerDatabase.GetAllReviewedMessageHashes() + if (err != nil) { return false, [26]byte{}, err } + + allReportedMessageHashesList, err := badgerDatabase.GetAllReportedMessageHashes() + if (err != nil) { return false, [26]byte{}, err } + + allMessageHashesList := helpers.CombineTwoListsAndAvoidDuplicates(allReviewedMessageHashesList, allReportedMessageHashesList) + + myModeratorIdentityExists, myReviewedMessageHashesList, err := myReviews.GetMyReviewedMessageHashesList(networkType) + if (err != nil) { return false, [26]byte{}, err } + if (myModeratorIdentityExists == false) { + return false, [26]byte{}, errors.New("GetMyHighestPriorityUnreviewedMessageHash called when my moderator identity does not exist.") + } + + myIdentityExists, myBannedIdentityHashesList, err := myReviews.GetMyBannedIdentityHashesList(networkType) + if (err != nil) { return false, [26]byte{}, err } + if (myIdentityExists == false) { + return false, [26]byte{}, errors.New("My moderator identity not found after being found already.") + } + + leastReviewedMessageFound := false + var leastReviewedMessageHash [26]byte + leastReviewedMessageNumberOfReviews := 0 + + // We will only serve skipped messages if all messages remaining to review are skipped + // This bool will be true, unless we encounter a non-skipped message + // If we do, we will overwrite the existing least reviewed messageHash and reject all skipped messages going forward + allMessagesAreSkipped := true + + for _, messageHash := range allMessageHashesList{ + + iHaveReviewed := slices.Contains(myReviewedMessageHashesList, messageHash) + if (iHaveReviewed == true){ + continue + } + + messageMetadataIsKnown, _, messageNetworkType, messageInbox, _, downloadingRequiredReviews, parametersExist, _, numberApprove, numberBan, _, _, _, _, err := verifiedVerdict.GetVerifiedMessageVerdict(messageHash) + if (err != nil) { return false, [26]byte{}, err } + if (messageMetadataIsKnown == false){ + // We do not have the message downloaded. It is not eligible for review + continue + } + if (messageNetworkType != networkType){ + // Message belongs to a different networkType. + continue + } + if (downloadingRequiredReviews == false){ + // Message is not within our moderation range. Skip message + continue + } + if (parametersExist == false){ + // Moderation parameters need to be downloaded to moderate content. Stop searching for messages. + break + } + moderatorModeEnabled, isInMyModerationRange, err := myRanges.CheckIfMessageInboxIsWithinMyModerationRange(messageInbox) + if (err != nil) { return false, [26]byte{}, err } + if (moderatorModeEnabled == false){ + // Mode must have been disabled during generation. + // No messages to review. + return false, [26]byte{}, nil + } + if (isInMyModerationRange == false){ + // Message is outside of our moderation range. Skip it. + continue + } + + messageExists, cipherKeyFound, messageIsDecryptable, _, senderIdentityHash, messageCommunication, err := chatMessageStorage.GetDecryptedMessageForModeration(messageHash) + if (err != nil) { return false, [26]byte{}, err } + if (messageExists == false){ + // We cannot review messages which we do not have downloaded + continue + } + if (cipherKeyFound == false){ + // We do not have a cipher key + // We cannot view the message contents + continue + } + if (messageIsDecryptable == false){ + // We cannot decrypt the message + // Message author must be malicious + // We should ban these kinds of messages automatically, and ban anyone who approves them + continue + } + + identityIsBanned := slices.Contains(myBannedIdentityHashesList, senderIdentityHash) + if (identityIsBanned == true){ + // We already banned the author of the message + // Review is not necessary + continue + } + + getImageOrTextStatus := func()string{ + isImage := strings.HasPrefix(messageCommunication, ">!>Photo=") + if (isImage == true){ + return "Image" + } + return "Text" + } + + imageOrTextStatus := getImageOrTextStatus() + if (imageOrTextStatus != imageOrText){ + // Message is not the imageOrText that we are interested in. + continue + } + + messageIsHidden, _, _, err := myHiddenContent.CheckIfMessageIsHidden(messageHash) + if (err != nil) { return false, [26]byte{}, err } + if (messageIsHidden == true){ + // The moderator has hidden this message. + continue + } + + messageIsSkipped, _, _, err := mySkippedContent.CheckIfMessageIsSkipped(messageHash) + if (err != nil) { return false, [26]byte{}, err } + if (messageIsSkipped == false){ + if (allMessagesAreSkipped == true){ + + // This is the first non-skipped message we have encountered + // This may not be the first message we have encountered + // Thus, we must clear any previously found content + leastReviewedMessageFound = false + leastReviewedMessageHash = [26]byte{} + leastReviewedMessageNumberOfReviews = 0 + + allMessagesAreSkipped = false + } + } else { + if (allMessagesAreSkipped == false){ + // We have some non-skipped content to review, so we will reject all skipped content + continue + } + } + + numberOfReviewers := numberApprove + numberBan + + if (numberOfReviewers == 0 && messageIsSkipped == false){ + // Message is high priority, it has no reviews. We stop searching. + return true, messageHash, nil + } + + if (leastReviewedMessageFound == false || numberOfReviewers < leastReviewedMessageNumberOfReviews){ + leastReviewedMessageFound = true + leastReviewedMessageHash = messageHash + leastReviewedMessageNumberOfReviews = numberOfReviewers + } + } + + if (leastReviewedMessageFound == false){ + // Nothing left to review. + return false, [26]byte{}, nil + } + + return true, leastReviewedMessageHash, nil +} + + +// This function only returns full profiles that have been banned or reported, without reference to the specific attribute +// To approve these profiles, moderators must approve the full profile, which requires viewing all attributes of the profile, including canonical ones +// For profiles whose reviewed attributes have been specified, use GetMyHighestPriorityUnreviewedProfileAttributeHash instead +//Outputs: +// -bool: Unreviewed profile exists +// -[28]byte: Highest priority unreviewed profile hash +// -error +func GetMyHighestPriorityUnreviewedProfileHash(profileType string, networkType byte)(bool, [28]byte, error){ + + if (profileType != "Mate" && profileType != "Host" && profileType != "Moderator"){ + return false, [28]byte{}, errors.New("GetMyHighestPriorityUnreviewedProfileHash called with invalid profileType: " + profileType) + } + + isValid := helpers.VerifyNetworkType(networkType) + if (isValid == false){ + networkTypeString := helpers.ConvertByteToString(networkType) + return false, [28]byte{}, errors.New("GetMyHighestPriorityUnreviewedProfileHash called with invalid networkType: " + networkTypeString) + } + + // For each profile, we must determine if we have reviewed it + // If we have approved/banned the entire profile, then it is considered reviewed + // If we have banned any attribute of the profile, then it is considered reviewed + + moderatorIdentityExists, myReviewedProfileHashesMap, err := myReviews.GetMyReviewedProfileHashesMap(profileType, networkType) + if (err != nil) { return false, [28]byte{}, err } + if (moderatorIdentityExists == false) { + return false, [28]byte{}, errors.New("GetMyHighestPriorityUnreviewedProfileHash called when my moderator identity does not exist.") + } + + myIdentityExists, myBannedIdentityHashesList, err := myReviews.GetMyBannedIdentityHashesList(networkType) + if (err != nil) { return false, [28]byte{}, err } + if (myIdentityExists == false) { + return false, [28]byte{}, errors.New("My moderator identity not found after being found already.") + } + + allReviewedProfileHashesList, err := badgerDatabase.GetAllReviewedProfileHashes() + if (err != nil) { return false, [28]byte{}, err } + + allReportedProfileHashesList, err := badgerDatabase.GetAllReportedProfileHashes() + if (err != nil) { return false, [28]byte{}, err } + + allProfileHashesList := helpers.CombineTwoListsAndAvoidDuplicates(allReviewedProfileHashesList, allReportedProfileHashesList) + + leastReviewedProfileHashFound := false + var leastReviewedProfileHash [28]byte + leastReviewedProfileHashReviewersCount := 0 + + // We will only serve skipped profiles if all remaining profiles to review are skipped + // This bool will be true, unless we encounter a non-skipped profile + // If we do, we will overwrite the existing least reviewed profileHash and reject all skipped profiles going forward + allProfilesAreSkipped := true + + for _, profileHash := range allProfileHashesList{ + + _, iHaveReviewed := myReviewedProfileHashesMap[profileHash] + if (iHaveReviewed == true){ + // We have already reviewed the profile, or banned one of its attributes + continue + } + + profileIsHidden, _, _, err := myHiddenContent.CheckIfProfileIsHidden(profileHash) + if (err != nil) { return false, [28]byte{}, err } + if (profileIsHidden == true){ + // The moderator has hidden this profile. + continue + } + + exists, _, err := profileStorage.GetStoredProfile(profileHash) + if (err != nil) { return false, [28]byte{}, err } + if (exists == false){ + // We need the profile bytes to review the profile + continue + } + + profileIsDisabled, profileMetadataIsKnown, _, profileNetworkType, profileAuthor, _, downloadingRequiredReviews, parametersExist, _, approveAdvocatesCount, banAdvocatesCount, _, _, _, _, _, _, _, _, fullProfileBanAdvocatesCount, _, _, err := verifiedVerdict.GetVerifiedProfileVerdict(profileHash) + if (err != nil) { return false, [28]byte{}, err } + if (profileIsDisabled == true){ + // No review is needed. Skip + continue + } + if (profileMetadataIsKnown == false){ + // We do not have the profile downloaded, we cannot review it. + // It must have been deleted after GetAllProfileHashes step. + continue + } + if (profileNetworkType != networkType){ + // Profile belongs to a different networkType. + continue + } + if (downloadingRequiredReviews == false){ + // Message is not within our moderation range. Skip profile + continue + } + if (parametersExist == false){ + // Moderation parameters need to be downloaded to moderate content. Stop searching for profiles. + return false, [28]byte{}, nil + } + + profileIsReported := slices.Contains(allReportedProfileHashesList, profileHash) + + if (fullProfileBanAdvocatesCount == 0 && profileIsReported == false){ + // Nobody has banned/reported the full profile. + // This means that the profile can be reviewed by reviewing attributes only + // We skip this profile + continue + } + + moderatorModeEnabled, isWithinMyRange, err := myRanges.CheckIfIdentityHashIsWithinMyModerationRange(profileAuthor) + if (err != nil) { return false, [28]byte{}, err } + if (moderatorModeEnabled == false){ + // Mode must have been disabled after previous check. Return no profiles to review. + return false, [28]byte{}, nil + } + if (isWithinMyRange == false){ + // Profile is outside of our moderation range, skip it. + continue + } + + iHaveBannedIdentity := slices.Contains(myBannedIdentityHashesList, profileAuthor) + if (iHaveBannedIdentity == true){ + // We have banned the profile author + // Reviewing the profile is not necessary + continue + } + + profileIsSkipped, _, _, err := mySkippedContent.CheckIfProfileIsSkipped(profileHash) + if (err != nil) { return false, [28]byte{}, err } + if (profileIsSkipped == false){ + if (allProfilesAreSkipped == true){ + + // This is the first non-skipped profile we have encountered + // This may not be the first profile we have encountered + // Thus, we must clear any previously found profiles + leastReviewedProfileHashFound = false + leastReviewedProfileHash = [28]byte{} + leastReviewedProfileHashReviewersCount = 0 + + allProfilesAreSkipped = false + } + } else { + if (allProfilesAreSkipped == false){ + // We have some non-skipped content to review, so we will reject all skipped content + continue + } + } + + numberOfEligibleReviewers := approveAdvocatesCount + banAdvocatesCount + + if (approveAdvocatesCount == 0 && profileIsSkipped == false){ + + // This profile has been either been reported or banned, and has no approvals by anyone eligible + // We consider this the highest priority kind of profile. + // Stop searching and return this profile + return true, profileHash, nil + } + + if (leastReviewedProfileHashFound == false || leastReviewedProfileHashReviewersCount < numberOfEligibleReviewers){ + leastReviewedProfileHashFound = true + leastReviewedProfileHash = profileHash + leastReviewedProfileHashReviewersCount = numberOfEligibleReviewers + } + } + + if (leastReviewedProfileHashFound == false){ + // No profiles to review + return false, [28]byte{}, nil + } + + return true, leastReviewedProfileHash, nil +} + + +//Outputs: +// -bool: Any unreviewed profile attribute hash exists +// -[27]byte: Profile attribute hash +// -[28]byte: Profile hash that contained this attribute (there may be many, we will return the first one we encounter) +// -[16]byte: Profile identity hash that authored the profile +// -error +func GetMyHighestPriorityUnreviewedProfileAttributeHash(profileType string, attributeIdentifier int, networkType byte)(bool, [27]byte, [28]byte, [16]byte, error){ + + if (profileType != "Mate" && profileType != "Host" && profileType != "Moderator"){ + return false, [27]byte{}, [28]byte{}, [16]byte{}, errors.New("GetMyHighestPriorityUnreviewedProfileAttributeHash called with invalid profileType: " + profileType) + } + + isValid := helpers.VerifyNetworkType(networkType) + if (isValid == false){ + networkTypeString := helpers.ConvertByteToString(networkType) + return false, [27]byte{}, [28]byte{}, [16]byte{}, errors.New("GetMyHighestPriorityUnreviewedProfileAttributeHash called with invalid networkType: " + networkTypeString) + } + + myIdentityExists, myReviewedProfileHashesMap, err := myReviews.GetMyReviewedProfileHashesMap(profileType, networkType) + if (err != nil) { return false, [27]byte{}, [28]byte{}, [16]byte{}, err } + if (myIdentityExists == false) { + return false, [27]byte{}, [28]byte{}, [16]byte{}, errors.New("GetMyHighestPriorityUnreviewedProfileAttributeHash called when my moderator identity does not exist.") + } + + myIdentityExists, myReviewedAttributeHashesMap, err := myReviews.GetMyReviewedProfileAttributeHashesMap(networkType) + if (err != nil) { return false, [27]byte{}, [28]byte{}, [16]byte{}, err } + if (myIdentityExists == false) { + return false, [27]byte{}, [28]byte{}, [16]byte{}, errors.New("GetMyHighestPriorityUnreviewedProfileAttributeHash called when my moderator identity does not exist.") + } + + myIdentityExists, myBannedIdentityHashesList, err := myReviews.GetMyBannedIdentityHashesList(networkType) + if (err != nil) { return false, [27]byte{}, [28]byte{}, [16]byte{}, err } + if (myIdentityExists == false) { + return false, [27]byte{}, [28]byte{}, [16]byte{}, errors.New("GetMyHighestPriorityUnreviewedProfileAttributeHash called when my moderator identity does not exist.") + } + + allReportedAttributeHashesList, err := badgerDatabase.GetAllReportedProfileAttributeHashes() + if (err != nil) { return false, [27]byte{}, [28]byte{}, [16]byte{}, err } + + allProfileHashesList, err := badgerDatabase.GetAllProfileHashes(profileType) + if (err != nil) { return false, [27]byte{}, [28]byte{}, [16]byte{}, err } + + // This map will store the attribute hashes we have already checked + // We need this because the same attribute may exist in multiple profiles + encounteredAttributeHashesMap := make(map[[27]byte]struct{}) + + // We use the below variables to find the least reviewed (highest priority) attribute hash + leastReviewedAttributeHashFound := false + var leastReviewedAttributeHash [27]byte + var leastReviewedAttributeProfileHash [28]byte + var leastReviewedAttributeIdentityHash [16]byte + leastReviewedAttributeHashReviewsCount := 0 + + // We will only serve skipped attributes if all remaining attributes to review are skipped + // This bool will be true, unless we encounter a non-skipped attribute + // If we do, we will overwrite the existing least reviewed attributeHash and reject all skipped attributes going forward + allAttributesAreSkipped := true + + for _, profileHash := range allProfileHashesList{ + + _, iHaveReviewed := myReviewedProfileHashesMap[profileHash] + if (iHaveReviewed == true){ + // We have already reviewed this profile. + // We don't need to review any of its component attributes + continue + } + + exists, _, err := profileStorage.GetStoredProfile(profileHash) + if (err != nil) { return false, [27]byte{}, [28]byte{}, [16]byte{}, err } + if (exists == false){ + // We need the profile bytes to review the profile + // Profile must have been deleted after we called GetAllProfileHashes + continue + } + + profileIsDisabled, profileMetadataIsKnown, _, profileNetworkType, profileAuthor, profileAttributeHashesMap, downloadingRequiredReviews, parametersExist, _, _, _, _, _, attributeApproveAdvocateCountsMap, attributeBanAdvocateCountsMap, _, _, _, _, _, _, allAttributeBanAdvocatesMap, err := verifiedVerdict.GetVerifiedProfileVerdict(profileHash) + if (err != nil) { return false, [27]byte{}, [28]byte{}, [16]byte{}, err } + if (profileIsDisabled == true){ + // No review is needed. Skip + continue + } + if (profileMetadataIsKnown == false){ + // We do not have the profile downloaded, we cannot review it. + // It must have been deleted after GetAllProfileHashes step. + continue + } + if (profileNetworkType != networkType){ + // Profile belongs to a different networkType. + continue + } + if (downloadingRequiredReviews == false){ + // Profile is not within our moderation range. Skip message + continue + } + if (parametersExist == false){ + // Moderation parameters need to be downloaded to moderate content. Stop searching for profile attributes. + break + } + moderatorModeEnabled, isWithinMyRange, err := myRanges.CheckIfIdentityHashIsWithinMyModerationRange(profileAuthor) + if (err != nil) { return false, [27]byte{}, [28]byte{}, [16]byte{}, err } + if (moderatorModeEnabled == false){ + // Mode must have been disabled after previous check. Return no profiles to review. + return false, [27]byte{}, [28]byte{}, [16]byte{}, nil + } + if (isWithinMyRange == false){ + // Profile is outside of our moderation range, skip it. + continue + } + + iHaveBannedIdentity := slices.Contains(myBannedIdentityHashesList, profileAuthor) + if (iHaveBannedIdentity == true){ + // We have already banned the profile author + continue + } + + attributeHash, exists := profileAttributeHashesMap[attributeIdentifier] + if (exists == false){ + // This profile does not have the attribute which we want to review + continue + } + + _, hasBeenEncountered := encounteredAttributeHashesMap[attributeHash] + if (hasBeenEncountered == true){ + // We already dealt with this attribute + continue + } + + encounteredAttributeHashesMap[attributeHash] = struct{}{} + + _, iHaveReviewed = myReviewedAttributeHashesMap[attributeHash] + if (iHaveReviewed == true){ + // We have already reviewed the attribute. + continue + } + + attributeIsHidden, _, _, err := myHiddenContent.CheckIfAttributeIsHidden(attributeHash) + if (err != nil) { return false, [27]byte{}, [28]byte{}, [16]byte{}, err } + if (attributeIsHidden == true){ + // The moderator has hidden this attribute. + continue + } + + // This will tell us if any moderators, banned or not, have banned the attribute + checkIfAnyAttributeBanAdvocatesExist := func()bool{ + + allAttributeBanAdvocatesList, exists := allAttributeBanAdvocatesMap[attributeIdentifier] + if (exists == false){ + return false + } + if (len(allAttributeBanAdvocatesList) == 0){ + return false + } + return true + } + + anyAttributeBanAdvocatesExist := checkIfAnyAttributeBanAdvocatesExist() + + if (anyAttributeBanAdvocatesExist == false){ + + attributeIsReported := slices.Contains(allReportedAttributeHashesList, attributeHash) + + if (attributeIsReported == false){ + + // Nobody has banned/reported this attribute + + if (profileType != "Mate"){ + // We do not need to review attributes that are not banned for non-mate profiles + continue + } + + attributeProfileType, attributeIsCanonical, err := readProfiles.ReadAttributeHashMetadata(attributeHash) + if (err != nil){ return false, [27]byte{}, [28]byte{}, [16]byte{}, err } + if (attributeProfileType != profileType){ + return false, [27]byte{}, [28]byte{}, [16]byte{}, errors.New("GetVerifiedProfileVerdict returning attribute hash with different profileType than author.") + } + + if (attributeIsCanonical == true){ + // We don't need to review it, even if profileType == Mate + continue + } + + // We see if the user has a newer profile + // If they do, we don't need to review this profile + + anyProfileExists, _, newestProfileHash, _, _, _, err := profileStorage.GetNewestUserProfile(profileAuthor, networkType) + if (err != nil) { return false, [27]byte{}, [28]byte{}, [16]byte{}, err } + if (anyProfileExists == false){ + // The user's profile must have been deleted after we performed GetAllProfileHashes + continue + } + if (profileHash != newestProfileHash){ + // There is a newer profile by this same Mate user + // We don't need to review the current profile, because it is not banned by anyone, and none of its attributes are banned + // We will review the newer profile instead + continue + } + + // We still need to review this attribute, because it is a non-canonical mate attribute + } + } + + // The attribute is worthy of review + // We see if it is less reviewed than our current leastReviewedAttributeHash + + // This will tell us the number of eligible attribute approve advocates + getAttributeApproveAdvocatesCount := func()int{ + + attributeApproveAdvocatesCount, exists := attributeApproveAdvocateCountsMap[attributeIdentifier] + if (exists == false){ + return 0 + } + return attributeApproveAdvocatesCount + } + + // This will tell us the number of eligible attribute ban advocates + getAttributeBanAdvocatesCount := func()int{ + + attributeBanAdvocatesCount, exists := attributeBanAdvocateCountsMap[attributeIdentifier] + if (exists == false){ + return 0 + } + return attributeBanAdvocatesCount + } + + attributeApproveAdvocatesCount := getAttributeApproveAdvocatesCount() + attributeBanAdvocatesCount := getAttributeBanAdvocatesCount() + + numberOfReviewers := attributeApproveAdvocatesCount + attributeBanAdvocatesCount + + attributeIsSkipped, _, _, err := mySkippedContent.CheckIfAttributeIsSkipped(attributeHash) + if (err != nil) { return false, [27]byte{}, [28]byte{}, [16]byte{}, err } + if (attributeIsSkipped == false){ + if (allAttributesAreSkipped == true){ + + // This is the first non-skipped attribute we have encountered + // It may not be the first attribute we have encountered + // Thus, we must clear any previously found attributes + leastReviewedAttributeHashFound = false + leastReviewedAttributeHash = [27]byte{} + leastReviewedAttributeProfileHash = [28]byte{} + leastReviewedAttributeIdentityHash = [16]byte{} + leastReviewedAttributeHashReviewsCount = 0 + + allAttributesAreSkipped = false + } + } else { + if (allAttributesAreSkipped == false){ + // We have some non-skipped attributes, so we will reject all skipped attributes + continue + } + } + + if (attributeApproveAdvocatesCount == 0 && attributeIsSkipped == false){ + // This is the highest priority kind of profile attribute + // It has no approvers, and needs to be reviewed + // We stop searching and return the attribute + return true, attributeHash, profileHash, profileAuthor, nil + } + + if (leastReviewedAttributeHashFound == false || numberOfReviewers < leastReviewedAttributeHashReviewsCount){ + leastReviewedAttributeHashFound = true + leastReviewedAttributeHash = attributeHash + leastReviewedAttributeProfileHash = profileHash + leastReviewedAttributeIdentityHash = profileAuthor + leastReviewedAttributeHashReviewsCount = numberOfReviewers + } + } + + if (leastReviewedAttributeHashFound == true){ + return true, leastReviewedAttributeHash, leastReviewedAttributeProfileHash, leastReviewedAttributeIdentityHash, nil + } + + // Nothing left to review + return false, [27]byte{}, [28]byte{}, [16]byte{}, nil +} + + + + + diff --git a/internal/moderation/readReports/readReports.go b/internal/moderation/readReports/readReports.go new file mode 100644 index 0000000..9eb8ef3 --- /dev/null +++ b/internal/moderation/readReports/readReports.go @@ -0,0 +1,302 @@ + +// readReports provides functions to read reports. + +package readReports + +import "seekia/internal/allowedText" +import "seekia/internal/cryptography/blake3" +import "seekia/internal/encoding" +import "seekia/internal/helpers" + +import messagepack "github.com/vmihailenco/msgpack/v5" + +import "errors" + +func VerifyReport(inputReport []byte)(bool, error){ + + ableToRead, _, _, _, _, _, _, err := ReadReport(true, inputReport) + if (err != nil){ + return false, err + } + if (ableToRead == false){ + return false, nil + } + + return true, nil +} + +// This is used to verify report hashes +func VerifyReportHash(inputHash [30]byte, reportTypeProvided bool, expectedReportType string)(bool, error){ + + if (reportTypeProvided == true){ + if (expectedReportType != "Identity" && expectedReportType != "Profile" && expectedReportType != "Attribute" && expectedReportType != "Message"){ + return false, errors.New("VerifyReportHash called with invalid expectedReportType: " + expectedReportType) + } + } + + reportType, err := GetReportTypeFromReportHash(inputHash) + if (err != nil){ + return false, nil + } + if (reportTypeProvided == true){ + if (reportType != expectedReportType){ + return false, nil + } + } + + return true, nil +} + + +func GetReportTypeFromReportHash(inputHash [30]byte)(string, error){ + + metadataByte := inputHash[29] + + if (metadataByte == 1){ + + return "Identity", nil + } + if (metadataByte == 2){ + + return "Profile", nil + } + if (metadataByte == 3){ + + return "Attribute", nil + } + if (metadataByte == 4){ + + return "Message", nil + } + + reportHashHex := encoding.EncodeBytesToHexString(inputHash[:]) + + return "", errors.New("GetReportTypeFromReportHash called with invalid reportHash: " + reportHashHex) +} + + +// This function computes the report hash and returns it +//Outputs: +// -bool: Able to read +// -[30]byte: Report Hash +// -int: Report Version +// -byte: Network Type (1 == Mainnet, 2 == Testnet1) +// -int64: Broadcast Time (alleged, could be fake, like all broadcast times) +// -string: Report Type +// -[]byte: Reported Hash +// -map[string]string: Report map +// -error +func ReadReportAndHash(verifyReport bool, inputReport []byte)(bool, [30]byte, int, byte, int64, string, []byte, map[string]string, error){ + + ableToRead, reportVersion, reportNetworkType, reportBroadcastTime, reportType, reportedHash, reportMap, err := ReadReport(verifyReport, inputReport) + if (err != nil){ return false, [30]byte{}, 0, 0, 0, "", nil, nil, err } + if (ableToRead == false){ + return false, [30]byte{}, 0, 0, 0, "", nil, nil, nil + } + + reportHashWithoutMetadataByte, err := blake3.GetBlake3HashAsBytes(29, inputReport) + if (err != nil){ return false, [30]byte{}, 0, 0, 0, "", nil, nil, err } + + getReportHashMetadataByte := func()(byte, error){ + + if (reportType == "Identity"){ + return 1, nil + } + if (reportType == "Profile"){ + return 2, nil + } + if (reportType == "Attribute"){ + return 3, nil + } + if (reportType == "Message"){ + return 4, nil + } + + return 0, errors.New("ReadReport returning invalid reportType: " + reportType) + } + + reportHashMetadataByte, err := getReportHashMetadataByte() + if (err != nil){ return false, [30]byte{}, 0, 0, 0, "", nil, nil, err } + + reportHashSlice := append(reportHashWithoutMetadataByte, reportHashMetadataByte) + + reportHash := [30]byte(reportHashSlice) + + return true, reportHash, reportVersion, reportNetworkType, reportBroadcastTime, reportType, reportedHash, reportMap, nil +} + +// This function does not compute the report hash, thus it is faster +//Outputs: +// -bool: Able to read +// -int: Report Version +// -byte: Network Type (1 == Mainnet, 2 == Testnet1) +// -int64: Broadcast Time (alleged, could be fake, like all broadcast times) +// -string: Report Type +// -[]byte: Reported Hash +// -map[string]string: Report map +// -error +func ReadReport(verifyReport bool, inputReport []byte)(bool, int, byte, int64, string, []byte, map[string]string, error){ + + reportContentMap := make(map[int]messagepack.RawMessage) + + err := messagepack.Unmarshal(inputReport, &reportContentMap) + if (err != nil) { + // Invalid Report: Invalid Messagepack + return false, 0, 0, 0, "", nil, nil, nil + } + + reportVersionMessagepack, exists := reportContentMap[1] + if (exists == false){ + // Invalid Report: Missing ReportVersion + return false, 0, 0, 0, "", nil, nil, nil + } + + reportVersion, err := encoding.DecodeRawMessagePackToInt(reportVersionMessagepack) + if (err != nil) { + // Invalid Report: Invalid report version + return false, 0, 0, 0, "", nil, nil, nil + } + + if (reportVersion != 1){ + // We cannot read this report. It was created by a newer version of Seekia. + return false, 0, 0, 0, "", nil, nil, nil + } + + networkTypeMessagepack, exists := reportContentMap[2] + if (exists == false){ + // Invalid Report: Missing NetworkType + return false, 0, 0, 0, "", nil, nil, nil + } + + reportNetworkType, err := encoding.DecodeRawMessagePackToByte(networkTypeMessagepack) + if (err != nil) { + // Invalid Report: Invalid NetworkType + return false, 0, 0, 0, "", nil, nil, nil + } + + isValid := helpers.VerifyNetworkType(reportNetworkType) + if (isValid == false){ + // Invalid Report: Invalid NetworkType + return false, 0, 0, 0, "", nil, nil, nil + } + + broadcastTimeMessagepack, exists := reportContentMap[3] + if (exists == false){ + // Invalid Report: Missing BroadcastTime + return false, 0, 0, 0, "", nil, nil, nil + } + + broadcastTime, err := encoding.DecodeRawMessagePackToInt64(broadcastTimeMessagepack) + if (err != nil) { + // Invalid Report: Invalid BroadcastTime + return false, 0, 0, 0, "", nil, nil, nil + } + + reportedHashMessagepack, exists := reportContentMap[4] + if (exists == false){ + // Invalid Report: Missing ReportedHash + return false, 0, 0, 0, "", nil, nil, nil + } + + reportedHash, err := encoding.DecodeRawMessagePackToBytes(reportedHashMessagepack) + if (err != nil) { + // Invalid Report: Invalid ReportedHash + return false, 0, 0, 0, "", nil, nil, nil + } + + reportType, err := helpers.GetReportedTypeFromReportedHash(reportedHash) + if (err != nil){ + // Invalid Report: Invalid ReportedHash + return false, 0, 0, 0, "", nil, nil, nil + } + + // These variables are only included in some reports + var reason string + var messageCipherKeyBytes []byte + + reasonMessagepack, exists := reportContentMap[5] + if (exists == true){ + err = messagepack.Unmarshal(reasonMessagepack, &reason) + if (err != nil) { + // Invalid Report: Invalid Reason + return false, 0, 0, 0, "", nil, nil, nil + } + } + + if (reportType == "Message"){ + + messageCipherKeyMessagepack, exists := reportContentMap[6] + if (exists == false){ + // Invalid Report: Missing MessageCipherKey + return false, 0, 0, 0, "", nil, nil, nil + } + + err = messagepack.Unmarshal(messageCipherKeyMessagepack, &messageCipherKeyBytes) + if (err != nil) { + // Invalid Report: Invalid Reason + return false, 0, 0, 0, "", nil, nil, nil + } + } + + if (verifyReport == true){ + + isValid := helpers.VerifyBroadcastTime(broadcastTime) + if (isValid == false){ + // Invalid report: Invalid broadcast time + return false, 0, 0, 0, "", nil, nil, nil + } + + if (reportType == "Message"){ + + if (len(messageCipherKeyBytes) != 32){ + // Invalid Report: Report contains invalid messageCipherKey + return false, 0, 0, 0, "", nil, nil, nil + } + } + + if (reason != "") { + + if (len(reason) > 200) { + // Invalid report: Report contains invalid reason + return false, 0, 0, 0, "", nil, nil, nil + } + + isAllowed := allowedText.VerifyStringIsAllowed(reason) + if (isAllowed == false){ + // Invalid report: Report contains invalid reason + return false, 0, 0, 0, "", nil, nil, nil + } + } + } + + reportVersionString := helpers.ConvertIntToString(reportVersion) + + reportNetworkTypeString := helpers.ConvertByteToString(reportNetworkType) + + broadcastTimeString := helpers.ConvertInt64ToString(broadcastTime) + + reportedHashString, err := helpers.EncodeReportedHashBytesToString(reportedHash) + if (err != nil) { return false, 0, 0, 0, "", nil, nil, err } + + reportMap := map[string]string{ + "ReportVersion": reportVersionString, + "NetworkType": reportNetworkTypeString, + "BroadcastTime": broadcastTimeString, + "ReportedHash": reportedHashString, + } + + if (reportType == "Message"){ + messageCipherKey := encoding.EncodeBytesToHexString(messageCipherKeyBytes) + + reportMap["MessageCipherKey"] = messageCipherKey + } + + if (reason != ""){ + reportMap["Reason"] = reason + } + + return true, 1, reportNetworkType, broadcastTime, reportType, reportedHash, reportMap, nil +} + + + diff --git a/internal/moderation/readReviews/readReviews.go b/internal/moderation/readReviews/readReviews.go new file mode 100644 index 0000000..f0cedcc --- /dev/null +++ b/internal/moderation/readReviews/readReviews.go @@ -0,0 +1,984 @@ + +// readReviews provides functions to read/verify moderator reviews + +package readReviews + +//TODO: Remove ReviewHash from ReadReview function outputs and create a seperate ReadReviewHash function +// This will prevent hashing from being performed every time we read reviews + +import "seekia/internal/allowedText" +import "seekia/internal/cryptography/blake3" +import "seekia/internal/cryptography/edwardsKeys" +import "seekia/internal/encoding" +import "seekia/internal/helpers" +import "seekia/internal/identity" +import "seekia/internal/messaging/readMessages" + +import messagepack "github.com/vmihailenco/msgpack/v5" + +import "bytes" +import "errors" + +func VerifyReview(inputReview []byte)(bool, error){ + + ableToRead, _, _, _, _, _, _, _, _, err := ReadReview(true, inputReview) + if (err != nil){ return false, err } + if (ableToRead == false){ + return false, nil + } + + return true, nil +} + +func VerifyReviewHash(inputHash [29]byte, reviewTypeProvided bool, expectedReviewType string)(bool, error){ + + if (reviewTypeProvided == true){ + if (expectedReviewType != "Identity" && expectedReviewType != "Profile" && expectedReviewType != "Attribute" && expectedReviewType != "Message"){ + return false, errors.New("VerifyReviewHash called with invalid expectedReviewType: " + expectedReviewType) + } + } + + reviewType, err := GetReviewTypeFromReviewHash(inputHash) + if (err != nil){ + return false, nil + } + + if (reviewTypeProvided == true){ + if (reviewType != expectedReviewType){ + return false, nil + } + } + + return true, nil +} + +//Outputs: +// -string: Review type +// -error +func GetReviewTypeFromReviewHash(inputHash [29]byte)(string, error){ + + metadataByte := inputHash[28] + + if (metadataByte == 1){ + return "Identity", nil + } + if (metadataByte == 2){ + return "Profile", nil + } + if (metadataByte == 3){ + return "Attribute", nil + } + if (metadataByte == 4){ + return "Message", nil + } + + reviewHashHex := encoding.EncodeBytesToHexString(inputHash[:]) + + return "", errors.New("GetReviewTypeFromReviewHash called with invalid reviewHash: " + reviewHashHex) +} + +// This function computes the review hash and returns it +//Outputs: +// -bool: Able to read +// -[29]byte: Review Hash +// -int: Review version +// -byte: Network Type (1 == Mainnet, 2 == Testnet1) +// -[16]byte: Identity Hash of reviewer (Review author) +// -int64: Broadcast time (alleged, can be faked) +// -string: Review type +// -[]byte: Reviewed hash +// -string: Review Verdict +// -map[string]string: Review Map +// -error (returns err if there is a bug in the code) +func ReadReviewAndHash(verifyReview bool, inputReview []byte)(bool, [29]byte, int, byte, [16]byte, int64, string, []byte, string, map[string]string, error){ + + ableToRead, reviewVersion, reviewNetworkType, reviewAuthor, reviewBroadcastTime, reviewType, reviewedHash, reviewVerdict, reviewMap, err := ReadReview(verifyReview, inputReview) + if (err != nil) { return false, [29]byte{}, 0, 0, [16]byte{}, 0, "", nil, "", nil, err } + if (ableToRead == false){ + return false, [29]byte{}, 0, 0, [16]byte{}, 0, "", nil, "", nil, nil + } + + reviewHashWithoutMetadataByte, err := blake3.GetBlake3HashAsBytes(28, inputReview) + if (err != nil) { return false, [29]byte{}, 0, 0, [16]byte{}, 0, "", nil, "", nil, err } + + getReviewHashMetadataByte := func()(byte, error){ + + if (reviewType == "Identity"){ + + return 1, nil + + } else if (reviewType == "Profile"){ + + return 2, nil + + } else if (reviewType == "Attribute"){ + + return 3, nil + + } else if (reviewType == "Message"){ + + return 4, nil + } + + return 0, errors.New("ReadReview returning invalid reviewType: " + reviewType) + } + + reviewHashMetadataByte, err := getReviewHashMetadataByte() + if (err != nil) { return false, [29]byte{}, 0, 0, [16]byte{}, 0, "", nil, "", nil, err } + + reviewHashSlice := append(reviewHashWithoutMetadataByte, reviewHashMetadataByte) + + reviewHash := [29]byte(reviewHashSlice) + + return true, reviewHash, reviewVersion, reviewNetworkType, reviewAuthor, reviewBroadcastTime, reviewType, reviewedHash, reviewVerdict, reviewMap, nil +} + + +//TODO: Encode Verdict as an int to save space + +// This function does not compute the review hash, thus it is faster +//Outputs: +// -bool: Able to read +// -int: Review version +// -byte: Network Type (1 == Mainnet, 2 == Testnet1) +// -[16]byte: Identity Hash of reviewer (Review author) +// -int64: Broadcast time (alleged, can be faked) +// -string: Review type +// -[]byte: Reviewed hash +// -string: Review Verdict +// -map[string]string: Review Map +// -error (returns err if there is a bug in the code) +func ReadReview(verifyReview bool, inputReview []byte)(bool, int, byte, [16]byte, int64, string, []byte, string, map[string]string, error){ + + var reviewSlice []messagepack.RawMessage + + err := messagepack.Unmarshal(inputReview, &reviewSlice) + if (err != nil) { + // Invalid review: Malformed messagepack + return false, 0, 0, [16]byte{}, 0, "", nil, "", nil, nil + } + + if (len(reviewSlice) != 2){ + // Invalid review: Malformed messagepack + return false, 0, 0, [16]byte{}, 0, "", nil, "", nil, nil + } + + signatureMessagepack := reviewSlice[0] + reviewContentMessagepack := reviewSlice[1] + + reviewSignature, err := encoding.DecodeRawMessagePackTo64ByteArray(signatureMessagepack) + if (err != nil) { + // Invalid review: Malformed messagepack + return false, 0, 0, [16]byte{}, 0, "", nil, "", nil, nil + } + + reviewContentMap := make(map[int]messagepack.RawMessage) + + err = encoding.DecodeMessagePackBytes(false, reviewContentMessagepack, &reviewContentMap) + if (err != nil) { + // Invalid review: Malformed messagepack + return false, 0, 0, [16]byte{}, 0, "", nil, "", nil, nil + } + + reviewVersionMessagepack, exists := reviewContentMap[1] + if (exists == false){ + // Invalid Review: Missing ReviewVersion + return false, 0, 0, [16]byte{}, 0, "", nil, "", nil, nil + } + + reviewVersion, err := encoding.DecodeRawMessagePackToInt(reviewVersionMessagepack) + if (err != nil) { + // Invalid Review: Invalid review version + return false, 0, 0, [16]byte{}, 0, "", nil, "", nil, nil + } + + if (reviewVersion != 1){ + // We cannot read this review. It was created by a newer version of Seekia. + return false, 0, 0, [16]byte{}, 0, "", nil, "", nil, nil + } + + reviewNetworkTypeMessagepack, exists := reviewContentMap[2] + if (exists == false){ + // Invalid Review: Missing NetworkType + return false, 0, 0, [16]byte{}, 0, "", nil, "", nil, nil + } + + reviewNetworkType, err := encoding.DecodeRawMessagePackToByte(reviewNetworkTypeMessagepack) + if (err != nil){ + // Invalid Review: Invalid network type messagepack + return false, 0, 0, [16]byte{}, 0, "", nil, "", nil, nil + } + + isValid := helpers.VerifyNetworkType(reviewNetworkType) + if (isValid == false){ + // Invalid Review: Invalid NetworkType + return false, 0, 0, [16]byte{}, 0, "", nil, "", nil, nil + } + + authorIdentityKeyMessagepack, exists := reviewContentMap[3] + if (exists == false){ + // Invalid Review: Missing IdentityKey + return false, 0, 0, [16]byte{}, 0, "", nil, "", nil, nil + } + + authorIdentityKey, err := encoding.DecodeRawMessagePackTo32ByteArray(authorIdentityKeyMessagepack) + if (err != nil) { + // Invalid Review: Invalid IdentityKey + return false, 0, 0, [16]byte{}, 0, "", nil, "", nil, nil + } + + if (verifyReview == true){ + + contentHash, err := blake3.Get32ByteBlake3Hash(reviewContentMessagepack) + if (err != nil) { return false, 0, 0, [16]byte{}, 0, "", nil, "", nil, err } + + isValid := edwardsKeys.VerifySignature(authorIdentityKey, reviewSignature, contentHash) + if (isValid == false) { + // Invalid review: Invalid signature + return false, 0, 0, [16]byte{}, 0, "", nil, "", nil, nil + } + } + + authorIdentityHash, err := identity.ConvertIdentityKeyToIdentityHash(authorIdentityKey, "Moderator") + if (err != nil) { return false, 0, 0, [16]byte{}, 0, "", nil, "", nil, err } + + broadcastTimeMessagepack, exists := reviewContentMap[4] + if (exists == false){ + // Invalid Review: Missing BroadcastTime + return false, 0, 0, [16]byte{}, 0, "", nil, "", nil, nil + } + + broadcastTime, err := encoding.DecodeRawMessagePackToInt64(broadcastTimeMessagepack) + if (err != nil) { + // Invalid Review: Invalid BroadcastTime + return false, 0, 0, [16]byte{}, 0, "", nil, "", nil, nil + } + + reviewedHashMessagepack, exists := reviewContentMap[5] + if (exists == false){ + // Invalid Review: Missing ReviewedHash + return false, 0, 0, [16]byte{}, 0, "", nil, "", nil, nil + } + + reviewedHash, err := encoding.DecodeRawMessagePackToBytes(reviewedHashMessagepack) + if (err != nil) { + // Invalid Review: Invalid ReviewedHash + return false, 0, 0, [16]byte{}, 0, "", nil, "", nil, nil + } + + reviewType, err := helpers.GetReviewedTypeFromReviewedHash(reviewedHash) + if (err != nil){ + // Invalid Review: Invalid ReviewedHash + return false, 0, 0, [16]byte{}, 0, "", nil, "", nil, nil + } + + verdictMessagepack, exists := reviewContentMap[6] + if (exists == false){ + // Invalid Review: Missing Verdict + return false, 0, 0, [16]byte{}, 0, "", nil, "", nil, nil + } + + reviewVerdict, err := encoding.DecodeRawMessagePackToString(verdictMessagepack) + if (err != nil) { + // Invalid Review: Invalid Verdict + return false, 0, 0, [16]byte{}, 0, "", nil, "", nil, nil + } + + // These fields are not included within all reviews + var reason string + var messageCipherKeyBytes []byte + + reasonMessagepack, exists := reviewContentMap[7] + if (exists == true){ + + err = messagepack.Unmarshal(reasonMessagepack, &reason) + if (err != nil) { + // Invalid Review: Invalid Reason + return false, 0, 0, [16]byte{}, 0, "", nil, "", nil, nil + } + } + + if (reviewType == "Message"){ + + messageCipherKeyMessagepack, exists := reviewContentMap[8] + if (exists == false){ + // Invalid Review: Message review missing MessageCipherKey + return false, 0, 0, [16]byte{}, 0, "", nil, "", nil, nil + } + + err = messagepack.Unmarshal(messageCipherKeyMessagepack, &messageCipherKeyBytes) + if (err != nil) { + // Invalid Review: Invalid MessageCipherKey + return false, 0, 0, [16]byte{}, 0, "", nil, "", nil, nil + } + } + + if (verifyReview == true){ + + isValid := helpers.VerifyBroadcastTime(broadcastTime) + if (isValid == false){ + // Invalid review: Review contains contains invalid BroadcastTime + return false, 0, 0, [16]byte{}, 0, "", nil, "", nil, nil + } + + if (reviewVerdict != "Ban" && reviewVerdict != "Approve" && reviewVerdict != "None"){ + // Invalid review: Review contains invalid Verdict + return false, 0, 0, [16]byte{}, 0, "", nil, "", nil, nil + } + + if (reviewType == "Identity" && reviewVerdict == "Approve"){ + // Invalid review: Review contains approve verdict for identity review + return false, 0, 0, [16]byte{}, 0, "", nil, "", nil, nil + } + + if (reviewType == "Identity"){ + + areEqual := bytes.Equal(reviewedHash, authorIdentityHash[:]) + if (areEqual == true){ + // Invalid review: Author cannot ban themselves. + return false, 0, 0, [16]byte{}, 0, "", nil, "", nil, nil + } + + //TODO: Verify ErrantProfiles, ErrantMessages, ErrantReviews, and ErrantAttributes + + } else if (reviewType == "Message"){ + + if (len(messageCipherKeyBytes) != 32){ + // Invalid review: Message review contains invalid messageCipherKey + return false, 0, 0, [16]byte{}, 0, "", nil, "", nil, nil + } + } + + if (reason != "") { + + if (len(reason) > 200) { + // Invalid review: ReviewMap contains invalid reason + return false, 0, 0, [16]byte{}, 0, "", nil, "", nil, nil + } + + isAllowed := allowedText.VerifyStringIsAllowed(reason) + if (isAllowed == false){ + // Invalid review: ReviewMap contains invalid reason + return false, 0, 0, [16]byte{}, 0, "", nil, "", nil, nil + } + } + } + + reviewNetworkTypeString := helpers.ConvertByteToString(reviewNetworkType) + + authorIdentityKeyHex := encoding.EncodeBytesToHexString(authorIdentityKey[:]) + + broadcastTimeString := helpers.ConvertInt64ToString(broadcastTime) + + reviewedHashString, err := helpers.EncodeReviewedHashBytesToString(reviewedHash) + if (err != nil){ + return false, 0, 0, [16]byte{}, 0, "", nil, "", nil, errors.New("GetReviewedTypeFromReviewedHash not verifying reviewedHash.") + } + + reviewMap := map[string]string{ + "ReviewVersion": "1", + "NetworkType": reviewNetworkTypeString, + "IdentityKey": authorIdentityKeyHex, + "BroadcastTime": broadcastTimeString, + "ReviewedHash": reviewedHashString, + "Verdict": reviewVerdict, + } + + if (reason != ""){ + reviewMap["Reason"] = reason + } + if (reviewType == "Message"){ + + messageCipherKey := encoding.EncodeBytesToHexString(messageCipherKeyBytes) + + reviewMap["MessageCipherKey"] = messageCipherKey + } + + return true, 1, reviewNetworkType, authorIdentityHash, broadcastTime, reviewType, reviewedHash, reviewVerdict, reviewMap, nil +} + + +// This struct is used to represent a review and its hash +// This is useful, because we do not need to hash each review to retrieve its hash +type ReviewWithHash struct{ + ReviewHash [29]byte + ReviewBytes []byte +} + +//Outputs: +// -map[[16]byte]int64: Ban advocates map (identity hash -> Time of ban) +// -error +func GetIdentityBanAdvocatesMapFromReviewsList(reviewsList []ReviewWithHash, identityHash [16]byte, networkType byte)(map[[16]byte]int64, error){ + + checkIfReviewIsValid := func(_ map[string]string)(bool, error){ + return true, nil + } + + approveAdvocatesMap, banAdvocatesMap, err := getModeratorNewestVerdictMapsFromReviewsList(reviewsList, "Identity", identityHash[:], networkType, checkIfReviewIsValid) + if (err != nil) { return nil, err } + if (len(approveAdvocatesMap) != 0){ + return nil, errors.New("getModeratorNewestVerdictMapsFromReviewsList returning non-empty approve advocates map for identity.") + } + + return banAdvocatesMap, nil +} + +// Outputs: +// -map[[16]byte]int64: Approve advocate identity hash -> Time of approval +// -map[[16]byte]int64: Ban advocate identity hash -> Time of ban +// -error +func GetMessageVerdictMapsFromReviewsList(reviewsList []ReviewWithHash, messageHash [26]byte, messageNetworkType byte, messageCipherKeyHash [25]byte)(map[[16]byte]int64, map[[16]byte]int64, error){ + + // We must check to make sure review has a valid messageCipherKey + // This will function as a proof that the reviewer has seen the message + // If the cipher key hash is valid but the key cannot decrypt the message, we know the reviewer and message author are both malicious + // We can detect these kinds of reviews and automatically ban the authors + + verifyReviewIsValidFunction := func(reviewMap map[string]string)(bool, error){ + + isValid, err := VerifyMessageReviewCipherKey(reviewMap, messageCipherKeyHash) + + return isValid, err + } + + approveAdvocatesMap, banAdvocatesMap, err := getModeratorNewestVerdictMapsFromReviewsList(reviewsList, "Message", messageHash[:], messageNetworkType, verifyReviewIsValidFunction) + if (err != nil) { return nil, nil, err } + + return approveAdvocatesMap, banAdvocatesMap, nil +} + + +// This function is used to verify that message reviews are valid +func VerifyMessageReviewCipherKey(reviewMap map[string]string, messageCipherKeyHash [25]byte)(bool, error){ + + reviewMessageCipherKey, exists := reviewMap["MessageCipherKey"] + if (exists == false) { + return false, errors.New("VerifyMessageReviewCipherKey called with message review missing MessageCipherKey") + } + + reviewMessageCipherKeyArray, err := readMessages.ReadMessageCipherKeyHex(reviewMessageCipherKey) + if (err != nil){ + return false, errors.New("VerifyMessageReviewCipherKey called with message review containing invalid MessageCipherKey: " + reviewMessageCipherKey + ". Reason: " + err.Error()) + } + + reviewCipherKeyHash, err := readMessages.ConvertMessageCipherKeyToCipherKeyHash(reviewMessageCipherKeyArray) + if (err != nil){ return false, err } + + if (reviewCipherKeyHash != messageCipherKeyHash){ + // Reviewer must be malicious. Skip review. + return false, nil + } + + return true, nil +} + + +//Outputs: +// -map[[16]byte]int64: Approve advocates map (identity hash -> Time of approve) +// -map[[16]byte]int64: Ban advocates map (identity hash -> Time of ban) +// -error +func GetProfileVerdictMapsFromReviewsList(reviewsList []ReviewWithHash, profileHash [28]byte, profileNetworkType byte)(map[[16]byte]int64, map[[16]byte]int64, error){ + + verifyReviewIsValid := func(_ map[string]string)(bool, error){ + return true, nil + } + + approveAdvocatesMap, banAdvocatesMap, err := getModeratorNewestVerdictMapsFromReviewsList(reviewsList, "Profile", profileHash[:], profileNetworkType, verifyReviewIsValid) + if (err != nil) { return nil, nil, err } + + return approveAdvocatesMap, banAdvocatesMap, nil +} + +//Outputs: +// -map[[16]byte]int64: Approve advocates map (identity hash -> Time of approval) +// -map[[16]byte]int64: Ban advocates map (identity hash -> Time of ban) +// -error +func GetProfileAttributeVerdictMapsFromReviewsList(reviewsList []ReviewWithHash, attributeHash [27]byte, attributeNetworkType byte)(map[[16]byte]int64, map[[16]byte]int64, error){ + + verifyReviewIsValid := func(_ map[string]string)(bool, error){ + return true, nil + } + + approveAdvocatesMap, banAdvocatesMap, err := getModeratorNewestVerdictMapsFromReviewsList(reviewsList, "Attribute", attributeHash[:], attributeNetworkType, verifyReviewIsValid) + if (err != nil) { return nil, nil, err } + + return approveAdvocatesMap, banAdvocatesMap, nil +} + + +// This function takes a list of reviews and a reviewedHash and returns maps for each moderator's newest verdict ("Approve"/"Ban") +// This requires keeping track of the newest review from each moderator. Moderators can undo reviews by using the None verdict. +//Outputs: +// -map[[16]byte]int64: Approve advocates map (identity hash -> Time approve review was submitted) +// -map[[16]byte]int64: Ban advocates map (identity hash -> Time ban review was submitted) +// -error +func getModeratorNewestVerdictMapsFromReviewsList(reviewsList []ReviewWithHash, reviewType string, reviewedHash []byte, networkType byte, verifyReviewIsValid func(map[string]string)(bool, error))(map[[16]byte]int64, map[[16]byte]int64, error){ + + if (reviewType != "Identity" && reviewType != "Profile" && reviewType != "Attribute" && reviewType != "Message"){ + return nil, nil, errors.New("getModeratorNewestVerdictMapsFromReviewsList called with invalid ReviewType: " + reviewType) + } + + reviewedType, err := helpers.GetReviewedTypeFromReviewedHash(reviewedHash) + if (err != nil){ + reviewedHashHex := encoding.EncodeBytesToHexString(reviewedHash) + return nil, nil, errors.New("getModeratorNewestVerdictMapsFromReviewsList called with invalid reviewedHash: " + reviewedHashHex) + } + if (reviewedType != reviewType){ + return nil, nil, errors.New("getModeratorNewestVerdictMapsFromReviewsList called with reviewType that does not match reviewedHash") + } + + isValid := helpers.VerifyNetworkType(networkType) + if (isValid == false){ + networkTypeString := helpers.ConvertByteToString(networkType) + return nil, nil, errors.New("getModeratorNewestVerdictMapsFromReviewsList called with invalid networkType: " + networkTypeString) + } + + // This stores info about a review + type ReviewInfoObject struct{ + ReviewHash [29]byte + BroadcastTime int64 + Verdict string + } + + // This map stores info about each reviewer's newest review for the provided reviewedHash + // Map Structure: Reviewer Identity Hash -> Newest Review Info Object + newestReviewsMap := make(map[[16]byte]ReviewInfoObject) + + for _, reviewWithHash := range reviewsList{ + + reviewHash := reviewWithHash.ReviewHash + reviewBytes := reviewWithHash.ReviewBytes + + ableToRead, _, reviewNetworkType, reviewAuthor, reviewBroadcastTime, currentReviewType, currentReviewedHash, reviewVerdict, reviewMap, err := ReadReview(false, reviewBytes) + if (err != nil) { return nil, nil, err } + if (ableToRead == false){ + return nil, nil, errors.New("getModeratorNewestVerdictMapsFromReviewsList called with invalid review.") + } + if (reviewNetworkType != networkType){ + continue + } + + if (currentReviewType != reviewType){ + continue + } + + areEqual := bytes.Equal(currentReviewedHash, reviewedHash) + if (areEqual == false){ + continue + } + + isValid, err := verifyReviewIsValid(reviewMap) + if (err != nil) { return nil, nil, err } + if (isValid == false){ + continue + } + + existingNewestReviewInfoObject, exists := newestReviewsMap[reviewAuthor] + if (exists == true){ + + existingNewestReviewBroadcastTime := existingNewestReviewInfoObject.BroadcastTime + + if (existingNewestReviewBroadcastTime == reviewBroadcastTime){ + // The reviewer must be malicious, or their computer clock was skewed somehow + // We compare the review hashes to see which review to use + // This is necessary so that a moderator's newest review will always be calculated the same way by all Seekia clients. + + existingNewestReviewHash := existingNewestReviewInfoObject.ReviewHash + + compareValue := bytes.Compare(reviewHash[:], existingNewestReviewHash[:]) + if (compareValue == 0){ + // This should not happen, because this function should be called with a list of unique reviews + return nil, nil, errors.New("getModeratorNewestVerdictMapsFromReviewsList called with reviewsList containing duplicate review.") + } + + if (compareValue == -1){ + continue + } + + } else if (existingNewestReviewBroadcastTime > reviewBroadcastTime){ + // This review was overwritten/replaced by a newer review + continue + } + } + + newNewestReviewInfoObject := ReviewInfoObject{ + ReviewHash: reviewHash, + BroadcastTime: reviewBroadcastTime, + Verdict: reviewVerdict, + } + + newestReviewsMap[reviewAuthor] = newNewestReviewInfoObject + } + + // Map Structure: Identity hash -> Time of approve review + approveAdvocatesMap := make(map[[16]byte]int64) + + // Map Structure: Identity hash -> Time of ban review + banAdvocatesMap := make(map[[16]byte]int64) + + for reviewerIdentityHash, reviewerNewestReviewInfoObject := range newestReviewsMap{ + + newestReviewVerdict := reviewerNewestReviewInfoObject.Verdict + reviewBroadcastTime := reviewerNewestReviewInfoObject.BroadcastTime + + if (newestReviewVerdict == "Approve"){ + + approveAdvocatesMap[reviewerIdentityHash] = reviewBroadcastTime + + continue + + } else if (newestReviewVerdict == "Ban"){ + + banAdvocatesMap[reviewerIdentityHash] = reviewBroadcastTime + + continue + + } else if (newestReviewVerdict == "None"){ + + continue + } + + return nil, nil, errors.New("ReadReview returning invalid verdict: " + newestReviewVerdict) + } + + return approveAdvocatesMap, banAdvocatesMap, nil +} + + +//Outputs: +// -bool: Review exists (this means the moderator has banned the identity) +// -[]byte: Newest Review bytes +// -map[string]string: Newest Review map +// -error +func GetModeratorNewestIdentityReviewFromReviewsList(inputReviewsList []ReviewWithHash, moderatorIdentityHash [16]byte, reviewedIdentityHash [16]byte, networkType byte)(bool, []byte, map[string]string, error){ + + isValid := helpers.VerifyNetworkType(networkType) + if (isValid == false){ + networkTypeString := helpers.ConvertByteToString(networkType) + return false, nil, nil, errors.New("GetModeratorNewestIdentityReviewFromReviewsList called with invalid networkType: " + networkTypeString) + } + + checkIfReviewIsValid := func(_ map[string]string)(bool, error){ + return true, nil + } + + reviewExists, reviewBytes, reviewMap, _, _, err := getNewestModeratorReviewFromReviewsList(inputReviewsList, moderatorIdentityHash, "Identity", reviewedIdentityHash[:], networkType, checkIfReviewIsValid) + + return reviewExists, reviewBytes, reviewMap, err +} + +//Outputs: +// -bool: Review exists +// -[]byte: Newest review bytes +// -map[string]string: Newest review map +// -string: Review verdict +// -int64: Time of review +// -error +func GetModeratorNewestProfileReviewFromReviewsList(inputReviewsList []ReviewWithHash, moderatorIdentityHash [16]byte, profileHash [28]byte, profileNetworkType byte)(bool, []byte, map[string]string, string, int64, error){ + + isValid := helpers.VerifyNetworkType(profileNetworkType) + if (isValid == false){ + profileNetworkTypeString := helpers.ConvertByteToString(profileNetworkType) + return false, nil, nil, "", 0, errors.New("GetModeratorNewestProfileReviewFromReviewsList called with invalid profileNetworkType: " + profileNetworkTypeString) + } + + verifyReviewIsValidFunction := func(_ map[string]string)(bool, error){ + return true, nil + } + + reviewExists, reviewBytes, reviewMap, reviewVerdict, reviewBroadcastTime, err := getNewestModeratorReviewFromReviewsList(inputReviewsList, moderatorIdentityHash, "Profile", profileHash[:], profileNetworkType, verifyReviewIsValidFunction) + + return reviewExists, reviewBytes, reviewMap, reviewVerdict, reviewBroadcastTime, err +} + +//Outputs: +// -bool: Review exists +// -[]byte: Newest review bytes +// -map[string]string: Newest review map +// -string: Newest Review verdict +// -int64: Time of review +// -error +func GetModeratorNewestProfileAttributeReviewFromReviewsList(inputReviewsList []ReviewWithHash, moderatorIdentityHash [16]byte, attributeHash [27]byte, attributeNetworkType byte)(bool, []byte, map[string]string, string, int64, error){ + + isValid := helpers.VerifyNetworkType(attributeNetworkType) + if (isValid == false){ + attributeNetworkTypeString := helpers.ConvertByteToString(attributeNetworkType) + return false, nil, nil, "", 0, errors.New("GetModeratorNewestProfileAttributeReviewFromReviewsList called with invalid attributeNetworkType: " + attributeNetworkTypeString) + } + + verifyReviewIsValidFunction := func(_ map[string]string)(bool, error){ + return true, nil + } + + reviewExists, reviewBytes, reviewMap, reviewVerdict, verdictTime, err := getNewestModeratorReviewFromReviewsList(inputReviewsList, moderatorIdentityHash, "Attribute", attributeHash[:], attributeNetworkType, verifyReviewIsValidFunction) + + return reviewExists, reviewBytes, reviewMap, reviewVerdict, verdictTime, err +} + +//Outputs: +// -bool: Review exists +// -[]byte: Newest review bytes +// -map[string]string: Newest review map +// -string: Review verdict +// -error +func GetModeratorNewestMessageReviewFromReviewsList(inputReviewsList []ReviewWithHash, moderatorIdentityHash [16]byte, messageHash [26]byte, messageNetworkType byte, messageCipherKeyHash [25]byte)(bool, []byte, map[string]string, string, error){ + + isValid := helpers.VerifyNetworkType(messageNetworkType) + if (isValid == false){ + messageNetworkTypeString := helpers.ConvertByteToString(messageNetworkType) + return false, nil, nil, "", errors.New("GetModeratorNewestMessageReviewFromReviewsList called with invalid messageNetworkType: " + messageNetworkTypeString) + } + + verifyReviewIsValidFunction := func(reviewMap map[string]string)(bool, error){ + + isValid, err := VerifyMessageReviewCipherKey(reviewMap, messageCipherKeyHash) + + return isValid, err + } + + reviewExists, reviewBytes, reviewMap, reviewVerdict, _, err := getNewestModeratorReviewFromReviewsList(inputReviewsList, moderatorIdentityHash, "Message", messageHash[:], messageNetworkType, verifyReviewIsValidFunction) + + return reviewExists, reviewBytes, reviewMap, reviewVerdict, err +} + +// This function takes input of a list of reviews +// Returns newest review for a specified moderator, reviewedHash and reviewType +// Will omit review with "None" verdict +// Inputs: +// -[][]byte: List of reviews created by moderator +// -[16]byte: Identity hash of moderator +// -byte: Network type +// -string: Review Type +// -[]byte: Reviewed hash +// -func(map[string]string)(bool, error): Takes review map as input, returns if review is valid +//Outputs: +// -bool: Review exists (will be false if newest review is a None verdict) +// -[]byte: Newest review bytes +// -map[string]string: Newest review map +// -string: Newest review verdict +// -int64: Newest review broadcast time +// -error +func getNewestModeratorReviewFromReviewsList(inputReviewsList []ReviewWithHash, moderatorIdentityHash [16]byte, reviewType string, reviewedHash []byte, networkType byte, verifyReviewIsValid func(map[string]string)(bool, error))(bool, []byte, map[string]string, string, int64, error){ + + isValid, err := identity.VerifyIdentityHash(moderatorIdentityHash, true, "Moderator") + if (err != nil) { return false, nil, nil, "", 0, err } + if (isValid == false){ + moderatorIdentityHashHex := encoding.EncodeBytesToHexString(moderatorIdentityHash[:]) + return false, nil, nil, "", 0, errors.New("getNewestModeratorReviewFromReviewsList called with invalid moderatorIdentityHash: " + moderatorIdentityHashHex) + } + + if (reviewType != "Identity" && reviewType != "Profile" && reviewType != "Attribute" && reviewType != "Message"){ + return false, nil, nil, "", 0, errors.New("getNewestModeratorReviewFromReviewsList called with invalid reviewType: " + reviewType) + } + + isValid = helpers.VerifyNetworkType(networkType) + if (isValid == false){ + networkTypeString := helpers.ConvertByteToString(networkType) + return false, nil, nil, "", 0, errors.New("getNewestModeratorReviewFromReviewsList called with invalid networkType: " + networkTypeString) + } + + anyReviewFound := false + newestReviewHash := [29]byte{} + newestReviewBytes := make([]byte, 0) + newestReviewMap := make(map[string]string) + newestReviewBroadcastTime := int64(0) + newestReviewVerdict := "" + + for _, reviewWithHash := range inputReviewsList{ + + reviewHash := reviewWithHash.ReviewHash + reviewBytes := reviewWithHash.ReviewBytes + + ableToRead, _, reviewNetworkType, reviewAuthor, reviewBroadcastTime, currentReviewType, currentReviewedHash, reviewVerdict, reviewMap, err := ReadReview(false, reviewBytes) + if (err != nil) { return false, nil, nil, "", 0, err } + if (ableToRead == false){ + return false, nil, nil, "", 0, errors.New("getNewestModeratorReviewFromReviewsList called with invalid review") + } + if (reviewNetworkType != networkType){ + continue + } + + if (reviewAuthor != moderatorIdentityHash){ + continue + } + + if (currentReviewType != reviewType) { + continue + } + + areEqual := bytes.Equal(currentReviewedHash, reviewedHash) + if (areEqual == false){ + continue + } + + reviewIsValid, err := verifyReviewIsValid(reviewMap) + if (err != nil){ return false, nil, nil, "", 0, err } + if (reviewIsValid == false){ + continue + } + + if (anyReviewFound == false || reviewBroadcastTime >= newestReviewBroadcastTime){ + + if (reviewBroadcastTime == newestReviewBroadcastTime){ + // The reviewer must be malicious, or their computer clock was skewed somehow + // We compare the review hashes to see which review to use + // This is necessary so that a moderator's newest review will always be calculated the same way by all Seekia clients. + + compareValue := bytes.Compare(reviewHash[:], newestReviewHash[:]) + if (compareValue == 0){ + // This should not happen, because this function should be called with a list of unique reviews + return false, nil, nil, "", 0, errors.New("getNewestModeratorReviewFromReviewsList called with reviewsList containing duplicate review.") + } + + if (compareValue == -1){ + continue + } + } + + anyReviewFound = true + newestReviewHash = reviewHash + newestReviewBytes = reviewBytes + newestReviewMap = reviewMap + newestReviewBroadcastTime = reviewBroadcastTime + newestReviewVerdict = reviewVerdict + } + } + + if (anyReviewFound == false){ + return false, nil, nil, "", 0, nil + } + if (newestReviewVerdict == "None"){ + return false, nil, nil, "", 0, nil + } + + return true, newestReviewBytes, newestReviewMap, newestReviewVerdict, newestReviewBroadcastTime, nil +} + + +// This function returns a map of reviews created by a particular moderator, of a specific reviewType (if provided) +// The map stores the moderator's newest review for each piece of content +// The function will omit reviews whose newest verdict is "None" +// The function will return error if any review is not created by moderator +// The function does not verify the reviews +//Outputs: +// -map[string][]byte: Reviewed Hash (bytes encoded as string) -> Newest Review bytes +// -error +func GetNewestModeratorReviewsMapFromReviewsList(inputReviewsList []ReviewWithHash, moderatorIdentityHash [16]byte, networkType byte, reviewTypeProvided bool, reviewType string)(map[string][]byte, error){ + + isValid, err := identity.VerifyIdentityHash(moderatorIdentityHash, true, "Moderator") + if (err != nil) { return nil, err } + if (isValid == false){ + + moderatorIdentityHashHex := encoding.EncodeBytesToHexString(moderatorIdentityHash[:]) + + return nil, errors.New("GetNewestModeratorReviewsMapFromReviewsList called with invalid moderatorIdentityHash: " + moderatorIdentityHashHex) + } + + isValid = helpers.VerifyNetworkType(networkType) + if (isValid == false){ + networkTypeString := helpers.ConvertByteToString(networkType) + + return nil, errors.New("GetNewestModeratorReviewsMapFromReviewsList called with invalid networkType: " + networkTypeString) + } + + if (reviewTypeProvided == true){ + if (reviewType != "Identity" && reviewType != "Profile" && reviewType != "Attribute" && reviewType != "Message"){ + return nil, errors.New("GetNewestModeratorReviewsMapFromReviewsList called with invalid reviewType: " + reviewType) + } + } + + type ReviewInfoObject struct{ + ReviewHash [29]byte + Verdict string + BroadcastTime int64 + ReviewBytes []byte + } + + // This map stores the newest review info for each reviewedHash + // Map Structure: Reviewed Hash -> Review info object of newest review + newestReviewInfoObjectsMap := make(map[string]ReviewInfoObject) + + for _, reviewWithHash := range inputReviewsList{ + + reviewHash := reviewWithHash.ReviewHash + reviewBytes := reviewWithHash.ReviewBytes + + ableToRead, _, reviewNetworkType, reviewAuthor, reviewBroadcastTime, currentReviewType, reviewedHash, reviewVerdict, _, err := ReadReview(false, reviewBytes) + if (err != nil) { return nil, err } + if (ableToRead == false){ + return nil, errors.New("GetNewestModeratorReviewsMapFromReviewsList called with invalid review") + } + if (reviewNetworkType != networkType){ + continue + } + + if (reviewAuthor != moderatorIdentityHash){ + return nil, errors.New("GetNewestModeratorReviewsMapFromReviewsList receiving review by different reviewer") + } + + if (reviewTypeProvided == true && currentReviewType != reviewType){ + continue + } + + existingNewestReviewInfoObject, exists := newestReviewInfoObjectsMap[string(reviewedHash)] + if (exists == true){ + + existingBroadcastTime := existingNewestReviewInfoObject.BroadcastTime + if (exists == false){ + return nil, errors.New("reviewBroadcastTimesMap missing reviewHash entry") + } + + if (existingBroadcastTime == reviewBroadcastTime){ + // The reviewer must be malicious, or their computer clock was skewed somehow + // We compare the review hashes to see which review to use + // This is necessary so that a moderator's newest review will always be calculated the same way by all Seekia clients. + + existingNewestReviewHash := existingNewestReviewInfoObject.ReviewHash + + compareValue := bytes.Compare(reviewHash[:], existingNewestReviewHash[:]) + if (compareValue == 0){ + // This should not happen, because this function should be called with a list of unique reviews + return nil, errors.New("GetNewestModeratorReviewsMapFromReviewsList called with reviewsList containing duplicate review.") + } + + if (compareValue == -1){ + continue + } + + } else if (existingBroadcastTime > reviewBroadcastTime){ + // The user has replaced their review with a newer one + continue + } + } + + newNewestReviewInfoObject := ReviewInfoObject{ + ReviewHash: reviewHash, + Verdict: reviewVerdict, + BroadcastTime: reviewBroadcastTime, + ReviewBytes: reviewBytes, + } + + newestReviewInfoObjectsMap[string(reviewedHash)] = newNewestReviewInfoObject + } + + //Map Structure: Reviewed Hash -> Newest Review bytes + newestReviewsMap := make(map[string][]byte) + + for reviewedHash, reviewInfoObject := range newestReviewInfoObjectsMap{ + + currentVerdict := reviewInfoObject.Verdict + + if (currentVerdict == "None"){ + continue + } + + reviewBytes := reviewInfoObject.ReviewBytes + + newestReviewsMap[reviewedHash] = reviewBytes + } + + return newestReviewsMap, nil +} + + + diff --git a/internal/moderation/reportStorage/reportStorage.go b/internal/moderation/reportStorage/reportStorage.go new file mode 100644 index 0000000..e755e3a --- /dev/null +++ b/internal/moderation/reportStorage/reportStorage.go @@ -0,0 +1,303 @@ + +// reportStorage provides functions to manage stored reports. +// Reports are created by users to report unruleful content. + +package reportStorage + +import "seekia/internal/badgerDatabase" +import "seekia/internal/encoding" +import "seekia/internal/helpers" +import "seekia/internal/messaging/readMessages" +import "seekia/internal/moderation/readReports" +import "seekia/internal/mySettings" + +import "bytes" +import "errors" + + +func GetNumberOfStoredReports()(int64, error){ + + numberOfReports, err := badgerDatabase.GetNumberOfReports() + if (err != nil) { return 0, err } + + return numberOfReports, nil +} + + +//Outputs: +// -bool: Report is well formed +// -error +func AddReport(newReport []byte)(bool, error){ + + ableToRead, reportHash, _, _, _, reportType, reportedHash, _, err := readReports.ReadReportAndHash(true, newReport) + if (err != nil){ return false, err } + if (ableToRead == false){ + // Report is malformed. + // Host who sent report must be malicious. + return false, nil + } + + exists, _, err := badgerDatabase.GetReport(reportHash) + if (err != nil) { return false, err } + if (exists == true){ + // Report is already imported. + return true, nil + } + + err = badgerDatabase.AddReport(reportHash, newReport) + if (err != nil) { return false, err } + + err = mySettings.SetSetting("ViewedContentNeedsRefreshYesNo", "Yes") + if (err != nil) { return false, err } + + if (reportType == "Identity"){ + + if (len(reportedHash) != 16){ + return false, errors.New("ReadReport returning invalid length reportedHash for Identity report.") + } + + reportedIdentityHash := [16]byte(reportedHash) + + err := badgerDatabase.AddIdentityReportToList(reportedIdentityHash, reportHash) + if (err != nil) { return false, err } + + return true, nil + } + if (reportType == "Profile"){ + + if (len(reportedHash) != 28){ + return false, errors.New("ReadReport returning invalid length reportedHash for Profile report.") + } + + reportedProfileHash := [28]byte(reportedHash) + + err = badgerDatabase.AddProfileReportToList(reportedProfileHash, reportHash) + if (err != nil) { return false, err } + + return true, nil + } + if (reportType == "Attribute"){ + + if (len(reportedHash) != 27){ + return false, errors.New("ReadReport returning invalid length reportedHash for Attribute report.") + } + + reportedAttributeHash := [27]byte(reportedHash) + + err = badgerDatabase.AddProfileAttributeReportToList(reportedAttributeHash, reportHash) + if (err != nil) { return false, err } + + return true, nil + } + if (reportType == "Message"){ + + if (len(reportedHash) != 26){ + return false, errors.New("ReadReport returning invalid length reportedHash for Message report.") + } + + reportedMessageHash := [26]byte(reportedHash) + + err = badgerDatabase.AddMessageReportToList(reportedMessageHash, reportHash) + if (err != nil) { return false, err } + + return true, nil + } + + return false, errors.New("ReadReport returning invalid reportType: " + reportType) +} + + +func GetNumberOfReportsForReportedHash(reportedHash []byte, networkType byte)(int64, error){ + + reportedType, err := helpers.GetReportedTypeFromReportedHash(reportedHash) + if (err != nil){ + reportedHashHex := encoding.EncodeBytesToHexString(reportedHash) + return 0, errors.New("GetNumberOfReportsForReportedHash called with invalid reportedHash: " + reportedHashHex) + } + + isValid := helpers.VerifyNetworkType(networkType) + if (isValid == false){ + networkTypeString := helpers.ConvertByteToString(networkType) + return 0, errors.New("GetNumberOfReportsForReportedHash called with invalid networkType: " + networkTypeString) + } + + //Outputs: + // -bool: Any report hashes found + // -[][30]byte: Report hashes list for reportedHash + // -error + getReportHashesList := func()(bool, [][30]byte, error){ + + if (reportedType == "Identity"){ + + if (len(reportedHash) != 16){ + return false, nil, errors.New("GetReportedTypeFromReportedHash returning Identity for invalid length identity hash.") + } + + reportedIdentityHash := [16]byte(reportedHash) + + exists, reportHashesList, err := badgerDatabase.GetIdentityReportsList(reportedIdentityHash) + + return exists, reportHashesList, err + } + if (reportedType == "Profile"){ + + if (len(reportedHash) != 28){ + return false, nil, errors.New("GetReportedTypeFromReportedHash returning Profile for invalid length profile hash.") + } + + reportedProfileHash := [28]byte(reportedHash) + + exists, reportHashesList, err := badgerDatabase.GetProfileReportsList(reportedProfileHash) + + return exists, reportHashesList, err + } + if (reportedType == "Attribute"){ + + if (len(reportedHash) != 27){ + return false, nil, errors.New("GetReportedTypeFromReportedHash returning Attribute for invalid length attribute hash.") + } + + reportedAttributeHash := [27]byte(reportedHash) + + exists, reportedHashesList, err := badgerDatabase.GetProfileAttributeReportsList(reportedAttributeHash) + + return exists, reportedHashesList, err + } + if (reportedType == "Message"){ + + if (len(reportedHash) != 26){ + return false, nil, errors.New("GetReportedTypeFromReportedHash returning Message for invalid length message hash.") + } + + reportedMessageHash := [26]byte(reportedHash) + + exists, reportHashesList, err := badgerDatabase.GetMessageReportsList(reportedMessageHash) + + return exists, reportHashesList, err + } + + return false, nil, errors.New("GetReportedTypeFromReportedHash returning invalid reportedType: " + reportedType) + } + + exists, reportHashesList, err := getReportHashesList() + if (err != nil) { return 0, err } + if (exists == false){ + return 0, nil + } + + numberOfReports := int64(0) + + for _, reportHash := range reportHashesList{ + + exists, reportBytes, err := badgerDatabase.GetReport(reportHash) + if (err != nil) { return 0, err } + if (exists == false){ + // Report has been deleted, reports list will be updated in the background + continue + } + + ableToRead, _, reportNetworkType, _, reportType, reportReportedHash, _, err := readReports.ReadReport(false, reportBytes) + if (err != nil) { return 0, err } + if (ableToRead == false){ + return 0, errors.New("Database corrupt: Contains invalid report.") + } + if (reportNetworkType != networkType){ + continue + } + if (reportType != reportedType){ + return 0, errors.New("getReportHashesList returning report of different reportedType.") + } + areEqual := bytes.Equal(reportedHash, reportReportedHash) + if (areEqual == false){ + return 0, errors.New("getReportHashesList returning report for different reportedHash.") + } + + numberOfReports += 1 + } + + return numberOfReports, nil +} + + +// We use this function to find a valid message cipher key to decrypt a message for moderation +// We verify the message CipherKeyHash to make sure the author of the report has seen the message +// If the cipher key does not decrypt the message, then we know the author of the report and the message are malicious +//Outputs: +// -bool: Message cipher key found +// -[32]byte: Message Cipher key +// -error +func GetMessageCipherKeyFromAnyReport(messageHash [26]byte, messageNetworkType byte, messageCipherKeyHash [25]byte)(bool, [32]byte, error){ + + isValid := helpers.VerifyNetworkType(messageNetworkType) + if (isValid == false){ + messageNetworkTypeString := helpers.ConvertByteToString(messageNetworkType) + return false, [32]byte{}, errors.New("GetMessageCipherKeyFromAnyReport called with invalid messageNetworkType: " + messageNetworkTypeString) + } + + exists, reportHashesList, err := badgerDatabase.GetMessageReportsList(messageHash) + if (err != nil) { return false, [32]byte{}, err } + if (exists == false){ + return false, [32]byte{}, nil + } + if (len(reportHashesList) == 0){ + return false, [32]byte{}, nil + } + + for _, reportHash := range reportHashesList{ + + exists, reportBytes, err := badgerDatabase.GetReport(reportHash) + if (err != nil) { return false, [32]byte{}, err } + if (exists == false){ + // Report must have been deleted. Database will be updated automatically. + continue + } + ableToRead, _, reportNetworkType, _, reportType, reportedHash, reportMap, err := readReports.ReadReport(false, reportBytes) + if (err != nil) { return false, [32]byte{}, err } + if (ableToRead == false){ + return false, [32]byte{}, errors.New("Database corrupt: Contains invalid report.") + } + if (reportNetworkType != messageNetworkType){ + // Person who created this report must be malicious + // We skip the report, even if we could use it to retrieve the message cipher key hash + // This is because we want all moderators to see the same reports, regardless of if they previously accessed a different network + continue + } + + if (reportType != "Message") { + return false, [32]byte{}, errors.New("Corrupt database: ReportType does not match reports list.") + } + + areEqual := bytes.Equal(reportedHash, messageHash[:]) + if (areEqual == false){ + return false, [32]byte{}, errors.New("Corrupt database: Reported message does not match reports list.") + } + + messageCipherKeyString, exists := reportMap["MessageCipherKey"] + if (exists == false) { + return false, [32]byte{}, errors.New("Corrupt database: Contains report missing MessageCipherKey") + } + + messageCipherKey, err := readMessages.ReadMessageCipherKeyHex(messageCipherKeyString) + if (err != nil){ + return false, [32]byte{}, errors.New("Corrupt database: Contains report with invalid MessageCipherKey: " + messageCipherKeyString + ". Reason: " + err.Error()) + } + + currentMessageCipherKeyHash, err := readMessages.ConvertMessageCipherKeyToCipherKeyHash(messageCipherKey) + if (err != nil){ return false, [32]byte{}, err } + + if (currentMessageCipherKeyHash != messageCipherKeyHash){ + // Report author is malicious. + continue + } + + return true, messageCipherKey, nil + } + + // No valid reports found for message + + return false, [32]byte{}, nil +} + + + + diff --git a/internal/moderation/reviewStorage/reviewStorage.go b/internal/moderation/reviewStorage/reviewStorage.go new file mode 100644 index 0000000..5be1d5e --- /dev/null +++ b/internal/moderation/reviewStorage/reviewStorage.go @@ -0,0 +1,1042 @@ + +// reviewStorage provides functions to manage stored moderator reviews +// Reviews are created by moderators and determine the moderation verdict of messages, profiles, and identities. + +package reviewStorage + +import "seekia/internal/badgerDatabase" +import "seekia/internal/contentMetadata" +import "seekia/internal/encoding" +import "seekia/internal/helpers" +import "seekia/internal/identity" +import "seekia/internal/messaging/readMessages" +import "seekia/internal/moderation/readReviews" +import "seekia/internal/mySettings" +import "seekia/internal/network/appNetworkType/getAppNetworkType" +import "seekia/internal/network/backgroundDownloads" +import "seekia/internal/profiles/readProfiles" + +import "bytes" +import "errors" + +func GetNumberOfStoredReviews()(int64, error){ + + numberOfReviews, err := badgerDatabase.GetNumberOfReviews() + if (err != nil) { return 0, err } + + return numberOfReviews, nil +} + + +//Outputs: +// -bool: Review is well formed +// -error +func AddReview(newReview []byte)(bool, error){ + + ableToRead, reviewHash, _, _, reviewerIdentityHash, _, reviewType, reviewedHash, _, _, err := readReviews.ReadReviewAndHash(true, newReview) + if (err != nil){ return false, err } + if (ableToRead == false){ + // Review is malformed, do nothing + // Host who sent review must be malicious. + return false, nil + } + + exists, _, err := badgerDatabase.GetReview(reviewHash) + if (err != nil) { return false, err } + if (exists == true){ + // Review already imported, nothing to do. + return true, nil + } + + err = badgerDatabase.AddReview(reviewHash, newReview) + if (err != nil) { return false, err } + + err = mySettings.SetSetting("ViewedContentNeedsRefreshYesNo", "Yes") + if (err != nil) { return false, err } + + err = badgerDatabase.AddReviewerReviewHash(reviewerIdentityHash, reviewHash) + if (err != nil) { return false, err } + + if (reviewType == "Identity"){ + + if (len(reviewedHash) != 16){ + reviewedHashHex := encoding.EncodeBytesToHexString(reviewedHash) + return false, errors.New("ReadReview returning invalid length reviewed Identity hash: " + reviewedHashHex) + } + + reviewedIdentityHash := [16]byte(reviewedHash) + + err := badgerDatabase.AddIdentityReviewToList(reviewedIdentityHash, reviewHash) + if (err != nil) { return false, err } + + return true, nil + } + if (reviewType == "Profile"){ + + if (len(reviewedHash) != 28){ + reviewedHashHex := encoding.EncodeBytesToHexString(reviewedHash) + return false, errors.New("ReadReview returning invalid length reviewed Profile hash: " + reviewedHashHex) + } + + reviewedProfileHash := [28]byte(reviewedHash) + + err = badgerDatabase.AddProfileReviewToList(reviewedProfileHash, reviewHash) + if (err != nil) { return false, err } + + return true, nil + } + if (reviewType == "Attribute"){ + + if (len(reviewedHash) != 27){ + reviewedHashHex := encoding.EncodeBytesToHexString(reviewedHash) + return false, errors.New("ReadReview returning invalid length reviewed Attribute hash: " + reviewedHashHex) + } + + reviewedAttributeHash := [27]byte(reviewedHash) + + err = badgerDatabase.AddProfileAttributeReviewToList(reviewedAttributeHash, reviewHash) + if (err != nil) { return false, err } + + return true, nil + } + if (reviewType == "Message"){ + + if (len(reviewedHash) != 26){ + reviewedHashHex := encoding.EncodeBytesToHexString(reviewedHash) + return false, errors.New("ReadReview returning invalid length reviewed Message hash: " + reviewedHashHex) + } + + reviewedMessageHash := [26]byte(reviewedHash) + + err = badgerDatabase.AddMessageReviewToList(reviewedMessageHash, reviewHash) + if (err != nil) { return false, err } + + return true, nil + } + + return false, errors.New("ReadReview returning invalid reviewType: " + reviewType) +} + +//Outputs: +// -bool: Review exists +// -[]byte: Review bytes +// -map[string]string: Review map +// -error +func GetModeratorNewestIdentityReview(moderatorIdentityHash [16]byte, reviewedIdentityHash [16]byte, networkType byte)(bool, []byte, map[string]string, error){ + + isValid, err := identity.VerifyIdentityHash(moderatorIdentityHash, true, "Moderator") + if (err != nil) { return false, nil, nil, err } + if (isValid == false){ + moderatorIdentityHashHex := encoding.EncodeBytesToHexString(moderatorIdentityHash[:]) + return false, nil, nil, errors.New("GetModeratorNewestIdentityReview called with invalid moderatorIdentityHash: " + moderatorIdentityHashHex) + } + + isValid, err = identity.VerifyIdentityHash(reviewedIdentityHash, false, "") + if (err != nil) { return false, nil, nil, err } + if (isValid == false){ + reviewedIdentityHashHex := encoding.EncodeBytesToHexString(reviewedIdentityHash[:]) + return false, nil, nil, errors.New("GetModeratorNewestIdentityReview called with invalid reviewedIdentityHash: " + reviewedIdentityHashHex) + } + + isValid = helpers.VerifyNetworkType(networkType) + if (isValid == false){ + networkTypeString := helpers.ConvertByteToString(networkType) + return false, nil, nil, errors.New("GetModeratorNewestIdentityReview called with invalid networkType: " + networkTypeString) + } + + anyReviewsExist, reviewHashesList, err := badgerDatabase.GetIdentityReviewsList(reviewedIdentityHash) + if (err != nil) { return false, nil, nil, err } + if (anyReviewsExist == false){ + return false, nil, nil, nil + } + + reviewsList := make([]readReviews.ReviewWithHash, 0) + + for _, reviewHash := range reviewHashesList{ + + exists, reviewBytes, err := badgerDatabase.GetReview(reviewHash) + if (err != nil) { return false, nil, nil, err } + if (exists == false){ + // Review has been deleted. This missing entry will be removed automatically. + continue + } + + newReviewObject := readReviews.ReviewWithHash{ + ReviewHash: reviewHash, + ReviewBytes: reviewBytes, + } + + reviewsList = append(reviewsList, newReviewObject) + } + + verdictExists, reviewBytes, newestReviewMap, err := readReviews.GetModeratorNewestIdentityReviewFromReviewsList(reviewsList, moderatorIdentityHash, reviewedIdentityHash, networkType) + if (err != nil) { return false, nil, nil, err } + if (verdictExists == false){ + return false, nil, nil, nil + } + return true, reviewBytes, newestReviewMap, nil +} + + +//Outputs: +// -bool: Profile verdict exists (At least 1 attribute ban, or a full profile approve/ban review exists) +// -string: Profile verdict "Ban"/"Approve" (incorporates full profile reviews and attribute bans) +// -bool: Full profile review exists +// -[]byte: Full profile review bytes +// -map[string]string: Full profile review map +// -map[[27]byte][]byte: Map of attribute approve reviews (Attribute hash -> Attribute review) +// -map[[27]byte][]byte: Map of attribute ban reviews (Attribute hash -> Attribute review) +// -error +func GetModeratorNewestProfileReviews(moderatorIdentityHash [16]byte, profileHash [28]byte, profileNetworkType byte, profileAttributeHashesList [][27]byte)(bool, string, bool, []byte, map[string]string, map[[27]byte][]byte, map[[27]byte][]byte, error){ + + isValid := helpers.VerifyNetworkType(profileNetworkType) + if (isValid == false){ + profileNetworkTypeString := helpers.ConvertByteToString(profileNetworkType) + return false, "", false, nil, nil, nil, nil, errors.New("GetModeratorNewestProfileReviews called with invalid profileNetworkType: " + profileNetworkTypeString) + } + + fullProfileReviewExists, fullProfileReviewBytes, fullProfileReviewMap, fullProfileReviewVerdict, fullProfileReviewTime, err := getModeratorNewestFullProfileReview(moderatorIdentityHash, profileHash, profileNetworkType) + if (err != nil) { return false, "", false, nil, nil, nil, nil, err } + + // Full profile approvals can be overwritten by an attribute ban of any attribute within the profile + // We check for attribute reviews + + //Map Structure: Attribute hash -> Attribute review bytes + approvedAttributesMap := make(map[[27]byte][]byte) + + //Map Structure: Attribute hash -> Attribute review bytes + bannedAttributesMap := make(map[[27]byte][]byte) + + for _, attributeHash := range profileAttributeHashesList{ + + attributeReviewExists, attributeReviewType, attributeReviewBytes, _, attributeReviewVerdict, attributeReviewTime, err := GetModeratorNewestProfileAttributeReview(moderatorIdentityHash, attributeHash, profileNetworkType, false) + if (err != nil){ return false, "", false, nil, nil, nil, nil, err } + if (attributeReviewExists == false){ + continue + } + + if (attributeReviewType != "Attribute"){ + return false, "", false, nil, nil, nil, nil, errors.New("GetModeratorNewestProfileAttributeReview returning non-attribute review when integrateFullProfileApprovals == false") + } + + if (attributeReviewVerdict == "Approve"){ + // Attribute approve reviews do not change full profile ban reviews + approvedAttributesMap[attributeHash] = attributeReviewBytes + continue + } + + if (fullProfileReviewExists == true && fullProfileReviewVerdict == "Approve"){ + if (fullProfileReviewTime > attributeReviewTime){ + // This attribute ban was created before the moderator approved the full profile + // It has therefore been overwritten + continue + } + } + + bannedAttributesMap[attributeHash] = attributeReviewBytes + } + + if (len(bannedAttributesMap) > 0){ + + // Profile is banned by moderator + + if (fullProfileReviewExists == true && fullProfileReviewVerdict == "Approve"){ + // An attribute was banned, which overwrites the full profile review + + return true, "Ban", false, nil, nil, approvedAttributesMap, bannedAttributesMap, nil + } + + return true, "Ban", fullProfileReviewExists, fullProfileReviewBytes, fullProfileReviewMap, approvedAttributesMap, bannedAttributesMap, nil + } + + if (fullProfileReviewExists == false){ + return false, "", false, nil, nil, approvedAttributesMap, bannedAttributesMap, nil + } + + return true, fullProfileReviewVerdict, true, fullProfileReviewBytes, fullProfileReviewMap, approvedAttributesMap, bannedAttributesMap, nil +} + + +// This function does not integrate the moderator's attribute reviews +//Outputs: +// -bool: Review exists +// -[]byte: Review bytes +// -map[string]string: Review map +// -string: Review verdict +// -int64: Review broadcast time +// -error +func getModeratorNewestFullProfileReview(moderatorIdentityHash [16]byte, profileHash [28]byte, profileNetworkType byte)(bool, []byte, map[string]string, string, int64, error){ + + isValid, err := identity.VerifyIdentityHash(moderatorIdentityHash, true, "Moderator") + if (err != nil) { return false, nil, nil, "", 0, err } + if (isValid == false){ + moderatorIdentityHashHex := encoding.EncodeBytesToHexString(moderatorIdentityHash[:]) + return false, nil, nil, "", 0, errors.New("getModeratorNewestFullProfileReview called with invalid moderatorIdentityHash: " + moderatorIdentityHashHex) + } + + isValid, err = readProfiles.VerifyProfileHash(profileHash, false, "", true, false) + if (err != nil){ return false, nil, nil, "", 0, err } + if (isValid == false){ + profileHashHex := encoding.EncodeBytesToHexString(profileHash[:]) + return false, nil, nil, "", 0, errors.New("getModeratorNewestFullProfileReview called with invalid profileHash: " + profileHashHex) + } + + isValid = helpers.VerifyNetworkType(profileNetworkType) + if (isValid == false){ + profileNetworkTypeString := helpers.ConvertByteToString(profileNetworkType) + return false, nil, nil, "", 0, errors.New("getModeratorNewestFullProfileReview called with invalid profileNetworkType: " + profileNetworkTypeString) + } + + anyReviewsExist, reviewHashesList, err := badgerDatabase.GetProfileReviewsList(profileHash) + if (err != nil) { return false, nil, nil, "", 0, err } + if (anyReviewsExist == false){ + return false, nil, nil, "", 0, nil + } + + reviewsList := make([]readReviews.ReviewWithHash, 0) + + for _, reviewHash := range reviewHashesList{ + + exists, reviewBytes, err := badgerDatabase.GetReview(reviewHash) + if (err != nil) { return false, nil, nil, "", 0, err } + if (exists == false){ + // Review has been deleted. This missing entry will be removed automatically. + continue + } + + reviewObject := readReviews.ReviewWithHash{ + ReviewHash: reviewHash, + ReviewBytes: reviewBytes, + } + + reviewsList = append(reviewsList, reviewObject) + } + + reviewExists, reviewBytes, reviewMap, reviewVerdict, reviewTime, err := readReviews.GetModeratorNewestProfileReviewFromReviewsList(reviewsList, moderatorIdentityHash, profileHash, profileNetworkType) + if (err != nil) { return false, nil, nil, "", 0, err } + if (reviewExists == false){ + return false, nil, nil, "", 0, nil + } + + return true, reviewBytes, reviewMap, reviewVerdict, reviewTime, nil +} + + +//Outputs: +// -bool: Review exists +// -string: Review Type ("Profile"/"Attribute") +// -[]byte: Review bytes +// -map[string]string: Review map +// -string: Review verdict +// -int64: Time of review +// -error +func GetModeratorNewestProfileAttributeReview(moderatorIdentityHash [16]byte, attributeHash [27]byte, attributeNetworkType byte, integrateFullProfileApprovals bool)(bool, string, []byte, map[string]string, string, int64, error){ + + isValid, err := identity.VerifyIdentityHash(moderatorIdentityHash, true, "Moderator") + if (err != nil) { return false, "", nil, nil, "", 0, err } + if (isValid == false){ + moderatorIdentityHashHex := encoding.EncodeBytesToHexString(moderatorIdentityHash[:]) + return false, "", nil, nil, "", 0, errors.New("GetModeratorNewestProfileAttributeReview called with invalid moderatorIdentityHash: " + moderatorIdentityHashHex) + } + + isValid, err = readProfiles.VerifyAttributeHash(attributeHash, false, "", false, false) + if (err != nil) { return false, "", nil, nil, "", 0, err } + if (isValid == false){ + attributeHashHex := encoding.EncodeBytesToHexString(attributeHash[:]) + return false, "", nil, nil, "", 0, errors.New("GetModeratorNewestProfileAttributeReview called with invalid attributeHash: " + attributeHashHex) + } + + isValid = helpers.VerifyNetworkType(attributeNetworkType) + if (isValid == false){ + attributeNetworkTypeString := helpers.ConvertByteToString(attributeNetworkType) + return false, "", nil, nil, "", 0, errors.New("GetModeratorNewestProfileAttributeReview called with invalid attributeNetworkType: " + attributeNetworkTypeString) + } + + //Outputs: + // -bool: Attribute review exists + // -[]byte: Review bytes + // -map[string]string: Review map + // -string: Review verdict + // -int64: Review broadcast time + // -error + getModeratorNewestAttributeReview := func()(bool, []byte, map[string]string, string, int64, error){ + + anyReviewsExist, reviewHashesList, err := badgerDatabase.GetProfileAttributeReviewsList(attributeHash) + if (err != nil) { return false, nil, nil, "", 0, err } + if (anyReviewsExist == false){ + return false, nil, nil, "", 0, nil + } + + reviewsList := make([]readReviews.ReviewWithHash, 0) + + for _, reviewHash := range reviewHashesList{ + + exists, reviewBytes, err := badgerDatabase.GetReview(reviewHash) + if (err != nil) { return false, nil, nil, "", 0, err } + if (exists == false){ + // Review has been deleted. This missing entry will be removed automatically. + continue + } + + reviewObject := readReviews.ReviewWithHash{ + ReviewHash: reviewHash, + ReviewBytes: reviewBytes, + } + + reviewsList = append(reviewsList, reviewObject) + } + + attributeReviewExists, attributeReviewBytes, attributeReviewMap, attributeReviewVerdict, attributeReviewTime, err := readReviews.GetModeratorNewestProfileAttributeReviewFromReviewsList(reviewsList, moderatorIdentityHash, attributeHash, attributeNetworkType) + if (err != nil) { return false, nil, nil, "", 0, err } + if (attributeReviewExists == false){ + return false, nil, nil, "", 0, nil + } + + return true, attributeReviewBytes, attributeReviewMap, attributeReviewVerdict, attributeReviewTime, nil + } + + attributeReviewExists, attributeReviewBytes, attributeReviewMap, attributeReviewVerdict, attributeReviewTime, err := getModeratorNewestAttributeReview() + if (err != nil) { return false, "", nil, nil, "", 0, err } + + if (integrateFullProfileApprovals == false){ + + if (attributeReviewExists == false){ + return false, "", nil, nil, "", 0, nil + } + + return true, "Attribute", attributeReviewBytes, attributeReviewMap, attributeReviewVerdict, attributeReviewTime, nil + } + + if (attributeReviewExists == true && attributeReviewVerdict == "Approve"){ + + // The user approved the attribute + // We don't have to check for full profile approvals, because they would not change this verdict + + return true, "Attribute", attributeReviewBytes, attributeReviewMap, "Approve", attributeReviewTime, nil + } + + // Now we check for full profile approvals + // A full profile approval is equivalent to an attribute approval for all attributes within the profile + // attributeProfileHashesList is a list of all profile hashes for profiles which contain the attribute + + anyExist, attributeProfileHashesList, err := badgerDatabase.GetAttributeProfilesList(attributeHash) + if (err != nil) { return false, "", nil, nil, "", 0, err } + if (anyExist == true){ + + for _, profileHash := range attributeProfileHashesList{ + + // We are only checking for full profile approvals + // We don't need to check for profile attribute bans, because we are only concerned with the provided attribute's status + // A full profile approval approves all attributes. + // If a user bans a full profile, it does not change the status of any of their attribute approvals of the profile + + fullProfileReviewExists, fullProfileReviewBytes, fullProfileReviewMap, fullProfileVerdict, fullProfileVerdictTime, err := getModeratorNewestFullProfileReview(moderatorIdentityHash, profileHash, attributeNetworkType) + if (err != nil){ return false, "", nil, nil, "", 0, err } + if (fullProfileReviewExists == true && fullProfileVerdict == "Approve"){ + + if (attributeReviewExists == true && fullProfileVerdictTime < attributeReviewTime){ + // The moderator banned the attribute after approving the full profile + // The profile approval is therefore disregarded. + continue + } + // The moderator approved the full profile after banning the attribute, thus, the attribute is approved + + return true, "Profile", fullProfileReviewBytes, fullProfileReviewMap, "Approve", fullProfileVerdictTime, nil + } + } + } + + if (attributeReviewExists == true){ + // We could not find any full profile approvals that undo the attribute ban + return true, "Attribute", attributeReviewBytes, attributeReviewMap, "Ban", attributeReviewTime, nil + } + + return false, "", nil, nil, "", 0, nil +} + + +//Outputs: +// -bool: Review exists +// -[]byte: Review bytes +// -map[string]string: Review map +// -error +func GetModeratorNewestMessageReview(moderatorIdentityHash [16]byte, messageHash [26]byte, messageNetworkType byte, messageCipherKey [25]byte)(bool, []byte, map[string]string, error){ + + isValid, err := identity.VerifyIdentityHash(moderatorIdentityHash, true, "Moderator") + if (err != nil) { return false, nil, nil, err } + if (isValid == false){ + moderatorIdentityHashHex := encoding.EncodeBytesToHexString(moderatorIdentityHash[:]) + return false, nil, nil, errors.New("GetModeratorNewestMessageReview called with invalid moderatorIdentityHash: " + moderatorIdentityHashHex) + } + + isValid = helpers.VerifyNetworkType(messageNetworkType) + if (isValid == false){ + messageNetworkTypeString := helpers.ConvertByteToString(messageNetworkType) + return false, nil, nil, errors.New("GetModeratorNewestMessageReview called with invalid messageNetworkType: " + messageNetworkTypeString) + } + + anyReviewsExist, reviewHashesList, err := badgerDatabase.GetMessageReviewsList(messageHash) + if (err != nil) { return false, nil, nil, err } + if (anyReviewsExist == false){ + return false, nil, nil, nil + } + + reviewsList := make([]readReviews.ReviewWithHash, 0) + + for _, reviewHash := range reviewHashesList{ + + exists, reviewBytes, err := badgerDatabase.GetReview(reviewHash) + if (err != nil) { return false, nil, nil, err } + if (exists == false){ + // Review has been deleted. This missing entry will be removed automatically. + continue + } + + reviewObject := readReviews.ReviewWithHash{ + ReviewHash: reviewHash, + ReviewBytes: reviewBytes, + } + + reviewsList = append(reviewsList, reviewObject) + } + + reviewExists, reviewBytes, reviewMap, _, err := readReviews.GetModeratorNewestMessageReviewFromReviewsList(reviewsList, moderatorIdentityHash, messageHash, messageNetworkType, messageCipherKey) + if (err != nil) { return false, nil, nil, err } + if (reviewExists == false){ + return false, nil, nil, nil + } + + return true, reviewBytes, reviewMap, nil +} + +// This function returns all reviews by a provided moderator, omitting older reviews for the same reviewed hashes +// Will omit None reviews +//Outputs: +// -[][]byte: List of reviews +// -error +func GetAllNewestReviewsCreatedByModerator(moderatorIdentityHash [16]byte, reviewType string, networkType byte)([][]byte, error){ + + isValid := helpers.VerifyNetworkType(networkType) + if (isValid == false){ + networkTypeString := helpers.ConvertByteToString(networkType) + return nil, errors.New("GetAllNewestReviewsCreatedByModerator called with invalid networkType: " + networkTypeString) + } + + exists, reviewHashesList, err := badgerDatabase.GetReviewerReviewHashesList(moderatorIdentityHash, reviewType) + if (err != nil) { return nil, err } + if (exists == false){ + emptyList := make([][]byte, 0) + return emptyList, nil + } + + reviewsList := make([]readReviews.ReviewWithHash, 0) + + for _, reviewHash := range reviewHashesList{ + + exists, reviewBytes, err := badgerDatabase.GetReview(reviewHash) + if (err != nil) { return nil, err } + if (exists == false){ + // Review has been deleted. The missing entry will be removed automatically in the background. + continue + } + + reviewObject := readReviews.ReviewWithHash{ + ReviewHash: reviewHash, + ReviewBytes: reviewBytes, + } + + reviewsList = append(reviewsList, reviewObject) + } + + reviewsMap, err := readReviews.GetNewestModeratorReviewsMapFromReviewsList(reviewsList, moderatorIdentityHash, networkType, true, reviewType) + if (err != nil) { return nil, err } + + newestReviewsList := helpers.GetListOfMapValues(reviewsMap) + + return newestReviewsList, nil +} + + +// This function returns the number of ban advocates for a user +// This returns all advocates, banned or not +//Outputs: +// -bool: Downloading required reviews and profiles +// -int: Number of ban advocates +// -error +func GetNumberOfBanAdvocatesForIdentity(identityHash [16]byte, networkType byte)(bool, int, error){ + + isValid := helpers.VerifyNetworkType(networkType) + if (isValid == false){ + networkTypeString := helpers.ConvertByteToString(networkType) + return false, 0, errors.New("GetNumberOfBanAdvocatesForIdentity called with invalid networkType: " + networkTypeString) + } + + appNetworkType, err := getAppNetworkType.GetAppNetworkType() + if (err != nil) { return false, 0, err } + if (appNetworkType != networkType){ + // We are not downloading reviews for this network type. + return false, 0, nil + } + + downloadingRequiredReviews, err := backgroundDownloads.CheckIfAppCanDetermineIdentityVerdicts(identityHash) + if (err != nil) { return false, 0, err } + if (downloadingRequiredReviews == false){ + return false, 0, nil + } + + banAdvocatesMap, err := GetIdentityBanAdvocatesMap(identityHash, networkType) + if (err != nil) { return false, 0, err } + + numberOfBanAdvocates := len(banAdvocatesMap) + + return true, numberOfBanAdvocates, nil +} + + +//Outputs: +// -bool: Message metadata is known +// -bool: Downloading required reviews and profiles +// -int: Number of reviewers +// -error +func GetNumberOfMessageReviewers(messageHash [26]byte)(bool, bool, int, error){ + + metadataExists, _, messageNetworkType, _, messageInbox, messageCipherKeyHash, err := contentMetadata.GetMessageMetadata(messageHash) + if (err != nil) { return false, false, 0, err } + if (metadataExists == false){ + return false, false, 0, nil + } + + downloadingRequiredData, err := backgroundDownloads.CheckIfAppCanDetermineMessageVerdict(messageNetworkType, messageInbox, true, messageHash) + if (err != nil) { return false, false, 0, err } + if (downloadingRequiredData == false){ + return true, false, 0, nil + } + + approveAdvocatesMap, banAdvocatesMap, err := GetMessageVerdictMaps(messageHash, messageNetworkType, messageCipherKeyHash) + if (err != nil) { return false, false, 0, err } + + numberOfReviewers := len(approveAdvocatesMap) + len(banAdvocatesMap) + + return true, true, numberOfReviewers, nil +} + +// This function will integrate attribute bans +//Outputs: +// -bool: Profile metadata is known +// -bool: Downloading required reviews and profiles +// -int: Number of reviewers +// -error +func GetNumberOfProfileReviewers(profileHash [28]byte)(bool, bool, int, error){ + + metadataExists, _, profileNetworkType, profileAuthor, _, profileIsDisabled, _, profileAttributeHashesMap, err := contentMetadata.GetProfileMetadata(profileHash) + if (err != nil) { return false, false, 0, err } + if (metadataExists == false){ + return false, false, 0, nil + } + if (profileIsDisabled == true){ + // Disabled profiles cannot be reviewed + return true, true, 0, nil + } + + appNetworkType, err := getAppNetworkType.GetAppNetworkType() + if (err != nil) { return false, false, 0, err } + if (appNetworkType != profileNetworkType){ + // We are not downloading reviews for this network type. + return true, false, 0, nil + } + + downloadingRequiredReviews, err := backgroundDownloads.CheckIfAppCanDetermineIdentityVerdicts(profileAuthor) + if (err != nil) { return false, false, 0, err } + if (downloadingRequiredReviews == false){ + return true, false, 0, nil + } + + // profileAttributeHashesMap is a map whose values are the attribute hashes of the profile + profileAttributeHashesList := helpers.GetListOfMapValues(profileAttributeHashesMap) + + approveAdvocatesMap, banAdvocatesMap, err := GetProfileVerdictMaps(profileHash, profileNetworkType, true, profileAttributeHashesList) + if (err != nil) { return false, false, 0, err } + + numberOfReviewers := len(approveAdvocatesMap) + len(banAdvocatesMap) + + return true, true, numberOfReviewers, nil +} + +// This function will go through all reviews for an identity and return the ban advocates +//Outputs: +// -map[[16]byte]int64: Ban advocate identity hash -> Time of ban +// -error +func GetIdentityBanAdvocatesMap(identityHash [16]byte, networkType byte)(map[[16]byte]int64, error){ + + isValid := helpers.VerifyNetworkType(networkType) + if (isValid == false){ + networkTypeString := helpers.ConvertByteToString(networkType) + return nil, errors.New("GetIdentityBanAdvocatesMap called with invalid networkType: " + networkTypeString) + } + + exists, reviewHashesList, err := badgerDatabase.GetIdentityReviewsList(identityHash) + if (err != nil) { return nil, err } + if (exists == false){ + emptyMap := make(map[[16]byte]int64) + return emptyMap, nil + } + + reviewsList := make([]readReviews.ReviewWithHash, 0) + + for _, reviewHash := range reviewHashesList{ + + exists, reviewBytes, err := badgerDatabase.GetReview(reviewHash) + if (err != nil) { return nil, err } + if (exists == false){ + // Review must have been deleted. The missing review entry will be pruned automatically + continue + } + + reviewObject := readReviews.ReviewWithHash{ + ReviewHash: reviewHash, + ReviewBytes: reviewBytes, + } + + reviewsList = append(reviewsList, reviewObject) + } + + banAdvocatesMap, err := readReviews.GetIdentityBanAdvocatesMapFromReviewsList(reviewsList, identityHash, networkType) + if (err != nil) { return nil, err } + + return banAdvocatesMap, nil +} + +// This function will go through all reviews for a profile and return the approve and ban advocates +// If integrateAttributeBans == false, we will only check for full profile approvals/bans +//Outputs: +// -map[[16]byte]int64: Approve advocates map (Moderator identity hash -> Time of review) +// -map[[16]byte]int64: Ban advocates map (Moderator identity hash -> Time of review) +// -error +func GetProfileVerdictMaps(profileHash [28]byte, profileNetworkType byte, integrateAttributeBans bool, attributeHashesList [][27]byte)(map[[16]byte]int64, map[[16]byte]int64, error){ + + isValid := helpers.VerifyNetworkType(profileNetworkType) + if (isValid == false){ + profileNetworkTypeString := helpers.ConvertByteToString(profileNetworkType) + return nil, nil, errors.New("GetProfileVerdictMaps called with invalid profileNetworkType: " + profileNetworkTypeString) + } + + exists, reviewHashesList, err := badgerDatabase.GetProfileReviewsList(profileHash) + if (err != nil) { return nil, nil, err } + if (exists == false){ + emptyMapA := make(map[[16]byte]int64) + emptyMapB := make(map[[16]byte]int64) + + return emptyMapA, emptyMapB, nil + } + + reviewsList := make([]readReviews.ReviewWithHash, 0) + + for _, reviewHash := range reviewHashesList{ + + exists, reviewBytes, err := badgerDatabase.GetReview(reviewHash) + if (err != nil) { return nil, nil, err } + if (exists == false){ + // Review has been deleted. The missing review entry will be pruned automatically. + continue + } + + reviewObject := readReviews.ReviewWithHash{ + ReviewHash: reviewHash, + ReviewBytes: reviewBytes, + } + + reviewsList = append(reviewsList, reviewObject) + } + + approveAdvocatesMap, banAdvocatesMap, err := readReviews.GetProfileVerdictMapsFromReviewsList(reviewsList, profileHash, profileNetworkType) + if (err != nil) { return nil, nil, err } + + if (integrateAttributeBans == false){ + return approveAdvocatesMap, banAdvocatesMap, nil + } + + // Now we check for attribute ban advocates + // A ban of any attribute within the profile is equivalent to a profile ban + // attributeHashesList is a list of all attribute hashes within the provided profile + + integrateProfileAttributeBans := func()error{ + + for _, attributeHash := range attributeHashesList{ + + // We have to check for full profile reviews of all profiles by the user that contain this attribute + // Any of these full profile approvals are equivalent to approving all profile attributes + + getAttributeProfileHashesList := func()([][28]byte, error){ + + exists, attributeProfilesList, err := badgerDatabase.GetAttributeProfilesList(attributeHash) + if (err != nil) { return nil, err } + if (exists == false){ + emptyList := make([][28]byte, 0) + return emptyList, nil + } + + // We can omit the current profileHash, because we are already checking it + attributeProfileHashesList, _ := helpers.DeleteAllMatchingItemsFromProfileHashList(attributeProfilesList, profileHash) + + return attributeProfileHashesList, nil + } + + attributeProfileHashesList, err := getAttributeProfileHashesList() + if (err != nil) { return err } + + _, attributeBanAdvocatesMap, err := GetProfileAttributeVerdictMaps(attributeHash, profileNetworkType, true, attributeProfileHashesList) + if (err != nil) { return err } + + for moderatorIdentityHash, attributeBanTime := range attributeBanAdvocatesMap{ + + existingApproveTime, exists := approveAdvocatesMap[moderatorIdentityHash] + if (exists == true){ + if (existingApproveTime < attributeBanTime){ + // The moderator banned a profile attribute after approving the profile + delete(approveAdvocatesMap, moderatorIdentityHash) + banAdvocatesMap[moderatorIdentityHash] = attributeBanTime + + // We don't have to check for any more attribute bans + // They can only cause an full profile approval to change to a ban + + return nil + } + } + } + } + + return nil + } + + err = integrateProfileAttributeBans() + if (err != nil) { return nil, nil, err } + + return approveAdvocatesMap, banAdvocatesMap, nil +} + +// This function will go through all reviews for a profile attribute and return the approve and ban advocates +// This also finds full profile approve reviews and treats them as approvals for all attributes within the profile +//Outputs: +// -map[[16]byte]int64: Approve advocates map (Moderator identity hash -> Time of approval) +// -map[[16]byte]int64: Ban advocates map (Moderator identity hash -> Time of ban) +// -error +func GetProfileAttributeVerdictMaps(attributeHash [27]byte, attributeNetworkType byte, integrateFullProfileApprovals bool, attributeProfileHashesList [][28]byte)(map[[16]byte]int64, map[[16]byte]int64, error){ + + isValid := helpers.VerifyNetworkType(attributeNetworkType) + if (isValid == false){ + attributeNetworkTypeString := helpers.ConvertByteToString(attributeNetworkType) + return nil, nil, errors.New("GetProfileAttributeVerdictMaps called with invalid attributeNetworkType: " + attributeNetworkTypeString) + } + + exists, reviewHashesList, err := badgerDatabase.GetProfileAttributeReviewsList(attributeHash) + if (err != nil) { return nil, nil, err } + if (exists == false){ + emptyMapA := make(map[[16]byte]int64) + emptyMapB := make(map[[16]byte]int64) + + return emptyMapA, emptyMapB, nil + } + + reviewsList := make([]readReviews.ReviewWithHash, 0) + + for _, reviewHash := range reviewHashesList{ + + exists, reviewBytes, err := badgerDatabase.GetReview(reviewHash) + if (err != nil) { return nil, nil, err } + if (exists == false){ + // Review has been deleted. The missing review entry will be pruned automatically + continue + } + + reviewObject := readReviews.ReviewWithHash{ + ReviewHash: reviewHash, + ReviewBytes: reviewBytes, + } + + reviewsList = append(reviewsList, reviewObject) + } + + approveAdvocatesMap, banAdvocatesMap, err := readReviews.GetProfileAttributeVerdictMapsFromReviewsList(reviewsList, attributeHash, attributeNetworkType) + if (err != nil) { return nil, nil, err } + + if (integrateFullProfileApprovals == false){ + return approveAdvocatesMap, banAdvocatesMap, nil + } + + // Now we check for full profile approve advocates + // A full profile approval is equivalent to an attribute approval for all attributes within the profile + // attributeProfileHashesList is a list of all profile hashes for profiles which contain the attribute + + integrateProfileApprovals := func()error{ + + for _, profileHash := range attributeProfileHashesList{ + + // We are only checking for full profile approvals + // We don't need to check for profile attribute bans, because we are only concerned with the provided attribute's status + // A full profile approval approves all attributes. + // If the user bans a profile's attribute after approving the full profile, the other attributes are still considered approved by the user + + fullProfileApproveAdvocatesMap, _, err := GetProfileVerdictMaps(profileHash, attributeNetworkType, false, nil) + if (err != nil) { return err } + + for moderatorIdentityHash, fullProfileApproveTime := range fullProfileApproveAdvocatesMap{ + + existingBanTime, exists := banAdvocatesMap[moderatorIdentityHash] + if (exists == true){ + if (existingBanTime < fullProfileApproveTime){ + // The moderator approved the full profile after banning the attribute + delete(banAdvocatesMap, moderatorIdentityHash) + approveAdvocatesMap[moderatorIdentityHash] = fullProfileApproveTime + + // We don't have to check for any more full profile approvals + // They can only cause an attribute ban to change to an approval + + return nil + } + } + } + } + + return nil + } + + err = integrateProfileApprovals() + if (err != nil) { return nil, nil, err } + + return approveAdvocatesMap, banAdvocatesMap, nil +} + + + +// This function will go through all reviews for a message and return the approve and ban advocates +// We use the provided message CipherKeyHash to ensure the reviewers have seen the message +//Outputs: +// -map[[16]byte]int64: Approve advocate identity hash -> Time of approval +// -map[[16]byte]int64: Ban advocate identity hash -> Time of ban +// -error +func GetMessageVerdictMaps(messageHash [26]byte, messageNetworkType byte, messageCipherKeyHash [25]byte)(map[[16]byte]int64, map[[16]byte]int64, error){ + + isValid := helpers.VerifyNetworkType(messageNetworkType) + if (isValid == false){ + messageNetworkTypeString := helpers.ConvertByteToString(messageNetworkType) + return nil, nil, errors.New("GetMessageVerdictMaps called with invalid messageNetworkType: " + messageNetworkTypeString) + } + + exists, reviewHashesList, err := badgerDatabase.GetMessageReviewsList(messageHash) + if (err != nil) { return nil, nil, err } + if (exists == false){ + + // There are no reviews for this message + + emptyMapA := make(map[[16]byte]int64) + emptyMapB := make(map[[16]byte]int64) + + return emptyMapA, emptyMapB, nil + } + + reviewsList := make([]readReviews.ReviewWithHash, 0) + + for _, reviewHash := range reviewHashesList{ + + exists, reviewBytes, err := badgerDatabase.GetReview(reviewHash) + if (err != nil) { return nil, nil, err } + if (exists == false){ + // Review has been deleted. Missing review entry will be pruned automatically + continue + } + + reviewObject := readReviews.ReviewWithHash{ + ReviewHash: reviewHash, + ReviewBytes: reviewBytes, + } + + reviewsList = append(reviewsList, reviewObject) + } + + approveAdvocatesMap, banAdvocatesMap, err := readReviews.GetMessageVerdictMapsFromReviewsList(reviewsList, messageHash, messageNetworkType, messageCipherKeyHash) + if (err != nil) { return nil, nil, err } + + return approveAdvocatesMap, banAdvocatesMap, nil +} + +// We use this function to find a valid message cipher key to decrypt a message for moderation +// We use the message's CipherKeyHash to verify that the author of the review has seen the decrypted message +// If the cipher key does not decrypt the message, then we know the author of the review and the message are malicious +//Outputs: +// -bool: Message cipher key found +// -[32]byte: Message Cipher key +// -error +func GetMessageCipherKeyFromAnyReview(messageHash [26]byte, messageNetworkType byte, messageCipherKeyHash [25]byte)(bool, [32]byte, error){ + + isValid := helpers.VerifyNetworkType(messageNetworkType) + if (isValid == false){ + messageNetworkTypeString := helpers.ConvertByteToString(messageNetworkType) + return false, [32]byte{}, errors.New("GetMessageCipherKeyFromAnyReview called with invalid messageNetworkType: " + messageNetworkTypeString) + } + + exists, reviewHashesList, err := badgerDatabase.GetMessageReviewsList(messageHash) + if (err != nil) { return false, [32]byte{}, err } + if (exists == false){ + return false, [32]byte{}, nil + } + if (len(reviewHashesList) == 0){ + return false, [32]byte{}, nil + } + + for _, reviewHash := range reviewHashesList{ + + exists, reviewBytes, err := badgerDatabase.GetReview(reviewHash) + if (err != nil) { return false, [32]byte{}, err } + if (exists == false){ + // Message must have been deleted. + // The missing reviewsList entry will be removed automatically. + continue + } + + ableToRead, _, reviewNetworkType, _, _, reviewType, reviewedHash, _, reviewMap, err := readReviews.ReadReview(false, reviewBytes) + if (err != nil) { return false, [32]byte{}, err } + if (ableToRead == false){ + return false, [32]byte{}, errors.New("Database corrupt: Contains invalid review.") + } + if (reviewNetworkType != messageNetworkType){ + // The author of this review is malicious + // We will not try to get the message cipher key from the review, even if it exists + // This is because we want all moderators to see the same messages to review, regardless of if they were previously on a different network. + continue + } + if (reviewType != "Message") { + return false, [32]byte{}, errors.New("Corrupt database: ReviewType does not match reviews list.") + } + + areEqual := bytes.Equal(reviewedHash, messageHash[:]) + if (areEqual == false){ + return false, [32]byte{}, errors.New("Corrupt database: Reviewed message hash does not match reviews list.") + } + + messageCipherKeyString, exists := reviewMap["MessageCipherKey"] + if (exists == false) { + return false, [32]byte{}, errors.New("Corrupt database: Contains review missing MessageCipherKey") + } + + messageCipherKey, err := readMessages.ReadMessageCipherKeyHex(messageCipherKeyString) + if (err != nil){ + return false, [32]byte{}, errors.New("Corrupt database: Contains review with invalid MessageCipherKey: " + messageCipherKeyString + ". Reason: " + err.Error()) + } + + currentMessageCipherKeyHash, err := readMessages.ConvertMessageCipherKeyToCipherKeyHash(messageCipherKey) + if (err != nil){ return false, [32]byte{}, err } + + if (currentMessageCipherKeyHash != messageCipherKeyHash){ + // Reviewer is malicious. + continue + } + + return true, messageCipherKey, nil + } + + // No valid reviews found for message + + return false, [32]byte{}, nil +} + + diff --git a/internal/moderation/trustedAddressDeposits/trustedAddressDeposits.go b/internal/moderation/trustedAddressDeposits/trustedAddressDeposits.go new file mode 100644 index 0000000..fdaa3d1 --- /dev/null +++ b/internal/moderation/trustedAddressDeposits/trustedAddressDeposits.go @@ -0,0 +1,48 @@ + +// trustedAddressDeposits provides functions to save and retrieve cryptocurrency address deposit information from hosts + +package trustedAddressDeposits + +//TODO: Build package +// Deposits should be retrieved from multiple hosts using the GetAddressDeposits request. +// We should store deposits in badgerDatabase. +// We must store each deposit, the host who told us about the deposit, and the time the deposit was made. +// The time of a deposit is derived from the block time, so all deposits within the same block should be represented as a single deposit. +// Hosts should only provide information about confirmed deposits (a defined number of blocks) + +// We can detect hosts who are lying by comparing their alleged deposits to what other hosts are telling us. +// We can then add those hosts to our malicious hosts list. +// Clients should be constantly updating deposits for all downloaded moderators to keep up with any new deposits. + +// For an address deposit history to be known, it needs to have been verified by at least 3 hosts + +// Regardless of which network type the host who shared them belongs to, address deposits should be identical. +// TODO: Take into consideration the network type of the host who shared each address deposit? + +import "seekia/internal/network/serverResponse" + +func CheckIfAddressDepositsAreKnown(cryptocurrency string, address string)(bool, error){ + + //TODO + + return true, nil +} + +func AddAddressDepositObjectsListToCache(hostIdentityHash [16]byte, cryptocurrency string, depositObjectsList []serverResponse.DepositStruct)error{ + + //TODO + + return nil +} + +func AddAddressesWithNoDepositsToCache(hostIdentityHash [16]byte, cryptocurrency string, addressesList []string)error{ + + //TODO + + return nil +} + + + + + diff --git a/internal/moderation/trustedViewableStatus/trustedViewableStatus.go b/internal/moderation/trustedViewableStatus/trustedViewableStatus.go new file mode 100644 index 0000000..96794b7 --- /dev/null +++ b/internal/moderation/trustedViewableStatus/trustedViewableStatus.go @@ -0,0 +1,94 @@ + +// trustedViewableStatus provides functions to keep track of the trusted viewable moderation consensus statuses of content/identities +// Viewable statuses are derived from sticky viewable statuses +// Sticky viewable statuses only change if the viewable status has been changed for a minimum defined period of time +// See verifiedStickyStatus.go for an explanation of sticky consensus statuses + +package trustedViewableStatus + +// We query from multiple different hosts who are hosting the profile/identity reviews to retrieve the trusted viewable statuses +// "Trusted" is in reference to the reality that we are trusting that the hosts we query are providing truthful viewable statuses +// To determine a "Verified" status one must download all of the moderator reviews to determine the viewable sticky consensus statuses +// Once the client has received the viewable status from the required threshold of hosts, we can display viewable profiles/identities to the user + +// Unlike verifiedStickyStatus, this package treats a profile whose identity is banned as not viewable +// Example: If an identity is banned but the profile is approved: +// -The profile's trusted sticky consensus will be Unviewable +// -The verified sticky consensus will be Viewable + +// Below describes what a viewable/unviewable status means: + +// Identity: +// -Viewable: Not Banned +// -Unviewable: Banned +// Profile +// -Viewable: +// -Mate: Profile is Approved and identity is not banned +// -Host/Moderator: Profile is Undecided/Approved and identity is not banned +// -Unviewable +// -Mate: Profile is Banned/Undecided or identity is banned +// -Host/Moderator: Profile is Banned or identity is banned +// Message: +// -Viewable: Approved/Undecided +// -Unviewable: Banned + +//TODO: Manually downloaded profiles should be viewable with only 1 trusted viewable status +// Otherwise it would be cumbersome to download a profile, wait for the download, and then wait to download trusted statuses from different hosts + +//TODO: Build package +// We should store statuses in badgerDatabase +// We need to store the time we received each status, and from which host we received it from +// This is required so we can replace older statuses we received from the same hosts, and so we can detect hosts who are sharing statuses +// that are contrary to the rest of the hosts so we can designate those hosts as untrustworthy + +// We don't need to use this package to store message sticky statuses +// Messages can only become unviewable when they are banned by the recipient or the author +// Thus, a user does not need to check if a message they are retrieving is viewable or not before decrypting it +// +// The only time a user will retrieve a trusted message sticky status is to see if the message has been banned after they reported it +// This status will be shown to the user in the GUI, and does not need to be stored in the database using this package + + +//Outputs: +// -bool: Status is known (is stored in database and agreed upon by minimum required hosts) +// -bool: Identity is viewable (true = Identity is banned, false = identity is not banned) +// -[][16]byte: Host identity hashes that have been queried +// -error +func GetTrustedIdentityIsViewableStatus(identityHash [16]byte, networkType byte)(bool, bool, [][16]byte, error){ + + //TODO + + return true, true, nil, nil +} + + +//Outputs: +// -bool: Status is known +// -bool: Profile Is Viewable +// -[][16]byte: Host identity hashes that have been queried +// -error +func GetTrustedProfileIsViewableStatus(profileHash [28]byte)(bool, bool, [][16]byte, error){ + + //TODO + + return true, true, nil, nil +} + + +func AddTrustedIdentityIsViewableStatus(userIdentityHash [16]byte, hostIdentityHash [16]byte, networkType byte, viewableStatus bool)error{ + + //TODO + + return nil +} + +func AddTrustedProfileIsViewableStatus(profileHash [28]byte, hostIdentityHash [16]byte, viewableStatus bool)error{ + + //TODO + + return nil +} + + + + diff --git a/internal/moderation/verdictHistory/verdictHistory.go b/internal/moderation/verdictHistory/verdictHistory.go new file mode 100644 index 0000000..2f2d9fc --- /dev/null +++ b/internal/moderation/verdictHistory/verdictHistory.go @@ -0,0 +1,461 @@ + +// verdictHistory provides functions to keep track of and retrieve the verdict history for messages, profiles, and identities + +package verdictHistory + +// Verdict histories are used for 2 things: + +// 1. Sticky consensus statuses are calculated using the verdict history of a reviewedHash + +// 2. Hosts use the verdict history of a reviewedHash to know when to delete banned content +// Once a profile/identity/message has been banned for a long enough time period, the host's client will delete the content + +//TODO: Add job to prune verdicts we no longer need +//TODO: Store the verdict history as messagePack files, so if a user briefly closes client, history is maintained upon restart + +import "seekia/internal/badgerDatabase" +import "seekia/internal/helpers" +import "seekia/internal/moderation/verifiedVerdict" +import "seekia/internal/profiles/profileStorage" + + +import "errors" +import "time" +import "sync" + +// Map Structure: Identity Hash -> Last time the verdict was recorded for this identityHash in the identityVerdictHistoryMap +var identityVerdictLastRecordedMap map[[16]byte]int64 = make(map[[16]byte]int64) +var identityVerdictLastRecordedMapMutex sync.RWMutex + +// Map Structure: Profile Hash -> Last time the verdict was recorded for this profileHash in the profileVerdictHistoryMap +var profileVerdictLastRecordedMap map[[28]byte]int64 = make(map[[28]byte]int64) +var profileVerdictLastRecordedMapMutex sync.RWMutex + +// Map Structure: Message Hash -> Last time the verdict was recorded for this messageHash in the messageVerdictHistoryMap +var messageVerdictLastRecordedMap map[[26]byte]int64 = make(map[[26]byte]int64) +var messageVerdictLastRecordedMapMutex sync.RWMutex + +type verdictObject struct{ + + // Consensus verdict ("Approved"/"Banned"/"Undecided"/"Not Banned") + Verdict string + + VerdictTime int64 +} + +// Map Structure: Identity hash -> List of verdict objects +var identityVerdictHistoryMap map[[16]byte][]verdictObject = make(map[[16]byte][]verdictObject) +var identityVerdictHistoryMapMutex sync.RWMutex + +// Map Structure: Profile hash -> List of verdict objects +var profileVerdictHistoryMap map[[28]byte][]verdictObject = make(map[[28]byte][]verdictObject) +var profileVerdictHistoryMapMutex sync.RWMutex + +// Map Structure: Message hash -> List of verdict objects +var messageVerdictHistoryMap map[[26]byte][]verdictObject = make(map[[26]byte][]verdictObject) +var messageVerdictHistoryMapMutex sync.RWMutex + + +// This is the minimum duration in seconds between recording the consensus verdicts for each reviewedHash +const VerdictIntervalDuration int64 = 180 + + +// This function should be called whenever we switch network types +func DeleteVerdictHistory(){ + + identityVerdictLastRecordedMapMutex.Lock() + clear(identityVerdictLastRecordedMap) + identityVerdictLastRecordedMapMutex.Unlock() + + profileVerdictLastRecordedMapMutex.Lock() + clear(profileVerdictLastRecordedMap) + profileVerdictLastRecordedMapMutex.Unlock() + + messageVerdictLastRecordedMapMutex.Lock() + clear(messageVerdictLastRecordedMap) + messageVerdictLastRecordedMapMutex.Unlock() + + identityVerdictHistoryMapMutex.Lock() + clear(identityVerdictHistoryMap) + identityVerdictHistoryMapMutex.Unlock() + + profileVerdictHistoryMapMutex.Lock() + clear(profileVerdictHistoryMap) + profileVerdictHistoryMapMutex.Unlock() + + messageVerdictHistoryMapMutex.Lock() + clear(messageVerdictHistoryMap) + messageVerdictHistoryMapMutex.Unlock() +} + + +//Outputs: +// -[][16]byte: Reviewed identity hashes list +// -error +func GetAllReviewedIdentityHashesList()([][16]byte, error){ + + identityVerdictLastRecordedMapMutex.RLock() + + allReviewedIdentityHashesList := helpers.GetListOfMapKeys(identityVerdictLastRecordedMap) + + identityVerdictLastRecordedMapMutex.RUnlock() + + return allReviewedIdentityHashesList, nil +} + +//Outputs: +// -[][28]byte: Reviewed profile hashes list +// -error +func GetAllReviewedProfileHashesList()([][28]byte, error){ + + profileVerdictLastRecordedMapMutex.RLock() + + allReviewedProfileHashesList := helpers.GetListOfMapKeys(profileVerdictLastRecordedMap) + + profileVerdictLastRecordedMapMutex.RUnlock() + + return allReviewedProfileHashesList, nil +} + +//Outputs: +// -[][26]byte: Reviewed message hashes list +// -error +func GetAllReviewedMessageHashesList()([][26]byte, error){ + + messageVerdictLastRecordedMapMutex.RLock() + + allReviewedMessageHashesList := helpers.GetListOfMapKeys(messageVerdictLastRecordedMap) + + messageVerdictLastRecordedMapMutex.RUnlock() + + return allReviewedMessageHashesList, nil +} + + +//Outputs: +// -map[int64]string: Verdict history map +// -error +func GetIdentityVerdictHistoryMap(identityHash [16]byte)(map[int64]string, error){ + + identityVerdictHistoryMapMutex.RLock() + defer identityVerdictHistoryMapMutex.RUnlock() + + verdictObjectsList, exists := identityVerdictHistoryMap[identityHash] + if (exists == false){ + + emptyMap := make(map[int64]string) + return emptyMap, nil + } + + verdictHistoryMap, err := getReviewedHashVerdictHistoryMap(verdictObjectsList) + if (err != nil) { return nil, err } + + return verdictHistoryMap, nil +} + +//Outputs: +// -map[int64]string: Verdict history map +// -error +func GetProfileVerdictHistoryMap(profileHash [28]byte)(map[int64]string, error){ + + profileVerdictHistoryMapMutex.RLock() + defer profileVerdictHistoryMapMutex.RUnlock() + + verdictObjectsList, exists := profileVerdictHistoryMap[profileHash] + if (exists == false){ + + emptyMap := make(map[int64]string) + return emptyMap, nil + } + + verdictHistoryMap, err := getReviewedHashVerdictHistoryMap(verdictObjectsList) + if (err != nil) { return nil, err } + + return verdictHistoryMap, nil +} + +//Outputs: +// -map[int64]string: Verdict history map +// -error +func GetMessageVerdictHistoryMap(messageHash [26]byte)(map[int64]string, error){ + + messageVerdictHistoryMapMutex.RLock() + defer messageVerdictHistoryMapMutex.RUnlock() + + verdictObjectsList, exists := messageVerdictHistoryMap[messageHash] + if (exists == false){ + + emptyMap := make(map[int64]string) + return emptyMap, nil + } + + verdictHistoryMap, err := getReviewedHashVerdictHistoryMap(verdictObjectsList) + if (err != nil) { return nil, err } + + return verdictHistoryMap, nil +} + +func getReviewedHashVerdictHistoryMap(verdictObjectsList []verdictObject)(map[int64]string, error){ + + // Map Structure: Unix Time -> Verdict of reviewedHash at unix time + reviewedHashVerdictHistoryMap := make(map[int64]string) + + for _, verdictObject := range verdictObjectsList{ + + verdict := verdictObject.Verdict + verdictTime := verdictObject.VerdictTime + + reviewedHashVerdictHistoryMap[verdictTime] = verdict + } + + return reviewedHashVerdictHistoryMap, nil +} + +// This is run automatically by backgroundJobs on an internal +func RecordIdentityVerdictsToHistoryMap(networkType byte)error{ + + isValid := helpers.VerifyNetworkType(networkType) + if (isValid == false){ + networkTypeString := helpers.ConvertByteToString(networkType) + return errors.New("RecordIdentityVerdictsToHistoryMap called with invalid networkType: " + networkTypeString) + } + + allDownloadedProfileIdentityHashes, err := profileStorage.GetAllStoredProfileIdentityHashes() + if (err != nil) { return err } + + allReviewedIdentityHashes, err := badgerDatabase.GetAllReviewedIdentityHashes() + if (err != nil) { return err } + + allUserIdentityHashesList := helpers.CombineTwoListsAndAvoidDuplicates(allDownloadedProfileIdentityHashes, allReviewedIdentityHashes) + + for _, identityHash := range allUserIdentityHashesList{ + + currentTime := time.Now().Unix() + + identityVerdictLastRecordedMapMutex.RLock() + verdictLastRecordedTime, exists := identityVerdictLastRecordedMap[identityHash] + identityVerdictLastRecordedMapMutex.RUnlock() + if (exists == true){ + + timeElapsed := currentTime - verdictLastRecordedTime + if (timeElapsed < VerdictIntervalDuration){ + // We have recorded this consensus verdict recently enough. + // We go to the next identity + continue + } + } + + downloadingRequiredReviews, parametersExist, identityIsBanned, _, _, _, err := verifiedVerdict.GetVerifiedIdentityVerdict(identityHash, networkType) + if (err != nil) { return err } + if (downloadingRequiredReviews == false || parametersExist == false){ + // We do not know consensus verdict + // We go to the next identity + continue + } + + getVerdictString := func()string{ + if (identityIsBanned == true){ + return "Banned" + } + return "Not Banned" + } + + verdictString := getVerdictString() + + newVerdictObject := verdictObject{ + Verdict: verdictString, + VerdictTime: currentTime, + } + + identityVerdictHistoryMapMutex.Lock() + + identityVerdictObjectsList, exists := identityVerdictHistoryMap[identityHash] + if (exists == false){ + identityVerdictHistoryMap[identityHash] = []verdictObject{newVerdictObject} + } else { + identityVerdictObjectsList = append(identityVerdictObjectsList, newVerdictObject) + identityVerdictHistoryMap[identityHash] = identityVerdictObjectsList + } + + identityVerdictHistoryMapMutex.Unlock() + + identityVerdictLastRecordedMapMutex.Lock() + identityVerdictLastRecordedMap[identityHash] = currentTime + identityVerdictLastRecordedMapMutex.Unlock() + } + + return nil +} + +// This is run automatically by backgroundJobs on an internal +func RecordProfileVerdictsToHistoryMap(networkType byte)error{ + + isValid := helpers.VerifyNetworkType(networkType) + if (isValid == false){ + networkTypeString := helpers.ConvertByteToString(networkType) + return errors.New("RecordProfileVerdictsToHistoryMap called with invalid networkType: " + networkTypeString) + } + + // We use a map to avoid duplicates + allUserProfileHashesMap := make(map[[28]byte]struct{}) + + allProfileHashesWithMetadataList, err := badgerDatabase.GetAllProfileHashesWithMetadata() + if (err != nil) { return err } + + for _, profileHash := range allProfileHashesWithMetadataList{ + allUserProfileHashesMap[profileHash] = struct{}{} + } + + allDownloadedProfileHashes, err := profileStorage.GetAllStoredProfileHashes() + if (err != nil) { return err } + + for _, profileHash := range allDownloadedProfileHashes{ + allUserProfileHashesMap[profileHash] = struct{}{} + } + + allReviewedProfileHashesList, err := badgerDatabase.GetAllReviewedProfileHashes() + if (err != nil) { return err } + + for _, profileHash := range allReviewedProfileHashesList{ + allUserProfileHashesMap[profileHash] = struct{}{} + } + + for profileHash, _ := range allUserProfileHashesMap{ + + currentTime := time.Now().Unix() + + profileVerdictLastRecordedMapMutex.RLock() + verdictLastRecordedTime, exists := profileVerdictLastRecordedMap[profileHash] + profileVerdictLastRecordedMapMutex.RUnlock() + if (exists == true){ + + timeElapsed := currentTime - verdictLastRecordedTime + if (timeElapsed < VerdictIntervalDuration){ + // We have recorded this consensus verdict recently enough. Skip to the next profile + continue + } + } + + profileIsDisabled, profileMetadataIsKnown, _, profileNetworkType, _, _, downloadingRequiredReviews, parametersExist, profileConsensusVerdict, _, _, _, _, _, _, _, _, _, _, _, _, _, err := verifiedVerdict.GetVerifiedProfileVerdict(profileHash) + if (err != nil) { return err } + if (profileIsDisabled == true){ + // Profile is disabled, it cannot be reviewed. + continue + } + if (profileMetadataIsKnown == false){ + // Metadata must have been deleted after earlier check + // We cannot determine verified verdict. + continue + } + if (profileNetworkType != networkType){ + // This profile belongs to a different network type + // We don't store its verdict in the verdict history + continue + } + if (downloadingRequiredReviews == false || parametersExist == false){ + // We do not know the consensus verdict + continue + } + + newVerdictObject := verdictObject{ + Verdict: profileConsensusVerdict, + VerdictTime: currentTime, + } + + profileVerdictHistoryMapMutex.Lock() + + profileVerdictObjectsList, exists := profileVerdictHistoryMap[profileHash] + if (exists == false){ + profileVerdictHistoryMap[profileHash] = []verdictObject{newVerdictObject} + } else { + profileVerdictObjectsList = append(profileVerdictObjectsList, newVerdictObject) + profileVerdictHistoryMap[profileHash] = profileVerdictObjectsList + } + + profileVerdictHistoryMapMutex.Unlock() + + profileVerdictLastRecordedMapMutex.Lock() + profileVerdictLastRecordedMap[profileHash] = currentTime + profileVerdictLastRecordedMapMutex.Unlock() + } + + return nil +} + +// This is run automatically by backgroundJobs on an internal +func RecordMessageVerdictsToHistoryMap(networkType byte)error{ + + isValid := helpers.VerifyNetworkType(networkType) + if (isValid == false){ + networkTypeString := helpers.ConvertByteToString(networkType) + return errors.New("RecordMessageVerdictsToHistoryMap called with invalid networkType: " + networkTypeString) + } + + allDownloadedMessageHashes, err := badgerDatabase.GetAllChatMessageHashes() + if (err != nil) { return err } + + allReviewedMessageHashes, err := badgerDatabase.GetAllReviewedMessageHashes() + if (err != nil) { return err } + + allMessageHashesList := helpers.CombineTwoListsAndAvoidDuplicates(allDownloadedMessageHashes, allReviewedMessageHashes) + + for _, messageHash := range allMessageHashesList{ + + currentTime := time.Now().Unix() + + messageVerdictLastRecordedMapMutex.RLock() + verdictLastRecordedTime, exists := messageVerdictLastRecordedMap[messageHash] + messageVerdictLastRecordedMapMutex.RUnlock() + if (exists == true){ + + timeElapsed := currentTime - verdictLastRecordedTime + if (timeElapsed < VerdictIntervalDuration){ + // We have recorded this verdict recently enough. Skip to the next message. + continue + } + } + + messageMetadataIsKnown, _, messageNetworkType, _, _, downloadingRequiredReviews, parametersExist, messageVerdict, _, _, _, _, _, _, err := verifiedVerdict.GetVerifiedMessageVerdict(messageHash) + if (err != nil) { return err } + if (messageMetadataIsKnown == false){ + // Message must have been deleted from database after we retrieved the message hashes list + continue + } + if (messageNetworkType != networkType){ + // The message belongs to a different network type + // We will not store its verdict in the verdict history + continue + } + if (downloadingRequiredReviews == false || parametersExist == false){ + // We do not know sticky consensus verdict + continue + } + + newVerdictObject := verdictObject{ + Verdict: messageVerdict, + VerdictTime: currentTime, + } + + messageVerdictHistoryMapMutex.Lock() + + messageVerdictObjectsList, exists := messageVerdictHistoryMap[messageHash] + if (exists == false){ + messageVerdictHistoryMap[messageHash] = []verdictObject{newVerdictObject} + } else { + messageVerdictObjectsList = append(messageVerdictObjectsList, newVerdictObject) + messageVerdictHistoryMap[messageHash] = messageVerdictObjectsList + } + + messageVerdictHistoryMapMutex.Unlock() + + messageVerdictLastRecordedMapMutex.Lock() + messageVerdictLastRecordedMap[messageHash] = currentTime + messageVerdictLastRecordedMapMutex.Unlock() + } + + return nil +} + + + + diff --git a/internal/moderation/verifiedAddressDeposits/verifiedAddressDeposits.go b/internal/moderation/verifiedAddressDeposits/verifiedAddressDeposits.go new file mode 100644 index 0000000..f9cca70 --- /dev/null +++ b/internal/moderation/verifiedAddressDeposits/verifiedAddressDeposits.go @@ -0,0 +1,21 @@ + +// verifiedAddressDeposits provides functions to retrieve cryptocurrency address deposits for blockchains which we are hosting locally +// We use trustedAddressDeposits for blockchains which we are not hosting + +package verifiedAddressDeposits + +// TODO: Build package +// It will require querying a local blockchain node and keeping track of deposits to addresses. +// Each deposit should have a time and an amount. +// All deposits within a block should be represented as 1 deposit. +// Each block will have an associated time which all hosts can agree upon. +// Deposit Time = Seekia Network Start Time + (BlockHeight - BlockHeightAtNetworkStartTime) * DurationOfBlockInSeconds) +// We should include the burned fee amount of each transaction as part of the deposit. +// Hosts should only provide information about confirmed deposits (a defined number of blocks) + +func CheckIfCryptocurrencyNodeIsBeingHosted(cryptocurrencyName string)(bool, error){ + + //TODO + + return false, nil +} diff --git a/internal/moderation/verifiedStickyStatus/verifiedStickyStatus.go b/internal/moderation/verifiedStickyStatus/verifiedStickyStatus.go new file mode 100644 index 0000000..efcde77 --- /dev/null +++ b/internal/moderation/verifiedStickyStatus/verifiedStickyStatus.go @@ -0,0 +1,510 @@ + +// verifiedStickyStatus provides functions to determine the sticky consensus viewable status for profiles/identities/messages +// Sticky consensus statuses are a kind of consensus status that requires a verdict to be present for a minimum defined period of time + +package verifiedStickyStatus + +// Sticky consensus is needed to defend against malicious moderators +// Here is an example: +// A malicious moderator bans all other moderators and all content on the network. All profiles on the network are now banned. +// 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 consensus attempts to solve this problem +// With sticky consensus, as long as a profile has been approved for a certain period of time, its sticky consensus will be stuck +// For the sticky consensus to be switched to unviewable, the profile's verdict would have to be Ban for a certain period of time +// Hosts will serve profiles to users based on each profile's sticky viewable consensus status, not its realtime consensus verdict + +// Hosts must be online for long enough to keep track of, or establish, the sticky consensus 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 consensus 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" consensus after those malicious moderators are banned. 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 a profile/message/identity's sticky consensus to requesting peers once the consensus is established + +// Each ReviewedType has two sticky status states: Viewable and Unviewable +// Below describes what defines a viewable/unviewable status for each reviewedType: + +// Identity: +// -Viewable: Not Banned +// -Unviewable: Banned +// Profile +// -Viewable: +// -Mate: Approved +// -Host/Moderator: Approved/Undecided +// -Unviewable +// -Mate: Banned/Undecided +// -Host/Moderator: Banned +// Message: +// -Viewable: Approved/Undecided +// -Unviewable: Banned + +// The calculation works as follows: +// The host keeps track of the verdict history for all downloaded and reviewed identities/profiles/messages at a regular interval +// To determine the current sticky status for a reviewedHash, we take all statuses between currentTime and currentTime - HistoricalExpirationTime +// We determine the percentage of all statuses within this set that are viewable/unviewable +// We check to see if the percentage is greater than the minimum viewable percentage for the reviewedType +// If the percentage of viewable statuses is greater than this percentage, the status is viewable. Otherwise, the status is unviewable. +// This must be an uninterrupted period of verdicts. If there is a gap in verdicts, the percentage is reset. + +// There are 9 variables within the moderation parameters that define the sticky status calculation +// -StatusEstablishingTime for each reviewedType +// -This is the amount of time a host must wait before they start to rely upon and share their sticky statuses to the network +// -The host must be downloading the reviewedType's reviews for this long +// -VerdictExpirationTime for each reviewedType +// -This is the amount of time that historical statuses will be kept and have an impact on the sticky consensus +// -MinimumViewablePercentage for each reviewedType +// -This is the percentage of time that the content must be viewable for the sticky status to be viewable + +// The trusted profileIsApproved sticky status is indicative of the identityIsBanned status +// The verified profileIsApproved sticky status is not. +// If a profile's author is banned but the profile is approved, the profile's trusted sticky consensus will be Unviewable, but the profile's verified sticky consensus will be Viewable + +import "seekia/internal/contentMetadata" +import "seekia/internal/encoding" +import "seekia/internal/helpers" +import "seekia/internal/moderation/verdictHistory" +import "seekia/internal/network/appNetworkType/getAppNetworkType" +import "seekia/internal/network/backgroundDownloads" +import "seekia/internal/profiles/readProfiles" + +import "slices" +import "time" +import "sync" +import "errors" + +// These maps should only store the sticky statuses for identities/content on the current app network type + +// Map structure: Identity Hash -> Current sticky consensus viewable status +var identityStickyStatusesMap map[[16]byte]bool = make(map[[16]byte]bool) +var identityStickyStatusesMapMutex sync.RWMutex + +// Map structure: Profile Hash -> Current sticky consensus viewable status +var profileStickyStatusesMap map[[28]byte]bool = make(map[[28]byte]bool) +var profileStickyStatusesMapMutex sync.RWMutex + +// Map structure: Message Hash -> Current sticky consensus viewable status +var messageStickyStatusesMap map[[26]byte]bool = make(map[[26]byte]bool) +var messageStickyStatusesMapMutex sync.RWMutex + + +// This function must be called whenever the app network type is switched +func DeleteVerifiedStickyStatuses(){ + + identityStickyStatusesMapMutex.Lock() + clear(identityStickyStatusesMap) + identityStickyStatusesMapMutex.Unlock() + + profileStickyStatusesMapMutex.Lock() + clear(profileStickyStatusesMap) + profileStickyStatusesMapMutex.Unlock() + + messageStickyStatusesMapMutex.Lock() + clear(messageStickyStatusesMap) + messageStickyStatusesMapMutex.Unlock() +} + + +//Outputs: +// -bool: Downloading required reviews and moderator profiles +// -bool: Necessary parameters exist +// -bool: Sticky consensus status established +// -bool: Sticky Identity is viewable consensus status (true == identity is viewable, false == Identity is not viewable) +// -error +func GetVerifiedIdentityIsViewableStickyStatus(identityHash [16]byte, networkType byte)(bool, bool, bool, bool, error){ + + isValid := helpers.VerifyNetworkType(networkType) + if (isValid == false){ + networkTypeString := helpers.ConvertByteToString(networkType) + return false, false, false, false, errors.New("GetVerifiedIdentityIsViewableStickyStatus called with invalid networkType: " + networkTypeString) + } + + clientIsDownloadingRequiredReviews, err := backgroundDownloads.CheckIfAppCanDetermineIdentityVerdicts(identityHash) + if (err != nil) { return false, false, false, false, err } + if (clientIsDownloadingRequiredReviews == false){ + return false, false, false, false, nil + } + + appNetworkType, err := getAppNetworkType.GetAppNetworkType() + if (err != nil) { return false, false, false, false, err } + if (appNetworkType != networkType){ + // We are not downloading the reviews for this network type. + return false, false, false, false, nil + } + + //TODO: Fix below + parametersExist := true + if (parametersExist == false){ + + return true, false, false, false, nil + } + + identityStickyStatusesMapMutex.RLock() + currentViewableStatus, exists := identityStickyStatusesMap[identityHash] + identityStickyStatusesMapMutex.RUnlock() + if (exists == false){ + return true, true, false, false, nil + } + + return true, true, true, currentViewableStatus, nil +} + +//Outputs: +// -bool: Profile is disabled +// -bool: Profile metadata is known +// -byte: Profile network type +// -[16]byte: Profile author identity hash +// -bool: Client is downloading required reviews and moderator profiles +// -bool: Necessary parameters exist +// -bool: Sticky consensus status established +// -bool: Sticky consensus viewable status (true == Viewable, false == Unviewable) +// -error +func GetVerifiedProfileIsViewableStickyStatus(profileHash [28]byte)(bool, bool, byte, [16]byte, bool, bool, bool, bool, error){ + + _, profileIsDisabled, err := readProfiles.ReadProfileHashMetadata(profileHash) + if (err != nil) { + profileHashHex := encoding.EncodeBytesToHexString(profileHash[:]) + return false, false, 0, [16]byte{}, false, false, false, false, errors.New("GetVerifiedProfileIsViewableStickyStatus called with invalid profileHash: " + profileHashHex) + } + + metadataExists, _, profileNetworkType, profileAuthor, _, profileIsDisabledB, _, _, err := contentMetadata.GetProfileMetadata(profileHash) + if (err != nil) { return false, false, 0, [16]byte{}, false, false, false, false, err } + if (metadataExists == false){ + return profileIsDisabled, false, 0, [16]byte{}, false, false, false, false, nil + } + if (profileIsDisabled != profileIsDisabledB){ + return false, false, 0, [16]byte{}, false, false, false, false, errors.New("GetProfileMetadata returning different profileIsDisabled status.") + } + if (profileIsDisabled == true){ + return true, true, profileNetworkType, profileAuthor, false, false, false, false, nil + } + + clientIsDownloadingRequiredReviews, err := backgroundDownloads.CheckIfAppCanDetermineIdentityVerdicts(profileAuthor) + if (err != nil) { return false, false, 0, [16]byte{}, false, false, false, false, err } + if (clientIsDownloadingRequiredReviews == false){ + + return false, true, profileNetworkType, profileAuthor, false, false, false, false, nil + } + + appNetworkType, err := getAppNetworkType.GetAppNetworkType() + if (err != nil) { return false, false, 0, [16]byte{}, false, false, false, false, err } + if (appNetworkType != profileNetworkType){ + // We are not downloading the reviews for this profile. + return false, true, profileNetworkType, profileAuthor, false, false, false, false, nil + } + + //TODO: Fix below + parametersExist := true + if (parametersExist == false){ + return false, true, profileNetworkType, profileAuthor, true, false, false, false, nil + } + + profileStickyStatusesMapMutex.RLock() + currentViewableStatus, exists := profileStickyStatusesMap[profileHash] + profileStickyStatusesMapMutex.RUnlock() + if (exists == false){ + return false, true, profileNetworkType, profileAuthor, true, true, false, false, nil + } + + return false, true, profileNetworkType, profileAuthor, true, true, true, currentViewableStatus, nil +} + + +//Outputs: +// -bool: Message metadata is known (inbox/cipher key hash) +// -byte: Message network type +// -[10]byte: Message inbox +// -[25]byte: Message cipher key hash +// -bool: Client is downloading required reviews and moderator profiles +// -bool: Necessary Parameters exist +// -bool: Message sticky consensus status established +// -bool: Message sticky consensus viewable status (true == Viewable, false == Unviewable) +// -error +func GetVerifiedMessageIsViewableStickyStatus(messageHash [26]byte)(bool, byte, [10]byte, [25]byte, bool, bool, bool, bool, error){ + + metadataExists, _, messageNetworkType, _, messageInbox, messageCipherKeyHash, err := contentMetadata.GetMessageMetadata(messageHash) + if (err != nil) { return false, 0, [10]byte{}, [25]byte{}, false, false, false, false, err } + if (metadataExists == false){ + return false, 0, [10]byte{}, [25]byte{}, false, false, false, false, nil + } + + clientIsDownloadingRequiredReviews, err := backgroundDownloads.CheckIfAppCanDetermineMessageVerdict(messageNetworkType, messageInbox, true, messageHash) + if (err != nil) { return false, 0, [10]byte{}, [25]byte{}, false, false, false, false, err } + if (clientIsDownloadingRequiredReviews == false){ + + return true, messageNetworkType, messageInbox, messageCipherKeyHash, false, false, false, false, nil + } + + //TODO: Fix below + parametersExist := true + if (parametersExist == false){ + return true, messageNetworkType, messageInbox, messageCipherKeyHash, true, false, false, false, nil + } + + messageStickyStatusesMapMutex.RLock() + currentViewableStatus, exists := messageStickyStatusesMap[messageHash] + messageStickyStatusesMapMutex.RUnlock() + if (exists == false){ + return true, messageNetworkType, messageInbox, messageCipherKeyHash, true, true, false, false, nil + } + + return true, messageNetworkType, messageInbox, messageCipherKeyHash, true, true, true, currentViewableStatus, nil +} + +func UpdateIdentityStickyStatusesMap()error{ + + allReviewedIdentityHashesList, err := verdictHistory.GetAllReviewedIdentityHashesList() + if (err != nil) { return err } + + //TODO: Fix below to retrieve from parameters + statusEstablishingTime := int64(100) + verdictExpirationTime := int64(1000) + minimumViewablePercentage := 60 + + for _, identityHash := range allReviewedIdentityHashesList{ + + identityVerdictHistoryMap, err := verdictHistory.GetIdentityVerdictHistoryMap(identityHash) + if (err != nil) { return err } + + statusIsEstablished, isViewableStatus, err := getStickyViewableStatusFromVerdictHistoryMap(identityVerdictHistoryMap, false, statusEstablishingTime, verdictExpirationTime, minimumViewablePercentage) + if (err != nil) { return err } + + identityStickyStatusesMapMutex.Lock() + + if (statusIsEstablished == false){ + delete(identityStickyStatusesMap, identityHash) + } else { + identityStickyStatusesMap[identityHash] = isViewableStatus + } + + identityStickyStatusesMapMutex.Unlock() + } + + return nil +} + + +func UpdateProfileStickyStatusesMap()error{ + + allReviewedProfileHashesList, err := verdictHistory.GetAllReviewedProfileHashesList() + if (err != nil) { return err } + + //TODO: Fix below to retrieve from parameters + statusEstablishingTime := int64(100) + verdictExpirationTime := int64(1000) + minimumViewablePercentage := 60 + + for _, profileHash := range allReviewedProfileHashesList{ + + getIsMateProfileBool := func()(bool, error){ + + profileType, _, err := readProfiles.ReadProfileHashMetadata(profileHash) + if (err != nil) { return false, err } + if (profileType == "Mate"){ + return true, nil + } + return false, nil + } + + isMateProfile, err := getIsMateProfileBool() + if (err != nil) { return err } + + profileVerdictHistoryMap, err := verdictHistory.GetProfileVerdictHistoryMap(profileHash) + if (err != nil) { return err } + + statusIsEstablished, isViewableStatus, err := getStickyViewableStatusFromVerdictHistoryMap(profileVerdictHistoryMap, isMateProfile, statusEstablishingTime, verdictExpirationTime, minimumViewablePercentage) + if (err != nil) { return err } + + profileStickyStatusesMapMutex.Lock() + + if (statusIsEstablished == false){ + delete(profileStickyStatusesMap, profileHash) + } else { + profileStickyStatusesMap[profileHash] = isViewableStatus + } + + profileStickyStatusesMapMutex.Unlock() + } + + return nil +} + + +func UpdateMessageStickyStatusesMap()error{ + + allReviewedMessageHashesList, err := verdictHistory.GetAllReviewedMessageHashesList() + if (err != nil) { return err } + + //TODO: Fix below to retrieve from parameters + statusEstablishingTime := int64(100) + verdictExpirationTime := int64(1000) + minimumViewablePercentage := 60 + + for _, messageHash := range allReviewedMessageHashesList{ + + messageVerdictHistoryMap, err := verdictHistory.GetMessageVerdictHistoryMap(messageHash) + if (err != nil) { return err } + + statusIsEstablished, isViewableStatus, err := getStickyViewableStatusFromVerdictHistoryMap(messageVerdictHistoryMap, false, statusEstablishingTime, verdictExpirationTime, minimumViewablePercentage) + if (err != nil) { return err } + + messageStickyStatusesMapMutex.Lock() + + if (statusIsEstablished == false){ + delete(messageStickyStatusesMap, messageHash) + } else { + messageStickyStatusesMap[messageHash] = isViewableStatus + } + + messageStickyStatusesMapMutex.Unlock() + } + + return nil +} + + +func PruneOldConsensusStatuses()error{ + + // TODO: Prune statuses for content that is expired and more... + + return nil +} + +//Inputs: +// -map[int64]string: Unix Time -> Verdict at unixTime +// -bool: ReviewedHash is a mate profile +// -int64: Time required for a status to be established. Verdicts must exist on a regular interval for this duration +// -int64: The length of time that a verdict will have an impact on the sticky viewable status +// -int: The percentage of verdicts that must be viewable for the status to be considered viewable +// -Example: 60 = 60% of statuses must be viewable for the sticky status to be viewable +//Outputs: +// -bool: Status is established +// -bool: Profile/Message/Identity is viewable +// -error +func getStickyViewableStatusFromVerdictHistoryMap(verdictHistoryMap map[int64]string, isMateProfile bool, establishingTime int64, verdictExpirationTime int64, minimumViewablePercentage int)(bool, bool, error){ + + currentTime := time.Now().Unix() + oldestEligibleVerdictTime := currentTime - verdictExpirationTime + + verdictTimesList := make([]int64, 0) + + for verdictTime, _ := range verdictHistoryMap{ + + if (verdictTime < oldestEligibleVerdictTime){ + // We do not count verdicts that are this old in our stickyConsensus calculation + continue + } + + verdictTimesList = append(verdictTimesList, verdictTime) + } + + if (len(verdictTimesList) <= 2){ + // Not enough verdict to calculate sticky consensus status + return false, false, nil + } + + // We sort verdicts in ascending order, oldest to newest + + slices.Sort(verdictTimesList) + + // Now we iterate through verdicts + // We record the number of verdicts and the number of verdicts that are viewable + // If we encounter a long enough break between verdicts, we reset our counts + // This is done because we need an uninterrupted period of verdicts to make our calculation + + numberOfEligibleVerdicts := 0 + numberOfViewableVerdicts := 0 + + // We keep track of the oldest verdict time to determine the beginning of the continuous period of verdicts + // If the period is not old enough, the consensus status is not established + oldestVerdictTime := verdictTimesList[0] + + // We keep track of previous verdict time to check the length of gaps between verdict recordings + previousVerdictTime := int64(0) + + for index, verdictTime := range verdictTimesList{ + + if (index != 0){ + + timeElapsedSinceLastVerdict := verdictTime - previousVerdictTime + + if (timeElapsedSinceLastVerdict > (verdictHistory.VerdictIntervalDuration * 2)){ + // There was too long of a gap between verdict recordings + // We reset our counts + numberOfEligibleVerdicts = 0 + numberOfViewableVerdicts = 0 + oldestVerdictTime = verdictTime + } + } + previousVerdictTime = verdictTime + + numberOfEligibleVerdicts += 1 + + verdict, exists := verdictHistoryMap[verdictTime] + if (exists == false){ + return false, false, errors.New("verdictHistoryMap missing verdictTime") + } + + checkIfVerdictIsViewable := func()bool{ + + if (verdict == "Banned"){ + return false + } + + if (verdict == "Approved"){ + return true + } + + // verdict == "Undecided" + // reviewedHash type is not identity + + if (isMateProfile == true){ + // Mate profiles must be approved to be viewable. "Undecided" is not enough. + return false + } + + // ReviewedHash is non-mate profile/message. These are only not viewable if they are banned + return true + } + + viewableStatus := checkIfVerdictIsViewable() + + if (viewableStatus == true){ + numberOfViewableVerdicts += 1 + } + } + + if (numberOfEligibleVerdicts == 0){ + // No eligible verdicts exist + return false, false, nil + } + + // This represents the amount of time we have been recording verdicts without a gap that is too long + trackedDuration := currentTime - oldestVerdictTime + + if (trackedDuration < establishingTime){ + // We have not been tracking verdicts for long enough to determine the sticky viewable status + // The sticky consensus will be established once the client has been enabled for long enough + return false, false, nil + } + + // Sticky viewable status is established. + // We see if status is viewable or not. + + percentageOfViewableVerdicts := (float64(numberOfViewableVerdicts)/float64(numberOfEligibleVerdicts)) * 100 + + if (percentageOfViewableVerdicts < float64(minimumViewablePercentage)){ + return true, false, nil + } + return true, true, nil +} + + + + diff --git a/internal/moderation/verifiedVerdict/verifiedVerdict.go b/internal/moderation/verifiedVerdict/verifiedVerdict.go new file mode 100644 index 0000000..cb75970 --- /dev/null +++ b/internal/moderation/verifiedVerdict/verifiedVerdict.go @@ -0,0 +1,982 @@ + +// verifiedVerdict provides functions to determine the current moderator consensus verdict for a particular identity/message/profile +// These verdicts are calculated based on the downloaded moderator reviews, and are thus verified + +package verifiedVerdict + +// We cannot determine the verified consensus verdict of a message/profile without knowing its metadata. +// See package contentMetadata for an explanation. + +// Possible verdicts: +// -Identity: Banned/Not Banned +// -Profile/Message: Banned/Approved/Undecided + +import "seekia/internal/contentMetadata" +import "seekia/internal/identity" +import "seekia/internal/encoding" +import "seekia/internal/moderation/bannedModeratorConsensus" +import "seekia/internal/moderation/enabledModerators" +import "seekia/internal/moderation/moderatorScores" +import "seekia/internal/moderation/reviewStorage" +import "seekia/internal/network/appNetworkType/getAppNetworkType" +import "seekia/internal/network/backgroundDownloads" +import "seekia/internal/profiles/readProfiles" + +import "errors" + +//Outputs: +// -bool: Client is downloading the required reviews/moderator profiles +// -bool: Parameters Exist +// -bool: Identity is banned +// -int: Number of eligible moderators who have banned the identity +// -float64: Ban Advocates identity score sum (Sum of identity score of all eligible moderators who banned the identity) +// -[][16]byte: List of all ban advocates (Eligible and banned) +// -error +func GetVerifiedIdentityVerdict(identityHash [16]byte, networkType byte)(bool, bool, bool, int, float64, [][16]byte, error){ + + clientIsDownloadingRequiredReviews, err := backgroundDownloads.CheckIfAppCanDetermineIdentityVerdicts(identityHash) + if (err != nil) { return false, false, false, 0, 0, nil, err } + if (clientIsDownloadingRequiredReviews == false){ + + return false, false, false, 0, 0, nil, nil + } + + appNetworkType, err := getAppNetworkType.GetAppNetworkType() + if (err != nil) { return false, false, false, 0, 0, nil, err } + if (appNetworkType != networkType){ + // We are not downloading reviews for this network type. + return false, false, false, 0, 0, nil, nil + } + + //TODO: Add parameters check + parametersExist := true + if (parametersExist == false){ + + // We do not have moderation parameters. We cannot determine verdict consensus + return true, false, false, 0, 0, nil, nil + } + + banAdvocatesMap, err := reviewStorage.GetIdentityBanAdvocatesMap(identityHash, networkType) + if (err != nil) { return false, false, false, 0, 0, nil, err } + + numberOfEligibleBanAdvocates := 0 + eligibleBanAdvocatesScoreSum := float64(0) + allEnabledBanAdvocatesList := make([][16]byte, 0) + + for moderatorIdentityHash, _ := range banAdvocatesMap{ + + moderatorIsEnabled, err := enabledModerators.CheckIfModeratorIsEnabled(true, moderatorIdentityHash, networkType) + if (err != nil){ return false, false, false, 0, 0, nil, err } + if (moderatorIsEnabled == false){ + // We will disregard all of this moderators reviews + continue + } + + allEnabledBanAdvocatesList = append(allEnabledBanAdvocatesList, moderatorIdentityHash) + + downloadingRequiredData, parametersExist, moderatorIsBanned, err := bannedModeratorConsensus.GetModeratorIsBannedStatus(true, moderatorIdentityHash, networkType) + if (err != nil) { return false, false, false, 0, 0, nil, err } + if (downloadingRequiredData == false){ + return false, true, false, 0, 0, nil, nil + } + if (parametersExist == false){ + return true, false, false, 0, 0, nil, nil + } + if (moderatorIsBanned == true){ + continue + } + + scoreIsKnown, moderatorScore, scoreIsSufficient, _, _, err := moderatorScores.GetModeratorIdentityScore(moderatorIdentityHash) + if (err != nil) { return false, false, false, 0, 0, nil, err } + if (scoreIsKnown == false || scoreIsSufficient == false){ + continue + } + + numberOfEligibleBanAdvocates += 1 + eligibleBanAdvocatesScoreSum += moderatorScore + } + + //Outputs: + // -bool: Downloading required data + // -bool: Parameters exist + // -bool: Identity is banned + // -error + getBanConsensusVerdict := func()(bool, bool, bool, error){ + + identityType, err := identity.GetIdentityTypeFromIdentityHash(identityHash) + if (err != nil){ + identityHashHex := encoding.EncodeBytesToHexString(identityHash[:]) + return false, false, false, errors.New("getBanConsensusVerdict reached with invalid identityHash: " + identityHashHex) + } + if (identityType != "Moderator"){ + + //TODO: Retrieve this variable from parameters + minimumBanAdvocatesNeeded := 3 + + if (numberOfEligibleBanAdvocates < minimumBanAdvocatesNeeded){ + return true, true, false, nil + } + return true, true, true, nil + } + + // identityType == "Moderator" + + // For moderators, the banning process is more complicated + // Moderators must have a higher score to ban another moderator + // We use another package to calculate the result + + downloadingRequiredData, parametersExist, moderatorIsBanned, err := bannedModeratorConsensus.GetModeratorIsBannedStatus(true, identityHash, networkType) + if (err != nil) { return false, false, false, err } + if (downloadingRequiredData == false){ + return false, false, false, nil + } + if (parametersExist == false){ + return true, false, false, nil + } + + return true, true, moderatorIsBanned, nil + } + + downloadingRequiredData, parametersExist, banConsensusVerdict, err := getBanConsensusVerdict() + if (err != nil) { return false, false, false, 0, 0, nil, err } + if (downloadingRequiredData == false){ + return false, true, false, 0, 0, nil, nil + } + if (parametersExist == false){ + return true, false, false, 0, 0, nil, nil + } + + return true, true, banConsensusVerdict, numberOfEligibleBanAdvocates, eligibleBanAdvocatesScoreSum, allEnabledBanAdvocatesList, nil +} + +// This function returns if the provided profile is banned. +// This function does not take into account if the profile author is banned. +// A profile with an insufficient number of reviews/reviewers can be "Undecided" +// Host/Moderator "Undecided" profiles are still considered viewable, only Mate profiles must be "Approved" to be viewable +//Outputs: +// -bool: Profile is disabled +// -bool: Profile metadata is known +// -int: Profile version +// -byte: Profile network type +// -[16]byte: Profile Identity Hash +// -map[int][27]byte: Profile attribute hashes map (Attribute identifier -> Attribute hash) +// -bool: Client is downloading required reviews and moderator profiles +// -bool: Parameters exist +// -string: Moderator consensus verdict ("Approved"/"Banned"/"Undecided") +// -int: Number of eligible moderators who have approved the profile +// -int: Number of eligible moderators who have banned the profile (including a single attribute) +// -float64: Approve Score Sum (sum of identity scores for all eligible moderators who approved the entire profile) +// -float64: Ban Score Sum (sum of identity scores for all eligible moderators who banned the profile, including a single attribute) +// -map[int]int: Attribute Identifier -> Number of eligible approve advocates +// -map[int]int: Attribute identifier -> Number of eligible ban advocates +// -This does not include users who banned the full profile without describing which attribute was unruleful +// -map[int]float64: Attribute identifier -> Sum of all eligible approve advocate identity scores +// -This includes users who banned the full profile. +// -map[int]float64: Attribute identifier -> Sum of all eligible ban advocate identity scores +// -This includes moderators who banned the entire profile +// -[][16]byte: List of all moderators who have approved the profile (Eligible and banned) +// -This does not include moderators who have only approved some attributes. +// -This only includes moderators who have approved the full profile. +// -[][16]byte: List of all moderators who have banned the profile (eligible and banned) +// -This includes moderators who have banned only 1 attribute. +// -int: Number of moderators who have banned the full profile (eligible and banned) +// -This only includes moderators who have banned the full profile (not including ones who only banned attributes) +// -map[int][][16]byte: Map of attribute identifier -> List of moderators who have approved attribute (eligible and banned) +// -map[int][][16]byte: Map of Attribute Identifier -> List of moderators who have banned attribute (eligible and banned) +// -This does not include moderators who have banned the entire profile without describing which attribute was unruleful +// -error +func GetVerifiedProfileVerdict(profileHash [28]byte)(bool, bool, int, byte, [16]byte, map[int][27]byte, bool, bool, string, int, int, float64, float64, map[int]int, map[int]int, map[int]float64, map[int]float64, [][16]byte, [][16]byte, int, map[int][][16]byte, map[int][][16]byte, error){ + + _, profileIsDisabled, err := readProfiles.ReadProfileHashMetadata(profileHash) + if (err != nil){ + profileHashHex := encoding.EncodeBytesToHexString(profileHash[:]) + return false, false, 0, 0, [16]byte{}, nil, false, false, "", 0, 0, 0, 0, nil, nil, nil, nil, nil, nil, 0, nil, nil, errors.New("GetVerifiedProfileVerdict called with invalid profileHash: " + profileHashHex) + } + + //TODO: Add parameters check + parametersExist := true + + metadataExists, profileVersion, profileNetworkType, profileIdentityHash, _, profileIsDisabledB, _, profileAttributeHashesMap, err := contentMetadata.GetProfileMetadata(profileHash) + if (err != nil) { return false, false, 0, 0, [16]byte{}, nil, false, false, "", 0, 0, 0, 0, nil, nil, nil, nil, nil, nil, 0, nil, nil, err } + if (metadataExists == false){ + // We do not have profile metadata, we cannot determine moderator consensus + return profileIsDisabled, false, 0, 0, [16]byte{}, nil, false, false, "", 0, 0, 0, 0, nil, nil, nil, nil, nil, nil, 0, nil, nil, nil + } + if (profileIsDisabled != profileIsDisabledB){ + return false, false, 0, 0, [16]byte{}, nil, false, false, "", 0, 0, 0, 0, nil, nil, nil, nil, nil, nil, 0, nil, nil, errors.New("GetProfileMetadata returning different profileIsDisabled status than ReadProfileHashMetadata.") + } + if (profileIsDisabled == true){ + + return true, true, profileVersion, profileNetworkType, profileIdentityHash, nil, true, parametersExist, "", 0, 0, 0, 0, nil, nil, nil, nil, nil, nil, 0, nil, nil, nil + } + + clientIsDownloadingRequiredReviews, err := backgroundDownloads.CheckIfAppCanDetermineIdentityVerdicts(profileIdentityHash) + if (err != nil) { return false, false, 0, 0, [16]byte{}, nil, false, false, "", 0, 0, 0, 0, nil, nil, nil, nil, nil, nil, 0, nil, nil, err } + if (clientIsDownloadingRequiredReviews == false){ + // We cannot determine verdict. Required reviews are not being downloaded. + + return false, true, profileVersion, profileNetworkType, profileIdentityHash, profileAttributeHashesMap, false, parametersExist, "", 0, 0, 0, 0, nil, nil, nil, nil, nil, nil, 0, nil, nil, nil + } + + if (parametersExist == false){ + // We dont have parameters. We cannot determine status. + return false, true, profileVersion, profileNetworkType, profileIdentityHash, profileAttributeHashesMap, true, false, "", 0, 0, 0, 0, nil, nil, nil, nil, nil, nil, 0, nil, nil, nil + } + + // This stores all moderator identity hashes who reviewed the profile/attributes + // We use a map to avoid duplicates + // Map Structure: Reviewer identity hash -> Nothing + allReviewersMap := make(map[[16]byte]struct{}) + + type fullProfileVerdictInfo struct{ + + // Verdict for full profile ("Approve"/"Ban") + Verdict string + + // Time of verdict for full profile + VerdictTime int64 + } + + // Map Structure: Author Identity hash -> fullProfileVerdictInfo + fullProfileVerdictsInfoMap := make(map[[16]byte]fullProfileVerdictInfo) + + type attributeReviewObject struct{ + + AttributeIdentifier int + + // Verdict for the attribute ("Approve"/"Ban") + Verdict string + + // Time at which the verdict was made + VerdictTime int64 + } + + // Map Structure: Reviewer identity hash -> List of attribute review objects + attributeReviewsMap := make(map[[16]byte][]attributeReviewObject) + + // First we add full profile reviews + + fullProfileApproveAdvocatesMap, fullProfileBanAdvocatesMap, err := reviewStorage.GetProfileVerdictMaps(profileHash, profileNetworkType, false, nil) + if (err != nil) { return false, false, 0, 0, [16]byte{}, nil, false, false, "", 0, 0, 0, 0, nil, nil, nil, nil, nil, nil, 0, nil, nil, err } + + addFullProfileVerdictsToMaps := func(reviewersMap map[[16]byte]int64, verdict string)error{ + + for reviewerIdentityHash, verdictTime := range reviewersMap{ + + allReviewersMap[reviewerIdentityHash] = struct{}{} + + newVerdictInfoObject := fullProfileVerdictInfo{ + Verdict: verdict, + VerdictTime: verdictTime, + } + + _, exists := fullProfileVerdictsInfoMap[reviewerIdentityHash] + if (exists == true){ + return errors.New("Trying to add reviewer to fullProfileVerdictsInfoMap and reviewer entry already exists.") + } + fullProfileVerdictsInfoMap[reviewerIdentityHash] = newVerdictInfoObject + } + + return nil + } + + err = addFullProfileVerdictsToMaps(fullProfileApproveAdvocatesMap, "Approve") + if (err != nil) { return false, false, 0, 0, [16]byte{}, nil, false, false, "", 0, 0, 0, 0, nil, nil, nil, nil, nil, nil, 0, nil, nil, err } + + err = addFullProfileVerdictsToMaps(fullProfileBanAdvocatesMap, "Ban") + if (err != nil) { return false, false, 0, 0, [16]byte{}, nil, false, false, "", 0, 0, 0, 0, nil, nil, nil, nil, nil, nil, 0, nil, nil, err } + + // Now we add profile attribute reviews + + for attributeIdentifier, attributeHash := range profileAttributeHashesMap{ + + addAttributeVerdictsToMap := func(reviewersMap map[[16]byte]int64, verdict string)error{ + + for reviewerIdentityHash, verdictTime := range reviewersMap{ + + allReviewersMap[reviewerIdentityHash] = struct{}{} + + newAttributeReviewObject := attributeReviewObject{ + AttributeIdentifier: attributeIdentifier, + Verdict: verdict, + VerdictTime: verdictTime, + } + + existingReviewObjectsList, exists := attributeReviewsMap[reviewerIdentityHash] + if (exists == false){ + newAttributeReviewObjectsList := []attributeReviewObject{newAttributeReviewObject} + attributeReviewsMap[reviewerIdentityHash] = newAttributeReviewObjectsList + } else { + newAttributeReviewObjectsList := append(existingReviewObjectsList, newAttributeReviewObject) + attributeReviewsMap[reviewerIdentityHash] = newAttributeReviewObjectsList + } + } + + return nil + } + + approveAdvocatesMap, banAdvocatesMap, err := reviewStorage.GetProfileAttributeVerdictMaps(attributeHash, profileNetworkType, false, nil) + if (err != nil){ return false, false, 0, 0, [16]byte{}, nil, false, false, "", 0, 0, 0, 0, nil, nil, nil, nil, nil, nil, 0, nil, nil, err } + + err = addAttributeVerdictsToMap(approveAdvocatesMap, "Approve") + if (err != nil){ return false, false, 0, 0, [16]byte{}, nil, false, false, "", 0, 0, 0, 0, nil, nil, nil, nil, nil, nil, 0, nil, nil, err } + + err = addAttributeVerdictsToMap(banAdvocatesMap, "Ban") + if (err != nil){ return false, false, 0, 0, [16]byte{}, nil, false, false, "", 0, 0, 0, 0, nil, nil, nil, nil, nil, nil, 0, nil, nil, err } + } + + // This keeps track of the number of eligible moderators who approved the full profile + numberOfEligibleApproveAdvocates := 0 + // This keeps track of the number of eligible moderators who banned the profile + numberOfEligibleBanAdvocates := 0 + + // This is the sum of all identity scores of all eligible moderators who approved the full profile + eligibleApproveAdvocateScoresSum := float64(0) + // This is the sum of all identity scores of all eligible moderators who banned the profile + eligibleBanAdvocateScoresSum := float64(0) + + // We need these maps when determining final verdict + // Map Structure: Attribute identifier -> Number of eligible approve advocates + eligibleAttributeApproveAdvocateCountsMap := make(map[int]int) + // Map Structure: Attribute identifier -> Number of eligible ban advocates (not including full profile bans) + eligibleAttributeBanAdvocateCountsMap := make(map[int]int) + + // This is a map that keeps track of each attribute's approve and ban weight + // This is used to determine if the profile is approved or banned + // Each approve/ban increases attribute weight by the moderator's identity score + // Approving the entire profile adds to each attribute approve weight + // Banning the entire profile adds to each attribute ban weight + // It ignores the reviews of banned/ineligible moderators + // Map Structure: Attribute identifier -> Attribute weight + attributeApproveWeightsMap := make(map[int]float64) + attributeBanWeightsMap := make(map[int]float64) + + // This is a list of all moderators who have approved the profile (Eligible and banned) + // This only includes moderators who have approved the full profile. + // This does not include moderators who have only approved some attributes. + allProfileApproveAdvocatesList := make([][16]byte, 0) + + // This is a list of all moderators who have banned the profile (eligible and banned) + // This includes moderators who have banned only 1 attribute. + allProfileBanAdvocatesList := make([][16]byte, 0) + + // This keeps track of the number of moderators who have banned the full profile (eligible and banned) + numberOfFullProfileBanAdvocates := 0 + + // -This includes moderators who have approved the full profile. + // Map Structure: Attribute identifier -> List of moderators who have approved attribute (eligible and banned) + allAttributeApproveAdvocatesMap := make(map[int][][16]byte) + // -This does not include moderators who have only banned the entire profile, but not the specified attribute + // Map Structure: Attribute Identifier -> List of moderators who have banned attribute (eligible and banned) + allAttributeBanAdvocatesMap := make(map[int][][16]byte) + + // Now we iterate through each moderator to populate the variables we just created + + for moderatorIdentityHash, _ := range allReviewersMap{ + + moderatorIsEnabled, err := enabledModerators.CheckIfModeratorIsEnabled(true, moderatorIdentityHash, profileNetworkType) + if (err != nil){ return false, false, 0, 0, [16]byte{}, nil, false, false, "", 0, 0, 0, 0, nil, nil, nil, nil, nil, nil, 0, nil, nil, err } + if (moderatorIsEnabled == false){ + // We will disregard all of this moderators reviews + continue + } + + downloadingRequiredData, parametersExist, moderatorIsBanned, err := bannedModeratorConsensus.GetModeratorIsBannedStatus(true, moderatorIdentityHash, profileNetworkType) + if (err != nil) { return false, false, 0, 0, [16]byte{}, nil, false, false, "", 0, 0, 0, 0, nil, nil, nil, nil, nil, nil, 0, nil, nil, err } + if (downloadingRequiredData == false){ + // We cannot determine verdict. Required reviews are not being downloaded. + return false, true, profileVersion, profileNetworkType, profileIdentityHash, profileAttributeHashesMap, false, true, "", 0, 0, 0, 0, nil, nil, nil, nil, nil, nil, 0, nil, nil, nil + } + if (parametersExist == false){ + // We dont have parameters. We cannot determine status. + return false, true, profileVersion, profileNetworkType, profileIdentityHash, profileAttributeHashesMap, true, false, "", 0, 0, 0, 0, nil, nil, nil, nil, nil, nil, 0, nil, nil, nil + } + + //Outputs: + // -bool: Moderator is eligible + // -float64: Moderator identity score + // -error + checkIfModeratorIsEligible := func()(bool, float64, error){ + + if (moderatorIsBanned == true){ + return false, 0, nil + } + + scoreIsKnown, moderatorScore, scoreIsSufficient, _, _, err := moderatorScores.GetModeratorIdentityScore(moderatorIdentityHash) + if (err != nil) { return false, 0, err } + if (scoreIsKnown == false || scoreIsSufficient == false){ + return false, 0, nil + } + + return true, moderatorScore, nil + } + + moderatorIsEligible, moderatorScore, err := checkIfModeratorIsEligible() + if (err != nil) { return false, false, 0, 0, [16]byte{}, nil, false, false, "", 0, 0, 0, 0, nil, nil, nil, nil, nil, nil, 0, nil, nil, err } + + // We use the function below to deal with the possibility of a conflict between the profile/attribute verdicts + // Outputs: + // -bool: Full profile verdict exists + // -string: Full profile verdict ("Approve"/"Ban") + // -map[int]string: Attribute identifier -> Attribute verdict ("Approve"/"Ban") + // -error + getModeratorVerdicts := func()(bool, string, map[int]string, error){ + + fullProfileReviewInfoObject, fullProfileReviewExists := fullProfileVerdictsInfoMap[moderatorIdentityHash] + + attributeReviewObjectsList, anyAttributeReviewExists := attributeReviewsMap[moderatorIdentityHash] + + if (fullProfileReviewExists == false && anyAttributeReviewExists == false){ + return false, "", nil, errors.New("allReviewersMap contains reviewer without any reviews.") + } + + if (fullProfileReviewExists == true && anyAttributeReviewExists == false){ + + fullProfileReviewVerdict := fullProfileReviewInfoObject.Verdict + + if (fullProfileReviewVerdict == "Approve"){ + // Full profile is approved. All attributes are approved + + // Map Structure: Attribute identifier -> Attribute verdict + attributeVerdictsMap := make(map[int]string) + + for attributeIdentifier, _ := range profileAttributeHashesMap{ + attributeVerdictsMap[attributeIdentifier] = "Approve" + } + + return true, "Approve", attributeVerdictsMap, nil + } + + emptyMap := make(map[int]string) + + return true, "Ban", emptyMap, nil + } + + if (fullProfileReviewExists == false && anyAttributeReviewExists == true){ + + // Map Structure: Attribute identifier -> Attribute verdict + attributeReviewsMap := make(map[int]string) + + for _, attributeReviewObject := range attributeReviewObjectsList{ + + attributeIdentifier := attributeReviewObject.AttributeIdentifier + attributeVerdict := attributeReviewObject.Verdict + + attributeReviewsMap[attributeIdentifier] = attributeVerdict + } + + return false, "", attributeReviewsMap, nil + } + + // The moderator has reviewed the full profile and at least 1 of the profile's attributes + // We have to take into account that newer reviews will cancel out older reviews + // + // These are the two possible conflicts: + // 1. if a user bans a profile attribute, and then later approves the entire profile, all of their + // previous attribute ban reviews for the profile are discarded + // 2. If a user approves a full profile, and then later bans an attribute, their full profile approval review is discarded + + fullProfileReviewVerdict := fullProfileReviewInfoObject.Verdict + fullProfileVerdictTime := fullProfileReviewInfoObject.VerdictTime + + // Outputs: + // -bool: Full profile verdict exists + // -string: Full profile verdict + // -error + getFullProfileVerdict := func()(bool, string, error){ + + if (fullProfileReviewVerdict == "Ban"){ + return true, "Ban", nil + } + + for _, attributeReviewObject := range attributeReviewObjectsList{ + + attributeVerdict := attributeReviewObject.Verdict + + if (attributeVerdict == "Ban"){ + + attributeVerdictTime := attributeReviewObject.VerdictTime + + if (attributeVerdictTime > fullProfileVerdictTime){ + // The moderator banned an attribute after approving the full profile + // We discard their full profile approval + return false, "", nil + } + } + } + + return true, "Approve", nil + } + + fullProfileReviewExists, fullProfileVerdict, err := getFullProfileVerdict() + if (err != nil) { return false, "", nil, err } + + if (fullProfileReviewExists == true && fullProfileVerdict == "Approve"){ + // Full profile is approved. All attributes are approved + + // Map Structure: Attribute identifier -> Attribute verdict + attributeVerdictsMap := make(map[int]string) + + for attributeIdentifier, _ := range profileAttributeHashesMap{ + attributeVerdictsMap[attributeIdentifier] = "Approve" + } + + return true, "Approve", attributeVerdictsMap, nil + } + + // Map Structure: Attribute identifier -> Attribute verdict + attributeVerdictsMap := make(map[int]string) + + for _, attributeReviewObject := range attributeReviewObjectsList{ + + attributeIdentifier := attributeReviewObject.AttributeIdentifier + attributeVerdict := attributeReviewObject.Verdict + + attributeVerdictsMap[attributeIdentifier] = attributeVerdict + } + + return fullProfileReviewExists, fullProfileVerdict, attributeVerdictsMap, nil + } + + fullProfileReviewExists, fullProfileVerdict, attributeVerdictsMap, err := getModeratorVerdicts() + if (err != nil) { return false, false, 0, 0, [16]byte{}, nil, false, false, "", 0, 0, 0, 0, nil, nil, nil, nil, nil, nil, 0, nil, nil, err } + + if (fullProfileReviewExists == true && fullProfileVerdict == "Ban"){ + + numberOfFullProfileBanAdvocates += 1 + } + + //Outputs: + // -bool: Verdict exists + // -string: Verdict + getModeratorProfileVerdict := func()(bool, string){ + + if (fullProfileReviewExists == true){ + + return true, fullProfileVerdict + } + + // A moderator who has banned a single attribute is considered to have banned the entire profile + + for _, attributeVerdict := range attributeVerdictsMap{ + + if (attributeVerdict == "Ban"){ + return true, "Ban" + } + } + + return false, "" + } + + profileVerdictExists, profileVerdict := getModeratorProfileVerdict() + if (profileVerdictExists == true){ + + if (profileVerdict == "Approve"){ + allProfileApproveAdvocatesList = append(allProfileApproveAdvocatesList, moderatorIdentityHash) + + if (moderatorIsEligible == true){ + numberOfEligibleApproveAdvocates += 1 + eligibleApproveAdvocateScoresSum += moderatorScore + } + + } else { + allProfileBanAdvocatesList = append(allProfileBanAdvocatesList, moderatorIdentityHash) + + if (moderatorIsEligible == true){ + numberOfEligibleBanAdvocates += 1 + eligibleBanAdvocateScoresSum += moderatorScore + } + } + } + + for attributeIdentifier, attributeVerdict := range attributeVerdictsMap{ + + addIdentityHashToMapEntryList := func(inputMap map[int][][16]byte){ + + currentList, exists := inputMap[attributeIdentifier] + if (exists == false){ + inputMap[attributeIdentifier] = [][16]byte{moderatorIdentityHash} + } else { + currentList = append(currentList, moderatorIdentityHash) + inputMap[attributeIdentifier] = currentList + } + } + + if (attributeVerdict == "Approve"){ + + addIdentityHashToMapEntryList(allAttributeApproveAdvocatesMap) + + if (moderatorIsEligible == true){ + eligibleAttributeApproveAdvocateCountsMap[attributeIdentifier] += 1 + } + + } else { + + addIdentityHashToMapEntryList(allAttributeBanAdvocatesMap) + + if (moderatorIsEligible == true){ + eligibleAttributeBanAdvocateCountsMap[attributeIdentifier] += 1 + } + } + } + + if (moderatorIsEligible == true){ + + // Now we add to attributeApproveWeightsMap and attributeBanWeightsMap + + if (fullProfileReviewExists == true && fullProfileVerdict == "Ban"){ + + // We add weight for every attribute entry + + for attributeIdentifier, _ := range profileAttributeHashesMap{ + + attributeBanWeightsMap[attributeIdentifier] += moderatorScore + } + } else { + + // We add to each attribute, only when user approved/banned them + + for attributeIdentifier, attributeVerdict := range attributeVerdictsMap{ + + if (attributeVerdict == "Approve"){ + attributeApproveWeightsMap[attributeIdentifier] += moderatorScore + } else { + attributeBanWeightsMap[attributeIdentifier] += moderatorScore + } + } + } + } + } + + userIdentityType, err := identity.GetIdentityTypeFromIdentityHash(profileIdentityHash) + if (err != nil){ return false, false, 0, 0, [16]byte{}, nil, false, false, "", 0, 0, 0, 0, nil, nil, nil, nil, nil, nil, 0, nil, nil, err } + + if (len(allProfileApproveAdvocatesList) == 0 && len(allProfileBanAdvocatesList) == 0 && len(allAttributeApproveAdvocatesMap) == 0 && len(allAttributeBanAdvocatesMap) == 0){ + + // No reviews exist. + // Profile is undecided. + emptyMapA := make(map[int]int) + emptyMapB := make(map[int]int) + emptyMapC := make(map[int]float64) + emptyMapD := make(map[int]float64) + emptyListA := make([][16]byte, 0) + emptyListB := make([][16]byte, 0) + emptyMapE := make(map[int][][16]byte) + emptyMapF := make(map[int][][16]byte) + + return false, true, profileVersion, profileNetworkType, profileIdentityHash, profileAttributeHashesMap, true, true, "Undecided", 0, 0, 0, 0, emptyMapA, emptyMapB, emptyMapC, emptyMapD, emptyListA, emptyListB, 0, emptyMapE, emptyMapF, nil + } + + //TODO: Retrieve from parameters + minimumAttributeApproveAdvocates_Mate := 0 // Applies to all non-canonical attributes even if they are not banned + minimumAttributeApproveAdvocates_Host := 0 // Only used if attribute/profile is banned + minimumAttributeApproveAdvocates_Moderator := 0 // Only used if attribute/profile is banned + + getMinimumNeededApprovers := func()int{ + if (userIdentityType == "Mate"){ + return minimumAttributeApproveAdvocates_Mate + } + if (userIdentityType == "Host"){ + return minimumAttributeApproveAdvocates_Host + } + // userIdentityType == "Moderator" + return minimumAttributeApproveAdvocates_Moderator + } + + minimumNeededApprovers := getMinimumNeededApprovers() + + //TODO: Retrieve from parameters + minimumApprovedRatio_Mate := float64(1.5) + minimumApprovedRatio_Host := float64(1.7) + minimumApprovedRatio_Moderator := float64(1.8) + + getMinimumApprovedRatio := func()float64{ + if (userIdentityType == "Mate"){ + return minimumApprovedRatio_Mate + } + if (userIdentityType == "Host"){ + return minimumApprovedRatio_Host + } + // userIdentityType == "Moderator" + return minimumApprovedRatio_Moderator + } + + minimumApprovedRatio := getMinimumApprovedRatio() + + getVerdictConsensus := func()(string, error){ + + // This will be true if any non-canonical attribute is undecided + nonCanonicalUndecidedExists := false + + for attributeIdentifier, attributeHash := range profileAttributeHashesMap{ + + _, attributeIsCanonical, err := readProfiles.ReadAttributeHashMetadata(attributeHash) + if (err != nil){ + attributeHashHex := encoding.EncodeBytesToHexString(attributeHash[:]) + return "", errors.New("profileAttributeHashesMap contains invalid attribute hash: " + attributeHashHex) + } + + getAttributeApproveWeight := func()float64{ + + attributeApproveWeight, approveWeightExists := attributeApproveWeightsMap[attributeIdentifier] + if (approveWeightExists == false){ + return 0 + } + return attributeApproveWeight + } + + getAttributeBanWeight := func()float64{ + + attributeBanWeight, banWeightExists := attributeBanWeightsMap[attributeIdentifier] + if (banWeightExists == false){ + return 0 + } + return attributeBanWeight + } + + approveWeight := getAttributeApproveWeight() + + banWeight := getAttributeBanWeight() + + if (approveWeight < 0 || banWeight < 0){ + // This should never happen. + return "", errors.New("Attribute approve/ban weight is negative.") + } + + if (approveWeight == 0 && banWeight == 0){ + + if (userIdentityType == "Mate" && attributeIsCanonical == false){ + // Attribute status is Undecided + nonCanonicalUndecidedExists = true + } + continue + } + if (approveWeight == 0 && banWeight > 0){ + // The attribute is banned, thus the entire profile is banned. + return "Banned", nil + } + + getNumberOfAttributeApproveAdvocates := func()int{ + + numberOfAttributeApproveAdvocates, exists := eligibleAttributeApproveAdvocateCountsMap[attributeIdentifier] + if (exists == false){ + return 0 + } + return numberOfAttributeApproveAdvocates + } + + if (approveWeight > 0 && banWeight == 0){ + + if (userIdentityType == "Mate" && attributeIsCanonical == false){ + + // The attribute must be approved by the minimum number of reviewers + numberOfAttributeApproveAdvocates := getNumberOfAttributeApproveAdvocates() + + if (numberOfAttributeApproveAdvocates < minimumAttributeApproveAdvocates_Mate){ + // Attribute status is Undecided + // The attribute is not approved by the minimum number of moderators. + nonCanonicalUndecidedExists = true + } + } + continue + } + // approveWeight > 0 && banWeight > 0 + + // There is at least 1 ban on the attribute/profile + // We must make sure that the minimum number of approvers have approved the attribute + + numberOfAttributeApproveAdvocates := getNumberOfAttributeApproveAdvocates() + if (numberOfAttributeApproveAdvocates < minimumNeededApprovers){ + // The attribute has at least 1 ban advocate, and does not have the minimum required approve advocates. + return "Banned", nil + } + + // We determine what the verdict is + + approveRatio := approveWeight/banWeight + + if (approveRatio < minimumApprovedRatio){ + return "Banned", nil + } + // This attribute has passed. We continue to the next attribute + } + + // All attributes are either approved or undecided + + if (nonCanonicalUndecidedExists == true){ + // Non-mate profiles do not need non-canonical attributes approved, if there are no ban reviews of the attribute + // For Mate profiles, all non-canonical attributes must be approved for the profile to be approved + return "Undecided", nil + } + + // All attributes passed. + return "Approved", nil + } + + verdictConsensus, err := getVerdictConsensus() + if (err != nil) { return false, false, 0, 0, [16]byte{}, nil, false, false, "", 0, 0, 0, 0, nil, nil, nil, nil, nil, nil, 0, nil, nil, err } + + return false, true, profileVersion, profileNetworkType, profileIdentityHash, profileAttributeHashesMap, true, true, verdictConsensus, numberOfEligibleApproveAdvocates, numberOfEligibleBanAdvocates, eligibleApproveAdvocateScoresSum, eligibleBanAdvocateScoresSum, eligibleAttributeApproveAdvocateCountsMap, eligibleAttributeBanAdvocateCountsMap, attributeApproveWeightsMap, attributeBanWeightsMap, allProfileApproveAdvocatesList, allProfileBanAdvocatesList, numberOfFullProfileBanAdvocates, allAttributeApproveAdvocatesMap, allAttributeBanAdvocatesMap, nil +} + + +//Outputs: +// -bool: Message metadata is known +// -int: Message version +// -byte: Message network type +// -[10]byte: Message inbox +// -[25]byte: Message cipher key hash +// -bool: Client is downloading required reviews and moderator profiles +// -bool: Parameters exist +// -string: Message verdict ("Approved"/"Banned"/"Undecided") +// -int: Number of eligible moderators who have approved the message +// -int: Number of eligible moderators who have banned the message +// -float64: Approve identity score sum (sum of identity scores of all eligible moderators who approved the message) +// -float64: Ban identity score sum (sum of identity scores of all eligible moderators who banned the message) +// -[][16]byte: List of approve advocates (eligible and banned) +// -[][16]byte: List of ban advocates (eligible and banned) +// -error +func GetVerifiedMessageVerdict(messageHash [26]byte)(bool, int, byte, [10]byte, [25]byte, bool, bool, string, int, int, float64, float64, [][16]byte, [][16]byte, error){ + + messageMetadataExists, messageVersion, messageNetworkType, _, messageInbox, messageCipherKeyHash, err := contentMetadata.GetMessageMetadata(messageHash) + if (err != nil) { return false, 0, 0, [10]byte{}, [25]byte{}, false, false, "", 0, 0, 0, 0, nil, nil, err } + if (messageMetadataExists == false){ + + // We do not know message metadata, so we cannot retrieve message moderated status + // Message must be downloaded at some point to get the message metadata + return false, 0, 0, [10]byte{}, [25]byte{}, false, false, "", 0, 0, 0, 0, nil, nil, nil + } + + clientIsDownloadingRequiredReviews, err := backgroundDownloads.CheckIfAppCanDetermineMessageVerdict(messageNetworkType, messageInbox, true, messageHash) + if (err != nil) { return false, 0, 0, [10]byte{}, [25]byte{}, false, false, "", 0, 0, 0, 0, nil, nil, err } + if (clientIsDownloadingRequiredReviews == false){ + + return true, messageVersion, messageNetworkType, messageInbox, messageCipherKeyHash, false, false, "", 0, 0, 0, 0, nil, nil, nil + } + + //TODO: Add parameters check + parametersExist := true + + if (parametersExist == false){ + // We do not have parameters downloaded. We cannot determine consensus verdict + return true, messageVersion, messageNetworkType, messageInbox, messageCipherKeyHash, true, false, "", 0, 0, 0, 0, nil, nil, nil + } + + approveAdvocatesMap, banAdvocatesMap, err := reviewStorage.GetMessageVerdictMaps(messageHash, messageNetworkType, messageCipherKeyHash) + if (err != nil) { return false, 0, 0, [10]byte{}, [25]byte{}, false, false, "", 0, 0, 0, 0, nil, nil, err } + if (len(approveAdvocatesMap) == 0 && len(banAdvocatesMap) == 0){ + + // There are no valid reviews for this message + + emptyListA := make([][16]byte, 0) + emptyListB := make([][16]byte, 0) + + return true, messageVersion, messageNetworkType, messageInbox, messageCipherKeyHash, true, true, "Undecided", 0, 0, 0, 0, emptyListA, emptyListB, nil + } + + getEnabledModeratorsFromMap := func(inputModeratorsMap map[[16]byte]int64)([][16]byte, error){ + + enabledModeratorsList := make([][16]byte, 0) + + for moderatorIdentityHash, _ := range inputModeratorsMap{ + + moderatorIsEnabled, err := enabledModerators.CheckIfModeratorIsEnabled(true, moderatorIdentityHash, messageNetworkType) + if (err != nil){ return nil, err } + if (moderatorIsEnabled == false){ + // We will disregard all of this moderators reviews + continue + } + + enabledModeratorsList = append(enabledModeratorsList, moderatorIdentityHash) + } + + return enabledModeratorsList, nil + } + + allEnabledApproveAdvocatesList, err := getEnabledModeratorsFromMap(approveAdvocatesMap) + if (err != nil){ return false, 0, 0, [10]byte{}, [25]byte{}, false, false, "", 0, 0, 0, 0, nil, nil, err } + + allEnabledBanAdvocatesList, err := getEnabledModeratorsFromMap(banAdvocatesMap) + if (err != nil){ return false, 0, 0, [10]byte{}, [25]byte{}, false, false, "", 0, 0, 0, 0, nil, nil, err } + + //Outputs: + // -bool: Downloading required data + // -bool: Parameters exist + // -int: Number of eligible moderators + // -float64: Sum of all moderator scores + // -error + getEligibleModeratorsCountAndScoresSum := func(inputModeratorsList [][16]byte)(bool, bool, int, float64, error){ + + numberOfEligibleModerators := 0 + eligibleModeratorScoresSum := float64(0) + + for _, moderatorIdentityHash := range inputModeratorsList{ + + downloadingRequiredData, parametersExist, moderatorIsBanned, err := bannedModeratorConsensus.GetModeratorIsBannedStatus(true, moderatorIdentityHash, messageNetworkType) + if (err != nil) { return false, false, 0, 0, err } + if (downloadingRequiredData == false){ + return false, false, 0, 0, nil + } + if (parametersExist == false){ + return false, true, 0, 0, nil + } + if (moderatorIsBanned == true){ + continue + } + + scoreIsKnown, moderatorScore, scoreIsSufficient, _, _, err := moderatorScores.GetModeratorIdentityScore(moderatorIdentityHash) + if (err != nil) { return false, false, 0, 0, err } + if (scoreIsKnown == false || scoreIsSufficient == false){ + continue + } + + numberOfEligibleModerators += 1 + eligibleModeratorScoresSum += moderatorScore + } + + return true, true, numberOfEligibleModerators, eligibleModeratorScoresSum, nil + } + + downloadingRequiredData, parametersExist, numberOfEligibleApproveAdvocates, eligibleApproveAdvocatesScoreSum, err := getEligibleModeratorsCountAndScoresSum(allEnabledApproveAdvocatesList) + if (downloadingRequiredData == false){ + return true, messageVersion, messageNetworkType, messageInbox, messageCipherKeyHash, false, false, "", 0, 0, 0, 0, nil, nil, nil + } + if (parametersExist == false){ + return true, messageVersion, messageNetworkType, messageInbox, messageCipherKeyHash, true, false, "", 0, 0, 0, 0, nil, nil, nil + } + + downloadingRequiredData, parametersExist, numberOfEligibleBanAdvocates, eligibleBanAdvocatesScoreSum, err := getEligibleModeratorsCountAndScoresSum(allEnabledBanAdvocatesList) + if (downloadingRequiredData == false){ + return true, messageVersion, messageNetworkType, messageInbox, messageCipherKeyHash, false, false, "", 0, 0, 0, 0, nil, nil, nil + } + if (parametersExist == false){ + return true, messageVersion, messageNetworkType, messageInbox, messageCipherKeyHash, true, false, "", 0, 0, 0, 0, nil, nil, nil + } + + getMessageVerdictConsensus := func()string{ + + //TODO: Fix this to use parameters + minimumBanAdvocatesCount := 3 + minimumApproveAdvocatesCount := 3 + + if (numberOfEligibleBanAdvocates < minimumBanAdvocatesCount){ + return "Undecided" + } + + if (eligibleApproveAdvocatesScoreSum < eligibleBanAdvocatesScoreSum){ + return "Banned" + } + if (numberOfEligibleApproveAdvocates >= minimumApproveAdvocatesCount){ + return "Approved" + } + + return "Undecided" + } + + messageVerdictConsensus := getMessageVerdictConsensus() + + return true, messageVersion, messageNetworkType, messageInbox, messageCipherKeyHash, true, true, messageVerdictConsensus, numberOfEligibleApproveAdvocates, numberOfEligibleBanAdvocates, eligibleApproveAdvocatesScoreSum, eligibleBanAdvocatesScoreSum, allEnabledApproveAdvocatesList, allEnabledBanAdvocatesList, nil +} + + + diff --git a/internal/moderation/viewedContent/viewedContent.go b/internal/moderation/viewedContent/viewedContent.go new file mode 100644 index 0000000..a76abef --- /dev/null +++ b/internal/moderation/viewedContent/viewedContent.go @@ -0,0 +1,623 @@ + +// viewedContent provides functions to generate and retrieve the viewedContent list +// This list is used to view content on the View Content page + +package viewedContent + +import "seekia/internal/appMemory" +import "seekia/internal/badgerDatabase" +import "seekia/internal/encoding" +import "seekia/internal/helpers" +import "seekia/internal/moderation/contentControversy" +import "seekia/internal/moderation/reviewStorage" +import "seekia/internal/myDatastores/myList" +import "seekia/internal/myDatastores/myMap" +import "seekia/internal/mySettings" + +import "slices" +import "sync" +import "errors" + +//TODO: Add more sort by attributes +// Examples: Number of reviews, continuous approval period + +// This mutex will be locked whenever we update the viewed content list +var updatingViewedContentMutex sync.Mutex + +var viewedContentListDatastore *myList.MyList + +var viewedContentFiltersMapDatastore *myMap.MyMap + +// This function must be called whenever an app user signs in +func InitializeViewedContentDatastores()error{ + + updatingViewedContentMutex.Lock() + defer updatingViewedContentMutex.Unlock() + + newViewedContentListDatastore, err := myList.CreateNewList("ViewedContent") + if (err != nil) { return err } + + newViewedContentFiltersMapDatastore, err := myMap.CreateNewMap("ViewedContentFilters") + if (err != nil){ return err } + + viewedContentListDatastore = newViewedContentListDatastore + + viewedContentFiltersMapDatastore = newViewedContentFiltersMapDatastore + + return nil +} + +func GetViewedContentSortByAttribute()(string, error){ + + exists, currentAttribute, err := mySettings.GetSetting("ViewedContentSortByAttribute") + if (err != nil) { return "", err } + if (exists == false){ + return "Controversy", nil + } + + return currentAttribute, nil +} + +func GetViewedContentSortDirection()(string, error){ + + exists, sortDirection, err := mySettings.GetSetting("ViewedContentSortDirection") + if (err != nil) { return "", err } + if (exists == false){ + return "Descending", nil + } + if (sortDirection != "Ascending" && sortDirection != "Descending"){ + return "", errors.New("MySettings malformed: Contains invalid ViewedContentSortDirection: " + sortDirection) + } + + return sortDirection, nil +} + +func CheckIfViewedContentNeedsRefresh()(bool, error){ + + exists, needsRefresh, err := mySettings.GetSetting("ViewedContentNeedsRefreshYesNo") + if (err != nil) { return false, err } + if (exists == true && needsRefresh == "No") { + return false, nil + } + + return true, nil +} + +func GetViewedContentIsReadyStatus(networkType byte)(bool, error){ + + isValid := helpers.VerifyNetworkType(networkType) + if (isValid == false){ + networkTypeString := helpers.ConvertByteToString(networkType) + return false, errors.New("GetViewedContentIsReadyStatus called with invalid networkType: " + networkTypeString) + } + + exists, contentGeneratedStatus, err := mySettings.GetSetting("ViewedContentGeneratedStatus") + if (err != nil) { return false, err } + if (exists == false || contentGeneratedStatus != "Yes"){ + return false, nil + } + + exists, contentSortedStatus, err := mySettings.GetSetting("ViewedContentSortedStatus") + if (err != nil) { return false, err } + if (exists == false || contentSortedStatus != "Yes"){ + return false, nil + } + + exists, contentNetworkTypeString, err := mySettings.GetSetting("ViewedContentNetworkType") + if (err != nil) { return false, err } + if (exists == false){ + // This should not happen, because ViewedContentNetworkType is created whenever content is generated + return false, errors.New("mySettings missing ViewedContentNetworkType when ViewedContentGeneratedStatus exists.") + } + + contentNetworkType, err := helpers.ConvertNetworkTypeStringToByte(contentNetworkTypeString) + if (err != nil) { + return false, errors.New("mySettings contains invalid ViewedContentNetworkType: " + contentNetworkTypeString) + } + if (contentNetworkType != networkType){ + // Content wes generated for a different networkType + // This should never happen, because we will always set ViewedContentGeneratedStatus to No when we switch network types, + // and we will always call the GetViewedContentIsReadyStatus function with the current appNetworkType + + //TODO: Log this. + + err := mySettings.SetSetting("ViewedContentGeneratedStatus", "No") + if (err != nil) { return false, err } + + return false, nil + } + + return true, nil +} + +//Outputs: +// -bool: Viewed content map list is ready +// -[]string +// -error +func GetViewedContentList(networkType byte)(bool, []string, error){ + + isValid := helpers.VerifyNetworkType(networkType) + if (isValid == false){ + networkTypeString := helpers.ConvertByteToString(networkType) + return false, nil, errors.New("GetViewedContentList called with invalid networkType: " + networkTypeString) + } + + areReady, err := GetViewedContentIsReadyStatus(networkType) + if (err != nil) { return false, nil, err } + if (areReady == false){ + return false, nil, nil + } + + viewedContentList, err := viewedContentListDatastore.GetList() + if (err != nil) { return false, nil, err } + + return true, viewedContentList, nil +} + +// This function returns the number of viewed contents +// It can be called before the list is ready (after being generated, but before being sorted) +func GetNumberOfGeneratedViewedContents()(int, error){ + + currentViewedContentList, err := viewedContentListDatastore.GetList() + if (err != nil) { return 0, err } + + lengthInt := len(currentViewedContentList) + + return lengthInt, nil +} + +//Outputs: +// -bool: Build encountered error +// -string: Error encountered +// -bool: Build is stopped (will be stopped if user went to different page) +// -bool: Viewed content is ready +// -float64: Percentage Progress (0 - 1) +// -error +func GetViewedContentBuildStatus(networkType byte)(bool, string, bool, bool, float64, error){ + + isValid := helpers.VerifyNetworkType(networkType) + if (isValid == false){ + networkTypeString := helpers.ConvertByteToString(networkType) + return false, "", false, false, 0, errors.New("GetViewedContentBuildStatus called with invalid networkType: " + networkTypeString) + } + + exists, encounteredError := appMemory.GetMemoryEntry("ViewedContentBuildEncounteredError") + if (exists == false){ + // No build exists. A build has not been started since Seekia was started + return false, "", false, false, 0, nil + } + if (encounteredError == "Yes"){ + exists, errorEncountered := appMemory.GetMemoryEntry("ViewedContentBuildError") + if (exists == false){ + return false, "", false, false, 0, errors.New("Viewed Content build encountered error is yes, but no error exists.") + } + + return true, errorEncountered, false, false, 0, nil + } + + isStopped := CheckIfBuildViewedContentIsStopped() + if (isStopped == true){ + return false, "", true, false, 0, nil + } + + contentIsReadyBool, err := GetViewedContentIsReadyStatus(networkType) + if (err != nil) { return false, "", false, false, 0, err } + if (contentIsReadyBool == true){ + + return false, "", false, true, 1, nil + } + + exists, currentPercentageString := appMemory.GetMemoryEntry("ViewedContentReadyProgressStatus") + if (exists == false){ + // No build exists. A build has not been started since Seekia was started + return false, "", false, false, 0, nil + } + + currentPercentageFloat, err := helpers.ConvertStringToFloat64(currentPercentageString) + if (err != nil){ + return false, "", false, false, 0, errors.New("ViewedContentReadyProgressStatus is invalid: Not a float: " + currentPercentageString) + } + + return false, "", false, false, currentPercentageFloat, nil +} + + +func CheckIfBuildViewedContentIsStopped()bool{ + + exists, buildStoppedStatus := appMemory.GetMemoryEntry("StopBuildViewedContentYesNo") + if (exists == false || buildStoppedStatus != "No") { + return true + } + + return false +} + + +// This function will cancel the current build (if one is running) +// It will then start updating our viewed content +func StartUpdatingViewedContent(networkType byte)error{ + + isValid := helpers.VerifyNetworkType(networkType) + if (isValid == false){ + networkTypeString := helpers.ConvertByteToString(networkType) + return errors.New("StartUpdatingViewedContent called with invalid networkType: " + networkTypeString) + } + + appMemory.SetMemoryEntry("StopBuildViewedContentYesNo", "Yes") + + // We wait for any existing build to stop + updatingViewedContentMutex.Lock() + + appMemory.SetMemoryEntry("ViewedContentBuildEncounteredError", "No") + appMemory.SetMemoryEntry("ViewedContentBuildError", "") + appMemory.SetMemoryEntry("ViewedContentReadyProgressStatus", "0") + + appMemory.SetMemoryEntry("StopBuildViewedContentYesNo", "No") + + updateViewedContent := func()error{ + + getViewedContentNeedsToBeGeneratedStatus := func()(bool, error){ + + exists, contentGeneratedStatus, err := mySettings.GetSetting("ViewedContentGeneratedStatus") + if (err != nil) { return false, err } + if (exists == false || contentGeneratedStatus != "Yes"){ + return true, nil + } + + exists, viewedContentNetworkTypeString, err := mySettings.GetSetting("ViewedContentNetworkType") + if (err != nil) { return false, err } + if (exists == false){ + // This should not happen, because ViewedContentNetworkType is set to Yes whenever viewed content is generated + return false, errors.New("ViewedContentNetworkType missing when ViewedContentGeneratedStatus exists.") + } + + viewedContentNetworkType, err := helpers.ConvertNetworkTypeStringToByte(viewedContentNetworkTypeString) + if (err != nil) { + return false, errors.New("mySettings contains invalid ViewedContentNetworkType: " + viewedContentNetworkTypeString) + } + if (viewedContentNetworkType != networkType){ + // This should not happen, because ViewedContentGeneratedStatus should be set to No whenever app network type is changed, + // and StartUpdatingViewedContent should only be called with the current app network type. + return true, nil + } + + return false, nil + } + + viewedContentNeedsToBeGenerated, err := getViewedContentNeedsToBeGeneratedStatus() + if (err != nil) { return err } + if (viewedContentNeedsToBeGenerated == true){ + + err := mySettings.SetSetting("ViewedContentSortedStatus", "No") + if (err != nil) { return err } + + //TODO: Check if content passes filters + //TODO: Omit disabled profiles + + viewedContentList := make([]string, 0) + + reportedMessageHashesList, err := badgerDatabase.GetAllReportedMessageHashes() + if (err != nil) { return err } + + appMemory.SetMemoryEntry("ViewedContentReadyProgressStatus", "0.10") + + reviewedMessageHashesList, err := badgerDatabase.GetAllReviewedMessageHashes() + if (err != nil) { return err } + + appMemory.SetMemoryEntry("ViewedContentReadyProgressStatus", "0.15") + + allMessageHashesList := helpers.CombineTwoListsAndAvoidDuplicates(reportedMessageHashesList, reviewedMessageHashesList) + + appMemory.SetMemoryEntry("ViewedContentReadyProgressStatus", "0.20") + + for _, messageHash := range allMessageHashesList{ + + messageHashString := encoding.EncodeBytesToHexString(messageHash[:]) + + viewedContentList = append(viewedContentList, messageHashString) + } + + appMemory.SetMemoryEntry("ViewedContentReadyProgressStatus", "0.25") + + mateProfileHashesList, err := badgerDatabase.GetAllProfileHashes("Mate") + if (err != nil) { return err } + + for _, profileHash := range mateProfileHashesList{ + + profileHashString := encoding.EncodeBytesToHexString(profileHash[:]) + + viewedContentList = append(viewedContentList, profileHashString) + } + + appMemory.SetMemoryEntry("ViewedContentReadyProgressStatus", "0.35") + + hostProfileHashesList, err := badgerDatabase.GetAllProfileHashes("Host") + if (err != nil) { return err } + + for _, profileHash := range hostProfileHashesList{ + + profileHashString := encoding.EncodeBytesToHexString(profileHash[:]) + + viewedContentList = append(viewedContentList, profileHashString) + } + + appMemory.SetMemoryEntry("ViewedContentReadyProgressStatus", "0.45") + + moderatorProfileHashesList, err := badgerDatabase.GetAllProfileHashes("Moderator") + if (err != nil) { return err } + + for _, profileHash := range moderatorProfileHashesList{ + + profileHashString := encoding.EncodeBytesToHexString(profileHash[:]) + + viewedContentList = append(viewedContentList, profileHashString) + } + + err = viewedContentListDatastore.OverwriteList(viewedContentList) + if (err != nil) { return err } + + err = mySettings.SetSetting("ViewedContentGeneratedStatus", "Yes") + if (err != nil) { return err } + + networkTypeString := helpers.ConvertByteToString(networkType) + + err = mySettings.SetSetting("ViewedContentNetworkType", networkTypeString) + if (err != nil) { return err } + } + + isStopped := CheckIfBuildViewedContentIsStopped() + if (isStopped == true){ + return nil + } + + appMemory.SetMemoryEntry("ViewedContentReadyProgressStatus", "0.50") + + contentReadyStatus, err := GetViewedContentIsReadyStatus(networkType) + if (err != nil) { return err } + if (contentReadyStatus == false){ + + // Now we sort content. + + currentSortByAttribute, err := GetViewedContentSortByAttribute() + if (err != nil) { return err } + currentSortDirection, err := GetViewedContentSortDirection() + if (err != nil) { return err } + + currentContentsList, err := viewedContentListDatastore.GetList() + if (err != nil) { return err } + + // We use this map to make sure there are no duplicate content hashes + // This should never happen, unless the user's stored list was edited or there is a bug + allContentHashesMap := make(map[string]struct{}) + + //Map Structure: Content Hash Hex -> Content attribute value + contentAttributeValuesMap := make(map[string]float64) + + maximumIndex := len(currentContentsList) - 1 + + for index, contentHashHex := range currentContentsList{ + + contentHash, err := encoding.DecodeHexStringToBytes(contentHashHex) + if (err != nil){ + return errors.New("viewedContentList contains invalid content hash during sort: " + contentHashHex) + } + + _, exists := allContentHashesMap[string(contentHash)] + if (exists == true){ + return errors.New("viewedContentList contains duplicate content hash.") + } + allContentHashesMap[string(contentHash)] = struct{}{} + + // Outputs: + // -bool: Content attribute value is known + // -float64: Content attribute value + // -error + getContentAttributeValue := func()(bool, float64, error){ + + if (currentSortByAttribute == "Controversy"){ + requiredDataExists, controversyRating, err := contentControversy.GetContentControversyRating(contentHash) + if (err != nil) { return false, 0, err } + if (requiredDataExists == false){ + // We do not know the content controversy. + return false, 0, nil + } + + return true, float64(controversyRating), nil + } + if (currentSortByAttribute == "NumberOfReviewers"){ + + contentType, err := helpers.GetContentTypeFromContentHash(contentHash) + if (err != nil) { return false, 0, err } + if (contentType == "Profile"){ + + if (len(contentHash) != 28){ + return false, 0, errors.New("GetContentTypeFromContentHash returning Profile for different length contentHash: " + contentHashHex) + } + + profileHash := [28]byte(contentHash) + + profileMetadataIsKnown, downloadingRequiredData, numberOfReviewers, err := reviewStorage.GetNumberOfProfileReviewers(profileHash) + if (err != nil) { return false, 0, err } + if (profileMetadataIsKnown == false || downloadingRequiredData == false){ + return false, 0, nil + } + + return true, float64(numberOfReviewers), nil + + } else if (contentType == "Message"){ + + if (len(contentHash) != 26){ + return false, 0, errors.New("GetContentTypeFromContentHash returning Message for different length contentHash: " + contentHashHex) + } + + messageHash := [26]byte(contentHash) + + messageMetadataIsKnown, downloadingRequiredData, numberOfReviewers, err := reviewStorage.GetNumberOfMessageReviewers(messageHash) + if (err != nil) { return false, 0, err } + if (messageMetadataIsKnown == false || downloadingRequiredData == false){ + return false, 0, nil + } + + return true, float64(numberOfReviewers), nil + } + + return false, 0, errors.New("Viewed content list contains invalid contentHash during sort: " + contentHashHex) + } + + //TODO: Add more attributes + + return false, 0, errors.New("GetViewedContentSortByAttribute returning unknown attribute: " + currentSortByAttribute) + } + + contentAttributeValueIsKnown, contentAttributeValue, err := getContentAttributeValue() + if (err != nil) { return err } + if (contentAttributeValueIsKnown == true){ + contentAttributeValuesMap[contentHashHex] = contentAttributeValue + } + + isStopped := CheckIfBuildViewedContentIsStopped() + if (isStopped == true){ + return nil + } + + newScaledPercentageInt, err := helpers.ScaleNumberProportionally(true, index, 0, maximumIndex, 50, 80) + if (err != nil) { return err } + + newProgressFloat := float64(newScaledPercentageInt)/100 + + newProgressString := helpers.ConvertFloat64ToString(newProgressFloat) + + appMemory.SetMemoryEntry("ViewedContentReadyProgressStatus", newProgressString) + } + + compareContentsFunction := func(contentHashA string, contentHashB string)int{ + + if (contentHashA == contentHashB){ + panic("Duplicate content hashes called to compare function during sort.") + } + + attributeValueA, attributeValueAExists := contentAttributeValuesMap[contentHashA] + + attributeValueB, attributeValueBExists := contentAttributeValuesMap[contentHashB] + + if (attributeValueAExists == false && attributeValueBExists == false){ + + // We don't know the attribute value for either content + // We sort contents in unicode order + if (contentHashA < contentHashB){ + return -1 + } + return 1 + + } else if (attributeValueAExists == true && attributeValueBExists == false){ + + // We sort unknown attribute contents to the back of the list + + return -1 + + } else if (attributeValueAExists == false && attributeValueBExists == true){ + + return 1 + } + + // Both attribute values exist + + if (attributeValueA == attributeValueB){ + + // We sort content hashes in unicode order + if (contentHashA < contentHashB){ + return -1 + } + + return 1 + } + + if (attributeValueA < attributeValueB){ + + if (currentSortDirection == "Ascending"){ + return -1 + } + return 1 + } + if (currentSortDirection == "Ascending"){ + return 1 + } + + return -1 + } + + slices.SortFunc(currentContentsList, compareContentsFunction) + + err = viewedContentListDatastore.OverwriteList(currentContentsList) + if (err != nil) { return err } + + err = mySettings.SetSetting("ViewedContentSortedStatus", "Yes") + if (err != nil) { return err } + } + + appMemory.SetMemoryEntry("ViewedContentReadyProgressStatus", "1") + + err = mySettings.SetSetting("ViewedContentNeedsRefreshYesNo", "No") + if (err != nil) { return err } + + return nil + } + + updateFunction := func(){ + + err := updateViewedContent() + if (err != nil){ + appMemory.SetMemoryEntry("ViewedContentBuildEncounteredError", "Yes") + appMemory.SetMemoryEntry("ViewedContentBuildError", err.Error()) + } + + updatingViewedContentMutex.Unlock() + } + + go updateFunction() + + return nil +} + +func GetNumberOfActiveViewedContentFilters()(int, error){ + + numberOfActiveFilters := 0 + + //TODO + + return numberOfActiveFilters, nil +} + + +func SetViewedContentFilterOnOffStatus(filterName string, newFilterStatus bool)error{ + + filterOnOffStatusString := helpers.ConvertBoolToYesOrNoString(newFilterStatus) + + err := viewedContentFiltersMapDatastore.SetMapEntry(filterName, filterOnOffStatusString) + if (err != nil) { return err } + + err = mySettings.SetSetting("ViewedContentGeneratedStatus", "No") + if (err != nil) { return err } + + return nil +} + +func GetViewedContentFilterOnOffStatus(filterName string)(bool, error){ + + filterStatusExists, currentFilterStatus, err := viewedContentFiltersMapDatastore.GetMapEntry(filterName) + if (err != nil) { return false, err } + if (filterStatusExists == false){ + return false, nil + } + + filterOnOffStatusBool, err := helpers.ConvertYesOrNoStringToBool(currentFilterStatus) + if (err != nil) { return false, err } + + return filterOnOffStatusBool, nil +} + + + + + + diff --git a/internal/moderation/viewedModerators/viewedModerators.go b/internal/moderation/viewedModerators/viewedModerators.go new file mode 100644 index 0000000..24864ff --- /dev/null +++ b/internal/moderation/viewedModerators/viewedModerators.go @@ -0,0 +1,600 @@ + +// viewedModerators provides functions to generate and retrieve the viewedModerators list +// This list is used to browse moderators on the View Moderators page + +package viewedModerators + +import "seekia/internal/appMemory" +import "seekia/internal/badgerDatabase" +import "seekia/internal/encoding" +import "seekia/internal/helpers" +import "seekia/internal/identity" +import "seekia/internal/myDatastores/myList" +import "seekia/internal/myDatastores/myMap" +import "seekia/internal/mySettings" +import "seekia/internal/profiles/viewableProfiles" + +import "slices" +import "errors" +import "sync" + +// This mutex will be locked whenever the moderators list is being updated +var updatingModeratorsMutex sync.Mutex + +var viewedModeratorsListDatastore *myList.MyList + +var viewedModeratorsFiltersMapDatastore *myMap.MyMap + +// This function must be called whenever an app user signs in +func InitializeViewedModeratorsDatastores()error{ + + updatingModeratorsMutex.Lock() + defer updatingModeratorsMutex.Unlock() + + newViewedModeratorsListDatastore, err := myList.CreateNewList("ViewedModerators") + if (err != nil) { return err } + + newViewedModeratorsFiltersMapDatastore, err := myMap.CreateNewMap("ViewedModeratorsFilters") + if (err != nil){ return err } + + viewedModeratorsListDatastore = newViewedModeratorsListDatastore + + viewedModeratorsFiltersMapDatastore = newViewedModeratorsFiltersMapDatastore + + return nil +} + + +func GetViewedModeratorsSortByAttribute()(string, error){ + + exists, currentAttribute, err := mySettings.GetSetting("ViewedModeratorsSortByAttribute") + if (err != nil) { return "", err } + if (exists == false){ + return "IdentityScore", nil + } + + return currentAttribute, nil +} + +func GetViewedModeratorsSortDirection()(string, error){ + + exists, sortDirection, err := mySettings.GetSetting("ViewedModeratorsSortDirection") + if (err != nil) { return "", err } + if (exists == false){ + return "Descending", nil + } + if (sortDirection != "Ascending" && sortDirection != "Descending"){ + return "", errors.New("mySettings malformed: Contains invalid ViewedModeratorsSortDirection: " + sortDirection) + } + + return sortDirection, nil +} + +// Will need a refresh any time a new moderator profile is downloaded +func CheckIfViewedModeratorsNeedsRefresh()(bool, error){ + + exists, needsRefresh, err := mySettings.GetSetting("ViewedModeratorsNeedsRefreshYesNo") + if (err != nil) { return true, err } + if (exists == true && needsRefresh == "No") { + return false, nil + } + + return true, nil +} + +func GetViewedModeratorsAreReadyStatus(networkType byte)(bool, error){ + + isValid := helpers.VerifyNetworkType(networkType) + if (isValid == false){ + networkTypeString := helpers.ConvertByteToString(networkType) + return false, errors.New("GetViewedModeratorsAreReadyStatus called with invalid networkType: " + networkTypeString) + } + + exists, moderatorsGeneratedStatus, err := mySettings.GetSetting("ViewedModeratorsGeneratedStatus") + if (err != nil) { return false, err } + if (exists == false || moderatorsGeneratedStatus != "Yes"){ + return false, nil + } + + exists, moderatorsSortedStatus, err := mySettings.GetSetting("ViewedModeratorsSortedStatus") + if (err != nil) { return false, err } + if (exists == false || moderatorsSortedStatus != "Yes"){ + return false, nil + } + + exists, viewedModeratorsNetworkTypeString, err := mySettings.GetSetting("ViewedModeratorsNetworkType") + if (err != nil) { return false, err } + if (exists == false){ + // This should not happen, because ViewedModeratorsNetworkType is created whenever viewedModerators are generated + return false, errors.New("mySettings missing ViewedModeratorsNetworkType when ViewedModeratorsGeneratedStatus exists.") + } + + viewedModeratorsNetworkType, err := helpers.ConvertNetworkTypeStringToByte(viewedModeratorsNetworkTypeString) + if (err != nil) { + return false, errors.New("mySettings contains invalid ViewedModeratorsNetworkType: " + viewedModeratorsNetworkTypeString) + } + if (viewedModeratorsNetworkType != networkType){ + // ViewedModerators were generated for a different networkType + // This should never happen, because we will always set ViewedModeratorsGeneratedStatus to No when we switch network types, + // and we will always call the GetViewedModeratorsAreReadyStatus function with the current appNetworkType + + //TODO: Log this. + + err := mySettings.SetSetting("ViewedModeratorsGeneratedStatus", "No") + if (err != nil) { return false, err } + + return false, nil + } + + return true, nil +} + +//Outputs: +// -bool: List is ready +// -[][16]byte: Viewed moderators list +// -error +func GetViewedModeratorsList(networkType byte)(bool, [][16]byte, error){ + + isValid := helpers.VerifyNetworkType(networkType) + if (isValid == false){ + networkTypeString := helpers.ConvertByteToString(networkType) + return false, nil, errors.New("GetViewedModeratorsList called with invalid networkType: " + networkTypeString) + } + + areReady, err := GetViewedModeratorsAreReadyStatus(networkType) + if (err != nil) { return false, nil, err } + if (areReady == false){ + return false, nil, nil + } + + readyModeratorIdentityHashesList, err := viewedModeratorsListDatastore.GetList() + if (err != nil) { return false, nil, err } + + viewedModeratorsList := make([][16]byte, 0, len(readyModeratorIdentityHashesList)) + + for _, moderatorIdentityHashString := range readyModeratorIdentityHashesList{ + + moderatorIdentityHash, identityType, err := identity.ReadIdentityHashString(moderatorIdentityHashString) + if (err != nil){ + return false, nil, errors.New("sortedViewedModeratorsListDatastore contains invalid identityHash: " + moderatorIdentityHashString) + } + if (identityType != "Moderator"){ + return false, nil, errors.New("sortedViewedModeratorsListDatastore contains non-Moderator identity hash: " + identityType) + } + + viewedModeratorsList = append(viewedModeratorsList, moderatorIdentityHash) + } + + return true, viewedModeratorsList, nil +} + +// This function returns the number of viewed moderators +// It can be called before the list is ready (after being generated, but before being sorted) +func GetNumberOfGeneratedViewedModerators()(int, error){ + + currentViewedModeratorsList, err := viewedModeratorsListDatastore.GetList() + if (err != nil) { return 0, err } + + numberOfModerators := len(currentViewedModeratorsList) + + return numberOfModerators, nil +} + +//Outputs: +// -bool: Build encountered error +// -string: Error encountered +// -bool: Build is stopped (will be stopped if user went to different page) +// -bool: Viewed moderators are ready +// -float64: Percentage Progress (0 - 1) +// -error +func GetViewedModeratorsBuildStatus(networkType byte)(bool, string, bool, bool, float64, error){ + + isValid := helpers.VerifyNetworkType(networkType) + if (isValid == false){ + networkTypeString := helpers.ConvertByteToString(networkType) + return false, "", false, false, 0, errors.New("GetViewedModeratorsBuildStatus called with invalid networkType: " + networkTypeString) + } + + exists, encounteredError := appMemory.GetMemoryEntry("ViewedModeratorsBuildEncounteredError") + if (exists == false){ + // No build exists. A build has not been started since Seekia was started + return false, "", false, false, 0, nil + } + if (encounteredError == "Yes"){ + exists, errorEncountered := appMemory.GetMemoryEntry("ViewedModeratorsBuildError") + if (exists == false){ + return false, "", false, false, 0, errors.New("ViewedModerators build encountered error is yes, but no error exists.") + } + + return true, errorEncountered, false, false, 0, nil + } + + isStopped := CheckIfBuildViewedModeratorsIsStopped() + if (isStopped == true){ + return false, "", true, false, 0, nil + } + + viewedModeratorsReadyBool, err := GetViewedModeratorsAreReadyStatus(networkType) + if (err != nil) { return false, "", false, false, 0, err } + if (viewedModeratorsReadyBool == true){ + + return false, "", false, true, 1, nil + } + + exists, currentPercentageString := appMemory.GetMemoryEntry("ViewedModeratorsReadyProgressStatus") + if (exists == false){ + // No build exists. A build has not been started since Seekia was started + return false, "", false, false, 0, nil + } + + currentPercentageFloat, err := helpers.ConvertStringToFloat64(currentPercentageString) + if (err != nil){ + return false, "", false, false, 0, errors.New("ViewedModeratorsReadyProgressStatus is invalid: Not a float: " + currentPercentageString) + } + + return false, "", false, false, currentPercentageFloat, nil +} + + +func CheckIfBuildViewedModeratorsIsStopped()bool{ + + exists, buildStoppedStatus := appMemory.GetMemoryEntry("StopBuildViewedModeratorsYesNo") + if (exists == false || buildStoppedStatus != "No") { + return true + } + + return false +} + + +// This function will cancel the current build (if one is running) +// It will then start updating our viewed moderators +func StartUpdatingViewedModerators(networkType byte)error{ + + isValid := helpers.VerifyNetworkType(networkType) + if (isValid == false){ + networkTypeString := helpers.ConvertByteToString(networkType) + return errors.New("StartUpdatingViewedModerators called with invalid networkType: " + networkTypeString) + } + + appMemory.SetMemoryEntry("StopBuildViewedModeratorsYesNo", "Yes") + + // We wait for any existing build to stop + updatingModeratorsMutex.Lock() + + appMemory.SetMemoryEntry("ViewedModeratorsBuildEncounteredError", "No") + appMemory.SetMemoryEntry("ViewedModeratorsBuildError", "") + appMemory.SetMemoryEntry("ViewedModeratorsReadyProgressStatus", "0") + + appMemory.SetMemoryEntry("StopBuildViewedModeratorsYesNo", "No") + + updateViewedModerators := func()error{ + + getViewedModeratorsNeedToBeGeneratedStatus := func()(bool, error){ + + exists, moderatorsGeneratedStatus, err := mySettings.GetSetting("ViewedModeratorsGeneratedStatus") + if (err != nil) { return false, err } + if (exists == false || moderatorsGeneratedStatus != "Yes"){ + return true, nil + } + + exists, viewedModeratorsNetworkTypeString, err := mySettings.GetSetting("ViewedModeratorsNetworkType") + if (err != nil) { return false, err } + if (exists == false){ + // This should not happen, because ViewedModeratorsNetworkType is set to Yes whenever viewed moderators are generated + return false, errors.New("ViewedModeratorsNetworkType missing when ViewedModeratorsGeneratedStatus exists.") + } + + viewedModeratorsNetworkType, err := helpers.ConvertNetworkTypeStringToByte(viewedModeratorsNetworkTypeString) + if (err != nil) { + return false, errors.New("mySettings contains invalid ViewedModeratorsNetworkType: " + viewedModeratorsNetworkTypeString) + } + if (viewedModeratorsNetworkType != networkType){ + // This should not happen, because ViewedModeratorsGeneratedStatus should be set to No whenever app network type is changed, + // and StartUpdatingViewedModerators should only be called with the current app network type. + return true, nil + } + + return false, nil + } + + viewedModeratorsNeedToBeGenerated, err := getViewedModeratorsNeedToBeGeneratedStatus() + if (err != nil) { return err } + if (viewedModeratorsNeedToBeGenerated == true){ + + err := mySettings.SetSetting("ViewedModeratorsSortedStatus", "No") + if (err != nil) { return err } + + // We use a map to avoid duplicates + allModeratorsMap := make(map[[16]byte]struct{}) + + allModeratorIdentityHashesList, err := badgerDatabase.GetAllProfileIdentityHashes("Moderator") + if (err != nil) { return err } + + appMemory.SetMemoryEntry("ViewedModeratorsReadyProgressStatus", ".05") + + for _, moderatorIdentityHash := range allModeratorIdentityHashesList{ + allModeratorsMap[moderatorIdentityHash] = struct{}{} + } + + appMemory.SetMemoryEntry("ViewedModeratorsReadyProgressStatus", ".10") + + // We also get all reviewed moderator identity hashes + // This is because moderators who are banned will have their profiles deleted + + allReviewedIdentityHashes, err := badgerDatabase.GetAllReviewedIdentityHashes() + if (err != nil) { return err } + + appMemory.SetMemoryEntry("ViewedModeratorsReadyProgressStatus", ".15") + + for _, identityHash := range allReviewedIdentityHashes{ + + identityType, err := identity.GetIdentityTypeFromIdentityHash(identityHash) + if (err != nil) { return err } + + if (identityType == "Moderator"){ + allModeratorsMap[identityHash] = struct{}{} + } + } + + appMemory.SetMemoryEntry("ViewedModeratorsReadyProgressStatus", ".20") + + numberOfModerators := len(allModeratorsMap) + + generatedModeratorIdentityHashesList := make([]string, 0) + + index := 0 + + for moderatorIdentityHash, _ := range allModeratorsMap{ + + moderatorPassesFilters, err := checkIfModeratorPassesFilters(moderatorIdentityHash, networkType) + if (err != nil) { return err } + if (moderatorPassesFilters == true){ + + moderatorIdentityHashString, _, err := identity.EncodeIdentityHashBytesToString(moderatorIdentityHash) + if (err != nil) { + moderatorIdentityHashHex := encoding.EncodeBytesToHexString(moderatorIdentityHash[:]) + return errors.New("allModeratorsMap contains invalid identity hash: " + moderatorIdentityHashHex) + } + + generatedModeratorIdentityHashesList = append(generatedModeratorIdentityHashesList, moderatorIdentityHashString) + } + + isStopped := CheckIfBuildViewedModeratorsIsStopped() + if (isStopped == true){ + return nil + } + + progressPercentage, err := helpers.ScaleNumberProportionally(true, index, 0, numberOfModerators-1, 20, 50) + if (err != nil) { return err } + + progressFloat := float64(progressPercentage)/100 + moderatorsReadyProgressPercentage := helpers.ConvertFloat64ToString(progressFloat) + + appMemory.SetMemoryEntry("ViewedModeratorsReadyProgressStatus", moderatorsReadyProgressPercentage) + + index += 1 + } + + err = viewedModeratorsListDatastore.OverwriteList(generatedModeratorIdentityHashesList) + if (err != nil) { return err } + + err = mySettings.SetSetting("ViewedModeratorsGeneratedStatus", "Yes") + if (err != nil) { return err } + + networkTypeString := helpers.ConvertByteToString(networkType) + + err = mySettings.SetSetting("ViewedModeratorsNetworkType", networkTypeString) + if (err != nil) { return err } + } + + isStopped := CheckIfBuildViewedModeratorsIsStopped() + if (isStopped == true){ + return nil + } + + appMemory.SetMemoryEntry("ViewedModeratorsReadyProgressStatus", "0.50") + + moderatorsReadyStatus, err := GetViewedModeratorsAreReadyStatus(networkType) + if (err != nil) { return err } + if (moderatorsReadyStatus == false){ + + // Now we sort moderators + + currentSortByAttribute, err := GetViewedModeratorsSortByAttribute() + if (err != nil) { return err } + currentSortDirection, err := GetViewedModeratorsSortDirection() + if (err != nil) { return err } + + currentViewedModeratorsList, err := viewedModeratorsListDatastore.GetList() + if (err != nil) { return err } + + // We use this map to make sure there are no duplicate moderators + // This should never happen, unless the user's stored list was edited or there is a bug + allModeratorsMap := make(map[[16]byte]struct{}) + + // Map structure: Moderator Identity Hash String -> Sort By Attribute Value + moderatorAttributeValuesMap := make(map[string]float64) + + maximumIndex := len(currentViewedModeratorsList) - 1 + + for index, moderatorIdentityHashString := range currentViewedModeratorsList{ + + moderatorIdentityHash, identityType, err := identity.ReadIdentityHashString(moderatorIdentityHashString) + if (err != nil) { + return errors.New("currentViewedModeratorsList contains invalid moderator identity hash: " + moderatorIdentityHashString) + } + if (identityType != "Moderator"){ + return errors.New("currentViewedModeratorsList contains non-moderator identity hash: " + identityType) + } + + _, exists := allModeratorsMap[moderatorIdentityHash] + if (exists == true){ + return errors.New("currentViewedModeratorsList contains duplicate identity hash.") + } + + allModeratorsMap[moderatorIdentityHash] = struct{}{} + + profileExists, _, attributeExists, attributeValue, err := viewableProfiles.GetAnyAttributeFromNewestViewableUserProfile(moderatorIdentityHash, networkType, currentSortByAttribute, true, true, true) + if (err != nil) { return err } + if (profileExists == true && attributeExists == true){ + + attributeValueFloat, err := helpers.ConvertStringToFloat64(attributeValue) + if (err != nil) { + return errors.New("Viewed moderators attribute cannot be converted to float: " + currentSortByAttribute) + } + + moderatorAttributeValuesMap[moderatorIdentityHashString] = attributeValueFloat + } + + isStopped := CheckIfBuildViewedModeratorsIsStopped() + if (isStopped == true){ + return nil + } + + newScaledPercentageInt, err := helpers.ScaleNumberProportionally(true, index, 0, maximumIndex, 50, 80) + if (err != nil) { return err } + + newProgressFloat := float64(newScaledPercentageInt)/100 + + newProgressString := helpers.ConvertFloat64ToString(newProgressFloat) + + appMemory.SetMemoryEntry("ViewedModeratorsReadyProgressStatus", newProgressString) + } + + compareModeratorsFunction := func(identityHashA string, identityHashB string)int{ + + if (identityHashA == identityHashB){ + panic("Duplicate moderator identity hashes called during sort.") + } + + attributeValueA, attributeValueAExists := moderatorAttributeValuesMap[identityHashA] + + attributeValueB, attributeValueBExists := moderatorAttributeValuesMap[identityHashB] + + if (attributeValueAExists == false && attributeValueBExists == false){ + + // We don't know the attribute value for either moderator + // We sort moderators in unicode order + if (identityHashA < identityHashB){ + return -1 + } + return 1 + + } else if (attributeValueAExists == true && attributeValueBExists == false){ + + // We sort unknown attribute moderators to the back of the list + + return -1 + + } else if (attributeValueAExists == false && attributeValueBExists == true){ + + return 1 + } + + // Both attribute values exist + + if (attributeValueA == attributeValueB){ + // We sort identity hashes in unicode order + if (identityHashA < identityHashB){ + return -1 + } + return 1 + } + + if (attributeValueA < attributeValueB){ + + if (currentSortDirection == "Ascending"){ + return -1 + } + return 1 + } + if (currentSortDirection == "Ascending"){ + return 1 + } + return -1 + } + + slices.SortFunc(currentViewedModeratorsList, compareModeratorsFunction) + + err = viewedModeratorsListDatastore.OverwriteList(currentViewedModeratorsList) + if (err != nil) { return err } + + err = mySettings.SetSetting("ViewedModeratorsSortedStatus", "Yes") + if (err != nil) { return err } + } + + appMemory.SetMemoryEntry("ViewedModeratorsReadyProgressStatus", "1") + + err = mySettings.SetSetting("ViewedModeratorsNeedsRefreshYesNo", "No") + if (err != nil) { return err } + + return nil + } + + updateFunction := func(){ + + err := updateViewedModerators() + if (err != nil) { + appMemory.SetMemoryEntry("ViewedModeratorsBuildEncounteredError", "Yes") + appMemory.SetMemoryEntry("ViewedModeratorsBuildError", err.Error()) + } + + updatingModeratorsMutex.Unlock() + } + + go updateFunction() + + return nil +} + + +func checkIfModeratorPassesFilters(inputModeratorIdentityHash [16]byte, networkType byte)(bool, error){ + + //TODO + + return true, nil +} + + + +func GetNumberOfActiveModeratorFilters()(int, error){ + + numberOfActiveFilters := 0 + + //TODO + + return numberOfActiveFilters, nil +} + + +func SetModeratorFilterOnOffStatus(filterName string, filterStatus bool)error{ + + filterOnOffStatusString := helpers.ConvertBoolToYesOrNoString(filterStatus) + + err := viewedModeratorsFiltersMapDatastore.SetMapEntry(filterName, filterOnOffStatusString) + if (err != nil) { return err } + + err = mySettings.SetSetting("ViewedModeratorsGeneratedStatus", "No") + if (err != nil) { return err } + + return nil +} + + +func GetModeratorFilterOnOffStatus(filterName string)(bool, error){ + + filterStatusExists, currentFilterStatus, err := viewedModeratorsFiltersMapDatastore.GetMapEntry(filterName) + if (err != nil) { return false, err } + if (filterStatusExists == false){ + return false, nil + } + + filterOnOffStatusBool, err := helpers.ConvertYesOrNoStringToBool(currentFilterStatus) + if (err != nil) { return false, err } + + return filterOnOffStatusBool, nil +} + + + diff --git a/internal/myBlockedUsers/myBlockedUsers.go b/internal/myBlockedUsers/myBlockedUsers.go new file mode 100644 index 0000000..f3128fc --- /dev/null +++ b/internal/myBlockedUsers/myBlockedUsers.go @@ -0,0 +1,162 @@ + +// myBlockedUsers provides functions to manage a user's blocked users + +package myBlockedUsers + +import "seekia/internal/encoding" +import "seekia/internal/helpers" +import "seekia/internal/identity" +import "seekia/internal/myDatastores/myMapList" + + +import "sync" +import "errors" +import "time" + +// We will lock this when we are adding users +// This is needed to prevent adding duplicates +var addingBlockedUsersMutex sync.Mutex + +var myBlockedUsersMapListDatastore *myMapList.MyMapList + +// This function must be called whenever an app user signs in +func InitializeMyBlockedUsersDatastore()error{ + + newMyBlockedUsersMapListDatastore, err := myMapList.CreateNewMapList("MyBlockedUsers") + if (err != nil) { return err } + + myBlockedUsersMapListDatastore = newMyBlockedUsersMapListDatastore + + return nil +} + +func BlockUser(identityHash [16]byte, reasonProvided bool, reason string)error{ + + addingBlockedUsersMutex.Lock() + defer addingBlockedUsersMutex.Unlock() + + userIsBlocked, _, _, _, err := CheckIfUserIsBlocked(identityHash) + if (err != nil) { return err } + if (userIsBlocked == true){ + // This should not happen, GUI should prevent this + return errors.New("BlockUser called on user who is already blocked.") + } + + identityHashString, _, err := identity.EncodeIdentityHashBytesToString(identityHash) + if (err != nil){ + identityHashHex := encoding.EncodeBytesToHexString(identityHash[:]) + return errors.New("BlockUser called with unvalid identityHash: " + identityHashHex) + } + + currentTimeInt := time.Now().Unix() + currentTimeString := helpers.ConvertInt64ToString(currentTimeInt) + + newMapItem := map[string]string{ + "IdentityHash": identityHashString, + "BlockedTime": currentTimeString, + } + + if (reasonProvided == true){ + newMapItem["Reason"] = reason + } + + err = myBlockedUsersMapListDatastore.AddMapListItem(newMapItem) + if (err != nil) { return err } + + return nil +} + +func UnblockUser(identityHash [16]byte)error{ + + identityHashString, _, err := identity.EncodeIdentityHashBytesToString(identityHash) + if (err != nil){ + identityHashHex := encoding.EncodeBytesToHexString(identityHash[:]) + return errors.New("UnblockUser called with invalid identityHash: " + identityHashHex) + } + + mapToDelete := map[string]string{ + "IdentityHash": identityHashString, + } + + err = myBlockedUsersMapListDatastore.DeleteMapListItems(mapToDelete) + if (err != nil) { return err } + + return nil +} + +func GetMyBlockedUsersMapList(identityType string)([]map[string]string, error){ + + allMyBlockedUsersMapList, err := myBlockedUsersMapListDatastore.GetMapList() + if (err != nil) { return nil, err } + + myBlockedUsersMapList := make([]map[string]string, 0) + + for _, userMap := range allMyBlockedUsersMapList{ + + currentIdentityHashString, exists := userMap["IdentityHash"] + if (exists == false) { + return nil, errors.New("MyBlockedUsers map list item missing IdentityHash") + } + + _, currentIdentityType, err := identity.ReadIdentityHashString(currentIdentityHashString) + if (err != nil) { + return nil, errors.New("MyBlockedUsers map list item contains invalid IdentityHash.") + } + if (currentIdentityType == identityType) { + + myBlockedUsersMapList = append(myBlockedUsersMapList, userMap) + } + } + + return myBlockedUsersMapList, nil +} + +//Outputs: +// -bool: User is blocked +// -int64: Unix time of blocking (if user is blocked) +// -bool: Reason provided +// -string: Reason for blocking +// -error +func CheckIfUserIsBlocked(identityHash [16]byte)(bool, int64, bool, string, error){ + + identityHashString, _, err := identity.EncodeIdentityHashBytesToString(identityHash) + if (err != nil){ + identityHashHex := encoding.EncodeBytesToHexString(identityHash[:]) + return false, 0, false, "", errors.New("CheckIfUserIsBlocked called with invalid identity hash: " + identityHashHex) + } + + lookupMap := map[string]string{ + "IdentityHash": identityHashString, + } + + anyItemsFound, blockedUsersMapList, err := myBlockedUsersMapListDatastore.GetMapListItems(lookupMap) + if (err != nil) { return false, 0, false, "", err } + if (anyItemsFound == false){ + return false, 0, false, "", nil + } + + if (len(blockedUsersMapList) != 1){ + return false, 0, false, "", errors.New("Blocked users map list contains more than 1 entry for an identity hash") + } + + blockedUserMapItem := blockedUsersMapList[0] + + blockedTimeString, exists := blockedUserMapItem["BlockedTime"] + if (exists == false) { + return false, 0, false, "", errors.New("Blocked users map list contains item missing BlockedTime") + } + + blockedTimeInt64, err := helpers.ConvertStringToInt64(blockedTimeString) + if (err != nil) { + return false, 0, false, "", errors.New("My Blocked Users map list contains invalid BlockedTime: " + blockedTimeString) + } + + reason, exists := blockedUserMapItem["Reason"] + if (exists == false){ + return true, blockedTimeInt64, false, "", nil + } + + return true, blockedTimeInt64, true, reason, nil +} + + diff --git a/internal/myBlockedUsers/myBlockedUsers_test.go b/internal/myBlockedUsers/myBlockedUsers_test.go new file mode 100644 index 0000000..8b93d75 --- /dev/null +++ b/internal/myBlockedUsers/myBlockedUsers_test.go @@ -0,0 +1,65 @@ +package myBlockedUsers_test + +import "seekia/internal/myBlockedUsers" +import "seekia/internal/identity" +import "seekia/internal/localFilesystem" +import "seekia/internal/appUsers" + +import "testing" + +func TestBlockUnblockUser(t *testing.T){ + + err := localFilesystem.InitializeAppDatastores() + if (err != nil){ + t.Fatalf("Failed to initialize app datastores: " + err.Error()) + } + + err = appUsers.InitializeAppUserForTests() + if (err != nil){ + t.Fatalf("Failed to initialize app user for tests: " + err.Error()) + } + + err = myBlockedUsers.InitializeMyBlockedUsersDatastore() + if (err != nil){ + t.Fatalf("Failed to initialize myBlockedUsers datastore: " + err.Error()) + } + + testIdentityHash, err := identity.GetNewRandomIdentityHash(false, "") + if (err != nil) { + t.Fatalf("Failed to get random identity hash: " + err.Error()) + } + + err = myBlockedUsers.BlockUser(testIdentityHash, true, "Test Reason") + if (err != nil) { + t.Fatalf("Failed to block user: " + err.Error()) + } + + isBlocked, _, reasonProvided, blockReason, err := myBlockedUsers.CheckIfUserIsBlocked(testIdentityHash) + if (err != nil) { + t.Fatalf("Failed to check if user is blocked: " + err.Error()) + } + if (isBlocked == false) { + t.Fatalf("Failed to block user.") + } + if (reasonProvided == false){ + t.Fatalf("Failed to get valid reasonProvided bool.") + } + if (blockReason != "Test Reason"){ + t.Fatalf("Failed to get valid blockReason.") + } + + err = myBlockedUsers.UnblockUser(testIdentityHash) + if (err != nil) { + t.Fatalf("Failed to block user: " + err.Error()) + } + + isBlocked, _, _, _, err = myBlockedUsers.CheckIfUserIsBlocked(testIdentityHash) + if (err != nil) { + t.Fatalf("Failed to check if user is blocked: " + err.Error()) + } + if (isBlocked == true) { + t.Fatalf("Failed to unblock user.") + } + +} + diff --git a/internal/myContacts/myContacts.go b/internal/myContacts/myContacts.go new file mode 100644 index 0000000..8ba7acb --- /dev/null +++ b/internal/myContacts/myContacts.go @@ -0,0 +1,490 @@ + +// myContacts provides functions to manage a user's contacts + +package myContacts + +import "seekia/internal/encoding" +import "seekia/internal/helpers" +import "seekia/internal/identity" +import "seekia/internal/myDatastores/myList" +import "seekia/internal/myDatastores/myMapList" + +import "errors" +import "strings" +import "sync" +import "slices" +import "time" + +// This mutex will be locked whenever we edit contacts and their categories +var updatingMyContactsMutex sync.Mutex + +var myContactsMapListDatastore *myMapList.MyMapList + +var myMateContactCategoriesListDatastore *myList.MyList +var myHostContactCategoriesListDatastore *myList.MyList +var myModeratorContactCategoriesListDatastore *myList.MyList + +func getContactCategoriesListDatastoreFromIdentityType(identityType string)(*myList.MyList, error){ + + if (identityType == "Mate"){ + return myMateContactCategoriesListDatastore, nil + } + if (identityType == "Host"){ + return myHostContactCategoriesListDatastore, nil + } + if (identityType == "Moderator"){ + return myModeratorContactCategoriesListDatastore, nil + } + return nil, errors.New("getContactCategoriesListDatastoreFromIdentityType called with invalid IdentityType: " + identityType) +} + +// This function must be called whenever an app user signs in +func InitializeMyContactDatastores()error{ + + updatingMyContactsMutex.Lock() + defer updatingMyContactsMutex.Unlock() + + newMyContactsMapListDatastore, err := myMapList.CreateNewMapList("MyContacts") + if (err != nil) { return err } + + myContactsMapListDatastore = newMyContactsMapListDatastore + + newMateContactCategoriesListDatastore, err := myList.CreateNewList("MyMateContactCategories") + if (err != nil) { return err } + + newHostContactCategoriesListDatastore, err := myList.CreateNewList("MyHostContactCategories") + if (err != nil) { return err } + + newModeratorContactCategoriesListDatastore, err := myList.CreateNewList("MyModeratorContactCategories") + if (err != nil) { return err } + + myMateContactCategoriesListDatastore = newMateContactCategoriesListDatastore + myHostContactCategoriesListDatastore = newHostContactCategoriesListDatastore + myModeratorContactCategoriesListDatastore = newModeratorContactCategoriesListDatastore + + return nil +} + +//Outputs: +// -bool: Contact already exists +// -error +func AddContact(userIdentityHash [16]byte, contactName string, categoriesList []string, contactDescription string)(bool, error){ + + updatingMyContactsMutex.Lock() + defer updatingMyContactsMutex.Unlock() + + userIdentityHashString, _, err := identity.EncodeIdentityHashBytesToString(userIdentityHash) + if (err != nil) { + userIdentityHashHex := encoding.EncodeBytesToHexString(userIdentityHash[:]) + return false, errors.New("AddContact called with invalid identity hash: " + userIdentityHashHex) + } + + if (contactName == ""){ + return false, errors.New("AddContact called with empty contactName.") + } + + containsDuplicates, _ := helpers.CheckIfListContainsDuplicates(categoriesList) + if (containsDuplicates == true){ + return false, errors.New("AddContact called with CategoriesList that contains duplicates.") + } + + contactExists, _, _, _, _, err := GetMyContactDetails(userIdentityHash) + if (err != nil) { return false, err } + if (contactExists == true){ + // GUI should prevent this from happening. + return false, errors.New("AddContact called with pre-existing contact.") + } + + currentTime := time.Now().Unix() + currentTimeString := helpers.ConvertInt64ToString(currentTime) + + newMapItem := map[string]string{ + "IdentityHash": userIdentityHashString, + "Name": contactName, + "AddedTime": currentTimeString, + "Description": contactDescription, + } + + if (len(categoriesList) != 0){ + + categoriesListBase64 := make([]string, 0, len(categoriesList)) + + for _, categoryName := range categoriesList{ + + if (categoryName == ""){ + return false, errors.New("AddContact called with categoriesList containing empty category.") + } + + categoryBase64 := encoding.EncodeBytesToBase64String([]byte(categoryName)) + + categoriesListBase64 = append(categoriesListBase64, categoryBase64) + } + + categoriesListJoined := strings.Join(categoriesListBase64, "+") + newMapItem["Categories"] = categoriesListJoined + } + + err = myContactsMapListDatastore.AddMapListItem(newMapItem) + if (err != nil) { return false, err } + + return false, nil +} + +func DeleteContact(userIdentityHash [16]byte)error{ + + updatingMyContactsMutex.Lock() + defer updatingMyContactsMutex.Unlock() + + userIdentityHashString, _, err := identity.EncodeIdentityHashBytesToString(userIdentityHash) + if (err != nil) { + userIdentityHashHex := encoding.EncodeBytesToHexString(userIdentityHash[:]) + return errors.New("DeleteContact called with invalid identity hash: " + userIdentityHashHex) + } + + mapToDelete := map[string]string{ + "IdentityHash": userIdentityHashString, + } + + err = myContactsMapListDatastore.DeleteMapListItems(mapToDelete) + if (err != nil) { return err } + + return nil +} + +// Outputs: +// -[][16]byte: My contact identity hashes list +// -error +func GetMyContactsList(identityType string)([][16]byte, error){ + + myContactsMapList, err := GetMyContactsMapList(identityType) + if (err != nil) { return nil, err } + + myContactIdentityHashesList := make([][16]byte, 0, len(myContactsMapList)) + + for _, contactMap := range myContactsMapList{ + + contactIdentityHashString, exists := contactMap["IdentityHash"] + if (exists == false) { + return nil, errors.New("Malformed contact map: Missing IdentityHash") + } + + contactIdentityHash, contactIdentityType, err := identity.ReadIdentityHashString(contactIdentityHashString) + if (err != nil){ + return nil, errors.New("Malformed contact map: Contains invalid identityHash: " + contactIdentityHashString) + } + if (contactIdentityType != identityType){ + return nil, errors.New("Malformed contact map: Contains identityHash of a different identityType.") + } + + myContactIdentityHashesList = append(myContactIdentityHashesList, contactIdentityHash) + } + + return myContactIdentityHashesList, nil +} + +//Outputs: +// -[]map[string]string: My contacts map list +// -error +func GetMyContactsMapList(identityType string)([]map[string]string, error){ + + if (identityType != "Mate" && identityType != "Host" && identityType != "Moderator"){ + return nil, errors.New("GetMyContactsMapList called with invalid identity type: " + identityType) + } + + myContactsMapList, err := myContactsMapListDatastore.GetMapList() + if (err != nil) { return nil, err } + + identityTypeContacts := make([]map[string]string, 0) + + for _, contactMap := range myContactsMapList{ + + currentIdentityHash, exists := contactMap["IdentityHash"] + if (exists == false) { + return nil, errors.New("MyContacts map missing IdentityHash") + } + + _, currentIdentityType, err := identity.ReadIdentityHashString(currentIdentityHash) + if (err != nil) { + return nil, errors.New("MyContacts map contains invalid IdentityHash: " + currentIdentityHash) + } + + if (currentIdentityType == identityType) { + + identityTypeContacts = append(identityTypeContacts, contactMap) + } + } + + return identityTypeContacts, nil +} + +func CheckIfUserIsMyContact(userIdentityHash [16]byte) (bool, error) { + + contactExists, _, _, _, _, err := GetMyContactDetails(userIdentityHash) + if (err != nil) { return false, err } + + return contactExists, nil +} + +//Outputs +// -bool: contact exists +// -string: Contact name +// -int64: Added time +// -[]string: List of categories +// -string: Description +// -error +func GetMyContactDetails(userIdentityHash [16]byte)(bool, string, int64, []string, string, error){ + + userIdentityHashString, userIdentityType, err := identity.EncodeIdentityHashBytesToString(userIdentityHash) + if (err != nil) { + userIdentityHashHex := encoding.EncodeBytesToHexString(userIdentityHash[:]) + return false, "", 0, nil, "", errors.New("GetMyContactDetails called with invalid identity hash: " + userIdentityHashHex) + } + + lookupMap := map[string]string{ + "IdentityHash": userIdentityHashString, + } + + anyItemsFound, contactsMapList, err := myContactsMapListDatastore.GetMapListItems(lookupMap) + if (err != nil) { return false, "", 0, nil, "", err } + if (anyItemsFound == false) { + return false, "", 0, nil, "", nil + } + + if (len(contactsMapList) != 1) { + return false, "", 0, nil, "", errors.New("Malformed contacts map. Two contacts found for 1 identity hash.") + } + + contactDetailsMap := contactsMapList[0] + + contactName, exists := contactDetailsMap["Name"] + if (exists == false) { + return false, "", 0, nil, "", errors.New("Malformed MyContacts map list: Item missing Name") + } + + addedTime, exists := contactDetailsMap["AddedTime"] + if (exists == false){ + return false, "", 0, nil, "", errors.New("Malformed MyContacts map list: Item missing AddedTime") + } + + addedTimeInt64, err := helpers.ConvertStringToInt64(addedTime) + if (err != nil) { + return false, "", 0, nil, "", errors.New("Malformed MyContacts map list: AddedTime is invalid: " + addedTime) + } + + contactDescription, exists := contactDetailsMap["Description"] + if (exists == false){ + return false, "", 0, nil, "", errors.New("Malformed MyContacts map list: Item missing Description") + } + + contactCategoriesListString, exists := contactDetailsMap["Categories"] + if (exists == false) { + emptyList := make([]string, 0) + return true, contactName, addedTimeInt64, emptyList, contactDescription, nil + } + + contactCategoriesListBase64 := strings.Split(contactCategoriesListString, "+") + + contactCategoriesList := make([]string, 0, len(contactCategoriesListBase64)) + + for _, categoryBase64 := range contactCategoriesListBase64{ + + categoryString, err := encoding.DecodeBase64StringToUnicodeString(categoryBase64) + if (err != nil) { + return false, "", 0, nil, "", errors.New("MyContacts map list malformed: Category name is not Base64: " + categoryBase64) + } + + contactCategoriesList = append(contactCategoriesList, categoryString) + } + + // We prune any deleted categories (there shouldn't be any) + + allContactCategoriesList, err := GetAllMyContactCategories(userIdentityType) + if (err != nil) { return false, "", 0, nil, "", err } + + contactCategoriesListPruned := helpers.GetSharedItemsOfTwoStringLists(contactCategoriesList, allContactCategoriesList) + + return true, contactName, addedTimeInt64, contactCategoriesListPruned, contactDescription, nil +} + +// A user's categories are always selected from the categoriesMapList +// Thus, there will never be a need to add a category to the categoriesMapList from this function (or the addContact function) +// All new categories must be added via AddContactCategory function +func EditContact(userIdentityHash [16]byte, newContactName string, newContactCategoriesList []string, newContactDescription string)error{ + + updatingMyContactsMutex.Lock() + defer updatingMyContactsMutex.Unlock() + + userIdentityHashString, userIdentityType, err := identity.EncodeIdentityHashBytesToString(userIdentityHash) + if (err != nil) { + userIdentityHashHex := encoding.EncodeBytesToHexString(userIdentityHash[:]) + return errors.New("EditContact called with invalid identity hash: " + userIdentityHashHex) + } + + containsDuplicates, _ := helpers.CheckIfListContainsDuplicates(newContactCategoriesList) + if (containsDuplicates == true){ + return errors.New("EditContact called with newContactCategoriesList that contains duplicates") + } + + myContactsMapList, err := GetMyContactsMapList(userIdentityType) + if (err != nil) { return err } + + newContactsMapList := make([]map[string]string, 0) + + for _, contactMap := range myContactsMapList{ + + currentIdentityHash, exists := contactMap["IdentityHash"] + if (exists == false) { + return errors.New("MyContacts map list item missing IdentityHash") + } + + if (currentIdentityHash != userIdentityHashString){ + newContactsMapList = append(newContactsMapList, contactMap) + continue + } + + contactAddedTime, exists := contactMap["AddedTime"] + if (exists == false) { + return errors.New("MyContacts map list item missing AddedTime") + } + + newContactMap := map[string]string{ + "IdentityHash": userIdentityHashString, + "AddedTime": contactAddedTime, + "Name": newContactName, + "Description": newContactDescription, + } + + if (len(newContactCategoriesList) != 0){ + + newCategoriesBase64List := make([]string, 0) + + for _, categoryName := range newContactCategoriesList{ + + if (categoryName == ""){ + return errors.New("EditContact called with newContactCategoriesList which contains empty string.") + } + + categoryNameBase64 := encoding.EncodeBytesToBase64String([]byte(categoryName)) + + newCategoriesBase64List = append(newCategoriesBase64List, categoryNameBase64) + } + + newCategoriesListString := strings.Join(newCategoriesBase64List, "+") + + newContactMap["Categories"] = newCategoriesListString + } + + newContactsMapList = append(newContactsMapList, newContactMap) + } + + err = myContactsMapListDatastore.OverwriteMapList(newContactsMapList) + if (err != nil) { return err } + + return nil +} + + +func GetAllMyContactCategories(identityType string)([]string, error){ + + contactCategoriesListDatastore, err := getContactCategoriesListDatastoreFromIdentityType(identityType) + if (err != nil) { return nil, err } + + myContactCategoriesList, err := contactCategoriesListDatastore.GetList() + if (err != nil) { return nil, err } + + allCategoriesListPruned := helpers.RemoveDuplicatesFromStringList(myContactCategoriesList) + + return allCategoriesListPruned, nil +} + + +func AddContactCategory(identityType string, categoryName string)error{ + + updatingMyContactsMutex.Lock() + defer updatingMyContactsMutex.Unlock() + + contactCategoriesListDatastore, err := getContactCategoriesListDatastoreFromIdentityType(identityType) + if (err != nil) { return err } + + myContactCategoriesList, err := contactCategoriesListDatastore.GetList() + if (err != nil) { return err } + + categoryExists := slices.Contains(myContactCategoriesList, categoryName) + if (categoryExists == true){ + // GUI should prevent this + return errors.New("AddContactCategory called with pre-existing category.") + } + + err = contactCategoriesListDatastore.AddListItem(categoryName) + if (err != nil) { return err } + + return nil +} + + +func DeleteContactCategory(identityType string, categoryToDeleteName string)error{ + + updatingMyContactsMutex.Lock() + defer updatingMyContactsMutex.Unlock() + + contactCategoriesListDatastore, err := getContactCategoriesListDatastoreFromIdentityType(identityType) + if (err != nil) { return err } + + err = contactCategoriesListDatastore.DeleteListItem(categoryToDeleteName) + if (err != nil) { return err } + + // Now we prune any deleted categories from each contact entry + + categoryToDeleteNameBase64 := encoding.EncodeBytesToBase64String([]byte(categoryToDeleteName)) + + myContactsMapList, err := GetMyContactsMapList(identityType) + if (err != nil) { return err } + + newContactsMapList := make([]map[string]string, 0) + + for _, contactMap := range myContactsMapList{ + + currentIdentityHash, exists := contactMap["IdentityHash"] + if (exists == false) { + return errors.New("MyContacts map list item missing IdentityHash") + } + + _, userIdentityType, err := identity.ReadIdentityHashString(currentIdentityHash) + if (err != nil) { + return errors.New("MyContacts map list item contains invalid IdentityHash: " + currentIdentityHash) + } + + if (userIdentityType != identityType){ + newContactsMapList = append(newContactsMapList, contactMap) + continue + } + + contactCategoriesListString, exists := contactMap["Categories"] + if (exists == false) { + newContactsMapList = append(newContactsMapList, contactMap) + continue + } + + contactCategoriesListBase64 := strings.Split(contactCategoriesListString, "+") + + newCategoriesList, deletedAny := helpers.DeleteAllMatchingItemsFromStringList(contactCategoriesListBase64, categoryToDeleteNameBase64) + if (deletedAny == false){ + newContactsMapList = append(newContactsMapList, contactMap) + continue + } + + newCategoriesListJoined := strings.Join(newCategoriesList, "+") + + contactMap["Categories"] = newCategoriesListJoined + + newContactsMapList = append(newContactsMapList, contactMap) + } + + err = myContactsMapListDatastore.OverwriteMapList(newContactsMapList) + if (err != nil) { return err } + + return nil +} + + + diff --git a/internal/myDatastores/myList/myList.go b/internal/myDatastores/myList/myList.go new file mode 100644 index 0000000..20cb988 --- /dev/null +++ b/internal/myDatastores/myList/myList.go @@ -0,0 +1,256 @@ + +// myList provides functions to create and manage user string lists +// Lists are concurrency safe, and are stored in-memory and on disk. +// Examples of lists include matches, viewed hosts, and viewed moderators + +package myList + +import "seekia/internal/appMemory" +import "seekia/internal/helpers" +import "seekia/internal/localFilesystem" + +import "encoding/json" +import "path/filepath" +import "errors" +import "sync" +import "slices" + +//TODO: We might want to create a new file, rename, delete old file, and rename new file, so we can better protect against losing power or computer crashing and data being lost? + +func InitializeMyListsFolder()error{ + + userDirectory, err := localFilesystem.GetAppUserFolderPath() + if (err != nil) { return err } + + myListsFolderpath := filepath.Join(userDirectory, "MyLists") + + _, err = localFilesystem.CreateFolder(myListsFolderpath) + if (err != nil) { return err } + + return nil +} + +type MyList struct{ + + // Name of list. Name of file is derived from this name + listName string + + // Folderpath of the list file. + listFolderpath string + + // We lock this whenever we are updating the memory list and list file + updatingListMutex sync.Mutex + + // We lock this when we edit the memoryList + // memoryMutex only needs to be RLock/RUnlocked by GetList, because updatingListMutex is locked for all other reads + memoryMutex sync.RWMutex + + // The list stored in memory + memoryList []string +} + +func CreateNewList(newListName string)(*MyList, error){ + + if (newListName == ""){ + return nil, errors.New("CreateNewList called with empty myList name.") + } + + var newList MyList + + newList.listName = newListName + + userDirectory, err := localFilesystem.GetAppUserFolderPath() + if (err != nil) { return nil, err } + + listFolderpath := filepath.Join(userDirectory, "MyLists") + + newList.listFolderpath = listFolderpath + + getListFromFile := func()([]string, error){ + + listFilepath := filepath.Join(listFolderpath, newListName + "List.json") + + fileExists, fileBytes, err := localFilesystem.GetFileContents(listFilepath) + if (err != nil) { return nil, err } + if (fileExists == false || len(fileBytes) == 0){ + emptyList := make([]string, 0) + return emptyList, nil + } + + var currentList []string + + err = json.Unmarshal(fileBytes, ¤tList) + if (err != nil){ return nil, err } + + return currentList, nil + } + + listFromFile, err := getListFromFile() + if (err != nil) { return nil, err } + + newList.memoryList = listFromFile + + return &newList, nil +} + +func (listObject *MyList) GetList()([]string, error){ + + if (listObject == nil){ + return nil, errors.New("GetList called when MyList is not initialized.") + } + + exists, _ := appMemory.GetMemoryEntry("AppUser") + if (exists == false){ + return nil, errors.New("GetList called when no user is signed in.") + } + + listObject.memoryMutex.RLock() + + currentList := listObject.memoryList + listCopy := slices.Clone(currentList) + + listObject.memoryMutex.RUnlock() + + return listCopy, nil +} + +func (listObject *MyList) AddListItem(newItem string)error{ + + if (listObject == nil){ + return errors.New("AddListItem called when MyList is not initialized.") + } + + exists, _ := appMemory.GetMemoryEntry("AppUser") + if (exists == false){ + return errors.New("AddListItem called when no user is signed in.") + } + + listObject.updatingListMutex.Lock() + defer listObject.updatingListMutex.Unlock() + + listObject.memoryMutex.Lock() + + currentList := listObject.memoryList + currentList = append(currentList, newItem) + listObject.memoryList = currentList + + listObject.memoryMutex.Unlock() + + listFolderpath := listObject.listFolderpath + listName := listObject.listName + + err := overwriteListFileWithList(listFolderpath, listName, currentList) + if (err != nil) { return err } + + return nil +} + +func (listObject *MyList) DeleteListItem(item string)error{ + + if (listObject == nil){ + return errors.New("DeleteListItem called when MyList is not initialized.") + } + + exists, _ := appMemory.GetMemoryEntry("AppUser") + if (exists == false){ + return errors.New("DeleteListItem called when no user is signed in.") + } + + listObject.updatingListMutex.Lock() + defer listObject.updatingListMutex.Unlock() + + listObject.memoryMutex.Lock() + + currentList := listObject.memoryList + + newList, anyDeleted := helpers.DeleteAllMatchingItemsFromStringList(currentList, item) + if (anyDeleted == false){ + listObject.memoryMutex.Unlock() + return nil + } + + listObject.memoryList = newList + listObject.memoryMutex.Unlock() + + listFolderpath := listObject.listFolderpath + listName := listObject.listName + + err := overwriteListFileWithList(listFolderpath, listName, newList) + if (err != nil) { return err } + + return nil +} + +func (listObject *MyList) DeleteList()error{ + + if (listObject == nil){ + return errors.New("DeleteList called when MyList is not initialized.") + } + + exists, _ := appMemory.GetMemoryEntry("AppUser") + if (exists == false){ + return errors.New("DeleteList called when no user is signed in.") + } + + listObject.updatingListMutex.Lock() + defer listObject.updatingListMutex.Unlock() + + emptyList := make([]string, 0) + + listObject.memoryMutex.Lock() + listObject.memoryList = emptyList + listObject.memoryMutex.Unlock() + + listFolderpath := listObject.listFolderpath + listName := listObject.listName + + err := overwriteListFileWithList(listFolderpath, listName, emptyList) + if (err != nil) { return err } + + return nil +} + +// Note: Input list is not copied +func (listObject *MyList) OverwriteList(newList []string) error{ + + if (listObject == nil){ + return errors.New("OverwriteList called when MyList is not initialized.") + } + + exists, _ := appMemory.GetMemoryEntry("AppUser") + if (exists == false){ + return errors.New("OverwriteList called when no user is signed in.") + } + + listObject.updatingListMutex.Lock() + defer listObject.updatingListMutex.Unlock() + + listObject.memoryMutex.Lock() + listObject.memoryList = newList + listObject.memoryMutex.Unlock() + + listFolderpath := listObject.listFolderpath + listName := listObject.listName + + err := overwriteListFileWithList(listFolderpath, listName, newList) + if (err != nil) { return err } + + return nil +} + + +func overwriteListFileWithList(listFolderpath string, listName string, inputList []string)error{ + + newFileBytes, err := json.MarshalIndent(inputList, "", "\t") + if (err != nil) { return err } + + listFileName := listName + "List.json" + + err = localFilesystem.CreateOrOverwriteFile(newFileBytes, listFolderpath, listFileName) + if (err != nil) { return err } + + return nil +} + + + diff --git a/internal/myDatastores/myList/myList_test.go b/internal/myDatastores/myList/myList_test.go new file mode 100644 index 0000000..841a65d --- /dev/null +++ b/internal/myDatastores/myList/myList_test.go @@ -0,0 +1,83 @@ +package myList_test + +import "seekia/internal/myDatastores/myList" +import "seekia/internal/appUsers" +import "seekia/internal/helpers" +import "seekia/internal/localFilesystem" + +import "testing" +import "slices" + +//TODO: Add concurrency tests + +func TestMyList(t *testing.T){ + + err := localFilesystem.InitializeAppDatastores() + if (err != nil){ + t.Fatalf("Failed to initialize app datastores: " + err.Error()) + } + err = appUsers.InitializeAppUserForTests() + if (err != nil){ + t.Fatalf("Failed to initalize app user for tests: " + err.Error()) + } + + newList := make([]string, 0, 100) + + for i:=1; i < 100; i++{ + + newItem := helpers.ConvertIntToString(i) + + newList = append(newList, newItem) + } + + newListObject, err := myList.CreateNewList("TestList") + if (err != nil){ + t.Fatalf("Failed to create new myList object: " + err.Error()) + } + + for _, item := range newList{ + + err := newListObject.AddListItem(item) + if (err != nil){ + t.Fatalf("Failed to add list item: " + err.Error()) + } + } + + retrievedList, err := newListObject.GetList() + if (err != nil){ + t.Fatalf("Failed to get list: " + err.Error()) + } + + areIdentical := slices.Equal(newList, retrievedList) + if (areIdentical == false){ + t.Fatalf("Stored list does not match retrieved list.") + } + + err = newListObject.DeleteListItem("1") + if (err != nil){ + t.Fatalf("Failed to delete list item: " + err.Error()) + } + + retrievedList, err = newListObject.GetList() + if (err != nil){ + t.Fatalf("Failed to get list: " + err.Error()) + } + + for index, element := range retrievedList{ + + expectedValue := newList[index+1] + + if (element != expectedValue){ + t.Fatalf("Retrieved list does not match after deleting item.") + } + } + + err = newListObject.DeleteList() + if (err != nil){ + t.Fatalf("Failed to delete list: " + err.Error()) + } +} + + + + diff --git a/internal/myDatastores/myMap/myMap.go b/internal/myDatastores/myMap/myMap.go new file mode 100644 index 0000000..8eb0383 --- /dev/null +++ b/internal/myDatastores/myMap/myMap.go @@ -0,0 +1,290 @@ + +// myMap provides functions for managing user maps +// Maps are concurrency safe, and are stored in-memory and on disk. + +package myMap + +import "seekia/internal/localFilesystem" +import "seekia/internal/appMemory" + +import "encoding/json" +import "path/filepath" +import "sync" +import "errors" +import "maps" + + +func InitializeMyMapsFolder()error{ + + userDirectory, err := localFilesystem.GetAppUserFolderPath() + if (err != nil) { return err } + + myMapsFolderpath := filepath.Join(userDirectory, "MyMaps") + + _, err = localFilesystem.CreateFolder(myMapsFolderpath) + if (err != nil) { return err } + + return nil +} + + +type MyMap struct{ + + // Name of the map. The map filename is derived from this name. + mapName string + + // The folderpath where the map file is stored. + mapFolderpath string + + // We lock this whenever we are editing the memory map and map file + updatingMapMutex sync.Mutex + + // We lock this when we edit the memoryMap + // memoryMutex only needs to be RLock/RUnlocked by GetMap/GetMapEntry, because updatingMapMutex is locked for all other reads + memoryMutex sync.RWMutex + + // The map stored in memory + memoryMap map[string]string +} + +func CreateNewMap(newMapName string)(*MyMap, error){ + + if (newMapName == ""){ + return nil, errors.New("CreateNewMap called with empty myMap name") + } + + var newMap MyMap + + newMap.mapName = newMapName + + userDirectory, err := localFilesystem.GetAppUserFolderPath() + if (err != nil) { return nil, err } + + mapFolderpath := filepath.Join(userDirectory, "MyMaps") + + newMap.mapFolderpath = mapFolderpath + + mapFilepath := filepath.Join(mapFolderpath, newMapName + "Map.json") + + getMapFromFile := func()(map[string]string, error){ + + fileExists, fileBytes, err := localFilesystem.GetFileContents(mapFilepath) + if (err != nil) { return nil, err } + if (fileExists == false){ + + emptyMap := make(map[string]string) + return emptyMap, nil + } + + currentMap := make(map[string]string) + + err = json.Unmarshal(fileBytes, ¤tMap) + if (err != nil) { + return nil, errors.New("Stored myMap is corrupted: " + newMapName) + } + + return currentMap, nil + } + + mapFromFile, err := getMapFromFile() + if (err != nil) { return nil, err } + + newMap.memoryMap = mapFromFile + + return &newMap, nil +} + + +func (inputMapObject *MyMap) GetMap()(map[string]string, error){ + + if (inputMapObject == nil){ + return nil, errors.New("GetMap called when MyMap is not initialized.") + } + + exists, _ := appMemory.GetMemoryEntry("AppUser") + if (exists == false){ + return nil, errors.New("GetMap called when no user is signed in.") + } + + inputMapObject.memoryMutex.RLock() + + currentMap := inputMapObject.memoryMap + + mapCopy := maps.Clone(currentMap) + + inputMapObject.memoryMutex.RUnlock() + + return mapCopy, nil +} + +//Outputs: +// -bool: Entry exists +// -string: Entry value +// -error +func (inputMapObject *MyMap) GetMapEntry(key string)(bool, string, error){ + + if (inputMapObject == nil){ + return false, "", errors.New("GetMapEntry called when MyMap is not initialized.") + } + + exists, _ := appMemory.GetMemoryEntry("AppUser") + if (exists == false){ + return false, "", errors.New("GetMapEntry called when no user is signed in.") + } + + inputMapObject.memoryMutex.RLock() + value, exists := inputMapObject.memoryMap[key] + inputMapObject.memoryMutex.RUnlock() + + if (exists == false){ + return false, "", nil + } + + return true, value, nil +} + +func (inputMapObject *MyMap) SetMapEntry(key string, value string)error{ + + if (inputMapObject == nil){ + return errors.New("SetMapEntry called when MyMap not initialized.") + } + + exists, _ := appMemory.GetMemoryEntry("AppUser") + if (exists == false){ + return errors.New("SetMapEntry called when no user is signed in.") + } + + // We see if identical entry already exists + exists, currentValue, err := inputMapObject.GetMapEntry(key) + if (err != nil) { return err } + if (exists == true && currentValue == value){ + return nil + } + + inputMapObject.updatingMapMutex.Lock() + defer inputMapObject.updatingMapMutex.Unlock() + + inputMapObject.memoryMutex.Lock() + inputMapObject.memoryMap[key] = value + inputMapObject.memoryMutex.Unlock() + + mapFolderpath := inputMapObject.mapFolderpath + mapName := inputMapObject.mapName + + newMap := inputMapObject.memoryMap + + err = overwriteMapFileWithMap(mapFolderpath, mapName, newMap) + if (err != nil) { return err } + + return nil +} + +func (inputMapObject *MyMap) DeleteMapEntry(key string)error{ + + if (inputMapObject == nil){ + return errors.New("DeleteMapEntry called when MyMap is not initialized.") + } + + exists, _ := appMemory.GetMemoryEntry("AppUser") + if (exists == false){ + return errors.New("DeleteMapEntry called when no user is signed in.") + } + + // We see if entry exists + exists, _, err := inputMapObject.GetMapEntry(key) + if (err != nil) { return err } + if (exists == false){ + // Nothing to delete + return nil + } + + inputMapObject.updatingMapMutex.Lock() + defer inputMapObject.updatingMapMutex.Unlock() + + inputMapObject.memoryMutex.Lock() + delete(inputMapObject.memoryMap, key) + inputMapObject.memoryMutex.Unlock() + + mapFolderpath := inputMapObject.mapFolderpath + mapName := inputMapObject.mapName + + newMap := inputMapObject.memoryMap + + err = overwriteMapFileWithMap(mapFolderpath, mapName, newMap) + if (err != nil) { return err } + + return nil +} + +func (inputMapObject *MyMap) DeleteMap() error{ + + if (inputMapObject == nil){ + return errors.New("DeleteMap called when MyMap is not initialized.") + } + + exists, _ := appMemory.GetMemoryEntry("AppUser") + if (exists == false){ + return errors.New("DeleteMap called when no user is signed in.") + } + + inputMapObject.updatingMapMutex.Lock() + defer inputMapObject.updatingMapMutex.Unlock() + + emptyMap := make(map[string]string) + + inputMapObject.memoryMutex.Lock() + inputMapObject.memoryMap = emptyMap + inputMapObject.memoryMutex.Unlock() + + mapFolderpath := inputMapObject.mapFolderpath + mapName := inputMapObject.mapName + + err := overwriteMapFileWithMap(mapFolderpath, mapName, emptyMap) + if (err != nil) { return err } + + return nil +} + + +func (inputMapObject *MyMap) OverwriteMap(newMap map[string]string) error{ + + if (inputMapObject == nil){ + return errors.New("OverwriteMap called when MyMap is not initialized.") + } + + exists, _ := appMemory.GetMemoryEntry("AppUser") + if (exists == false){ + return errors.New("OverwriteMap called when no user is signed in.") + } + + inputMapObject.updatingMapMutex.Lock() + defer inputMapObject.updatingMapMutex.Unlock() + + inputMapObject.memoryMutex.Lock() + inputMapObject.memoryMap = newMap + inputMapObject.memoryMutex.Unlock() + + mapFolderpath := inputMapObject.mapFolderpath + mapName := inputMapObject.mapName + + err := overwriteMapFileWithMap(mapFolderpath, mapName, newMap) + if (err != nil) { return err } + + return nil +} + + +func overwriteMapFileWithMap(mapFolderpath string, mapName string, inputMap map[string]string)error{ + + fileContents, err := json.MarshalIndent(inputMap, "", "\t") + if (err != nil) { return err } + + mapFileName := mapName + "Map.json" + + err = localFilesystem.CreateOrOverwriteFile(fileContents, mapFolderpath, mapFileName) + if (err != nil) { return err } + + return nil +} + + diff --git a/internal/myDatastores/myMap/myMap_test.go b/internal/myDatastores/myMap/myMap_test.go new file mode 100644 index 0000000..6420cc9 --- /dev/null +++ b/internal/myDatastores/myMap/myMap_test.go @@ -0,0 +1,131 @@ +package myMap_test + +import "seekia/internal/myDatastores/myMap" +import "seekia/internal/appUsers" +import "seekia/internal/localFilesystem" + +import "testing" +import "strconv" +import "sync" + +func TestMyMap(t *testing.T) { + + err := localFilesystem.InitializeAppDatastores() + if (err != nil){ + t.Fatalf("Failed to initialize app datastores: " + err.Error()) + } + err = appUsers.InitializeAppUserForTests() + if (err != nil){ + t.Fatalf("Failed to initalize app user for tests: " + err.Error()) + } + + newMapObject, err := myMap.CreateNewMap("TestMap") + if (err != nil) { + t.Fatalf("Failed to create new map: " + err.Error()) + } + + err = newMapObject.DeleteMap() + if (err != nil) { + t.Fatalf("Failed to delete map: " + err.Error()) + } + + _, err = newMapObject.GetMap() + if (err != nil) { + t.Fatalf("Failed to get new map: " + err.Error()) + } + + err = newMapObject.SetMapEntry("TestKeyA", "TestValueA") + if (err != nil) { + t.Fatalf("Failed to add map entry: " + err.Error()) + } + + err = newMapObject.SetMapEntry("TestKeyB", "TestValueB") + if (err != nil) { + t.Fatalf("Failed to add map entry: " + err.Error()) + } + + exists, entryValue, err := newMapObject.GetMapEntry("TestKeyA") + if (err != nil) { + t.Fatalf("Failed to get map value: " + err.Error()) + } + if (exists == false) { + t.Fatalf("Failed to get map value.") + } + + if (entryValue != "TestValueA"){ + t.Fatalf("Map value does not match.") + } + + err = newMapObject.DeleteMapEntry("TestKeyA") + if (err != nil){ + t.Fatalf("Failed to delete map entry: " + err.Error()) + } + + exists, _, err = newMapObject.GetMapEntry("TestKeyA") + if (err != nil) { + t.Fatalf("Failed to get map value: " + err.Error()) + } + if (exists == true) { + t.Fatalf("Failed to delete map value.") + } + + err = newMapObject.DeleteMap() + if (err != nil) { + t.Fatalf("Failed to delete map: " + err.Error()) + } + + exists, _, err = newMapObject.GetMapEntry("TestKeyB") + if (err != nil) { + t.Fatalf("Failed to get map entry: " + err.Error()) + } + if (exists == true) { + t.Fatalf("Failed to delete map.") + } + + // Now we test concurrency + + err = newMapObject.SetMapEntry("TestA", "TestB") + if (err != nil) { + t.Fatalf("Failed to add map entry: " + err.Error()) + } + + currentMap, err := newMapObject.GetMap() + if (err != nil) { + t.Fatalf("Failed to get map: " + err.Error()) + } + + var mapWaitgroup sync.WaitGroup + + for i:=0; i<1000; i++{ + + mapWaitgroup.Add(1) + + indexString := strconv.Itoa(i) + + addFunction := func(){ + + err := newMapObject.SetMapEntry("TestKey" + indexString, "TestValue" + indexString) + if (err != nil) { + t.Fatalf("Failed to add map value: " + err.Error()) + } + + mapWaitgroup.Done() + } + + go addFunction() + } + + mapWaitgroup.Wait() + + if (len(currentMap) != 1){ + t.Fatalf("Map not copied.") + } + + err = newMapObject.DeleteMap() + if (err != nil) { + t.Fatalf("Failed to delete map: " + err.Error()) + } +} + + + diff --git a/internal/myDatastores/myMapList/myMapList.go b/internal/myDatastores/myMapList/myMapList.go new file mode 100644 index 0000000..d43c8f8 --- /dev/null +++ b/internal/myDatastores/myMapList/myMapList.go @@ -0,0 +1,344 @@ + +// myMapList provides functions for managing user map lists +// Map lists are concurrency safe, and are stored in-memory and on disk. +// Examples of map lists include saved contacts, my chat messages, and my chat conversations + +package myMapList + +import "seekia/internal/localFilesystem" +import "seekia/internal/helpers" +import "seekia/internal/appMemory" + +import "encoding/json" +import "path/filepath" +import "errors" +import "sync" +import "maps" + + +func InitializeMyMapListsFolder()error{ + + userDirectory, err := localFilesystem.GetAppUserFolderPath() + if (err != nil) { return err } + + myMapListsFolderpath := filepath.Join(userDirectory, "MyMapLists") + + _, err = localFilesystem.CreateFolder(myMapListsFolderpath) + if (err != nil) { return err } + + return nil +} + +type MyMapList struct{ + + // Name of map list. The file name is derived from this name. + mapListName string + + // Folderpath of the mapList file + mapListFolderpath string + + // We lock this whenever we are editing the memory map and map file + updatingMapListMutex sync.Mutex + + // We lock this whenever we edit the memoryMapList + // memoryMutex only needs to be RLock/RUnlocked by GetMapList/GetMapListItems, because updatingMapListMutex is locked for all other reads + memoryMutex sync.RWMutex + + // The map list stored in memory + memoryMapList []map[string]string +} + +func CreateNewMapList(newMapListName string)(*MyMapList, error){ + + if (newMapListName == ""){ + return nil, errors.New("CreateNewMapList called with empty myMapList name") + } + + var newMapList MyMapList + + newMapList.mapListName = newMapListName + + userDirectory, err := localFilesystem.GetAppUserFolderPath() + if (err != nil) { return nil, err } + + mapListFolderpath := filepath.Join(userDirectory, "MyMapLists") + + newMapList.mapListFolderpath = mapListFolderpath + + getMapListFromFile := func()([]map[string]string, error){ + + mapListFilepath := filepath.Join(mapListFolderpath, newMapListName + "MapList.json") + + fileExists, fileBytes, err := localFilesystem.GetFileContents(mapListFilepath) + if (err != nil) { return nil, err } + if (fileExists == false){ + emptyMapList := make([]map[string]string, 0) + return emptyMapList, nil + } + + currentMapList := make([]map[string]string, 0) + + err = json.Unmarshal(fileBytes, ¤tMapList) + if (err != nil) { + return nil, errors.New("My Map list is corrupted: " + newMapListName) + } + + return currentMapList, nil + } + + mapListFromFile, err := getMapListFromFile() + if (err != nil) { return nil, err } + + newMapList.memoryMapList = mapListFromFile + + return &newMapList, nil +} + + +//Outputs: +// -[]map[string]string +// -error +func (inputMapListObject *MyMapList) GetMapList()([]map[string]string, error){ + + if (inputMapListObject == nil){ + return nil, errors.New("GetMapList called when MyMapList object is not initialized.") + } + + exists, _ := appMemory.GetMemoryEntry("AppUser") + if (exists == false){ + return nil, errors.New("GetMapList called when no user is signed in.") + } + + inputMapListObject.memoryMutex.RLock() + + currentMapList := inputMapListObject.memoryMapList + + mapListCopy := helpers.DeepCopyStringToStringMapList(currentMapList) + + inputMapListObject.memoryMutex.RUnlock() + + return mapListCopy, nil +} + +// Note: Added items are not copied +func (inputMapListObject *MyMapList) AddMapListItem(newItem map[string]string)error{ + + if (inputMapListObject == nil){ + return errors.New("AddMapListItem called when MyMapList object is not initialized.") + } + + exists, _ := appMemory.GetMemoryEntry("AppUser") + if (exists == false){ + return errors.New("AddMapListItem called when no user is signed in.") + } + + inputMapListObject.updatingMapListMutex.Lock() + defer inputMapListObject.updatingMapListMutex.Unlock() + + inputMapListObject.memoryMutex.Lock() + + currentMapList := inputMapListObject.memoryMapList + currentMapList = append(currentMapList, newItem) + inputMapListObject.memoryMapList = currentMapList + + inputMapListObject.memoryMutex.Unlock() + + mapListFolderpath := inputMapListObject.mapListFolderpath + mapListName := inputMapListObject.mapListName + + err := overwriteMapListFileWithMapList(mapListFolderpath, mapListName, currentMapList) + if (err != nil) { return err } + + return nil +} + +// This can be used to delete multiple entries at a time. +// Any maps that contain the same entries as the input map will be deleted +func (inputMapListObject *MyMapList) DeleteMapListItems(mapItem map[string]string)error{ + + if (inputMapListObject == nil){ + return errors.New("DeleteMapListItems called when MyMapList object is not initialized.") + } + + exists, _ := appMemory.GetMemoryEntry("AppUser") + if (exists == false){ + return errors.New("DeleteMapListItems called when no user is signed in.") + } + + inputMapListObject.updatingMapListMutex.Lock() + defer inputMapListObject.updatingMapListMutex.Unlock() + + currentMapList := inputMapListObject.memoryMapList + + if (len(currentMapList) == 0){ + return nil + } + + newMapList := make([]map[string]string, 0) + + anyItemDeleted := false + + for _, element := range currentMapList{ + + areEqual := checkIfMapContainsMapEntries(mapItem, element) + if (areEqual == false){ + newMapList = append(newMapList, element) + continue + } + + anyItemDeleted = true + } + + if (anyItemDeleted == false){ + // No items were deleted. Nothing left to do. + return nil + } + + inputMapListObject.memoryMutex.Lock() + inputMapListObject.memoryMapList = newMapList + inputMapListObject.memoryMutex.Unlock() + + mapListFolderpath := inputMapListObject.mapListFolderpath + mapListName := inputMapListObject.mapListName + + err := overwriteMapListFileWithMapList(mapListFolderpath, mapListName, newMapList) + if (err != nil) { return err } + + return nil +} + + +//Outputs: +// bool: Any item was found +// []map[string]string: list of matching items +// error +func (inputMapListObject *MyMapList) GetMapListItems(lookupItem map[string]string)(bool, []map[string]string, error){ + + if (inputMapListObject == nil){ + return false, nil, errors.New("GetMapListItems called when MyMapList object is not initialized.") + } + + exists, _ := appMemory.GetMemoryEntry("AppUser") + if (exists == false){ + return false, nil, errors.New("GetMapListItems called when no user is signed in.") + } + + inputMapListObject.memoryMutex.RLock() + currentMapList := inputMapListObject.memoryMapList + + matchingItemsMapList := make([]map[string]string, 0) + + for _, element := range currentMapList{ + + areEqual := checkIfMapContainsMapEntries(lookupItem, element) + if (areEqual == true){ + + matchingItemCopy := maps.Clone(element) + + matchingItemsMapList = append(matchingItemsMapList, matchingItemCopy) + } + + } + inputMapListObject.memoryMutex.RUnlock() + + if (len(matchingItemsMapList) == 0) { + + return false, matchingItemsMapList, nil + } + + return true, matchingItemsMapList, nil +} + +// Note: Input map list is not copied +func (inputMapListObject *MyMapList) OverwriteMapList(newMapList []map[string]string) error{ + + if (inputMapListObject == nil){ + return errors.New("OverwriteMapList called when MyMapList object is not initialized.") + } + + exists, _ := appMemory.GetMemoryEntry("AppUser") + if (exists == false){ + return errors.New("OverwriteMapList called when no user is signed in.") + } + + inputMapListObject.updatingMapListMutex.Lock() + defer inputMapListObject.updatingMapListMutex.Unlock() + + inputMapListObject.memoryMutex.Lock() + inputMapListObject.memoryMapList = newMapList + inputMapListObject.memoryMutex.Unlock() + + mapListFolderpath := inputMapListObject.mapListFolderpath + mapListName := inputMapListObject.mapListName + + err := overwriteMapListFileWithMapList(mapListFolderpath, mapListName, newMapList) + if (err != nil) { return err } + + return nil +} + +func (inputMapListObject *MyMapList) DeleteMapList() error{ + + if (inputMapListObject == nil){ + return errors.New("DeleteMapList called when MyMapList object is not initialized.") + } + + exists, _ := appMemory.GetMemoryEntry("AppUser") + if (exists == false){ + return errors.New("DeleteMapList called when no user is signed in.") + } + + inputMapListObject.updatingMapListMutex.Lock() + defer inputMapListObject.updatingMapListMutex.Unlock() + + emptyMapList := make([]map[string]string, 0) + + inputMapListObject.memoryMutex.Lock() + inputMapListObject.memoryMapList = emptyMapList + inputMapListObject.memoryMutex.Unlock() + + mapListFolderpath := inputMapListObject.mapListFolderpath + mapListName := inputMapListObject.mapListName + + err := overwriteMapListFileWithMapList(mapListFolderpath, mapListName, emptyMapList) + if (err != nil) { return err } + + return nil +} + +// We use this function to find matching items to delete/retrieve +// Map2 must contain all of map1's entries. +// If any map2 entries do not match match1 entries, return false +// If map1 contains more entries than map2, return false +// Map2 is allowed to contain more entries than map1 +func checkIfMapContainsMapEntries(map1 map[string]string, map2 map[string]string) bool { + + for key, value := range map1 { + + currentValue, exists := map2[key] + if (exists == false) { + return false + } + + if (value != currentValue) { + return false + } + } + + return true +} + +func overwriteMapListFileWithMapList(mapListFolderpath string, mapListName string, inputMapList []map[string]string)error{ + + fileContents, err := json.MarshalIndent(inputMapList, "", "\t") + if (err != nil) { return err } + + mapListFileName := mapListName + "MapList.json" + + err = localFilesystem.CreateOrOverwriteFile(fileContents, mapListFolderpath, mapListFileName) + if (err != nil) { return err } + + return nil +} + + diff --git a/internal/myDatastores/myMapList/myMapList_test.go b/internal/myDatastores/myMapList/myMapList_test.go new file mode 100644 index 0000000..fb8bcf3 --- /dev/null +++ b/internal/myDatastores/myMapList/myMapList_test.go @@ -0,0 +1,231 @@ +package myMapList_test + +import "testing" + +import "seekia/internal/myDatastores/myMapList" + +import "seekia/internal/appUsers" +import "seekia/internal/helpers" +import "seekia/internal/localFilesystem" + +import "sync" + +func TestAddDeleteMapListData(t *testing.T) { + + err := localFilesystem.InitializeAppDatastores() + if (err != nil){ + t.Fatalf("Failed to initialize app datastores: " + err.Error()) + } + err = appUsers.InitializeAppUserForTests() + if (err != nil){ + t.Fatalf("Failed to initalize app user for tests: " + err.Error()) + } + + newMapListObject, err := myMapList.CreateNewMapList("TestMapList") + if (err != nil) { + t.Fatalf("Failed to create new map: " + err.Error()) + } + + err = newMapListObject.DeleteMapList() + if (err != nil) { + t.Fatalf("Failed to delete map list: " + err.Error()) + } + + _, err = newMapListObject.GetMapList() + if (err != nil) { + t.Fatalf("Failed to get new map list: " + err.Error()) + } + + newMapA := map[string]string{ + "TestKeyA": "TestValueA", + } + err = newMapListObject.AddMapListItem(newMapA) + if (err != nil) { + t.Fatalf("Failed to add map list item: " + err.Error()) + } + + newMapB := map[string]string{ + "TestKeyB": "TestValueB", + } + err = newMapListObject.AddMapListItem(newMapB) + if (err != nil) { + t.Fatalf("Failed to add map list item: " + err.Error()) + } + + lookupMapA := map[string]string{ + "TestKeyA": "WrongValue", + } + exists, retrievedMapListItems, err := newMapListObject.GetMapListItems(lookupMapA) + if (err != nil) { + t.Fatalf("Failed to get map list items: " + err.Error()) + } + if (exists == true) { + t.Fatalf("Failed to get map list item: Returning items we should not get.") + } + + lookupMapB := map[string]string{ + "TestKeyA": "TestValueA", + } + exists, retrievedMapListItems, err = newMapListObject.GetMapListItems(lookupMapB) + if (err != nil) { + t.Fatalf("Failed to get map list items: " + err.Error()) + } + if (exists == false) { + t.Fatalf("Failed to get map list item.") + } + + if (len(retrievedMapListItems) != 1){ + t.Fatalf("Map list retrieved items list is not correct length.") + } + + returnedMap := retrievedMapListItems[0] + + if (len(returnedMap) != 1){ + t.Fatalf("Map list value does not match.") + } + + returnedValue, exists := returnedMap["TestKeyA"] + if (exists == false) { + t.Fatalf("Map list retrieved item TestKeyA key not found.") + } + if (returnedValue != "TestValueA"){ + t.Fatalf("Map list retrieved item value does not match.") + } + + err = newMapListObject.DeleteMapListItems(lookupMapB) + if (err != nil){ + t.Fatalf("Failed to delete map list items: " + err.Error()) + } + + exists, _, err = newMapListObject.GetMapListItems(lookupMapB) + if (err != nil) { + t.Fatalf("Failed to get map list items: " + err.Error()) + } + if (exists == true) { + t.Fatalf("Failed to delete map list item.") + } + + err = newMapListObject.DeleteMapList() + if (err != nil) { + t.Fatalf("Failed to delete map list: " + err.Error()) + } + + exists, _, err = newMapListObject.GetMapListItems(newMapB) + if (err != nil) { + t.Fatalf("Failed to get map list items: " + err.Error()) + } + if (exists == true) { + t.Fatalf("Failed to delete map list.") + } + + // Now we test concurrency + + newMapC := map[string]string{ + "TestKeyC": "TestValueC", + } + err = newMapListObject.AddMapListItem(newMapC) + if (err != nil) { + t.Fatalf("Failed to add map list item: " + err.Error()) + } + + currentMapList, err := newMapListObject.GetMapList() + if (err != nil) { + t.Fatalf("Failed to get map: " + err.Error()) + } + + // Now we test concurrency + + var mapWaitgroup sync.WaitGroup + + for i:=0; i<1000; i++{ + + mapWaitgroup.Add(1) + + indexString := helpers.ConvertIntToString(i) + + addFunction := func(){ + + newItem := make(map[string]string) + newItem["TestKey" + indexString] = "TestValue" + indexString + + err := newMapListObject.AddMapListItem(newItem) + if (err != nil) { + t.Fatalf("Failed to add map list item: " + err.Error()) + } + + mapWaitgroup.Done() + } + + go addFunction() + } + + mapWaitgroup.Wait() + + if (len(currentMapList) != 1){ + t.Fatalf("Map list not copied.") + } + + err = newMapListObject.DeleteMapList() + if (err != nil) { + t.Fatalf("Failed to delete map list: " + err.Error()) + } +} + + +func TestMapListCopying(t *testing.T){ + + err := localFilesystem.InitializeAppDatastores() + if (err != nil){ + t.Fatalf("Failed to initialize app datastores: " + err.Error()) + } + err = appUsers.InitializeAppUserForTests() + if (err != nil){ + t.Fatalf("Failed to initalize app user for tests: " + err.Error()) + } + + newMapListObject, err := myMapList.CreateNewMapList("TestMapList") + if (err != nil) { + t.Fatalf("Failed to create new map: " + err.Error()) + } + + err = newMapListObject.DeleteMapList() + if (err != nil) { + t.Fatalf("Failed to delete map list: " + err.Error()) + } + + testMapList := make([]map[string]string, 0, 1) + + testMapA := map[string]string{ + "TestMapA": "TestValueA", + } + + testMapList = append(testMapList, testMapA) + + err = newMapListObject.OverwriteMapList(testMapList) + if (err != nil) { + t.Fatalf("Failed to overwrite map list: " + err.Error()) + } + + existingMapListA, err := newMapListObject.GetMapList() + if (err != nil) { + t.Fatalf("Failed to get map list: " + err.Error()) + } + + existingMapA := existingMapListA[0] + existingMapA["TestMapC"] = "TestValueC" + + existingMapB, err := newMapListObject.GetMapList() + if (err != nil) { + t.Fatalf("Failed to get map list: " + err.Error()) + } + + existingMapItem := existingMapB[0] + + _, exists := existingMapItem["TestMapC"] + if (exists == true) { + t.Fatalf("Map list not copied on GetMapList.") + } +} + + + diff --git a/internal/myDevice/myDevice.go b/internal/myDevice/myDevice.go new file mode 100644 index 0000000..f4ce860 --- /dev/null +++ b/internal/myDevice/myDevice.go @@ -0,0 +1,89 @@ + +// myDevice provides functions to generate device identifiers +// These are used to let other users know if the user has changed devices +// Upon learning this, users will wait to download new chat keys and discard any secret inboxes they had stored from the user + +package myDevice + +import "seekia/internal/cryptography/blake3" +import "seekia/internal/encoding" +import "seekia/internal/helpers" +import "seekia/internal/mySeedPhrases" +import "seekia/internal/mySettings" + +import "errors" +import "slices" +import "sync" + + +var initializingDeviceSeedMutex sync.Mutex + +//Outputs: +// -bool: My identity found +// -[11]byte: Device identifier +// -error +func GetMyDeviceIdentifier(myIdentityHash [16]byte, networkType byte)(bool, [11]byte, error){ + + isValid := helpers.VerifyNetworkType(networkType) + if (isValid == false){ + networkTypeString := helpers.ConvertByteToString(networkType) + return false, [11]byte{}, errors.New("GetMyDeviceIdentifier called with invalid networkType: " + networkTypeString) + } + + identityFound, mySeedPhraseHash, err := mySeedPhrases.GetMySeedPhraseHashFromIdentityHash(myIdentityHash) + if (err != nil) { return false, [11]byte{}, err } + if (identityFound == false){ + return false, [11]byte{}, nil + } + + getMyDeviceSeed := func()(string, error){ + + initializingDeviceSeedMutex.Lock() + defer initializingDeviceSeedMutex.Unlock() + + exists, myDeviceSeed, err := mySettings.GetSetting("DeviceSeed") + if (err != nil){ return "", err } + if (exists == true){ + return myDeviceSeed, nil + } + + newDeviceSeed, err := helpers.GetNewRandomHexString(64) + if (err != nil) { return "", err } + + err = mySettings.SetSetting("DeviceSeed", newDeviceSeed) + if (err != nil) { return "", err } + + return newDeviceSeed, nil + } + + myDeviceSeed, err := getMyDeviceSeed() + if (err != nil) { return false, [11]byte{}, err } + + deviceSeedBytes, err := encoding.DecodeHexStringToBytes(myDeviceSeed) + if (err != nil) { + return false, [11]byte{}, errors.New("My device seed is invalid: Not Hex: " + myDeviceSeed) + } + + if (len(deviceSeedBytes) != 64){ + return false, [11]byte{}, errors.New("My device seed is invalid: Invalid length: " + myDeviceSeed) + } + + hashSalt, err := encoding.DecodeBase32StringToBytes("mydevice") + if (err != nil) { return false, [11]byte{}, err } + + hashInput := slices.Concat(mySeedPhraseHash[:], hashSalt) + hashInput = append(hashInput, networkType) + hashInput = append(hashInput, deviceSeedBytes...) + + deviceIdentifier, err := blake3.GetBlake3HashAsBytes(11, hashInput) + if (err != nil) { return false, [11]byte{}, err } + + deviceIdentifierArray := [11]byte(deviceIdentifier) + + return true, deviceIdentifierArray, nil +} + + + + + diff --git a/internal/myIdentity/myIdentity.go b/internal/myIdentity/myIdentity.go new file mode 100644 index 0000000..77e79c7 --- /dev/null +++ b/internal/myIdentity/myIdentity.go @@ -0,0 +1,116 @@ + +// myIdentity provides functions to retrieve a user's identity keys and identity hashes + +package myIdentity + +import "seekia/internal/encoding" +import "seekia/internal/identity" +import "seekia/internal/mySeedPhrases" + +import "errors" + +//Outputs: +// -bool: My identity hash exists for provided identityType +// -[16]byte: My identity hash +// -error +func GetMyIdentityHash(myIdentityType string) (bool, [16]byte, error){ + + if (myIdentityType != "Mate" && myIdentityType != "Moderator" && myIdentityType != "Host"){ + return false, [16]byte{}, errors.New("GetMyIdentityHash called with invalid identity type: " + myIdentityType) + } + + exists, myPublicIdentityKey, _, err := GetMyPublicPrivateIdentityKeys(myIdentityType) + if (err != nil) { return false, [16]byte{}, err } + if (exists == false) { + return false, [16]byte{}, nil + } + + identityHash, err := identity.ConvertIdentityKeyToIdentityHash(myPublicIdentityKey, myIdentityType) + if (err != nil) { return false, [16]byte{}, err } + + return true, identityHash, nil +} + +//Outputs: +// -bool: Identity exists +// -[32]byte: Public identity key +// -[64]byte: Private identity key +// -error +func GetMyPublicPrivateIdentityKeys(myIdentityType string) (bool, [32]byte, [64]byte, error){ + + if (myIdentityType != "Mate" && myIdentityType != "Moderator" && myIdentityType != "Host"){ + return false, [32]byte{}, [64]byte{}, errors.New("GetMyPublicPrivateIdentityKeys called with invalid identity type: " + myIdentityType) + } + + exists, mySeedPhraseHash, err := mySeedPhrases.GetMySeedPhraseHash(myIdentityType) + if (err != nil) { return false, [32]byte{}, [64]byte{}, err } + if (exists == false) { + return false, [32]byte{}, [64]byte{}, nil + } + + publicKeyArray, privateKeyArray, err := identity.GetPublicPrivateIdentityKeysFromSeedPhraseHash(mySeedPhraseHash) + if (err != nil) { return false, [32]byte{}, [64]byte{}, err } + + return true, publicKeyArray, privateKeyArray, nil +} + + +// This function will verify that the identity key matches the requested identity hash +// This is necessary when you need to guarantee that the identity keys belong to the requested identity hash +//Outputs: +// -bool: Identity exists +// -[32]byte: Public identity key +// -[64]byte: Private identity key +// -error +func GetMyPublicPrivateIdentityKeysFromIdentityHash(inputIdentityHash [16]byte)(bool, [32]byte, [64]byte, error){ + + identityType, err := identity.GetIdentityTypeFromIdentityHash(inputIdentityHash) + if (err != nil) { + identityHashHex := encoding.EncodeBytesToHexString(inputIdentityHash[:]) + return false, [32]byte{}, [64]byte{}, errors.New("GetMyPublicPrivateIdentityKeysFromIdentityHash called with invalid identity hash: " + identityHashHex) + } + + exists, publicIdentityKey, privateIdentityKey, err := GetMyPublicPrivateIdentityKeys(identityType) + if (err != nil) { return false, [32]byte{}, [64]byte{}, err } + if (exists == false) { + return false, [32]byte{}, [64]byte{}, nil + } + + myIdentityHash, err := identity.ConvertIdentityKeyToIdentityHash(publicIdentityKey, identityType) + if (err != nil) { return false, [32]byte{}, [64]byte{}, err } + + if (inputIdentityHash != myIdentityHash) { + // Identity must have been replaced after identity hash was initially retrieved + return false, [32]byte{}, [64]byte{}, nil + } + + return true, publicIdentityKey, privateIdentityKey, nil +} + + +//Outputs: +// -bool: Identity hash is mine +// -string: IdentityType +// -error +func CheckIfIdentityHashIsMine(inputIdentityHash [16]byte)(bool, string, error){ + + identityType, err := identity.GetIdentityTypeFromIdentityHash(inputIdentityHash) + if (err != nil) { + inputIdentityHashHex := encoding.EncodeBytesToHexString(inputIdentityHash[:]) + return false, "", errors.New("CheckIfIdentityHashIsMine called with invalid identity hash: " + inputIdentityHashHex) + } + + exists, myIdentityHash, err := GetMyIdentityHash(identityType) + if (err != nil) { return false, "", err } + if (exists == false) { + return false, identityType, nil + } + if (myIdentityHash != inputIdentityHash) { + return false, identityType, nil + } + + return true, identityType, nil +} + + + diff --git a/internal/myIgnoredUsers/myIgnoredUsers.go b/internal/myIgnoredUsers/myIgnoredUsers.go new file mode 100644 index 0000000..dc9b2da --- /dev/null +++ b/internal/myIgnoredUsers/myIgnoredUsers.go @@ -0,0 +1,156 @@ + +// myIgnoredUsers provides functions to manage a mate user's ignored users +// Ignored users can be toggled to be hidden or shown in the user's matches and chat conversations +// This toggle can be useful if the user has run out of matches and wants to give users they were initially not interested in a second chance + +package myIgnoredUsers + +import "seekia/internal/encoding" +import "seekia/internal/helpers" +import "seekia/internal/identity" +import "seekia/internal/myDatastores/myMapList" + +import "sync" +import "errors" +import "time" + +// We lock this mutex whenever we add a new ignored user +// We do this to prevent adding duplicates +var addingIgnoredUsersMutex sync.Mutex + +var myIgnoredUsersMapListDatastore *myMapList.MyMapList + +// This function must be called whenever an app user signs in +func InitializeMyIgnoredUsersDatastore()error{ + + addingIgnoredUsersMutex.Lock() + defer addingIgnoredUsersMutex.Unlock() + + newMyIgnoredUsersMapListDatastore, err := myMapList.CreateNewMapList("MyIgnoredUsers") + if (err != nil) { return err } + + myIgnoredUsersMapListDatastore = newMyIgnoredUsersMapListDatastore + + return nil +} + +func IgnoreUser(userIdentityHash [16]byte, reasonProvided bool, reason string)error{ + + userIdentityHashString, userIdentityType, err := identity.EncodeIdentityHashBytesToString(userIdentityHash) + if (err != nil) { + userIdentityHashHex := encoding.EncodeBytesToHexString(userIdentityHash[:]) + return errors.New("IgnoreUser called with invalid identity hash: " + userIdentityHashHex) + } + if (userIdentityType != "Mate"){ + return errors.New("IgnoreUser called with non-mate identity hash: " + userIdentityType) + } + + addingIgnoredUsersMutex.Lock() + defer addingIgnoredUsersMutex.Unlock() + + userIsIgnored, _, _, _, err := CheckIfUserIsIgnored(userIdentityHash) + if (err != nil){ return err } + if (userIsIgnored == true){ + // The GUI should prevent this. + return errors.New("IgnoreUser called when user is already ignored.") + } + + currentTimeInt := time.Now().Unix() + currentTimeString := helpers.ConvertInt64ToString(currentTimeInt) + + newMapItem := map[string]string{ + "IdentityHash": userIdentityHashString, + "IgnoredTime": currentTimeString, + } + + if (reasonProvided == true){ + newMapItem["Reason"] = reason + } + + err = myIgnoredUsersMapListDatastore.AddMapListItem(newMapItem) + if (err != nil) { return err } + + return nil +} + +func UnignoreUser(userIdentityHash [16]byte)error{ + + userIdentityHashString, userIdentityType, err := identity.EncodeIdentityHashBytesToString(userIdentityHash) + if (err != nil) { + userIdentityHashHex := encoding.EncodeBytesToHexString(userIdentityHash[:]) + return errors.New("UnignoreUser called with invalid identity hash: " + userIdentityHashHex) + } + if (userIdentityType != "Mate"){ + return errors.New("UnignoreUser called with non-mate identity hash: " + userIdentityType) + } + + mapToDelete := map[string]string{ + "IdentityHash": userIdentityHashString, + } + + err = myIgnoredUsersMapListDatastore.DeleteMapListItems(mapToDelete) + if (err != nil) { return err } + + return nil +} + +func GetMyIgnoredUsersMapList()([]map[string]string, error){ + + myIgnoredUsersMapList, err := myIgnoredUsersMapListDatastore.GetMapList() + if (err != nil) { return nil, err } + + return myIgnoredUsersMapList, nil +} + +//Outputs: +// -bool: User is Ignored +// -int64: Unix time of ignoring +// -bool: Reason provided +// -string: Reason for ignoring +// -error +func CheckIfUserIsIgnored(userIdentityHash [16]byte)(bool, int64, bool, string, error){ + + userIdentityHashString, userIdentityType, err := identity.EncodeIdentityHashBytesToString(userIdentityHash) + if (err != nil) { + userIdentityHashHex := encoding.EncodeBytesToHexString(userIdentityHash[:]) + return false, 0, false, "", errors.New("CheckIfUserIsIgnored called with invalid identity hash: " + userIdentityHashHex) + } + if (userIdentityType != "Mate"){ + return false, 0, false, "", errors.New("CheckIfUserIsIgnored called with non-mate identity hash: " + userIdentityType) + } + + lookupMap := map[string]string{ + "IdentityHash": userIdentityHashString, + } + + anyItemsFound, ignoredUsersMapList, err := myIgnoredUsersMapListDatastore.GetMapListItems(lookupMap) + if (err != nil) { return false, 0, false, "", err } + if (anyItemsFound == false){ + return false, 0, false, "", nil + } + + if (len(ignoredUsersMapList) != 1){ + return false, 0, false, "", errors.New("Ignored users mapList contains more than 1 entry for an identity hash") + } + + ignoredUserMapItem := ignoredUsersMapList[0] + + ignoredTimeString, exists := ignoredUserMapItem["IgnoredTime"] + if (exists == false) { + return false, 0, false, "", errors.New("Ignored users mapList contains item missing IgnoredTime") + } + + ignoredTimeInt64, err := helpers.ConvertStringToInt64(ignoredTimeString) + if (err != nil) { + return false, 0, false, "", errors.New("Ignored users mapList contains item with invalid IgnoredTime: " + ignoredTimeString) + } + + reason, reasonExists := ignoredUserMapItem["Reason"] + if (reasonExists == false){ + return true, ignoredTimeInt64, false, "", nil + } + + return true, ignoredTimeInt64, true, reason, nil +} + + diff --git a/internal/myLikedUsers/myLikedUsers.go b/internal/myLikedUsers/myLikedUsers.go new file mode 100644 index 0000000..f442b7c --- /dev/null +++ b/internal/myLikedUsers/myLikedUsers.go @@ -0,0 +1,167 @@ + +// myLikedUsers provides functions to manage a Mate user's liked users +// Users can filter their matches/chat conversations to only show users they have liked +// Users can also view their liked users in the GUI + +package myLikedUsers + +import "seekia/internal/encoding" +import "seekia/internal/helpers" +import "seekia/internal/identity" +import "seekia/internal/myDatastores/myMapList" + +import "sync" +import "errors" +import "time" + +// We lock this whenever we are adding a new liked user +// We need this so we don't add duplicates +var addingLikedUserMutex sync.Mutex + +var myLikedUsersMapListDatastore *myMapList.MyMapList + +// This function must be called whenever an app user signs in +func InitializeMyLikedUsersDatastore()error{ + + newMyLikedUsersMapListDatastore, err := myMapList.CreateNewMapList("MyLikedUsers") + if (err != nil) { return err } + + myLikedUsersMapListDatastore = newMyLikedUsersMapListDatastore + + return nil +} + +func LikeUser(userIdentityHash [16]byte)error{ + + userIdentityHashString, identityType, err := identity.EncodeIdentityHashBytesToString(userIdentityHash) + if (err != nil) { + userIdentityHashHex := encoding.EncodeBytesToHexString(userIdentityHash[:]) + return errors.New("LikeUser called with invalid identity hash: " + userIdentityHashHex) + } + if (identityType != "Mate"){ + return errors.New("LikeUser called with non-mate identity hash: " + userIdentityHashString) + } + + addingLikedUserMutex.Lock() + defer addingLikedUserMutex.Unlock() + + isLiked, _, err := CheckIfUserIsLiked(userIdentityHash) + if (err != nil) { return err } + if (isLiked == true){ + // The GUI should prevent this. + return errors.New("LikeUser called when user is already liked.") + } + + currentTimeInt := time.Now().Unix() + currentTimeString := helpers.ConvertInt64ToString(currentTimeInt) + + newMapItem := map[string]string{ + "IdentityHash": userIdentityHashString, + "LikedTime": currentTimeString, + } + + err = myLikedUsersMapListDatastore.AddMapListItem(newMapItem) + if (err != nil) { return err } + + return nil +} + +func UnlikeUser(userIdentityHash [16]byte)error{ + + userIdentityHashString, identityType, err := identity.EncodeIdentityHashBytesToString(userIdentityHash) + if (err != nil) { + userIdentityHashHex := encoding.EncodeBytesToHexString(userIdentityHash[:]) + return errors.New("UnlikeUser called with invalid identity hash: " + userIdentityHashHex) + } + if (identityType != "Mate"){ + return errors.New("UnlikeUser called with non-mate identity hash: " + userIdentityHashString) + } + + mapToDelete := map[string]string{ + "IdentityHash": userIdentityHashString, + } + + err = myLikedUsersMapListDatastore.DeleteMapListItems(mapToDelete) + if (err != nil) { return err } + + return nil +} + +func GetMyLikedUsersList()([][16]byte, error){ + + myLikedUsersMapList, err := myLikedUsersMapListDatastore.GetMapList() + if (err != nil) { return nil, err } + + likedUsersList := make([][16]byte, 0, len(myLikedUsersMapList)) + + for _, userMap := range myLikedUsersMapList{ + + userIdentityHashString, exists := userMap["IdentityHash"] + if (exists == false){ + return nil, errors.New("myLikedUsersMapListDatastore is malformed: Contains item missing IdentityHash") + } + userIdentityHash, userIdentityType, err := identity.ReadIdentityHashString(userIdentityHashString) + if (err != nil){ + return nil, errors.New("myLikedUsersMapListDatastore is malformed: Contains item with invalid IdentityHash: " + userIdentityHashString) + } + if (userIdentityType != "Mate"){ + return nil, errors.New("myLikedUsersMapListDatastore is malformed: Contains non-Mate IdentityHash: " + userIdentityType) + } + + likedUsersList = append(likedUsersList, userIdentityHash) + } + + return likedUsersList, nil +} + +func GetMyLikedUsersMapList()([]map[string]string, error){ + + myLikedUsersMapList, err := myLikedUsersMapListDatastore.GetMapList() + if (err != nil) { return nil, err } + + return myLikedUsersMapList, nil +} + +//Outputs: +// -bool: Is Liked +// -int64: Unix time of liking (if user is liked) +// -error +func CheckIfUserIsLiked(userIdentityHash [16]byte)(bool, int64, error){ + + userIdentityHashString, identityType, err := identity.EncodeIdentityHashBytesToString(userIdentityHash) + if (err != nil) { + userIdentityHashHex := encoding.EncodeBytesToHexString(userIdentityHash[:]) + return false, 0, errors.New("CheckIfUserIsLiked called with invalid identity hash: " + userIdentityHashHex) + } + if (identityType != "Mate"){ + return false, 0, errors.New("CheckIfUserIsLiked called with non-mate identity hash.") + } + + lookupMap := map[string]string{ + "IdentityHash": userIdentityHashString, + } + + exists, likedUsersMapList, err := myLikedUsersMapListDatastore.GetMapListItems(lookupMap) + if (err != nil) { return false, 0, err } + if (exists == false){ + return false, 0, nil + } + + if (len(likedUsersMapList) != 1){ + return false, 0, errors.New("Liked users map list contains more than 1 entry for an identity hash") + } + + likedUserMapItem := likedUsersMapList[0] + + likedTimeString, exists := likedUserMapItem["LikedTime"] + if (exists == false) { + return false, 0, errors.New("Liked users map list contains item missing LikedTime") + } + + likedTimeInt64, err := helpers.ConvertStringToInt64(likedTimeString) + if (err != nil) { return false, 0, err } + + return true, likedTimeInt64, nil +} + + diff --git a/internal/myMatchScore/myMatchScore.go b/internal/myMatchScore/myMatchScore.go new file mode 100644 index 0000000..d2f36fb --- /dev/null +++ b/internal/myMatchScore/myMatchScore.go @@ -0,0 +1,67 @@ + +// myMatchScore provides functions to set and retrieve match score point values. +// Each mate desire has a point value. +// We add a desire's point value to a peer's match score if they fulfill a user's desires. + +package myMatchScore + +import "seekia/internal/myDatastores/myMap" +import "seekia/internal/helpers" + +import "errors" + +var myMatchScorePointsMapDatastore *myMap.MyMap + +// This function must be called whenever we sign in to an app user +func InitializeMyMatchScorePointsDatastore()error{ + + newMyMatchScorePointsMapDatastore, err := myMap.CreateNewMap("MyMatchScorePoints") + if (err != nil) { return err } + + myMatchScorePointsMapDatastore = newMyMatchScorePointsMapDatastore + + return nil +} + +func SetMyMatchScoreDesirePoints(desireName string, points int)error{ + + if (desireName == ""){ + return errors.New("SetMyMatchScoreDesirePoints called with empty desireName.") + } + + if (points < 1 || points > 100) { + return errors.New("SetMyMatchScoreDesirePoints called with invalid points value.") + } + + pointsString := helpers.ConvertIntToString(points) + + err := myMatchScorePointsMapDatastore.SetMapEntry(desireName, pointsString) + if (err != nil) { return err } + + return nil +} + +func GetMyMatchScoreDesirePoints(desireName string)(int, error){ + + if (desireName == ""){ + return 0, errors.New("GetMyMatchScoreDesirePoints called with empty desireName.") + } + + entryExists, currentPoints, err := myMatchScorePointsMapDatastore.GetMapEntry(desireName) + if (err != nil) { return 0, err } + if (entryExists == false){ + return 1, nil + } + + currentPointsInt, err := helpers.ConvertStringToInt(currentPoints) + if (err != nil){ + return 0, errors.New("My current match score desire points is invalid: Not an int: " + currentPoints) + } + if (currentPointsInt < 1 || currentPointsInt > 100){ + return 0, errors.New("My current match score desire points is invalid: Out of range: " + currentPoints) + } + + return currentPointsInt, nil +} + + diff --git a/internal/myMatches/myMatches.go b/internal/myMatches/myMatches.go new file mode 100644 index 0000000..801d1a1 --- /dev/null +++ b/internal/myMatches/myMatches.go @@ -0,0 +1,551 @@ + +// myMatches provides functions to generate and retrieve a user's mate matches +// Matches are the users whom fulfill a user's desires. + +package myMatches + +import "seekia/internal/appMemory" +import "seekia/internal/badgerDatabase" +import "seekia/internal/desires/myMateDesires" +import "seekia/internal/helpers" +import "seekia/internal/identity" +import "seekia/internal/myBlockedUsers" +import "seekia/internal/myDatastores/myList" +import "seekia/internal/myIdentity" +import "seekia/internal/mySettings" +import "seekia/internal/profiles/viewableProfiles" + +import "slices" +import "sync" +import "errors" + +// This mutex will be locked whenever we are updating matches. +var updatingMatchesMutex sync.Mutex + +var myMatchesListDatastore *myList.MyList + +// This function must be called whenever an app user signs in +func InitializeMyMatchesDatastores()error{ + + updatingMatchesMutex.Lock() + defer updatingMatchesMutex.Unlock() + + newMyMatchesListDatastore, err := myList.CreateNewList("MyMatches") + if (err != nil) { return err } + + myMatchesListDatastore = newMyMatchesListDatastore + + return nil +} + +func GetMatchesSortByAttribute()(string, error){ + + exists, currentAttribute, err := mySettings.GetSetting("MatchesSortByAttribute") + if (err != nil) { return "", err } + if (exists == false){ + return "MatchScore", nil + } + + return currentAttribute, nil +} + + +func GetMatchesSortDirection()(string, error){ + + exists, currentDirection, err := mySettings.GetSetting("MatchesSortDirection") + if (err != nil) { return "", err } + if (exists == false){ + return "Descending", nil + } + if (currentDirection != "Ascending" && currentDirection != "Descending"){ + return "", errors.New("MySettings malformed: Invalid MatchesSortDirection: " + currentDirection) + } + + return currentDirection, nil +} + + +func GetMatchesReadyStatus(networkType byte)(bool, error) { + + isValid := helpers.VerifyNetworkType(networkType) + if (isValid == false){ + networkTypeString := helpers.ConvertByteToString(networkType) + return false, errors.New("GetMatchesReadyStatus called with invalid networkType: " + networkTypeString) + } + + exists, matchesGeneratedStatus, err := mySettings.GetSetting("MatchesGeneratedStatus") + if (err != nil) { return false, err } + if (exists == false || matchesGeneratedStatus != "Yes") { + return false, nil + } + + exists, matchesSortedStatus, err := mySettings.GetSetting("MatchesSortedStatus") + if (err != nil) { return false, err } + if (exists == false || matchesSortedStatus != "Yes") { + return false, nil + } + + exists, matchesNetworkTypeString, err := mySettings.GetSetting("MatchesNetworkType") + if (err != nil) { return false, err } + if (exists == false){ + // This should not happen, because Matches network type is created whenever matches are generated + return false, errors.New("mySettings missing MatchesNetworkType when MatchesGeneratedStatus exists.") + } + + matchesNetworkType, err := helpers.ConvertNetworkTypeStringToByte(matchesNetworkTypeString) + if (err != nil) { + return false, errors.New("mySettings contains invalid MatchesNetworkType: " + matchesNetworkTypeString) + } + if (matchesNetworkType != networkType){ + // Matches were generated for a different networkType + // This should never happen, because we will always set MatchesGeneratedStatus to No when we switch network types, + // and we will always call the GetMatchesReadyStatus function with the current appNetworkType + + //TODO: Log this. + + err := mySettings.SetSetting("MatchesGeneratedStatus", "No") + if (err != nil) { return false, err } + + return false, nil + } + + return true, nil +} + +// Will need a refresh any time a new mate profile is downloaded +func CheckIfMyMatchesNeedRefresh()(bool, error){ + + exists, needsRefresh, err := mySettings.GetSetting("MatchesNeedRefreshYesNo") + if (err != nil) { return true, err } + if (exists == true && needsRefresh == "No"){ + return false, nil + } + + return true, nil +} + +// This function should only be called once matches are ready (generated and sorted) +//Outputs: +// -bool: Matches are ready +// -[][16]byte: List of sorted matches +// -error +func GetMyMatchesList(networkType byte)(bool, [][16]byte, error) { + + isValid := helpers.VerifyNetworkType(networkType) + if (isValid == false){ + networkTypeString := helpers.ConvertByteToString(networkType) + return false, nil, errors.New("GetMyMatchesList called with invalid networkType: " + networkTypeString) + } + + matchesReady, err := GetMatchesReadyStatus(networkType) + if (err != nil) { return false, nil, err } + if (matchesReady == false){ + return false, nil, nil + } + + myMatchIdentityHashesList, err := myMatchesListDatastore.GetList() + if (err != nil) { return false, nil, err } + + myMatchesList := make([][16]byte, 0, len(myMatchIdentityHashesList)) + + for _, matchIdentityHashString := range myMatchIdentityHashesList{ + + matchIdentityHash, identityType, err := identity.ReadIdentityHashString(matchIdentityHashString) + if (err != nil){ + return false, nil, errors.New("myMatchesListDatastore contains invalid match identity hash: " + matchIdentityHashString) + } + if (identityType != "Mate"){ + return false, nil, errors.New("myMatchesListDatastore contains non-Mate match identity hash.") + } + + myMatchesList = append(myMatchesList, matchIdentityHash) + } + + return true, myMatchesList, nil +} + +// This function returns the number of matches +// It can be called before the matches are ready (after being generated, but before being sorted) +func GetNumberOfGeneratedMatches()(int, error){ + + myMatchesList, err := myMatchesListDatastore.GetList() + if (err != nil) { return 0, err } + + numberOfGeneratedMatches := len(myMatchesList) + + return numberOfGeneratedMatches, nil +} + +//Outputs: +// -bool: Build encountered error +// -string: Error encountered +// -bool: Build is stopped (will be stopped if user went to different page) +// -bool: Matches are ready +// -float64: Progress status (0 - 1) +// -error +func GetMyMatchesBuildStatus(networkType byte)(bool, string, bool, bool, float64, error){ + + isValid := helpers.VerifyNetworkType(networkType) + if (isValid == false){ + networkTypeString := helpers.ConvertByteToString(networkType) + return false, "", false, false, 0, errors.New("GetMyMatchesBuildStatus called with invalid networkType: " + networkTypeString) + } + + exists, encounteredError := appMemory.GetMemoryEntry("MatchesBuildEncounteredError") + if (exists == false){ + // No build exists. A build has not been started since Seekia was started + return false, "", false, false, 0, nil + } + if (encounteredError == "Yes"){ + exists, errorEncountered := appMemory.GetMemoryEntry("MatchesBuildError") + if (exists == false){ + return false, "", false, false, 0, errors.New("Matches build encountered error is yes, but no error exists.") + } + + return true, errorEncountered, false, false, 0, nil + } + + isStopped := CheckIfBuildMatchesIsStopped() + if (isStopped == true){ + return false, "", true, false, 0, nil + } + + matchesReadyBool, err := GetMatchesReadyStatus(networkType) + if (err != nil) { return false, "", false, false, 0, err } + if (matchesReadyBool == true){ + return false, "", false, true, 1, nil + } + + exists, currentPercentageString := appMemory.GetMemoryEntry("MatchesReadyProgressPercentage") + if (exists == false){ + // No build exists. A build has not been started since Seekia was started + return false, "", false, false, 0, nil + } + + currentPercentageFloat, err := helpers.ConvertStringToFloat64(currentPercentageString) + if (err != nil){ + return false, "", false, false, 0, errors.New("MatchesReadyProgressPercentage is not a float: " + currentPercentageString) + } + + return false, "", false, false, currentPercentageFloat, nil +} + + +// True == Stop building matches +// False = Don't stop building matches +func CheckIfBuildMatchesIsStopped()bool{ + + exists, stopBuildMatchesYesNo := appMemory.GetMemoryEntry("StopBuildMatchesYesNo") + if (exists == false || stopBuildMatchesYesNo != "No"){ + return true + } + + return false +} + +// This function will cancel the current build (if one is running) +// It will then start updating our matches +func StartUpdatingMyMatches(networkType byte)error{ + + isValid := helpers.VerifyNetworkType(networkType) + if (isValid == false){ + networkTypeString := helpers.ConvertByteToString(networkType) + return errors.New("StartUpdatingMyMatches called with invalid networkType: " + networkTypeString) + } + + appMemory.SetMemoryEntry("StopBuildMatchesYesNo", "Yes") + + // We wait for any existing builds to stop. + updatingMatchesMutex.Lock() + + appMemory.SetMemoryEntry("MatchesReadyProgressPercentage", "0") + appMemory.SetMemoryEntry("MatchesBuildEncounteredError", "No") + appMemory.SetMemoryEntry("MatchesBuildError", "") + + appMemory.SetMemoryEntry("StopBuildMatchesYesNo", "No") + + updateMatches := func()error{ + + appMemory.SetMemoryEntry("MatchesReadyProgressPercentage", "0") + + getMatchesNeedToBeGeneratedStatus := func()(bool, error){ + + exists, matchesGeneratedStatus, err := mySettings.GetSetting("MatchesGeneratedStatus") + if (err != nil) { return false, err } + if (exists == false || matchesGeneratedStatus != "Yes"){ + return true, nil + } + + exists, matchesNetworkTypeString, err := mySettings.GetSetting("MatchesNetworkType") + if (err != nil) { return false, err } + if (exists == false){ + // This should not happen, because MatchesNetworkType is set to Yes whenever matches are generated + return false, errors.New("MatchesNetworkType missing when MatchesGeneratedStatus exists.") + } + + matchesNetworkType, err := helpers.ConvertNetworkTypeStringToByte(matchesNetworkTypeString) + if (err != nil) { + return false, errors.New("mySettings contains invalid MatchesNetworkType: " + matchesNetworkTypeString) + } + if (matchesNetworkType != networkType){ + // This should not happen, because MatchesGeneratedStatus should be set to No whenever app network type is changed, + // and StartUpdatingMyMatches should only be called with the current app network type. + return true, nil + } + + return false, nil + } + + matchesNeedToBeGenerated, err := getMatchesNeedToBeGeneratedStatus() + if (err != nil) { return err } + if (matchesNeedToBeGenerated == true){ + + err := mySettings.SetSetting("MatchesSortedStatus", "No") + if (err != nil) { return err } + + // We use below to make sure we do not add ourselves as a match + myIdentityExists, myIdentityHash, err := myIdentity.GetMyIdentityHash("Mate") + if (err != nil) { return err } + + mateIdentityHashesList, err := badgerDatabase.GetAllProfileIdentityHashes("Mate") + if (err != nil) { return err } + + maximumIndex := len(mateIdentityHashesList) - 1 + + // This is a list of match identity hashes + matchesList := make([]string, 0) + + for index, peerIdentityHash := range mateIdentityHashesList{ + + stopBuildMatchesYesNo := CheckIfBuildMatchesIsStopped() + if (stopBuildMatchesYesNo == true){ + // User has gone to a different page, match generation was stopped + return nil + } + + progressPercentage, err := helpers.ScaleNumberProportionally(true, index, 0, maximumIndex, 0, 50) + if (err != nil) { return err } + + progressFloat := float64(progressPercentage)/100 + matchesReadyProgressPercentage := helpers.ConvertFloat64ToString(progressFloat) + + appMemory.SetMemoryEntry("MatchesReadyProgressPercentage", matchesReadyProgressPercentage) + + if (myIdentityExists == true && peerIdentityHash == myIdentityHash){ + // We never show ourselves as a match + continue + } + + userIsBlocked, _, _, _, err := myBlockedUsers.CheckIfUserIsBlocked(peerIdentityHash) + if (err != nil) { return err } + if (userIsBlocked == true){ + continue + } + + profileExists, _, getAnyUserProfileAttributeFunction, err := viewableProfiles.GetRetrieveAnyNewestViewableUserProfileAttributeFunction(peerIdentityHash, networkType, true, false, true) + if (err != nil) { return err } + if (profileExists == false) { + // Profile must have been deleted or it is not viewable + // User cannot be a match, because they have no viewable profile. + continue + } + + exists, _, userIsDisabled, err := getAnyUserProfileAttributeFunction("Disabled") + if (err != nil) { return err } + if (exists == true && userIsDisabled == "Yes"){ + // User's newest viewable profile is disabled. + continue + } + + profilePassesDesires, err := myMateDesires.CheckIfMateProfilePassesAllMyDesires(false, "", getAnyUserProfileAttributeFunction) + if (err != nil) { return err } + if (profilePassesDesires == true){ + + peerIdentityHashString, _, err := identity.EncodeIdentityHashBytesToString(peerIdentityHash) + if (err != nil) { return err } + + matchesList = append(matchesList, peerIdentityHashString) + } + } + + err = myMatchesListDatastore.OverwriteList(matchesList) + if (err != nil) { return err } + + err = mySettings.SetSetting("MatchesGeneratedStatus", "Yes") + if (err != nil) { return err } + + networkTypeString := helpers.ConvertByteToString(networkType) + + err = mySettings.SetSetting("MatchesNetworkType", networkTypeString) + if (err != nil) { return err } + + err = mySettings.SetSetting("MatchesViewIndex", "0") + if (err != nil) { return err } + } + + stopBuildMatchesBool := CheckIfBuildMatchesIsStopped() + if (stopBuildMatchesBool == true){ + return nil + } + + appMemory.SetMemoryEntry("MatchesReadyProgressPercentage", "0.50") + + exists, matchesSortedStatus, err := mySettings.GetSetting("MatchesSortedStatus") + if (err != nil) { return err } + if (exists == false || matchesSortedStatus != "Yes"){ + + // Now we sort matches. + + myMatchesList, err := myMatchesListDatastore.GetList() + if (err != nil) { return err } + + currentSortBy, err := GetMatchesSortByAttribute() + if (err != nil) { return err } + currentSortDirection, err := GetMatchesSortDirection() + if (err != nil) { return err } + + // We use this map to make sure there are no duplicate matches + // This should never happen, unless the user's stored list was edited or there is a bug + allMatchesMap := make(map[[16]byte]struct{}) + + // Map structure: Match Identity Hash -> Sort By Attribute Value + matchAttributeValuesMap := make(map[string]float64) + + maximumIndex := len(myMatchesList) - 1 + + for index, matchIdentityHashString := range myMatchesList{ + + matchIdentityHash, _, err := identity.ReadIdentityHashString(matchIdentityHashString) + if (err != nil){ + return errors.New("myMatchesList contains invalid identity hash during sort: " + matchIdentityHashString) + } + + _, exists := allMatchesMap[matchIdentityHash] + if (exists == true){ + return errors.New("myMatchesList contains duplicate match.") + } + allMatchesMap[matchIdentityHash] = struct{}{} + + profileExists, _, attributeExists, attributeValue, err := viewableProfiles.GetAnyAttributeFromNewestViewableUserProfile(matchIdentityHash, networkType, currentSortBy, true, false, false) + if (err != nil) { return err } + if (profileExists == false){ + // Profile must have been deleted, or it became unviewable + // The gui will let the user know this when they navigate to the profile + // The user will be placed towards the end of the sort, with all of the other No Response users + continue + } + if (attributeExists == true){ + attributeValueFloat, err := helpers.ConvertStringToFloat64(attributeValue) + if (err != nil) { + return errors.New("Mate profile attribute cannot be converted to float during myMatches sort: " + attributeValue) + } + + matchAttributeValuesMap[matchIdentityHashString] = attributeValueFloat + } + + isStopped := CheckIfBuildMatchesIsStopped() + if (isStopped == true){ + // User has gone to a different page, match generation was stopped + return nil + } + + newScaledPercentageInt, err := helpers.ScaleNumberProportionally(true, index, 0, maximumIndex, 50, 80) + if (err != nil) { return err } + + newProgressFloat := float64(newScaledPercentageInt)/100 + + newProgressString := helpers.ConvertFloat64ToString(newProgressFloat) + + appMemory.SetMemoryEntry("MatchesReadyProgressPercentage", newProgressString) + } + + compareMatchesFunction := func(identityHashA string, identityHashB string) int { + + if (identityHashA == identityHashB){ + panic("Duplicate match identity hashes called during sort.") + } + + attributeValueA, attributeValueAExists := matchAttributeValuesMap[identityHashA] + + attributeValueB, attributeValueBExists := matchAttributeValuesMap[identityHashB] + + if (attributeValueAExists == false && attributeValueBExists == false){ + + // We don't know the attribute value for either match + // We sort matches in unicode order + if (identityHashA < identityHashB){ + return -1 + } + return 1 + + } else if (attributeValueAExists == true && attributeValueBExists == false){ + + // We sort unknown attribute matches to the back of the list + + return -1 + + } else if (attributeValueAExists == false && attributeValueBExists == true){ + + return 1 + } + + // Both attribute values exist + + if (attributeValueA == attributeValueB){ + // We sort identity hashes in unicode order + if (identityHashA < identityHashB){ + return -1 + } + return 1 + } + + if (attributeValueA < attributeValueB){ + + if (currentSortDirection == "Ascending"){ + return -1 + } + return 1 + } + if (currentSortDirection == "Ascending"){ + return 1 + } + return -1 + } + + slices.SortFunc(myMatchesList, compareMatchesFunction) + + err = myMatchesListDatastore.OverwriteList(myMatchesList) + if (err != nil) { return err } + + err = mySettings.SetSetting("MatchesSortedStatus", "Yes") + if (err != nil) { return err } + } + + appMemory.SetMemoryEntry("MatchesReadyProgressPercentage", "1") + + err = mySettings.SetSetting("MatchesNeedRefreshYesNo", "No") + if (err != nil) { return err } + + return nil + } + + updateFunction := func(){ + + err := updateMatches() + if (err != nil) { + appMemory.SetMemoryEntry("MatchesBuildEncounteredError", "Yes") + appMemory.SetMemoryEntry("MatchesBuildError", err.Error()) + } + + updatingMatchesMutex.Unlock() + } + + go updateFunction() + + return nil +} + + + + + diff --git a/internal/myQuestionnaireHistory/myQuestionnaireHistory.go b/internal/myQuestionnaireHistory/myQuestionnaireHistory.go new file mode 100644 index 0000000..83ced3c --- /dev/null +++ b/internal/myQuestionnaireHistory/myQuestionnaireHistory.go @@ -0,0 +1,11 @@ + +// myQuestionnaireHistory provides functions to manage a user's questionnaire history +// This includes the questions they have broadcast, and the responses that they have received +// It also includes any questionnaire responses they have sent + +package myQuestionnaireHistory + +//TODO: Build package +// We should save any broadcasted questions during the profile broadcast process +// We should save the responses we send in the myMessageQueue package +// We should save any responses received when we are importing new messages diff --git a/internal/myRanges/myRanges.go b/internal/myRanges/myRanges.go new file mode 100644 index 0000000..4377d41 --- /dev/null +++ b/internal/myRanges/myRanges.go @@ -0,0 +1,371 @@ + +// myRanges provides functions to retrieve and modify a user's Host and Moderator ranges +// These ranges define what portion of the Seekia network the user will host and/or moderate + +package myRanges + +// Moderator and Host Mode each have 4 types of ranges: +// -Mate identities +// -Host identities (For Host mode, we either host all or none) +// -Moderator identities (For Host or Moderator mode, we either host all or none) +// -Messages inboxes + +// A user's ranges define what profiles and messages they will download +// Profiles to download are determined by the identity hash of the profile author +// Messages to download are determined by the Message's inbox + +// For profiles/messages a user is hosting/moderating, they will also download all reviews and reports for the profiles/messages. + +// Identity reviews/reports to download are determined by the reviewed/reported identity hash +// Profile/Attribute reviews/reports to download are determined by the reviewed/reported profile author identity hash +// Message reviews/reports to download are determined by the reviewed/reported message's inbox + +//TODO: For moderation ranges, there must be a way to stop hosting content within our range, after we have reviewed it +// We have to keep track of this dropped content, and check for it within the backgroundDownloads package + +import "seekia/internal/byteRange" +import "seekia/internal/encoding" +import "seekia/internal/identity" +import "seekia/internal/messaging/inbox" +import "seekia/internal/mySettings" + +import "errors" + +func AdjustMyRanges(myIdentityType string)error{ + + //TODO + // This function will see how much disk space is available, and adjust the moderation and host ranges accordingly + // This function is run by backgroundJobs on a regular schedule + + return nil +} + +//Outputs: +// -bool: Host mode enabled +// -bool: Identity is within my host range +// -error +func CheckIfIdentityHashIsWithinMyHostedRange(identityHash [16]byte)(bool, bool, error){ + + identityType, err := identity.GetIdentityTypeFromIdentityHash(identityHash) + if (err != nil) { + identityHashHex := encoding.EncodeBytesToHexString(identityHash[:]) + return false, false, errors.New("CheckIfIdentityHashIsWithinMyHostedRange called with invalid identityHash: " + identityHashHex) + } + + hostModeEnabled, hostingAnyIdentities, myIdentityRangeStart, myIdentityRangeEnd, err := GetMyIdentitiesToHostRange(identityType) + if (err != nil) { return false, false, err } + if (hostModeEnabled == false){ + return false, false, nil + } + if (hostingAnyIdentities == false){ + return true, false, nil + } + if (identityType != "Mate"){ + // Hosts must host either all or none of the Host/moderator identities, so no range check is needed + return true, true, nil + } + + isWithinRange, err := byteRange.CheckIfIdentityHashIsWithinRange(myIdentityRangeStart, myIdentityRangeEnd, identityHash) + if (err != nil) { return false, false, err } + + return true, isWithinRange, nil +} + +//Outputs: +// -bool: Host mode enabled +// -bool: Hosting any identities of provided identityType +// -[16]byte: Identity hash range start +// -[16]byte: Identity hash range end +// -error +func GetMyIdentitiesToHostRange(identityType string)(bool, bool, [16]byte, [16]byte, error){ + + if (identityType != "Mate" && identityType != "Host" && identityType != "Moderator"){ + return false, false, [16]byte{}, [16]byte{}, errors.New("GetMyIdentitiesToHostRange called with invalid identityType: " + identityType) + } + + settingExists, hostModeOnOffStatus, err := mySettings.GetSetting("HostModeOnOffStatus") + if (err != nil) { return false, false, [16]byte{}, [16]byte{}, err } + if (settingExists == false || hostModeOnOffStatus != "On"){ + return false, false, [16]byte{}, [16]byte{}, nil + } + settingExists, hostingIdentityTypeContent, err := mySettings.GetSetting("Hosting" + identityType + "Content") + if (err != nil) { return false, false, [16]byte{}, [16]byte{}, err } + if (settingExists == true && hostingIdentityTypeContent == "No"){ + // The default is for all identityType content to be hosted + return true, false, [16]byte{}, [16]byte{}, nil + } + + if (identityType != "Mate"){ + // Hosts will host either all or none of host/moderator identities + minimumIdentityBound, maximumIdentityBound := byteRange.GetMinimumMaximumIdentityHashBounds() + + return true, true, minimumIdentityBound, maximumIdentityBound, nil + } + + settingExists, hostedMateRangeStartString, err := mySettings.GetSetting("HostedMateContentRangeStart") + if (err != nil) { return false, false, [16]byte{}, [16]byte{}, err } + if (settingExists == false){ + // The default range is all identities. + // The client will automatically reduce this range if storage limits are reached. + minimumIdentityBound, maximumIdentityBound := byteRange.GetMinimumMaximumIdentityHashBounds() + return true, true, minimumIdentityBound, maximumIdentityBound, nil + } + + settingExists, hostedMateRangeEndString, err := mySettings.GetSetting("HostedMateContentRangeEnd") + if (err != nil) { return false, false, [16]byte{}, [16]byte{}, err } + if (settingExists == false){ + // This should not happen. + return false, false, [16]byte{}, [16]byte{}, errors.New("HostedMateContentRangeStart exists but no end range found.") + } + + hostedMateRangeStart, _, err := identity.ReadIdentityHashString(hostedMateRangeStartString) + if (err != nil){ + return false, false, [16]byte{}, [16]byte{}, errors.New("HostedMateContentRangeStart is invalid: " + hostedMateRangeStartString) + } + + hostedMateRangeEnd, _, err := identity.ReadIdentityHashString(hostedMateRangeEndString) + if (err != nil){ + return false, false, [16]byte{}, [16]byte{}, errors.New("HostedMateContentRangeEnd is invalid: " + hostedMateRangeEndString) + } + + return true, true, hostedMateRangeStart, hostedMateRangeEnd, nil +} + + +//Outputs: +// -bool: Host mode enabled +// -bool: Inbox is within my hosted range +// -error +func CheckIfMessageInboxIsWithinMyHostedRange(messageInbox [10]byte)(bool, bool, error){ + + hostModeEnabled, hostingAnyInboxes, myInboxRangeStart, myInboxRangeEnd, err := GetMyInboxesToHostRange() + if (err != nil) { return false, false, err } + if (hostModeEnabled == false){ + return false, false, nil + } + if (hostingAnyInboxes == false){ + return true, false, nil + } + + isWithinRange, err := byteRange.CheckIfInboxIsWithinRange(myInboxRangeStart, myInboxRangeEnd, messageInbox) + if (err != nil) { return false, false, err } + + return true, isWithinRange, nil +} + +//Outputs: +// -bool: Host mode enabled +// -bool: Hosting any inboxes +// -[10]byte: Inbox range start +// -[10]byte: Inbox range end +// -error +func GetMyInboxesToHostRange()(bool, bool, [10]byte, [10]byte, error){ + + settingExists, hostModeOnOffStatus, err := mySettings.GetSetting("HostModeOnOffStatus") + if (err != nil) { return false, false, [10]byte{}, [10]byte{}, err } + if (settingExists == false || hostModeOnOffStatus != "On"){ + return false, false, [10]byte{}, [10]byte{}, nil + } + settingExists, hostMessagesOnOffStatus, err := mySettings.GetSetting("HostMessagesOnOffStatus") + if (err != nil) { return false, false, [10]byte{}, [10]byte{}, err } + if (settingExists == false || hostMessagesOnOffStatus != "On"){ + // The default is off + return true, false, [10]byte{}, [10]byte{}, nil + } + + settingExists, hostedInboxRangeStartString, err := mySettings.GetSetting("HostedInboxRangeStart") + if (err != nil) { return false, false, [10]byte{}, [10]byte{}, err } + if (settingExists == false){ + + // The default range is all inboxes + + minimumInboxBound, maximumInboxBound := byteRange.GetMinimumMaximumInboxBounds() + + return true, true, minimumInboxBound, maximumInboxBound, nil + } + + settingExists, hostedInboxRangeEndString, err := mySettings.GetSetting("HostedInboxRangeEnd") + if (err != nil) { return false, false, [10]byte{}, [10]byte{}, err } + if (settingExists == false){ + // This should not happen. + return false, false, [10]byte{}, [10]byte{}, errors.New("HostedInboxRangeStart exists but no end range found.") + } + + hostedInboxRangeStart, err := inbox.ReadInboxString(hostedInboxRangeStartString) + if (err != nil) { + return false, false, [10]byte{}, [10]byte{}, errors.New("HostedInboxRangeStart is invalid: " + hostedInboxRangeStartString) + } + + hostedInboxRangeEnd, err := inbox.ReadInboxString(hostedInboxRangeEndString) + if (err != nil) { + return false, false, [10]byte{}, [10]byte{}, errors.New("HostedInboxRangeEnd is invalid: " + hostedInboxRangeEndString) + } + + return true, true, hostedInboxRangeStart, hostedInboxRangeEnd, nil +} + + +//Outputs: +// -bool: Moderator mode enabled +// -bool: Identity is within my moderation range +// -error +func CheckIfIdentityHashIsWithinMyModerationRange(identityHash [16]byte)(bool, bool, error){ + + identityType, err := identity.GetIdentityTypeFromIdentityHash(identityHash) + if (err != nil) { + identityHashHex := encoding.EncodeBytesToHexString(identityHash[:]) + return false, false, errors.New("CheckIfIdentityHashIsWithinMyModerationRange called with invalid identityHash: " + identityHashHex) + } + + moderatorModeEnabled, moderatingAny, myIdentityRangeStart, myIdentityRangeEnd, err := GetMyIdentitiesToModerateRange(identityType) + if (err != nil) { return false, false, err } + if (moderatorModeEnabled == false){ + return false, false, nil + } + if (moderatingAny == false){ + return true, false, nil + } + + isWithinRange, err := byteRange.CheckIfIdentityHashIsWithinRange(myIdentityRangeStart, myIdentityRangeEnd, identityHash) + if (err != nil) { return false, false, err } + + return true, isWithinRange, nil +} + +//Outputs: +// -bool: Moderator mode enabled +// -bool: Moderating any identities of requested identityType +// -[16]byte: Identity hash Range start +// -[16]byte: Identity hash Range end +// -error +func GetMyIdentitiesToModerateRange(identityType string)(bool, bool, [16]byte, [16]byte, error){ + + if (identityType != "Mate" && identityType != "Host" && identityType != "Moderator"){ + return false, false, [16]byte{}, [16]byte{}, errors.New("GetMyIdentitiesToModerateRange called with invalid identityType: " + identityType) + } + + settingExists, moderatorModeOnOffStatus, err := mySettings.GetSetting("ModeratorModeOnOffStatus") + if (err != nil) { return false, false, [16]byte{}, [16]byte{}, err } + if (settingExists == false || moderatorModeOnOffStatus != "On"){ + return false, false, [16]byte{}, [16]byte{}, nil + } + settingExists, moderatingContentStatus, err := mySettings.GetSetting("Moderate" + identityType + "ContentOnOffStatus") + if (err != nil) { return false, false, [16]byte{}, [16]byte{}, err } + if (settingExists == true && moderatingContentStatus == "Off"){ + // This setting is on by default + return true, false, [16]byte{}, [16]byte{}, nil + } + + if (identityType == "Moderator"){ + // The default is all identities + minimumIdentityBound, maximumIdentityBound := byteRange.GetMinimumMaximumIdentityHashBounds() + + return true, true, minimumIdentityBound, maximumIdentityBound, nil + } + + settingExists, moderatedContentRangeStartString, err := mySettings.GetSetting("Moderated" + identityType + "ContentRangeStart") + if (err != nil) { return false, false, [16]byte{}, [16]byte{}, err } + if (settingExists == false){ + // The default is all identities + minimumIdentityBound, maximumIdentityBound := byteRange.GetMinimumMaximumIdentityHashBounds() + + return true, true, minimumIdentityBound, maximumIdentityBound, nil + } + + settingExists, moderatedContentRangeEndString, err := mySettings.GetSetting("Moderated" + identityType + "ContentRangeEnd") + if (err != nil) { return false, false, [16]byte{}, [16]byte{}, err } + if (settingExists == false){ + // This should not happen. + return false, false, [16]byte{}, [16]byte{}, errors.New("Moderated" + identityType + "ContentRangeStart exists, but no end range found.") + } + + moderatedContentRangeStart, _, err := identity.ReadIdentityHashString(moderatedContentRangeStartString) + if (err != nil){ + return false, false, [16]byte{}, [16]byte{}, errors.New("Moderated" + identityType + "ContentRangeStart is invalid: " + moderatedContentRangeStartString) + } + + moderatedContentRangeEnd, _, err := identity.ReadIdentityHashString(moderatedContentRangeEndString) + if (err != nil){ + return false, false, [16]byte{}, [16]byte{}, errors.New("Moderated" + identityType + "ContentRangeEnd is invalid: " + moderatedContentRangeEndString) + } + + return true, true, moderatedContentRangeStart, moderatedContentRangeEnd, nil +} + + +//Outputs: +// -bool: Moderator mode enabled +// -bool: Inbox is within my moderation range +// -error +func CheckIfMessageInboxIsWithinMyModerationRange(messageInbox [10]byte)(bool, bool, error){ + + moderatorModeEnabled, moderatingAny, myInboxRangeStart, myInboxRangeEnd, err := GetMyInboxesToModerateRange() + if (err != nil) { return false, false, err } + if (moderatorModeEnabled == false){ + return false, false, nil + } + if (moderatingAny == false){ + return true, false, nil + } + + isWithinRange, err := byteRange.CheckIfInboxIsWithinRange(myInboxRangeStart, myInboxRangeEnd, messageInbox) + if (err != nil) { return false, false, err } + + return true, isWithinRange, nil +} + + +//Outputs: +// -bool: Moderator mode enabled +// -bool: Moderating any messages +// -[10]byte: Inbox Range start +// -[10]byte: Inbox Range end +// -error +func GetMyInboxesToModerateRange()(bool, bool, [10]byte, [10]byte, error){ + + settingExists, moderatorModeOnOffStatus, err := mySettings.GetSetting("ModeratorModeOnOffStatus") + if (err != nil) { return false, false, [10]byte{}, [10]byte{}, err } + if (settingExists == false || moderatorModeOnOffStatus != "On"){ + return false, false, [10]byte{}, [10]byte{}, nil + } + exists, moderatingMessages, err := mySettings.GetSetting("ModerateMessagesOnOffStatus") + if (err != nil) { return false, false, [10]byte{}, [10]byte{}, err } + if (exists == false || moderatingMessages != "On"){ + // The default is off + return true, false, [10]byte{}, [10]byte{}, nil + } + + exists, moderatedInboxRangeStartString, err := mySettings.GetSetting("ModeratedInboxRangeStart") + if (err != nil) { return false, false, [10]byte{}, [10]byte{}, err } + if (exists == false){ + + // The default is to moderate all messages + // This will be automatically reduced if size exceeds available disk space + + minimumInboxBound, maximumInboxBound := byteRange.GetMinimumMaximumInboxBounds() + + return true, true, minimumInboxBound, maximumInboxBound, nil + } + + exists, moderatedInboxRangeEndString, err := mySettings.GetSetting("ModeratedInboxRangeEnd") + if (err != nil) { return false, false, [10]byte{}, [10]byte{}, err } + if (exists == false){ + return false, false, [10]byte{}, [10]byte{}, errors.New("ModeratedInboxRangeStart exists but no end range found.") + } + + moderatedInboxRangeStart, err := inbox.ReadInboxString(moderatedInboxRangeStartString) + if (err != nil) { + return false, false, [10]byte{}, [10]byte{}, errors.New("ModeratedInboxRangeStart is invalid: " + moderatedInboxRangeStartString) + } + + moderatedInboxRangeEnd, err := inbox.ReadInboxString(moderatedInboxRangeEndString) + if (err != nil) { + return false, false, [10]byte{}, [10]byte{}, errors.New("ModeratedInboxRangeEnd is invalid: " + moderatedInboxRangeEndString) + } + + return true, true, moderatedInboxRangeStart, moderatedInboxRangeEnd, nil +} + + + + diff --git a/internal/mySeedPhrases/mySeedPhrases.go b/internal/mySeedPhrases/mySeedPhrases.go new file mode 100644 index 0000000..813f0ff --- /dev/null +++ b/internal/mySeedPhrases/mySeedPhrases.go @@ -0,0 +1,134 @@ + +// mySeedPhrases provides functions to manage a user's seed phrases + +package mySeedPhrases + +import "seekia/internal/encoding" +import "seekia/internal/identity" +import "seekia/internal/myDatastores/myMap" +import "seekia/internal/seedPhrase" + +import "errors" + +// Map Structure: Identity Type -> My seed phrase +var mySeedPhrasesMapDatastore *myMap.MyMap + +// This function must be called whenever an app user signs in +func InitializeMySeedPhrasesDatastore()error{ + + newMySeedPhrasesMapDatastore, err := myMap.CreateNewMap("MySeedPhrases") + if (err != nil) { return err } + + mySeedPhrasesMapDatastore = newMySeedPhrasesMapDatastore + + return nil +} + +//Outputs: +// -bool: My seed phrase exists +// -[32]byte: My seed phrase hash +// -error +func GetMySeedPhraseHash(myIdentityType string)(bool, [32]byte, error){ + + exists, mySeedPhrase, err := GetMySeedPhrase(myIdentityType) + if (err != nil) { return false, [32]byte{}, err } + if (exists == false) { + return false, [32]byte{}, nil + } + + mySeedPhraseHash, err := seedPhrase.ConvertSeedPhraseToSeedPhraseHash(mySeedPhrase) + if (err != nil) { return false, [32]byte{}, err } + + return true, mySeedPhraseHash, nil +} + + + +// This function will verify that the seed phrase hash belongs to the identity hash +// This is necessary because the seed phrase can be changed by the user +// Use this function when you need to guarantee that the seed phrase belongs to the requested identityHash +//Outputs: +// -bool: Found identity hash seed phrase hash +// -[32]byte: Seed phrase hash +// -error +func GetMySeedPhraseHashFromIdentityHash(inputIdentityHash [16]byte)(bool, [32]byte, error){ + + identityType, err := identity.GetIdentityTypeFromIdentityHash(inputIdentityHash) + if (err != nil) { + inputIdentityHashHex := encoding.EncodeBytesToHexString(inputIdentityHash[:]) + return false, [32]byte{}, errors.New("GetMySeedPhraseHashFromIdentityHash called with invalid identityHash: " + inputIdentityHashHex) + } + + exists, mySeedPhraseHash, err := GetMySeedPhraseHash(identityType) + if (err != nil) { return false, [32]byte{}, err } + if (exists == false) { + return false, [32]byte{}, nil + } + + myIdentityHash, err := identity.GetIdentityHashFromSeedPhraseHash(mySeedPhraseHash, identityType) + if (err != nil) { return false, [32]byte{}, err } + if (myIdentityHash != inputIdentityHash) { + // Identity must have been replaced after identity hash was initially retrieved + return false, [32]byte{}, nil + } + + return true, mySeedPhraseHash, nil +} + + + +//Outputs: +// -bool: My seed phrase exists +// -string: My seed phrase +// -error +func GetMySeedPhrase(identityType string)(bool, string, error){ + + if (identityType != "Mate" && identityType != "Host" && identityType != "Moderator"){ + return false, "", errors.New("GetMySeedPhrase called with invalid identity type: " + identityType) + } + + exists, mySeedPhrase, err := mySeedPhrasesMapDatastore.GetMapEntry(identityType) + if (err != nil) { return false, "", err } + if (exists == false) { + return false, "", nil + } + + isValid := seedPhrase.VerifySeedPhrase(mySeedPhrase) + if (isValid == false) { + return false, "", errors.New("My existing seed phrase is invalid: " + mySeedPhrase) + } + + return true, mySeedPhrase, nil +} + +func SetMySeedPhrase(identityType string, newSeedPhrase string)error{ + + if (identityType != "Mate" && identityType != "Host" && identityType != "Moderator"){ + return errors.New("SetMySeedPhrase called with invalid identity type: " + identityType) + } + + isValid := seedPhrase.VerifySeedPhrase(newSeedPhrase) + if (isValid == false) { + return errors.New("SetMySeedPhrase called with invalid seed phrase: " + newSeedPhrase) + } + + err := mySeedPhrasesMapDatastore.SetMapEntry(identityType, newSeedPhrase) + if (err != nil) { return err } + + return nil +} + + +func DeleteMySeedPhrase(identityType string)error{ + + if (identityType != "Mate" && identityType != "Host" && identityType != "Moderator"){ + return errors.New("DeleteMySeedPhrase called with invalid identity type: " + identityType) + } + + err := mySeedPhrasesMapDatastore.DeleteMapEntry(identityType) + if (err != nil){ return err } + + return nil +} + + diff --git a/internal/mySettings/mySettings.go b/internal/mySettings/mySettings.go new file mode 100644 index 0000000..1c470d2 --- /dev/null +++ b/internal/mySettings/mySettings.go @@ -0,0 +1,70 @@ + +// mySettings provides functions to access a user's settings + +package mySettings + +import "seekia/internal/myDatastores/myMap" + +import "errors" + +var mySettingsMapDatastore *myMap.MyMap + +// This function must be called whenever we sign in to an app user +func InitializeMySettingsDatastore()error{ + + newMySettingsMapDatastore, err := myMap.CreateNewMap("MySettings") + if (err != nil) { return err } + + mySettingsMapDatastore = newMySettingsMapDatastore + + return nil +} + +//Outputs: +// -bool: Setting exists +// -string: Setting value +// -error +func GetSetting(settingName string) (bool, string, error){ + + if (settingName == ""){ + return false, "", errors.New("GetSetting called with empty settingName.") + } + + exists, settingString, err := mySettingsMapDatastore.GetMapEntry(settingName) + if (err != nil) { return false, "", err } + if (exists == false){ + return false, "", nil + } + + return true, settingString, nil +} + +func SetSetting(settingName string, content string)error{ + + if (settingName == ""){ + return errors.New("SetSetting called with empty SettingName.") + } + if (content == ""){ + return errors.New("SetSetting called with empty content.") + } + + err := mySettingsMapDatastore.SetMapEntry(settingName, content) + if (err != nil){ return err } + + return nil +} + +func DeleteSetting(settingName string)error{ + + if (settingName == ""){ + return errors.New("DeleteSetting called with empty SettingName.") + } + + err := mySettingsMapDatastore.DeleteMapEntry(settingName) + if (err != nil) { return err } + + return nil +} + + + diff --git a/internal/network/accountKeys/accountKeys.go b/internal/network/accountKeys/accountKeys.go new file mode 100644 index 0000000..1b6d537 --- /dev/null +++ b/internal/network/accountKeys/accountKeys.go @@ -0,0 +1,123 @@ + +// accountKeys provides functions to derive account identifiers and cryptocurrency addresses from credit account keys. +// These keys are used to interface with the credit accounts servers to manage a user's account credits. + +package accountKeys + +// A user's account private key is required to check their account balance +// Each public key corresponds to an account identifier and crypto address(es) +// We only use each key for a single purpose. +// The current purposes are either to use its identifier or Ethereum address. +// In the future, we will add more cryptocurrencies. + +//TODO: Add checksum byte to account identifiers + +import "seekia/internal/cryptography/blake3" +import "seekia/internal/cryptocurrency/ethereumAddress" +import "seekia/internal/cryptocurrency/cardanoAddress" +import "seekia/internal/cryptography/edwardsKeys" +import "seekia/internal/helpers" + +import "slices" +import "encoding/binary" +import "errors" + + +func GetAccountIdentifierFromAccountPublicKey(accountPublicKey [32]byte)(string, error){ + + // This sequence of bytes is the string "accountidentifierkeyhash" decoded from base32 to bytes + hashInputSuffix := []byte{0, 132, 234, 54, 104, 25, 27, 52, 21, 4, 138, 137, 131, 130, 71} + + hashInput := slices.Concat(accountPublicKey[:], hashInputSuffix) + + accountIdentifier, err := blake3.GetBlake3HashAsHexString(14, hashInput) + if (err != nil) { return "", err } + + return accountIdentifier, nil +} + +func GetCryptocurrencyAddressFromAccountPublicKey(cryptocurrency string, accountPublicKey [32]byte)(string, error){ + + if (cryptocurrency != "Ethereum" && cryptocurrency != "Cardano"){ + return "", errors.New("GetCryptocurrencyAddressFromAccountPublicKey called with invalid cryptocurrency: " + cryptocurrency) + } + if (cryptocurrency == "Ethereum"){ + + accountEthereumAddress, err := ethereumAddress.GetCreditAccountEthereumAddressFromAccountPublicKey(accountPublicKey) + if (err != nil) { return "", err } + + return accountEthereumAddress, nil + } + + accountCardanoAddress, err := cardanoAddress.GetCreditAccountCardanoAddressFromAccountPublicKey(accountPublicKey) + if (err != nil) { return "", err } + + return accountCardanoAddress, nil +} + + +// KeysType refers to the type of key +// Different KeyTypes need to be derived, because each key should only be used for its intended keysType purpose. +// "Identifier" keys are used for receiving to an account via an account identifier +// "Ethereum" keys are used for receiving to an account via an Ethereum address +// "Cardano" keys are used for receiving to an account via an Cardano address +// Each cryptocurrency which we add will have its own keysType + +// We must be able to generate many different keys for each purpose, which we accomplish with the keysIndex parameter +// This allows us to create as many addresses as we need, which we do because we need to be able to avoid address/identifier reuse. + +// Each networkType has its own keys, because each network type has its own account credit database and servers. + +//Outputs: +// -[32]byte: Public key +// -[64]byte: Private key +// -error +func GetCreditAccountPublicPrivateKeys(seedPhraseHash [32]byte, networkType byte, keysType string, keysIndex int)([32]byte, [64]byte, error){ + + isValid := helpers.VerifyNetworkType(networkType) + if (isValid == false){ + networkTypeString := helpers.ConvertByteToString(networkType) + return [32]byte{}, [64]byte{}, errors.New("GetCreditAccountPublicPrivateKeys called with invalid networkType: " + networkTypeString) + } + + if (keysType != "Identifier" && keysType != "Ethereum" && keysType != "Cardano"){ + return [32]byte{}, [64]byte{}, errors.New("GetCreditAccountPublicPrivateKeys called with invalid keysType: " + keysType) + } + + if (keysIndex < 1 || keysIndex > 4294967295){ + return [32]byte{}, [64]byte{}, errors.New("GetCreditAccountPublicPrivateKeys called with invalid keysIndex.") + } + + // This sequence of bytes is the string "creditaccountkeyhashsalt" decoded from base32 to bytes + saltBytes := []byte{20, 72, 52, 76, 2, 19, 168, 217, 168, 152, 56, 36, 121, 1, 115} + + getKeysTypeByte := func()byte{ + if (keysType == "Identifier"){ + return 1 + } + if (keysType == "Ethereum"){ + return 2 + } + // keysType == "Cardano" + return 3 + } + + keysTypeByte := getKeysTypeByte() + + keysIndexUint32 := uint32(keysIndex) + + hashInput := slices.Concat(seedPhraseHash[:], saltBytes) + + hashInput = append(hashInput, networkType, keysTypeByte) + + hashInput = binary.LittleEndian.AppendUint32(hashInput, keysIndexUint32) + + seedBytes, err := blake3.Get32ByteBlake3Hash(hashInput) + if (err != nil) { return [32]byte{}, [64]byte{}, err } + + publicKeyArray, privateKeyArray := edwardsKeys.GetSeededEdwardsPublicAndPrivateKeys(seedBytes) + + return publicKeyArray, privateKeyArray, nil +} + + diff --git a/internal/network/accountKeys/accountKeys_test.go b/internal/network/accountKeys/accountKeys_test.go new file mode 100644 index 0000000..5ae71a1 --- /dev/null +++ b/internal/network/accountKeys/accountKeys_test.go @@ -0,0 +1,153 @@ +package accountKeys_test + +import "seekia/internal/network/accountKeys" + +import "seekia/internal/encoding" + +import "testing" +import "bytes" + +func TestAccountKeys(t *testing.T){ + + testSeedPhraseHashHex := "2a66776f76662444f2cadbdb408485db11e58ef1f262c9afcc11ab8d07c0e762" + + testSeedPhraseHashBytes, err := encoding.DecodeHexStringToBytes(testSeedPhraseHashHex) + if (err != nil){ + t.Fatalf("Invalid testSeedPhraseHash: Not Hex: " + err.Error()) + } + + if (len(testSeedPhraseHashBytes) != 32){ + t.Fatalf("Invalid testSeedPhraseHash: Invalid length.") + } + + testSeedPhraseHashArray := [32]byte(testSeedPhraseHashBytes) + + expectedPublicKeyHex := "4819fbbac7d06bc21e3e55e7f0286465b72b5468814bf77b9feb38088de21f7c" + + expectedPublicKeyBytes, err := encoding.DecodeHexStringToBytes(expectedPublicKeyHex) + if (err != nil){ + t.Fatalf("expectedPublicKeyHex is invalid: Not Hex: " + err.Error()) + } + + expectedPrivateKeyHex := "30832da4d36f9ec2fd1ce227cad6b3089683fb761fde7242b137cb5d659a48f74819fbbac7d06bc21e3e55e7f0286465b72b5468814bf77b9feb38088de21f7c" + + expectedPrivateKeyBytes, err := encoding.DecodeHexStringToBytes(expectedPrivateKeyHex) + if (err != nil){ + t.Fatalf("expectedPrivateKeyBytes is invalid: Not Hex: " + err.Error()) + } + + publicKey, privateKey, err := accountKeys.GetCreditAccountPublicPrivateKeys(testSeedPhraseHashArray, 1, "Identifier", 1) + if (err != nil){ + t.Fatalf("Failed to derive credit account public/private keys: " + err.Error()) + } + + areEqual := bytes.Equal(publicKey[:], expectedPublicKeyBytes) + if (areEqual == false){ + publicKeyHex := encoding.EncodeBytesToHexString(publicKey[:]) + t.Fatalf("Account Identifier public key is not expected: " + publicKeyHex) + } + + areEqual = bytes.Equal(privateKey[:], expectedPrivateKeyBytes) + if (areEqual == false){ + privateKeyHex := encoding.EncodeBytesToHexString(privateKey[:]) + t.Fatalf("Account Identifier private key is not expected: " + privateKeyHex) + } + + newIdentifier, err := accountKeys.GetAccountIdentifierFromAccountPublicKey(publicKey) + if (err != nil){ + t.Fatalf("Failed to derive Identifier from account public key: " + err.Error()) + } + + if (newIdentifier != "d7d1458ee1ae30f530b6276b3e71"){ + t.Fatalf("Account Identifier is not expected: " + newIdentifier) + } + + { + expectedAccountPublicKeyHex := "4388ea42cce699c93f2d0f5a44846363620b933f2ccf4038e991aedf96b8ebba" + + expectedAccountPublicKeyBytes, err := encoding.DecodeHexStringToBytes(expectedAccountPublicKeyHex) + if (err != nil){ + t.Fatalf("expectedAccountPublicKeyHex is invalid: Not Hex: " + err.Error()) + } + + expectedAccountPrivateKeyHex := "ecbc905488d03f2dc0ec5cd8b5cd9805ae7bd599cacd33d65de1558c4f8ac3be4388ea42cce699c93f2d0f5a44846363620b933f2ccf4038e991aedf96b8ebba" + + expectedAccountPrivateKeyBytes, err := encoding.DecodeHexStringToBytes(expectedAccountPrivateKeyHex) + if (err != nil){ + t.Fatalf("expectedAccountPrivateKeyHex is invalid: Not Hex: " + err.Error()) + } + + accountPublicKey, accountPrivateKey, err := accountKeys.GetCreditAccountPublicPrivateKeys(testSeedPhraseHashArray, 2, "Ethereum", 1) + if (err != nil){ + t.Fatalf("Failed to derive credit accout public/private keys: " + err.Error()) + } + + areEqual = bytes.Equal(accountPublicKey[:], expectedAccountPublicKeyBytes) + if (areEqual == false){ + accountPublicKeyHex := encoding.EncodeBytesToHexString(accountPublicKey[:]) + t.Fatalf("Account Ethereum public key is not expected: " + accountPublicKeyHex) + } + + areEqual = bytes.Equal(accountPrivateKey[:], expectedAccountPrivateKeyBytes) + if (areEqual == false){ + accountPrivateKeyHex := encoding.EncodeBytesToHexString(accountPrivateKey[:]) + t.Fatalf("Account Ethereum private key is not expected: " + accountPrivateKeyHex) + } + + newAddress, err := accountKeys.GetCryptocurrencyAddressFromAccountPublicKey("Ethereum", accountPublicKey) + if (err != nil){ + t.Fatalf("Failed to derive Ethereum address from account public key: " + err.Error()) + } + if (newAddress != "0x9470096c6a003F215D5F5497Ab14B9138D36eBAB"){ + t.Fatalf("Account Ethereum address is not expected: " + newAddress) + } + } + + { + + expectedAccountPublicKeyHex := "58af93381ab57eab44d087213737197b99706354f76c717cf10713175f759eff" + + expectedAccountPublicKeyBytes, err := encoding.DecodeHexStringToBytes(expectedAccountPublicKeyHex) + if (err != nil){ + t.Fatalf("expectedAccountPublicKeyHex is invalid: Not Hex: " + err.Error()) + } + + expectedAccountPrivateKeyHex := "776583a8077c411b86ed7ebb431b1e34885020e701ee15d7e80b5919517b24ad58af93381ab57eab44d087213737197b99706354f76c717cf10713175f759eff" + + expectedAccountPrivateKeyBytes, err := encoding.DecodeHexStringToBytes(expectedAccountPrivateKeyHex) + if (err != nil){ + t.Fatalf("expectedAccountPrivateKeyHex is invalid: Not Hex: " + err.Error()) + } + + accountPublicKey, accountPrivateKey, err := accountKeys.GetCreditAccountPublicPrivateKeys(testSeedPhraseHashArray, 2, "Cardano", 1) + if (err != nil){ + t.Fatalf("Failed to derive credit accout public/private keys: " + err.Error()) + } + + areEqual = bytes.Equal(accountPublicKey[:], expectedAccountPublicKeyBytes) + if (areEqual == false){ + accountPublicKeyHex := encoding.EncodeBytesToHexString(accountPublicKey[:]) + t.Fatalf("Account Cardano public key is not expected: " + accountPublicKeyHex) + } + + areEqual = bytes.Equal(accountPrivateKey[:], expectedAccountPrivateKeyBytes) + if (areEqual == false){ + accountPrivateKeyHex := encoding.EncodeBytesToHexString(accountPrivateKey[:]) + t.Fatalf("Account Cardano private key is not expected: " + accountPrivateKeyHex) + } + + newAddress, err := accountKeys.GetCryptocurrencyAddressFromAccountPublicKey("Cardano", accountPublicKey) + if (err != nil){ + t.Fatalf("Failed to derive Cardano address from account public key: " + err.Error()) + } + if (newAddress != "addr1v9j3g9hlca5x9akv2vjm9sp33uu646narslp8pjnr5venuq5c705l"){ + t.Fatalf("Account Cardano address is not expected: " + newAddress) + } + } + + +} + + + + diff --git a/internal/network/activeConnections/activeConnections.go b/internal/network/activeConnections/activeConnections.go new file mode 100644 index 0000000..c0ccb1c --- /dev/null +++ b/internal/network/activeConnections/activeConnections.go @@ -0,0 +1,12 @@ + +// activeConnections provides functions to monitor the active connections the client is making +// Active connections can be viewed through the GUI. + +package activeConnections + +//TODO: Build this package +// We should store connection info in memory which will be deleted upon application closure. +// We want the user to be able to view active connections in the GUI. +// Each connection will show the IP address of the peer and how much data is being transferred in each direction. +// We should take inspiration from torrent clients that show the details of connected peers. +// We should include IP location info within Seekia so we can show the estimated peer locations. diff --git a/internal/network/appNetworkType/getAppNetworkType/getAppNetworkType.go b/internal/network/appNetworkType/getAppNetworkType/getAppNetworkType.go new file mode 100644 index 0000000..d82f677 --- /dev/null +++ b/internal/network/appNetworkType/getAppNetworkType/getAppNetworkType.go @@ -0,0 +1,33 @@ + +// getAppNetworkType provides functions to get the Seekia application network type. +// Examples of network types: "Mainnet" and "Testnet1". +// The Seekia client will only interface with and download content for the current app network type. + +package getAppNetworkType + +// Each network type is represented by a single byte +// 1 = Mainnet +// 2 = Testnet1 + +import "seekia/internal/globalSettings" +import "seekia/internal/helpers" + +import "errors" + + +func GetAppNetworkType()(byte, error){ + + exists, currentAppNetworkTypeString, err := globalSettings.GetSetting("AppNetworkType") + if (err != nil) { return 0, err } + if (exists == false){ + // We default to Mainnet + return 1, nil + } + + currentAppNetworkType, err := helpers.ConvertNetworkTypeStringToByte(currentAppNetworkTypeString) + if (err != nil) { + return 0, errors.New("GlobalSettings malformed: Contains invalid AppNetworkType: " + currentAppNetworkTypeString) + } + + return currentAppNetworkType, nil +} diff --git a/internal/network/appNetworkType/setAppNetworkType/setAppNetworkType.go b/internal/network/appNetworkType/setAppNetworkType/setAppNetworkType.go new file mode 100644 index 0000000..fc0c346 --- /dev/null +++ b/internal/network/appNetworkType/setAppNetworkType/setAppNetworkType.go @@ -0,0 +1,94 @@ + +// setAppNetworkType provides functions to change the Seekia application network type. +// Examples of network types: "Mainnet" and "Testnet1". +// The Seekia client will only interface with and download content for the current app network type. + +package setAppNetworkType + +// Each network type is represented by a single byte +// 1 = Mainnet +// 2 = Testnet1 + +import "seekia/internal/backgroundJobs" +import "seekia/internal/globalSettings" +import "seekia/internal/helpers" +import "seekia/internal/moderation/bannedModeratorConsensus" +import "seekia/internal/moderation/enabledModerators" +import "seekia/internal/moderation/verdictHistory" +import "seekia/internal/moderation/verifiedStickyStatus" +import "seekia/internal/mySettings" +import "seekia/internal/network/enabledHosts" + +import "time" +import "errors" + +func SetAppNetworkType(newNetworkType byte)error{ + + isValid := helpers.VerifyNetworkType(newNetworkType) + if (isValid == false){ + newNetworkTypeString := helpers.ConvertByteToString(newNetworkType) + return errors.New("SetAppNetworkType called with unknown network type: " + newNetworkTypeString) + } + + err := backgroundJobs.StopBackgroundJobs() + if (err != nil) { return err } + + newNetworkTypeString := helpers.ConvertByteToString(newNetworkType) + + err = globalSettings.SetSetting("AppNetworkType", newNetworkTypeString) + if (err != nil) { return err } + + // Now we update cache variables which are different for each network type + // TODO: Add more + + err = bannedModeratorConsensus.UpdateBannedModeratorsList(newNetworkType) + if (err != nil) { return err } + + err = enabledModerators.UpdateEnabledModeratorsList(newNetworkType) + if (err != nil) { return err } + + err = enabledHosts.UpdateEnabledHostsList(newNetworkType) + if (err != nil) { return err } + + //TODO: Clear temporary downloads for profiles/messages/attributes + + verdictHistory.DeleteVerdictHistory() + + verifiedStickyStatus.DeleteVerifiedStickyStatuses() + + // We must check for new chat messages + // This is because when we generate chat messages, we only import messages for the current app network type + err = mySettings.SetSetting("MateChatMessagesReadyStatus", "No") + if (err != nil) { return err } + + err = mySettings.SetSetting("ModeratorChatMessagesReadyStatus", "No") + if (err != nil) { return err } + + err = mySettings.SetSetting("MateChatConversationsGeneratedStatus", "No") + if (err != nil) { return err } + + err = mySettings.SetSetting("ModeratorChatConversationsGeneratedStatus", "No") + if (err != nil) { return err } + + + err = mySettings.SetSetting("MatchesGeneratedStatus", "No") + if (err != nil) { return err } + + err = mySettings.SetSetting("ViewedContentGeneratedStatus", "No") + if (err != nil) { return err } + + err = mySettings.SetSetting("ViewedModeratorsGeneratedStatus", "No") + if (err != nil) { return err } + + err = mySettings.SetSetting("ViewedHostsGeneratedStatus", "No") + if (err != nil) { return err } + + err = backgroundJobs.StartBackgroundJobs() + if (err != nil) { return err } + + // We simulate time, which we will remove eventually + time.Sleep(time.Second) + + return nil +} + diff --git a/internal/network/backgroundDownloads/backgroundDownloads.go b/internal/network/backgroundDownloads/backgroundDownloads.go new file mode 100644 index 0000000..1e8f42a --- /dev/null +++ b/internal/network/backgroundDownloads/backgroundDownloads.go @@ -0,0 +1,117 @@ + +// backgroundDownloads provides functions to check what content the app is downloading in the background +// One example is checking if the app can determine a message/identity's verdict +// To determine an identity's verdict, the app must be downloading all moderator profiles, all moderator ban reviews, and all reviews for the identity + +package backgroundDownloads + +import "seekia/internal/helpers" +import "seekia/internal/myRanges" +import "seekia/internal/network/appNetworkType/getAppNetworkType" +import "seekia/internal/network/temporaryDownloads" + +import "errors" + +// This function will tell us if we are downloading the required reviews to determine this identity's moderation verdict +// This includes the verdict for all profiles created by this identity +// To do this, we must download all moderator profiles, all moderator-banning-moderator reviews, and all reviews of the identity +func CheckIfAppCanDetermineIdentityVerdicts(identityHash [16]byte)(bool, error){ + + // We first check if the provided identity hash is in our host/moderator range + // If it is, we will download all reviews/reports for the identity and its profiles + // In host/moderator mode, we download all moderator profiles and and all moderator-banning-moderator reviews + // In both modes, if the identity is within our range, we will download all of this identity's profiles + // If we are in moderator mode, we will delete their profiles after we have reviewed them, and retain each profile's metadata + + hostModeEnabled, isWithinRange, err := myRanges.CheckIfIdentityHashIsWithinMyHostedRange(identityHash) + if (err != nil) { return false, err } + if (hostModeEnabled == true && isWithinRange == true){ + return true, nil + } + + moderatorModeEnabled, isWithinRange, err := myRanges.CheckIfIdentityHashIsWithinMyModerationRange(identityHash) + if (err != nil) { return false, err } + if (moderatorModeEnabled == true && isWithinRange == true){ + return true, nil + } + + if (hostModeEnabled == false && moderatorModeEnabled == false){ + // We cannot determine the identity's verdict + // This is because we are not downloading all moderator profiles and all moderator-banning-moderator reviews + return false, nil + } + + // Now we check our temporary downloads + + isWithinTemporaryDownloads, err := temporaryDownloads.CheckIfIdentityReviewsAreWithinMyTemporaryDownloads(identityHash) + if (err != nil) { return false, err } + if (isWithinTemporaryDownloads == true){ + return true, nil + } + + return false, nil +} + +// This function will tell us if we are downloading the required reviews to determine this message's moderation verdict +// To do this, we must download all moderator profiles, all moderator-banning-moderator reviews, and all reviews of the message +func CheckIfAppCanDetermineMessageVerdict(messageNetworkType byte, messageInbox [10]byte, messageHashProvided bool, messageHash [26]byte)(bool, error){ + + isValid := helpers.VerifyNetworkType(messageNetworkType) + if (isValid == false){ + messageNetworkTypeString := helpers.ConvertByteToString(messageNetworkType) + return false, errors.New("CheckIfAppCanDetermineMessageVerdict called with invalid messageNetworkType: " + messageNetworkTypeString) + } + + // We first check to see if our client is connected to the same network type the message was broadcasted on + + appNetworkType, err := getAppNetworkType.GetAppNetworkType() + if (err != nil) { return false, err } + if (appNetworkType != messageNetworkType){ + return false, nil + } + + // We then check if the provided inbox is in our host/moderator range + // If it is, we will download all reviews/reports for any messages sent to the inbox + // In host/moderator mode, we download all moderator profiles and and all moderator-banning-moderator reviews + // If we are in Host mode, we will also download all messages sent to the inbox, regardless of if they are reviewed/reported. + // If we are in moderator mode, we will download only reviewed/reported messages, + // delete the messages after we have reviewed them, and retain each message's metadata + + hostModeEnabled, isWithinRange, err := myRanges.CheckIfMessageInboxIsWithinMyHostedRange(messageInbox) + if (err != nil) { return false, err } + if (hostModeEnabled == true && isWithinRange == true){ + return true, nil + } + + moderatorModeEnabled, isWithinRange, err := myRanges.CheckIfMessageInboxIsWithinMyModerationRange(messageInbox) + if (err != nil) { return false, err } + if (moderatorModeEnabled == true && isWithinRange == true){ + return true, nil + } + + if (hostModeEnabled == false && moderatorModeEnabled == false){ + // We cannot determine the message/inbox verdict + // This is because we are not downloading all moderator profiles and all moderator-banning-moderator reviews + return false, nil + } + + // Now we check our temporary downloads + + if (messageHashProvided == false){ + // Temporary downloads do not exist for message inboxes + // Thus, we are not download the required reviews for this inbox + return false, nil + } + + isWithinMyTemporaryDownloads, err := temporaryDownloads.CheckIfMessageReviewsAreWithinMyTemporaryDownloads(messageHash) + if (err != nil) { return false, err } + if (isWithinMyTemporaryDownloads == true){ + return true, nil + } + + return false, nil +} + + + + diff --git a/internal/network/connectionPool/connectionPool.go b/internal/network/connectionPool/connectionPool.go new file mode 100644 index 0000000..5c21e69 --- /dev/null +++ b/internal/network/connectionPool/connectionPool.go @@ -0,0 +1,14 @@ + +// connectionPool provides functions to create and manage long-term connections to hosts + +package connectionPool + +//TODO: Build package +// Currently, each connection made to hosts requires establishing a new connection for each requestType. +// This is good for privacy for things like message downloads, where each connection +// creates a fresh requestor identity established over a different tor circuit. +// For hosts, we need a way to establish long term connections which can be reused for different request types +// This package should interface with peerClient in a way that allows us to manage these connections +// We must make sure we only use each long-term connection for its intended Mate/Host/Moderator identity +// For example, if we have a long term connection that we use to download content for hosting, we should +// not use that same connection to download our mate messages diff --git a/internal/network/craftResponses/craftResponses.go b/internal/network/craftResponses/craftResponses.go new file mode 100644 index 0000000..5b1cfc7 --- /dev/null +++ b/internal/network/craftResponses/craftResponses.go @@ -0,0 +1,2934 @@ + +// craftResponses provides a function to craft server responses to peer requests + +package craftResponses + +//TODO: Restrict responses based on if host is in HostViewable..Only mode +//TODO: Add a response called Response Too Large, which means that the requestor has to +// reduce their requested range for the host's response to fit within the maximum allowed size +// We can check if a response has become too large as we craft each response + +import "seekia/internal/badgerDatabase" +import "seekia/internal/byteRange" +import "seekia/internal/contentMetadata" +import "seekia/internal/encoding" +import "seekia/internal/helpers" +import "seekia/internal/identity" +import "seekia/internal/logger" +import "seekia/internal/messaging/chatMessageStorage" +import "seekia/internal/messaging/readMessages" +import "seekia/internal/moderation/moderatorScores" +import "seekia/internal/moderation/readReports" +import "seekia/internal/moderation/readReviews" +import "seekia/internal/moderation/reportStorage" +import "seekia/internal/moderation/reviewStorage" +import "seekia/internal/moderation/verifiedStickyStatus" +import "seekia/internal/myIdentity" +import "seekia/internal/myRanges" +import "seekia/internal/network/mateCriteria" +import "seekia/internal/network/serverRequest" +import "seekia/internal/network/serverResponse" +import "seekia/internal/network/verifiedFundedStatus" +import "seekia/internal/parameters/parametersStorage" +import "seekia/internal/parameters/readParameters" +import "seekia/internal/profiles/profileStorage" +import "seekia/internal/profiles/readProfiles" +import "seekia/internal/profiles/viewableProfiles" + +import "errors" +import "slices" + +var invalidRequestResponse []byte = []byte("Invalid Request") + +//Outputs: +// -bool: Response successfully crafted (request was well formed) +// -bool: Identity hash of request matches our own +// -bool: Network type of request matches provided network type +// -[]byte: establishConnectionKey response +// -[32]byte: Connection key in establishConnectionKey response +// -error +func GetEstablishConnectionKeyResponse(requestBytes []byte, networkType byte)(bool, bool, bool, []byte, [32]byte, error){ + + isValid := helpers.VerifyNetworkType(networkType) + if (isValid == false){ + networkTypeString := helpers.ConvertByteToString(networkType) + return false, false, false, nil, [32]byte{}, errors.New("GetEstablishConnectionKeyResponse called with invalid networkType: " + networkTypeString) + } + + myIdentityExists, myHostPublicIdentityKey, myHostPrivateIdentityKey, err := myIdentity.GetMyPublicPrivateIdentityKeys("Host") + if (err != nil) { return false, false, false, nil, [32]byte{}, err } + if (myIdentityExists == false){ + return false, false, false, nil, [32]byte{}, errors.New("GetEstablishConnectionKeyResponse called when my host identity is missing.") + } + + exists, myHostIdentityHash, err := myIdentity.GetMyIdentityHash("Host") + if (err != nil) { return false, false, false, nil, [32]byte{}, err } + if (exists == false){ + return false, false, false, nil, [32]byte{}, errors.New("My identity not found after being found already.") + } + + recipientHost, requestIdentifier, requestNetworkType, requestorNaclKey, requestorKyberKey, err := serverRequest.ReadServerRequest_EstablishConnectionKey(requestBytes) + if (err != nil) { + // Requestor must be malicious + return false, false, false, nil, [32]byte{}, nil + } + + if (recipientHost != myHostIdentityHash){ + // Requestor is sending request to invalid recipient + // Requestor could be malicious or another host could be posting our clearnet/onion address as their own + // Either way, we don't continue connection + + return true, false, false, nil, [32]byte{}, nil + } + if (requestNetworkType != networkType){ + // Either requestor is malicious, or we recently changed our network type + // If we recently changed our network type, our old host profile may still exist on the old network + // This request could be coming from someone who has our old host profile downloaded + + // The Seekia app should broadcast a disabled host profile for the old network type before switching network types + // Doing this will never solve this issue fully, because the new profile will take time to propagate to users + // Users may also connect to outdated host profiles upon startup, so this problem will always exist, which is fine. + + return true, true, false, nil, [32]byte{}, nil + } + + responseBytes, newConnectionKey, err := serverResponse.CreateServerResponse_EstablishConnectionKey(myHostPublicIdentityKey, myHostPrivateIdentityKey, requestIdentifier, requestorNaclKey, requestorKyberKey) + if (err != nil) { return false, false, false, nil, [32]byte{}, err } + + return true, true, true, responseBytes, newConnectionKey, nil +} + +//Outputs: +// -[]byte: Response Bytes +// -error +func GetServerResponseForRequest(requestBytes []byte, networkType byte, connectionKey [32]byte)([]byte, error){ + + isValid := helpers.VerifyNetworkType(networkType) + if (isValid == false){ + networkTypeString := helpers.ConvertByteToString(networkType) + return nil, errors.New("GetServerResponseForRequest called with invalid networkType: " + networkTypeString) + } + + myIdentityExists, myHostPublicIdentityKey, myHostPrivateIdentityKey, err := myIdentity.GetMyPublicPrivateIdentityKeys("Host") + if (err != nil) { return nil, err } + if (myIdentityExists == false){ + return nil, errors.New("GetServerResponseForRequest called when my host identity is missing.") + } + + exists, myHostIdentityHash, err := myIdentity.GetMyIdentityHash("Host") + if (err != nil) { return nil, err } + if (exists == false){ + return nil, errors.New("My host identity not found after being found already.") + } + + // First we decrypt the request + + ableToRead, decryptedRequestBytes, err := serverRequest.ReadEncryptedRequest(requestBytes, connectionKey) + if (err != nil) { return nil, err } + if (ableToRead == false){ + // Cannot read, request is malformed + return invalidRequestResponse, nil + } + + // Now we read request to retrieve and verify attributes included in all requests + + //TODO: Verify everything + + requestRecipientHost, requestType, requestIdentifier, requestNetworkType, err := serverRequest.ReadDecryptedServerRequest_StandardData(decryptedRequestBytes) + if (err != nil){ + // decryptedRequestBytes are malformed, requestor must be malicious + return invalidRequestResponse, nil + } + if (requestRecipientHost != myHostIdentityHash){ + // Requestor must be malicious, because identity hash never changes during connection + return invalidRequestResponse, nil + } + if (requestNetworkType != networkType){ + // Requestor must be malicious, because network type never changes during connection + return invalidRequestResponse, nil + } + + // Now we get response based on the requestType + + if (requestType == "GetParametersInfo"){ + + //TODO: Check if host is hosting parameters + + allParametersTypesList := readParameters.GetAllParametersTypesList() + + parametersInfoMap := make(map[string]int64) + + for _, parametersType := range allParametersTypesList{ + + parametersFound, _, _, parametersBroadcastTime, err := parametersStorage.GetAuthorizedParameters(parametersType, networkType) + if (err != nil) { return nil, err } + if (parametersFound == false){ + continue + } + parametersInfoMap[parametersType] = parametersBroadcastTime + } + + responseBytes, err := serverResponse.CreateServerResponse_GetParametersInfo(myHostPublicIdentityKey, myHostPrivateIdentityKey, connectionKey, requestIdentifier, parametersInfoMap) + if (err != nil) { return nil, err } + + return responseBytes, nil + } + if (requestType == "GetParameters"){ + + //TODO: Verify host is hosting network parameters + + requestedParametersTypesList, err := serverRequest.ReadDecryptedServerRequest_GetParameters(decryptedRequestBytes) + if (err != nil){ + return invalidRequestResponse, nil + } + + parametersList := make([][]byte, 0) + + for _, parametersType := range requestedParametersTypesList{ + + parametersFound, parametersBytes, _, _, err := parametersStorage.GetAuthorizedParameters(parametersType, networkType) + if (err != nil) { return nil, err } + if (parametersFound == false){ + continue + } + parametersList = append(parametersList, parametersBytes) + } + + responseBytes, err := serverResponse.CreateServerResponse_GetParameters(myHostPublicIdentityKey, myHostPrivateIdentityKey, connectionKey, requestIdentifier, parametersList) + if (err != nil) { return nil, err } + + return responseBytes, nil + } + + if (requestType == "GetProfilesInfo"){ + + acceptableProfileVersionsList, profileTypeToRetrieve, requestRangeStart, requestRangeEnd, requestedIdentityHashesList, requestCriteria, getNewestProfilesOnly, getViewableProfilesOnly, err := serverRequest.ReadDecryptedServerRequest_GetProfilesInfo(decryptedRequestBytes) + if (err != nil){ + return invalidRequestResponse, nil + } + + getRelevantIdentityHashesList := func()([][16]byte, error){ + + // We will find the relevant identity hashes based on either the request range or requested identities list + + if (len(requestedIdentityHashesList) != 0){ + + return requestedIdentityHashesList, nil + } + + storedProfileIdentityHashesList, err := badgerDatabase.GetAllProfileIdentityHashes(profileTypeToRetrieve) + if (err != nil) { return nil, err } + + anyExist, identityHashesInRequestRange, err := byteRange.GetAllIdentityHashesInListWithinRange(requestRangeStart, requestRangeEnd, storedProfileIdentityHashesList) + if (err != nil) { return nil, err } + if (anyExist == false){ + + emptyList := make([][16]byte, 0) + return emptyList, nil + } + + return identityHashesInRequestRange, nil + } + + relevantIdentityHashesList, err := getRelevantIdentityHashesList() + if (err != nil) { return nil, err } + + // We limit request to profiles hosted by host + // This is to prevent the requestor from learning information about profiles the host has downloaded + // This would be a privacy leak if the host is using Seekia to find matches/moderate. + // The requestor would know which profiles outside of the hosted range the host had downloaded + // This could potentially tell them the host's location and desires + + // We reduce requested identity hashes list based on our host range + + getIdentitiesInMyHostRangeList := func()([][16]byte, error){ + + hostModeEnabled, hostingAny, myHostRangeStart, myHostRangeEnd, err := myRanges.GetMyIdentitiesToHostRange(profileTypeToRetrieve) + if (err != nil) { return nil, err } + if (hostModeEnabled == false){ + // User must have disabled host mode + emptyList := make([][16]byte, 0) + return emptyList, nil + } + if (hostingAny == false){ + // We are not hosting any of the requested ProfileType + emptyList := make([][16]byte, 0) + return emptyList, nil + } + + anyExist, identityHashesInMyHostRangeList, err := byteRange.GetAllIdentityHashesInListWithinRange(myHostRangeStart, myHostRangeEnd, relevantIdentityHashesList) + if (err != nil) { return nil, err } + if (anyExist == false){ + emptyList := make([][16]byte, 0) + return emptyList, nil + } + + return identityHashesInMyHostRangeList, nil + } + + requestedIdentitiesInMyHostRangeList, err := getIdentitiesInMyHostRangeList() + if (err != nil){ return nil, err } + + // Now we reduce list to make sure identities are funded + + fundedIdentitiesList := make([][16]byte, 0) + + for _, identityHash := range requestedIdentitiesInMyHostRangeList{ + + if (profileTypeToRetrieve == "Moderator"){ + + statusIsKnown, _, scoreIsSufficient, _, _, err := moderatorScores.GetModeratorIdentityScore(identityHash) + if (err != nil) { return nil, err } + if (statusIsKnown == false || scoreIsSufficient == false){ + continue + } + + } else { + + // profileType == "Mate" or "Host" + + statusIsKnown, isFunded, _, _, err := verifiedFundedStatus.GetVerifiedIdentityIsFundedStatus(identityHash, networkType) + if (err != nil) { return nil, err } + if (statusIsKnown == false || isFunded == false){ + continue + } + } + + fundedIdentitiesList = append(fundedIdentitiesList, identityHash) + } + + // Now we iterate through all relevant identity hashes, find which profile(s) to send in response + // Then we add those profiles to profilesInfoMapList + + profileInfoObjectsList := make([]serverResponse.ProfileInfoStruct, 0) + + for _, identityHash := range fundedIdentitiesList{ + + //Outputs: + // -[][28]byte: List of profile hashes + // -error + getUserProfileHashesForRequest := func()([][28]byte, error){ + + if (getNewestProfilesOnly == true){ + + if (getViewableProfilesOnly == true){ + + profileExists, _, profileHash, _, _, _, err := viewableProfiles.GetNewestViewableUserProfile(identityHash, networkType, false, false, false) + if (err != nil) { return nil, err } + if (profileExists == false){ + emptyList := make([][28]byte, 0) + return emptyList, nil + } + profileHashesList := [][28]byte{profileHash} + + return profileHashesList, nil + } + // getNewestProfilesOnly = true & getViewableProfilesOnly = false + + profileExists, _, profileHash, _, _, _, err := profileStorage.GetNewestUserProfile(identityHash, networkType) + if (err != nil) { return nil, err } + if (profileExists == false){ + emptyList := make([][28]byte, 0) + return emptyList, nil + } + profileHashesList := [][28]byte{profileHash} + + return profileHashesList, nil + } + // getNewestProfilesOnly == false + + if (getViewableProfilesOnly == true){ + + // We will check to see if identity is banned + + downloadingRequiredReviews, parametersExist, stickyStatusEstablished, identityIsViewable, err := verifiedStickyStatus.GetVerifiedIdentityIsViewableStickyStatus(identityHash, networkType) + if (err != nil) { return nil, err } + if (downloadingRequiredReviews == false || parametersExist == false || stickyStatusEstablished == false){ + //We do not know if this identity is banned or not. + // We cannot seed any profiles from this user + emptyList := make([][28]byte, 0) + return emptyList, nil + } + if (identityIsViewable == false){ + // Identity is banned, therefore all profiles are unviewable + emptyList := make([][28]byte, 0) + return emptyList, nil + } + } + + getAllProfileHashesForIdentity := func()([][28]byte, error){ + + exists, profileHashesList, err := badgerDatabase.GetIdentityProfileHashesList(identityHash) + if (err != nil) { return nil, err } + if (exists == false){ + emptyList := make([][28]byte, 0) + return emptyList, nil + } + return profileHashesList, nil + } + + allProfileHashesForIdentityList, err := getAllProfileHashesForIdentity() + if (err != nil) { return nil, err } + + if (getViewableProfilesOnly == false){ + return allProfileHashesForIdentityList, nil + } + + // Now we prune unviewable profiles + + viewableProfileHashesList := make([][28]byte, 0) + + for _, profileHash := range allProfileHashesForIdentityList{ + + profileIsDisabled, profileMetadataIsKnown, profileNetworkType, profileAuthor, downloadingRequiredReviews, parametersExist, stickyStatusEstablished, profileIsViewableStatus, err := verifiedStickyStatus.GetVerifiedProfileIsViewableStickyStatus(profileHash) + if (err != nil) { return nil, err } + if (profileMetadataIsKnown == false){ + continue + } + if (profileNetworkType != networkType){ + continue + } + if (profileIsDisabled == true){ + viewableProfileHashesList = append(viewableProfileHashesList, profileHash) + continue + } + if (profileAuthor != identityHash){ + return nil, errors.New("GetVerifiedProfileIsViewableStickyStatus returning different profileAuthor") + } + if (downloadingRequiredReviews == false){ + // This should not happen, because this was checked earlier in function + // Maybe client changed hosted range + logger.AddLogError("Network", errors.New("Profile not in hosted range anymore.")) + break + } + if (parametersExist == false){ + // We cannot determine sticky consensus status for this profile. + // We shouldn't be hosting without the parameters anyway + emptyList := make([][28]byte, 0) + return emptyList, nil + } + if (stickyStatusEstablished == false){ + // We cannot determine sticky consensus for this profile + // Continue to next profile + continue + } + if (profileIsViewableStatus == false){ + continue + } + viewableProfileHashesList = append(viewableProfileHashesList, profileHash) + } + + return viewableProfileHashesList, nil + } + + userProfileHashesForRequestList, err := getUserProfileHashesForRequest() + if (err != nil) { return nil, err } + + for _, profileHash := range userProfileHashesForRequestList{ + + if (profileTypeToRetrieve == "Mate"){ + + statusIsKnown, isFunded, _, _, err := verifiedFundedStatus.GetVerifiedMateProfileIsFundedStatus(profileHash) + if (err != nil) { return nil, err } + if (statusIsKnown == false || isFunded == false){ + continue + } + } + + profileExists, profileBytes, err := badgerDatabase.GetUserProfile(profileTypeToRetrieve, profileHash) + if (err != nil) { return nil, err } + if (profileExists == false){ + // Profile must have been deleted + continue + } + + ableToRead, profileHash_Retrieved, profileVersion, profileNetworkType, profileAuthor, profileBroadcastTime, profileIsDisabled, rawProfileMap, err := readProfiles.ReadProfileAndHash(false, profileBytes) + if (err != nil) { return nil, err } + if (ableToRead == false){ + return nil, errors.New("Database corrupt: contains invalid " + profileTypeToRetrieve + " profile") + } + if (profileHash != profileHash_Retrieved){ + return nil, errors.New("Database corrupt: Contains profile with different hash than entry key.") + } + if (profileAuthor != identityHash){ + return nil, errors.New("getIdentityProfilesForRequest returning profile from a different identity hash") + } + if (profileNetworkType != networkType){ + continue + } + + isRequestedVersion := slices.Contains(acceptableProfileVersionsList, profileVersion) + if (isRequestedVersion == false){ + // The requestor cannot accept this profile's version + // We must have a newer or older Seekia application version + continue + } + + if (requestCriteria != nil){ + + if (profileIsDisabled == true){ + // Disabled profiles will not fulfill any criteria + continue + } + + criteriaIsValid, fulfillsCriteria, err := mateCriteria.CheckIfMateProfileFulfillsCriteria(true, profileVersion, rawProfileMap, requestCriteria) + if (err != nil) { return nil, err } + if (criteriaIsValid == false){ + return nil, errors.New("ReadDecryptedServerRequest_GetProfilesInfo not verifying criteria.") + } + if (fulfillsCriteria == false){ + continue + } + } + + profileInfoObject := serverResponse.ProfileInfoStruct{ + ProfileHash: profileHash, + ProfileAuthor: profileAuthor, + ProfileBroadcastTime: profileBroadcastTime, + } + + profileInfoObjectsList = append(profileInfoObjectsList, profileInfoObject) + } + } + + responseBytes, err := serverResponse.CreateServerResponse_GetProfilesInfo(myHostPublicIdentityKey, myHostPrivateIdentityKey, connectionKey, requestIdentifier, profileInfoObjectsList) + if (err != nil) { return nil, err } + + return responseBytes, nil + } + + if (requestType == "GetProfiles"){ + + profileTypeToRetrieve, profileHashesToRetrieveList, err := serverRequest.ReadDecryptedServerRequest_GetProfiles(decryptedRequestBytes) + if (err != nil){ + // Request is malformed + return invalidRequestResponse, nil + } + + // First we get all profiles that we have that the peer is requesting + // Then we prune requested profiles list + // We prune profiles whose authors are not funded, and profiles that are not funded + // We prune profiles whose author is not within our hosted range + // This is done for host privacy + // Otherwise a malicious requestor could try to find other profiles the host has + // This could fingerprint the host and make it possible to link the host identity to their mate/moderator identity/activities + + getPrunedProfilesList := func()([][]byte, error){ + + hostModeEnabled, hostingAny, myHostRangeStart, myHostRangeEnd, err := myRanges.GetMyIdentitiesToHostRange(profileTypeToRetrieve) + if (err != nil) { return nil, err } + if (hostModeEnabled == false){ + // Host must have disabled host mode in the background + emptyList := make([][]byte, 0) + return emptyList, nil + } + if (hostingAny == false){ + // Host is not hosting any of the requested profiles + emptyList := make([][]byte, 0) + return emptyList, nil + } + + requestedProfilesList := make([][]byte, 0) + + for _, profileHash := range profileHashesToRetrieveList{ + + if (profileTypeToRetrieve == "Mate"){ + + // We make sure profile is funded + + statusIsKnown, isFunded, _, _, err := verifiedFundedStatus.GetVerifiedMateProfileIsFundedStatus(profileHash) + if (err != nil) { return nil, err } + if (statusIsKnown == false || isFunded == false){ + continue + } + } + + profileExists, profileBytes, err := badgerDatabase.GetUserProfile(profileTypeToRetrieve, profileHash) + if (err != nil) { return nil, err } + if (profileExists == false){ + // Profile could have been deleted after profile hashes response was made, or peer is malicious + // Either way, skip profile + continue + } + + ableToRead, profileHash_Retrieved, _, profileNetworkType, profileAuthor, _, _, _, err := readProfiles.ReadProfileAndHash(false, profileBytes) + if (err != nil) { return nil, err } + if (ableToRead == false){ + return nil, errors.New("Database corrupt: Contains invalid profile.") + } + if (profileHash_Retrieved != profileHash){ + return nil, errors.New("Database corrupt: Contains profile with different hash than entry key.") + } + if (profileNetworkType != networkType){ + // This should not happen unless requestor is malicious + // Requestor should only request profile hashes which we served in our GetProfilesInfo response + continue + } + + if (profileTypeToRetrieve == "Mate"){ + // Because hosts will provide all or no moderator/host profiles + // We only need to check if identity is in our range if profileType is Mate + + isWithinRange, err := byteRange.CheckIfIdentityHashIsWithinRange(myHostRangeStart, myHostRangeEnd, profileAuthor) + if (err != nil) { return nil, err } + if (isWithinRange == false){ + continue + } + } + + // We check if author is funded + + if (profileTypeToRetrieve == "Moderator"){ + + statusIsKnown, _, scoreIsSufficient, _, _, err := moderatorScores.GetModeratorIdentityScore(profileAuthor) + if (err != nil) { return nil, err } + if (statusIsKnown == false || scoreIsSufficient == false){ + continue + } + + } else { + + // profileType == "Mate" or "Host" + + statusIsKnown, isFunded, _, _, err := verifiedFundedStatus.GetVerifiedIdentityIsFundedStatus(profileAuthor, networkType) + if (err != nil) { return nil, err } + if (statusIsKnown == false || isFunded == false){ + continue + } + } + + requestedProfilesList = append(requestedProfilesList, profileBytes) + } + + return requestedProfilesList, nil + } + + prunedProfilesList, err := getPrunedProfilesList() + if (err != nil) { return nil, err } + + responseBytes, err := serverResponse.CreateServerResponse_GetProfiles(myHostPublicIdentityKey, myHostPrivateIdentityKey, connectionKey, requestIdentifier, prunedProfilesList) + if (err != nil) { return nil, err } + + return responseBytes, nil + } + + if (requestType == "GetMessageHashesList"){ + + acceptableMessageVersionsList, requestInboxRangeStart, requestInboxRangeEnd, requestedInboxesList, getViewableMessagesOnly, getDecryptableMessagesOnly, err := serverRequest.ReadDecryptedServerRequest_GetMessageHashesList(decryptedRequestBytes) + if (err != nil){ + return invalidRequestResponse, nil + } + + getMessageHashesForResponseList := func()([][26]byte, error){ + + hostModeEnabled, hostingAnyInboxes, myHostedInboxesRangeStart, myHostedInboxesRangeEnd, err := myRanges.GetMyInboxesToHostRange() + if (err != nil) { return nil, err } + if (hostModeEnabled == false){ + // User must have disabled host mode recently + emptyList := make([][26]byte, 0) + return emptyList, nil + } + if (hostingAnyInboxes == false){ + emptyList := make([][26]byte, 0) + return emptyList, nil + } + + getRequestedMessageInboxesList := func()([][10]byte, error){ + + if (len(requestedInboxesList) != 0){ + // We will ignore range and only retrieve based on inboxes list + + return requestedInboxesList, nil + } + + // We will retrieve messages based on request inbox range + + allMessageInboxesList, err := badgerDatabase.GetAllMessageInboxes() + if (err != nil) { return nil, err } + + anyFound, intersectionInboxesList, err := byteRange.GetAllInboxesInListWithinRange(requestInboxRangeStart, requestInboxRangeEnd, allMessageInboxesList) + if (err != nil) { return nil, err } + if (anyFound == false){ + emptyList := make([][10]byte, 0) + return emptyList, nil + } + + return intersectionInboxesList, nil + } + + requestedMessageInboxesList, err := getRequestedMessageInboxesList() + if (err != nil) { return nil, err } + + messageHashesForResponseList := make([][26]byte, 0) + + for _, inboxToRetrieve := range requestedMessageInboxesList{ + + // We check if inbox is in our range + + isWithinMyRange, err := byteRange.CheckIfInboxIsWithinRange(myHostedInboxesRangeStart, myHostedInboxesRangeEnd, inboxToRetrieve) + if (err != nil) { return nil, err } + if (isWithinMyRange == false){ + continue + } + + anyExist, messageHashesList, err := badgerDatabase.GetChatInboxMessageHashesList(inboxToRetrieve) + if (err != nil) { return nil, err } + if (anyExist == false){ + continue + } + + for _, messageHash := range messageHashesList{ + + // We make sure each message is of an acceptable version + + metadataExists, messageVersion, messageNetworkType, _, messageInbox, messageCipherKeyHash, err := contentMetadata.GetMessageMetadata(messageHash) + if (err != nil) { return nil, err } + if (metadataExists == false){ + // We don't have the message stored. + continue + } + if (messageInbox != inboxToRetrieve){ + return nil, errors.New("GetChatInboxMessageHashesList returning list with message of different inbox.") + } + if (messageNetworkType != networkType){ + // Message belongs to a different networkType + continue + } + + isAcceptable := slices.Contains(acceptableMessageVersionsList, messageVersion) + if (isAcceptable == false){ + continue + } + + // We make sure each message is funded + + statusIsKnown, messageIsFunded, _, _, err := verifiedFundedStatus.GetVerifiedMessageIsFundedStatus(messageHash) + if (err != nil) { return nil, err } + if (statusIsKnown == false || messageIsFunded == false){ + continue + } + + if (getViewableMessagesOnly == true){ + + // We make sure message is viewable + + messageMetadataIsKnown, messageNetworkType, messageInbox, _, downloadingRequiredReviews, parametersExist, statusEstablished, messageIsViewableStatus, err := verifiedStickyStatus.GetVerifiedMessageIsViewableStickyStatus(messageHash) + if (err != nil) { return nil, err } + if (messageMetadataIsKnown == false){ + // Message was deleted, skip to next message + continue + } + if (messageNetworkType != networkType){ + return nil, errors.New("GetVerifiedMessageIsViewableStickyStatus returning different message networkType than contentMetadata.") + } + if (messageInbox != inboxToRetrieve){ + return nil, errors.New("GetVerifiedMessageIsViewableStickyStatus returning invalid message inbox") + } + if (downloadingRequiredReviews == false){ + // This should only happen if hosted inbox range was changed during this operation + // All messages for this inbox will be out of range + break + } + if (parametersExist == false){ + //We cannot determine sticky status for any messages. Return empty response to peer. + emptyList := make([][26]byte, 0) + return emptyList, nil + } + if (statusEstablished == false){ + // We do not know sticky consensus for message. Skip message. + continue + } + if (messageIsViewableStatus == false){ + continue + } + } + + if (getDecryptableMessagesOnly == true){ + + // We make sure at least 1 review/report with valid cipher key exists for message + checkIfMessageCipherKeyExists := func()(bool, error){ + + cipherKeyFound, _, err := reportStorage.GetMessageCipherKeyFromAnyReport(messageHash, messageNetworkType, messageCipherKeyHash) + if (err != nil) { return false, err } + if (cipherKeyFound == true){ + return true, nil + } + + cipherKeyFound, _, err = reviewStorage.GetMessageCipherKeyFromAnyReview(messageHash, messageNetworkType, messageCipherKeyHash) + if (err != nil) { return false, err } + if (cipherKeyFound == true){ + return true, nil + } + + return false, nil + } + + messageCipherKeyExists, err := checkIfMessageCipherKeyExists() + if (err != nil) { return nil, err } + if (messageCipherKeyExists == false){ + continue + } + } + + messageHashesForResponseList = append(messageHashesForResponseList, messageHash) + } + } + + return messageHashesForResponseList, nil + } + + messageHashesForResponseList, err := getMessageHashesForResponseList() + if (err != nil) { return nil, err } + + responseBytes, err := serverResponse.CreateServerResponse_GetMessageHashesList(myHostPublicIdentityKey, myHostPrivateIdentityKey, connectionKey, requestIdentifier, messageHashesForResponseList) + if (err != nil) { return nil, err } + + return responseBytes, nil + } + if (requestType == "GetMessages"){ + + requestedMessageHashesList, err := serverRequest.ReadDecryptedServerRequest_GetMessages(decryptedRequestBytes) + if (err != nil) { return nil, err } + + getMessagesListForResponse := func()([][]byte, error){ + + hostModeEnabled, hostingAny, myInboxRangeStart, myInboxRangeEnd, err := myRanges.GetMyInboxesToHostRange() + if (err != nil) { return nil, err } + if (hostModeEnabled == false){ + // User must have disabled host mode + emptyList := make([][]byte, 0) + return emptyList, nil + } + + if (hostingAny == false){ + // We are not hosting any messages + emptyList := make([][]byte, 0) + return emptyList, nil + } + + messagesListForResponse := make([][]byte, 0) + + for _, messageHash := range requestedMessageHashesList{ + + // We make sure each message is funded + + statusIsKnown, messageIsFunded, _, _, err := verifiedFundedStatus.GetVerifiedMessageIsFundedStatus(messageHash) + if (err != nil) { return nil, err } + if (statusIsKnown == false || messageIsFunded == false){ + continue + } + + messageExists, messageBytes, err := badgerDatabase.GetChatMessage(messageHash) + if (err != nil) { return nil, err } + if (messageExists == false){ + continue + } + + ableToRead, currentMessageHash, _, messageNetworkType, messageInbox, _, _, _, _, _, _, err := readMessages.ReadChatMessagePublicDataAndHash(false, messageBytes) + if (err != nil) { return nil, err } + if (ableToRead == false){ + return nil, errors.New("Database corrupt: Contains malformed message.") + } + if (messageHash != currentMessageHash){ + return nil, errors.New("Database corrupt: Contains message with mismatched messageHash entry key") + } + if (messageNetworkType != networkType){ + // This should not happen unless requestor is malicious + // The requestor should only requests messages which we already said were on the connection networkType. + continue + } + + isWithinMyRange, err := byteRange.CheckIfInboxIsWithinRange(myInboxRangeStart, myInboxRangeEnd, messageInbox) + if (err != nil) { return nil, err } + if (isWithinMyRange == false){ + continue + } + + messagesListForResponse = append(messagesListForResponse, messageBytes) + } + + return messagesListForResponse, nil + } + + messagesListForResponse, err := getMessagesListForResponse() + if (err != nil) { return nil, err } + + responseBytes, err := serverResponse.CreateServerResponse_GetMessages(myHostPublicIdentityKey, myHostPrivateIdentityKey, connectionKey, requestIdentifier, messagesListForResponse) + if (err != nil) { return nil, err } + + return responseBytes, nil + } + if (requestType == "GetIdentityReviewsInfo"){ + + acceptableReviewVersions, identityTypeToRetrieve, requestRangeStart, requestRangeEnd, requestedReviewedIdentityHashesList, requestedReviewersList, err := serverRequest.ReadDecryptedServerRequest_GetIdentityReviewsInfo(decryptedRequestBytes) + if (err != nil){ + return invalidRequestResponse, nil + } + + getReviewsInfoMap := func()(map[[29]byte][]byte, error){ + + hostModeEnabled, hostingAny, myIdentityRangeStart, myIdentityRangeEnd, err := myRanges.GetMyIdentitiesToHostRange(identityTypeToRetrieve) + if (err != nil) { return nil, err } + if (hostModeEnabled == false){ + // We must have turned off host mode recently + emptyReviewsInfoMap := make(map[[29]byte][]byte) + return emptyReviewsInfoMap, nil + } + if (hostingAny == false){ + emptyReviewsInfoMap := make(map[[29]byte][]byte) + return emptyReviewsInfoMap, nil + } + + // We include identity, profile and attribute reviews + + relevantReviewHashesList := make([][29]byte, 0) + + allReviewedIdentityHashesList, err := badgerDatabase.GetAllReviewedIdentityHashes() + if (err != nil) { return nil, err } + + for _, identityHash := range allReviewedIdentityHashesList{ + + anyExist, reviewHashesList, err := badgerDatabase.GetIdentityReviewsList(identityHash) + if (err != nil) { return nil, err } + if (anyExist == true){ + relevantReviewHashesList = append(relevantReviewHashesList, reviewHashesList...) + } + } + + allReviewedProfileHashesList, err := badgerDatabase.GetAllReviewedProfileHashes() + if (err != nil) { return nil, err } + + for _, profileHash := range allReviewedProfileHashesList{ + + anyExist, reviewHashesList, err := badgerDatabase.GetProfileReviewsList(profileHash) + if (err != nil) { return nil, err } + if (anyExist == true){ + relevantReviewHashesList = append(relevantReviewHashesList, reviewHashesList...) + } + } + + allReviewedAttributeHashesList, err := badgerDatabase.GetAllReviewedProfileAttributeHashes() + if (err != nil) { return nil, err } + + for _, attributeHash := range allReviewedAttributeHashesList{ + + anyExist, reviewHashesList, err := badgerDatabase.GetProfileAttributeReviewsList(attributeHash) + if (err != nil) { return nil, err } + if (anyExist == true){ + relevantReviewHashesList = append(relevantReviewHashesList, reviewHashesList...) + } + } + + // Map structure: Review hash -> Reviewed hash + reviewsInfoMap := make(map[[29]byte][]byte) + + for _, reviewHash := range relevantReviewHashesList{ + + reviewExists, reviewBytes, err := badgerDatabase.GetReview(reviewHash) + if (err != nil) { return nil, err } + if (reviewExists == false){ + // Review must have been deleted automatically. + // The database entry that referenced the review will be deleted automatically + continue + } + + ableToRead, currentReviewHash, reviewVersion, reviewNetworkType, reviewerIdentityHash, _, reviewType, reviewedHash, _, _, err := readReviews.ReadReviewAndHash(false, reviewBytes) + if (err != nil) { return nil, err } + if (ableToRead == false){ + return nil, errors.New("Database corrupt: Contains invalid review.") + } + if (reviewHash != currentReviewHash){ + return nil, errors.New("Database corrupt: Review entry key does not match review hash") + } + isAcceptableVersion := slices.Contains(acceptableReviewVersions, reviewVersion) + if (isAcceptableVersion == false){ + // Review is of a different version than requestor can accept + // We must have a different Seekia app version than them. + continue + } + if (reviewNetworkType != networkType){ + // Review belongs to a different networkType + continue + } + + if (len(requestedReviewersList) != 0){ + isInRequestedList := slices.Contains(requestedReviewersList, reviewerIdentityHash) + if (isInRequestedList == false){ + continue + } + } + + statusIsKnown, _, scoreIsSufficient, _, _, err := moderatorScores.GetModeratorIdentityScore(reviewerIdentityHash) + if (err != nil) { return nil, err } + if (statusIsKnown == false || scoreIsSufficient == false){ + continue + } + + //Outputs: + // -bool: Review is malicious + // -bool: Reviewed identity hash is known + // -[16]byte: Reviewed identity hash + // -error + getReviewedIdentityHash := func()(bool, bool, [16]byte, error){ + + if (reviewType == "Identity"){ + + if (len(reviewedHash) != 16){ + reviewedHashHex := encoding.EncodeBytesToHexString(reviewedHash) + return false, false, [16]byte{}, errors.New("ReadReview returning invalid reviewedHash length for Identity review: " + reviewedHashHex) + } + + reviewedIdentityHash := [16]byte(reviewedHash) + + return false, true, reviewedIdentityHash, nil + } + if (reviewType == "Profile"){ + + if (len(reviewedHash) != 28){ + reviewedHashHex := encoding.EncodeBytesToHexString(reviewedHash) + return false, false, [16]byte{}, errors.New("ReadReview returning invalid reviewedHash length for Profile review: " + reviewedHashHex) + } + + reviewedProfileHash := [28]byte(reviewedHash) + + metadataExists, _, profileNetworkType, profileAuthor, _, _, _, _, err := contentMetadata.GetProfileMetadata(reviewedProfileHash) + if (err != nil) { return false, false, [16]byte{}, err } + if (metadataExists == false){ + return false, false, [16]byte{}, nil + } + if (profileNetworkType != reviewNetworkType){ + // The review is reviewing a profile from a different networkType + // The review author must be malicious + return true, false, [16]byte{}, nil + } + + return false, true, profileAuthor, nil + } + if (reviewType == "Attribute"){ + + if (len(reviewedHash) != 27){ + reviewedHashHex := encoding.EncodeBytesToHexString(reviewedHash) + return false, false, [16]byte{}, errors.New("ReadReview returning invalid reviewedHash length for Attribute review: " + reviewedHashHex) + } + + reviewedAttributeHash := [27]byte(reviewedHash) + + attributeMetadataFound, _, authorIdentityHash, attributeNetworkType, _, err := profileStorage.GetProfileAttributeMetadata(reviewedAttributeHash) + if (err != nil) { return false, false, [16]byte{}, err } + if (attributeMetadataFound == false){ + return false, false, [16]byte{}, nil + } + if (attributeNetworkType != reviewNetworkType){ + // The review is reviewing an attribute from a different networkType + // The review author must be malicious + return true, false, [16]byte{}, nil + } + + return false, true, authorIdentityHash, nil + } + + return false, false, [16]byte{}, errors.New("getReviewedIdentityHash reached during GetIdentityReviewsInfo response craft with invalid reviewType: " + reviewType) + } + + reviewIsMalicious, reviewedIdentityHashIsKnown, reviewedIdentityHash, err := getReviewedIdentityHash() + if (err != nil) { return nil, err } + if (reviewIsMalicious == true){ + // Review is malicious. We will not seed reviews which are known to be malicious + // TODO: Skipping these kinds of reviews could be a fingerprinting risk? + continue + } + if (reviewedIdentityHashIsKnown == false){ + // We cannot seed reviews whose reviewed identity hash is not known. + continue + } + + reviewedIdentityType, err := identity.GetIdentityTypeFromIdentityHash(reviewedIdentityHash) + if (err != nil){ return nil, err } + if (reviewedIdentityType != identityTypeToRetrieve){ + continue + } + + if (len(requestedReviewedIdentityHashesList) != 0){ + + // We make sure all reviewed identity hashes are within this list + + isWithinList := slices.Contains(requestedReviewedIdentityHashesList, reviewedIdentityHash) + if (isWithinList == false){ + continue + } + } else { + + identityIsWithinRequestRange, err := byteRange.CheckIfIdentityHashIsWithinRange(requestRangeStart, requestRangeEnd, reviewedIdentityHash) + if (err != nil) { return nil, err } + if (identityIsWithinRequestRange == false){ + continue + } + } + + // We make sure identity is within our range + + isWithinMyRange, err := byteRange.CheckIfIdentityHashIsWithinRange(myIdentityRangeStart, myIdentityRangeEnd, reviewedIdentityHash) + if (err != nil) { return nil, err } + if (isWithinMyRange == false){ + continue + } + + // We make sure reviewed identity is funded + + if (identityTypeToRetrieve == "Moderator"){ + + statusIsKnown, _, scoreIsSufficient, _, _, err := moderatorScores.GetModeratorIdentityScore(reviewedIdentityHash) + if (err != nil) { return nil, err } + if (statusIsKnown == false || scoreIsSufficient == false){ + continue + } + + } else { + + // identityTypeToRetrieve == "Mate" or "Host" + + statusIsKnown, isFunded, _, _, err := verifiedFundedStatus.GetVerifiedIdentityIsFundedStatus(reviewedIdentityHash, networkType) + if (err != nil) { return nil, err } + if (statusIsKnown == false || isFunded == false){ + continue + } + } + + if (identityTypeToRetrieve == "Mate" && reviewType == "Profile"){ + + // We make sure the mate profile is funded + + if (len(reviewedHash) != 28){ + reviewedHashHex := encoding.EncodeBytesToHexString(reviewedHash) + return nil, errors.New("ReadReview returning invalid length reviewedHash for Profile review: " + reviewedHashHex) + } + + reviewedProfileHash := [28]byte(reviewedHash) + + statusIsKnown, isFunded, _, _, err := verifiedFundedStatus.GetVerifiedMateProfileIsFundedStatus(reviewedProfileHash) + if (err != nil) { return nil, err } + if (statusIsKnown == false || isFunded == false){ + continue + } + + } else if (identityTypeToRetrieve == "Mate" && reviewType == "Attribute"){ + + // We make sure at least 1 profile with this attribute is funded + + checkIfAnyAttributeProfileIsFunded := func()(bool, error){ + + if (len(reviewedHash) != 27){ + reviewedHashHex := encoding.EncodeBytesToHexString(reviewedHash) + return false, errors.New("ReadReview returning invalid length reviewedHash for attribute review: " + reviewedHashHex) + } + + reviewedAttributeHash := [27]byte(reviewedHash) + + anyExist, attributeProfileHashesList, err := badgerDatabase.GetAttributeProfilesList(reviewedAttributeHash) + if (err != nil) { return false, err } + if (anyExist == false){ + return false, nil + } + + for _, profileHash := range attributeProfileHashesList{ + + statusIsKnown, isFunded, _, _, err := verifiedFundedStatus.GetVerifiedMateProfileIsFundedStatus(profileHash) + if (err != nil) { return false, err } + if (statusIsKnown == true && isFunded == true){ + return true, nil + } + } + return false, nil + } + + anyAttributeProfileIsFunded, err := checkIfAnyAttributeProfileIsFunded() + if (err != nil) { return nil, err } + if (anyAttributeProfileIsFunded == false){ + continue + } + } + + reviewsInfoMap[reviewHash] = reviewedHash + } + + return reviewsInfoMap, nil + } + + reviewsInfoMap, err := getReviewsInfoMap() + if (err != nil) { return nil, err } + + responseBytes, err := serverResponse.CreateServerResponse_GetIdentityReviewsInfo(myHostPublicIdentityKey, myHostPrivateIdentityKey, connectionKey, requestIdentifier, reviewsInfoMap) + if (err != nil) { return nil, err } + + return responseBytes, nil + } + if (requestType == "GetMessageReviewsInfo"){ + + acceptableReviewVersions, requestRangeStart, requestRangeEnd, requestedReviewedMessageHashesList, requestedReviewersList, err := serverRequest.ReadDecryptedServerRequest_GetMessageReviewsInfo(decryptedRequestBytes) + if (err != nil){ + return invalidRequestResponse, nil + } + + getReviewsInfoMap := func()(map[[29]byte][26]byte, error){ + + hostModeEnabled, hostingAnyInboxes, myInboxRangeStart, myInboxRangeEnd, err := myRanges.GetMyInboxesToHostRange() + if (err != nil) { return nil, err } + if (hostModeEnabled == false){ + // Host mode must have been disabled recently + reviewsInfoMap := make(map[[29]byte][26]byte) + return reviewsInfoMap, nil + } + if (hostingAnyInboxes == false){ + // We are not hosting any messages/ message reviews + reviewsInfoMap := make(map[[29]byte][26]byte) + return reviewsInfoMap, nil + } + + getRelevantMessageHashesList := func()([][26]byte, error){ + + if (len(requestedReviewedMessageHashesList) != 0){ + + // We only want to return reviews that review message hashes that are within the requestedReviewedMessageHashesList + + return requestedReviewedMessageHashesList, nil + } + + // We want to share reviews which review all reviewed message hashes + // We will reduce based on requested range later + + allReviewedMessageHashesList, err := badgerDatabase.GetAllReviewedMessageHashes() + if (err != nil) { return nil, err } + + return allReviewedMessageHashesList, nil + } + + relevantMessageHashesList, err := getRelevantMessageHashesList() + if (err != nil){ return nil, err } + + relevantReviewHashesList := make([][29]byte, 0) + + for _, messageHash := range relevantMessageHashesList{ + + anyExist, reviewHashesList, err := badgerDatabase.GetMessageReviewsList(messageHash) + if (err != nil) { return nil, err } + if (anyExist == true){ + relevantReviewHashesList = append(relevantReviewHashesList, reviewHashesList...) + } + } + + // Map Structure: Review hash -> Reviewed message hash + reviewsInfoMap := make(map[[29]byte][26]byte) + + for _, reviewHash := range relevantReviewHashesList{ + + reviewExists, reviewBytes, err := badgerDatabase.GetReview(reviewHash) + if (err != nil) { return nil, err } + if (reviewExists == false){ + continue + } + + ableToRead, currentReviewHash, reviewVersion, reviewNetworkType, reviewerIdentityHash, _, reviewType, reviewedHash, _, _, err := readReviews.ReadReviewAndHash(false, reviewBytes) + if (err != nil) { return nil, err } + if (ableToRead == false){ + return nil, errors.New("Database corrupt: Contains invalid review.") + } + if (reviewHash != currentReviewHash){ + return nil, errors.New("Database corrupt: Review entry key does not match review hash") + } + isAcceptableVersion := slices.Contains(acceptableReviewVersions, reviewVersion) + if (isAcceptableVersion == false){ + // Review is of a different version than requestor can accept + // We must have a different Seekia app version than them. + continue + } + if (reviewNetworkType != networkType){ + // Review belongs to a different networkType + continue + } + + if (len(requestedReviewersList) != 0){ + isInRequestedList := slices.Contains(requestedReviewersList, reviewerIdentityHash) + if (isInRequestedList == false){ + continue + } + } + + if (reviewType != "Message"){ + return nil, errors.New("Database corrupt: Message reviews list contains non-message review") + } + + // We will reduce the requested reviews based on requested inbox range + + if (len(reviewedHash) != 26){ + reviewedHashHex := encoding.EncodeBytesToHexString(reviewedHash) + return nil, errors.New("ReadReview returning invalid length reviewedHash for Message review: " + reviewedHashHex) + } + + reviewedMessageHash := [26]byte(reviewedHash) + + metadataExists, _, messageNetworkType, _, messageInbox, _, err := contentMetadata.GetMessageMetadata(reviewedMessageHash) + if (err != nil){ return nil, err } + if (metadataExists == false){ + // We cannot seed reviews for messages whose metadata we do not know + continue + } + if (messageNetworkType != reviewNetworkType){ + // The review author must be malicious + continue + } + + isWithinMyRange, err := byteRange.CheckIfInboxIsWithinRange(myInboxRangeStart, myInboxRangeEnd, messageInbox) + if (err != nil) { return nil, err } + if (isWithinMyRange == false){ + continue + } + + // We make sure message inbox is within requested inbox range + + isWithinRequestRange, err := byteRange.CheckIfInboxIsWithinRange(requestRangeStart, requestRangeEnd, messageInbox) + if (err != nil) { return nil, err } + if (isWithinRequestRange == false){ + continue + } + + // We make sure message is funded + + statusIsKnown, messageIsFunded, _, _, err := verifiedFundedStatus.GetVerifiedMessageIsFundedStatus(reviewedMessageHash) + if (err != nil) { return nil, err } + if (statusIsKnown == false || messageIsFunded == false){ + continue + } + + reviewsInfoMap[reviewHash] = reviewedMessageHash + } + + return reviewsInfoMap, nil + } + + reviewsInfoMap, err := getReviewsInfoMap() + if (err != nil) { return nil, err } + + responseBytes, err := serverResponse.CreateServerResponse_GetMessageReviewsInfo(myHostPublicIdentityKey, myHostPrivateIdentityKey, connectionKey, requestIdentifier, reviewsInfoMap) + if (err != nil) { return nil, err } + + return responseBytes, nil + } + if (requestType == "GetReviews"){ + + requestedReviewHashesList, err := serverRequest.ReadDecryptedServerRequest_GetReviews(decryptedRequestBytes) + if (err != nil) { return nil, err } + + getReviewsListForResponse := func()([][]byte, error){ + + // We will reduce reviews to only our hosted range + // This is done to prevent fingerprinting + // We also make sure all reviewers, reviewed content, and reviewed identities are funded + + reviewsListForResponse := make([][]byte, 0) + + for _, reviewHash := range requestedReviewHashesList{ + + reviewExists, reviewBytes, err := badgerDatabase.GetReview(reviewHash) + if (err != nil) { return nil, err } + if (reviewExists == false){ + continue + } + + ableToRead, currentReviewHash, _, reviewNetworkType, reviewerIdentityHash, _, reviewType, reviewedHash, _, _, err := readReviews.ReadReviewAndHash(false, reviewBytes) + if (err != nil) { return nil, err } + if (ableToRead == false){ + return nil, errors.New("Database corrupt: Contains invalid review.") + } + if (reviewHash != currentReviewHash){ + return nil, errors.New("Database corrupt: Contains review with mismatched reviewHash entry key") + } + if (reviewNetworkType != networkType){ + // Requestor must be malicious + // They should only request reviews which we told them were on the connection networkType + continue + } + + statusIsKnown, _, scoreIsSufficient, _, _, err := moderatorScores.GetModeratorIdentityScore(reviewerIdentityHash) + if (err != nil) { return nil, err } + if (statusIsKnown == false || scoreIsSufficient == false){ + continue + } + + if (reviewType == "Message"){ + + if (len(reviewedHash) != 26){ + reviewedHashHex := encoding.EncodeBytesToHexString(reviewedHash) + return nil, errors.New("ReadReview returning invalid length reviewedHash for Message review: " + reviewedHashHex) + } + + reviewedMessageHash := [26]byte(reviewedHash) + + metadataExists, _, messageNetworkType, _, messageInbox, _, err := contentMetadata.GetMessageMetadata(reviewedMessageHash) + if (err != nil) { return nil, err } + if (metadataExists == false){ + // We cannot serve reviews whose reviewed content we do not have saved + continue + } + if (messageNetworkType != reviewNetworkType){ + // Review author must be malicious + // We will not serve malicious reviews + // Requestor must be malicious, because we would have already checked for this condition + // before we offered this review to requestor + continue + } + + hostModeEnabled, isWithinMyRange, err := myRanges.CheckIfMessageInboxIsWithinMyHostedRange(messageInbox) + if (err != nil) { return nil, err } + if (hostModeEnabled == false){ + // Host mode must have been disabled after earlier check. + emptyList := make([][]byte, 0) + return emptyList, nil + } + if (isWithinMyRange == false){ + continue + } + + statusIsKnown, messageIsFunded, _, _, err := verifiedFundedStatus.GetVerifiedMessageIsFundedStatus(reviewedMessageHash) + if (err != nil) { return nil, err } + if (statusIsKnown == false || messageIsFunded == false){ + continue + } + + } else { + // reviewType == "Identity" or "Profile" or "Attribute" + + //Outputs: + // -bool: Review is malicious + // -bool: Identity hash is known + // -[16]byte: Reviewed identity hash + // -error + getReviewedIdentityHash := func()(bool, bool, [16]byte, error){ + + if (reviewType == "Identity"){ + + if (len(reviewedHash) != 16){ + reviewedHashHex := encoding.EncodeBytesToHexString(reviewedHash) + return false, false, [16]byte{}, errors.New("ReadReview returning invalid length reviewedHash for Identity review: " + reviewedHashHex) + } + + reviewedIdentityHash := [16]byte(reviewedHash) + + return false, true, reviewedIdentityHash, nil + } + + if (reviewType == "Profile"){ + + if (len(reviewedHash) != 28){ + reviewedHashHex := encoding.EncodeBytesToHexString(reviewedHash) + return false, false, [16]byte{}, errors.New("ReadReview returning invalid length reviewedHash for Profile review: " + reviewedHashHex) + } + + reviewedProfileHash := [28]byte(reviewedHash) + + metadataExists, _, profileNetworkType, profileAuthor, _, _, _, _, err := contentMetadata.GetProfileMetadata(reviewedProfileHash) + if (err != nil) { return false, false, [16]byte{}, err } + if (metadataExists == false){ + return false, false, [16]byte{}, nil + } + if (profileNetworkType != reviewNetworkType){ + // Review author must be malicious + return true, false, [16]byte{}, nil + } + + return false, true, profileAuthor, nil + } + if (reviewType == "Attribute"){ + + if (len(reviewedHash) != 27){ + reviewedHashHex := encoding.EncodeBytesToHexString(reviewedHash) + return false, false, [16]byte{}, errors.New("ReadReview returning invalid length reviewedHash for Attribute review: " + reviewedHashHex) + } + + reviewedAttributeHash := [27]byte(reviewedHash) + + attributeMetadataFound, _, authorIdentityHash, attributeNetworkType, _, err := profileStorage.GetProfileAttributeMetadata(reviewedAttributeHash) + if (err != nil) { return false, false, [16]byte{}, err } + if (attributeMetadataFound == false){ + return false, false, [16]byte{}, nil + } + if (attributeNetworkType != reviewNetworkType){ + // The review is reviewing an attribute from a different networkType + // The review author must be malicious + return true, true, [16]byte{}, nil + } + + return false, true, authorIdentityHash, nil + } + + return false, false, [16]byte{}, errors.New("ReadReview returning invalid reviewType: " + reviewType) + } + + reviewIsMalicious, metadataExists, reviewedIdentityHash, err := getReviewedIdentityHash() + if (err != nil) { return nil, err } + if (reviewIsMalicious == true){ + // We will not serve malicious reviews + // Requestor must be malicious, because we should have already checked for this before + // offering a review hash in the Get...ReviewsInfo responses + continue + } + if (metadataExists == false){ + // We cannot seed reviews whose reviewed identity hash we do not know + continue + } + + hostModeEnabled, isInMyRange, err := myRanges.CheckIfIdentityHashIsWithinMyHostedRange(reviewedIdentityHash) + if (err != nil) { return nil, err } + if (hostModeEnabled == false){ + // Host mode must have been disabled after earlier check. Return empty list. + emptyList := make([][]byte, 0) + return emptyList, nil + } + + if (isInMyRange == false){ + continue + } + + // We make sure reviewed identity is funded + + reviewedIdentityType, err := identity.GetIdentityTypeFromIdentityHash(reviewedIdentityHash) + if (err != nil) { return nil, err } + if (reviewedIdentityType == "Moderator"){ + + statusIsKnown, _, scoreIsSufficient, _, _, err := moderatorScores.GetModeratorIdentityScore(reviewedIdentityHash) + if (err != nil) { return nil, err } + if (statusIsKnown == false || scoreIsSufficient == false){ + continue + } + + } else { + + // reviewedIdentityType == "Mate" or "Host" + + statusIsKnown, isFunded, _, _, err := verifiedFundedStatus.GetVerifiedIdentityIsFundedStatus(reviewedIdentityHash, networkType) + if (err != nil) { return nil, err } + if (statusIsKnown == false || isFunded == false){ + continue + } + } + + if (reviewedIdentityType == "Mate" && reviewType == "Profile"){ + + // We make sure profile is funded + + if (len(reviewedHash) != 28){ + reviewedHashHex := encoding.EncodeBytesToHexString(reviewedHash) + return nil, errors.New("ReadReview returning invalid length reviewedHash for Profile review: " + reviewedHashHex) + } + + reviewedProfileHash := [28]byte(reviewedHash) + + statusIsKnown, isFunded, _, _, err := verifiedFundedStatus.GetVerifiedMateProfileIsFundedStatus(reviewedProfileHash) + if (err != nil) { return nil, err } + if (statusIsKnown == false || isFunded == false){ + continue + } + + } else if (reviewedIdentityType == "Mate" && reviewType == "Attribute"){ + + // We make sure at least 1 profile with this attribute is funded + + checkIfAnyAttributeProfileIsFunded := func()(bool, error){ + + if (len(reviewedHash) != 27){ + reviewedHashHex := encoding.EncodeBytesToHexString(reviewedHash) + return false, errors.New("ReadReview returning invalid length reviewedHash for Attribute review: " + reviewedHashHex) + } + + reviewedAttributeHash := [27]byte(reviewedHash) + + anyExist, attributeProfileHashesList, err := badgerDatabase.GetAttributeProfilesList(reviewedAttributeHash) + if (err != nil) { return false, err } + if (anyExist == false){ + return false, nil + } + + for _, profileHash := range attributeProfileHashesList{ + + statusIsKnown, isFunded, _, _, err := verifiedFundedStatus.GetVerifiedMateProfileIsFundedStatus(profileHash) + if (err != nil) { return false, err } + if (statusIsKnown == true && isFunded == true){ + return true, nil + } + } + + return false, nil + } + + anyAttributeProfileIsFunded, err := checkIfAnyAttributeProfileIsFunded() + if (err != nil) { return nil, err } + if (anyAttributeProfileIsFunded == false){ + continue + } + } + } + + reviewsListForResponse = append(reviewsListForResponse, reviewBytes) + } + + return reviewsListForResponse, nil + } + + reviewsListForResponse, err := getReviewsListForResponse() + if (err != nil) { return nil, err } + + responseBytes, err := serverResponse.CreateServerResponse_GetReviews(myHostPublicIdentityKey, myHostPrivateIdentityKey, connectionKey, requestIdentifier, reviewsListForResponse) + if (err != nil) { return nil, err } + + return responseBytes, nil + } + if (requestType == "GetIdentityReportsInfo"){ + + acceptableVersionsList, identityTypeToRetrieve, requestRangeStart, requestRangeEnd, requestedReportedIdentityHashesList, err := serverRequest.ReadDecryptedServerRequest_GetIdentityReportsInfo(decryptedRequestBytes) + if (err != nil){ + return invalidRequestResponse, nil + } + + getReportsInfoMap := func()(map[[30]byte][]byte, error){ + + hostModeEnabled, hostingAny, myIdentityRangeStart, myIdentityRangeEnd, err := myRanges.GetMyIdentitiesToHostRange(identityTypeToRetrieve) + if (err != nil) { return nil, err } + if (hostModeEnabled == false){ + // We must have turned off host mode recently + reportsInfoMap := make(map[[30]byte][]byte) + return reportsInfoMap, nil + } + if (hostingAny == false){ + reportsInfoMap := make(map[[30]byte][]byte) + return reportsInfoMap, nil + } + + // We include identity, profile and attribute reports + + relevantReportHashesList := make([][30]byte, 0) + + allReportedIdentityHashesList, err := badgerDatabase.GetAllReportedIdentityHashes() + if (err != nil) { return nil, err } + + for _, identityHash := range allReportedIdentityHashesList{ + + anyExist, reportHashesList, err := badgerDatabase.GetIdentityReportsList(identityHash) + if (err != nil) { return nil, err } + if (anyExist == true){ + relevantReportHashesList = append(relevantReportHashesList, reportHashesList...) + } + } + + allReportedProfileHashesList, err := badgerDatabase.GetAllReportedProfileHashes() + if (err != nil) { return nil, err } + + for _, profileHash := range allReportedProfileHashesList{ + + anyExist, reportHashesList, err := badgerDatabase.GetProfileReportsList(profileHash) + if (err != nil) { return nil, err } + if (anyExist == true){ + relevantReportHashesList = append(relevantReportHashesList, reportHashesList...) + } + } + + allReportedAttributeHashesList, err := badgerDatabase.GetAllReportedProfileAttributeHashes() + if (err != nil) { return nil, err } + + for _, attributeHash := range allReportedAttributeHashesList{ + + anyExist, reportHashesList, err := badgerDatabase.GetProfileAttributeReportsList(attributeHash) + if (err != nil) { return nil, err } + if (anyExist == true){ + relevantReportHashesList = append(relevantReportHashesList, reportHashesList...) + } + } + + // Map Structure: Report Hash -> Reported Hash + reportsInfoMap := make(map[[30]byte][]byte) + + for _, reportHash := range relevantReportHashesList{ + + statusIsKnown, reportIsFunded, _, _, err := verifiedFundedStatus.GetVerifiedReportIsFundedStatus(reportHash) + if (err != nil) { return nil, err } + if (statusIsKnown == false || reportIsFunded == false){ + continue + } + + reportExists, reportBytes, err := badgerDatabase.GetReport(reportHash) + if (err != nil) { return nil, err } + if (reportExists == false){ + continue + } + + ableToRead, currentReportHash, reportVersion, reportNetworkType, _, reportType, reportedHash, _, err := readReports.ReadReportAndHash(false, reportBytes) + if (err != nil) { return nil, err } + if (ableToRead == false){ + return nil, errors.New("Database corrupt: Contains invalid report.") + } + if (reportHash != currentReportHash){ + return nil, errors.New("Database corrupt: Report entry key does not match report hash") + } + isAcceptableVersion := slices.Contains(acceptableVersionsList, reportVersion) + if (isAcceptableVersion == false){ + // This report is of a version that the requestor cannot accept + // We must be running a different version of Seekia than them. + continue + } + if (reportNetworkType != networkType){ + // Report belongs to a different networkType + continue + } + + //Outputs: + // -bool: Report is malicious + // -bool: Metadata exists + // -[16]byte: Reported identity hash + // -error + getReportedIdentityHash := func()(bool, bool, [16]byte, error){ + + if (reportType == "Identity"){ + + if (len(reportedHash) != 16){ + reportedHashHex := encoding.EncodeBytesToHexString(reportedHash) + return false, false, [16]byte{}, errors.New("ReadReport returning invalid length reportedHash for Identity report: " + reportedHashHex) + } + + reportedIdentityHash := [16]byte(reportedHash) + + return false, true, reportedIdentityHash, nil + } + if (reportType == "Profile"){ + + if (len(reportedHash) != 28){ + reportedHashHex := encoding.EncodeBytesToHexString(reportedHash) + return false, false, [16]byte{}, errors.New("ReadReport returning invalid length reportedHash for Profile report: " + reportedHashHex) + } + + reportedProfileHash := [28]byte(reportedHash) + + metadataExists, _, profileNetworkType, profileAuthor, _, _, _, _, err := contentMetadata.GetProfileMetadata(reportedProfileHash) + if (err != nil) { return false, false, [16]byte{}, err } + if (metadataExists == false){ + return false, false, [16]byte{}, nil + } + if (profileNetworkType != reportNetworkType){ + // Report author must be malicious + // Report is reporting a profile on a different network + return true, false, [16]byte{}, nil + } + + return false, true, profileAuthor, nil + } + if (reportType == "Attribute"){ + + if (len(reportedHash) != 27){ + reportedHashHex := encoding.EncodeBytesToHexString(reportedHash) + return false, false, [16]byte{}, errors.New("ReadReport returning invalid length reportedHash for Attribute report: " + reportedHashHex) + } + + reportedAttributeHash := [27]byte(reportedHash) + + metadataExists, _, authorIdentityHash, attributeNetworkType, _, err := profileStorage.GetProfileAttributeMetadata(reportedAttributeHash) + if (err != nil){ return false, false, [16]byte{}, err } + if (metadataExists == false){ + return false, false, [16]byte{}, nil + } + if (attributeNetworkType != networkType){ + // Report author must be malicious + // Report is reporting an attribute on a different network + return true, false, [16]byte{}, nil + } + + return false, true, authorIdentityHash, nil + } + + return false, false, [16]byte{}, errors.New("getReportedIdentityHash reached with invalid reportType: " + reportType) + } + + reportIsMalicious, metadataExists, reportedIdentityHash, err := getReportedIdentityHash() + if (err != nil) { return nil, err } + if (reportIsMalicious == true){ + // We will not serve malicious reports + continue + } + if (metadataExists == false){ + // We cannot serve reports whose reported author we do not know. + continue + } + + reportedIdentityType, err := identity.GetIdentityTypeFromIdentityHash(reportedIdentityHash) + if (err != nil){ return nil, err } + if (reportedIdentityType != identityTypeToRetrieve){ + continue + } + + if (len(requestedReportedIdentityHashesList) != 0){ + + // We will find report hashes from requested reportedHashes list + + isWithinList := slices.Contains(requestedReportedIdentityHashesList, reportedIdentityHash) + if (isWithinList == false){ + continue + } + } else { + + // We make sure reported identity is within requested range + + identityIsInRequestRange, err := byteRange.CheckIfIdentityHashIsWithinRange(requestRangeStart, requestRangeEnd, reportedIdentityHash) + if (err != nil) { return nil, err } + if (identityIsInRequestRange == false){ + continue + } + } + + // We make sure identity is within our range + + identityIsInMyRange, err := byteRange.CheckIfIdentityHashIsWithinRange(myIdentityRangeStart, myIdentityRangeEnd, reportedIdentityHash) + if (err != nil) { return nil, err } + if (identityIsInMyRange == false){ + continue + } + + // We make sure that the profile/identity being reported is funded + + if (reportedIdentityType == "Moderator"){ + + statusIsKnown, _, scoreIsSufficient, _, _, err := moderatorScores.GetModeratorIdentityScore(reportedIdentityHash) + if (err != nil) { return nil, err } + if (statusIsKnown == false || scoreIsSufficient == false){ + continue + } + + } else { + + // reviewedIdentityType == "Mate" or "Host" + + statusIsKnown, isFunded, _, _, err := verifiedFundedStatus.GetVerifiedIdentityIsFundedStatus(reportedIdentityHash, networkType) + if (err != nil) { return nil, err } + if (statusIsKnown == false || isFunded == false){ + continue + } + } + + if (reportedIdentityType == "Mate" && reportType == "Profile"){ + + // We make sure profile is funded + + if (len(reportedHash) != 28){ + reportedHashHex := encoding.EncodeBytesToHexString(reportedHash) + return nil, errors.New("ReadReport returning invalid length reportedHash for Profile report: " + reportedHashHex) + } + + reportedProfileHash := [28]byte(reportedHash) + + statusIsKnown, isFunded, _, _, err := verifiedFundedStatus.GetVerifiedMateProfileIsFundedStatus(reportedProfileHash) + if (err != nil) { return nil, err } + if (statusIsKnown == false || isFunded == false){ + continue + } + + } else if (reportedIdentityType == "Mate" && reportType == "Attribute"){ + + // We make sure at least 1 profile with this attribute is funded + + checkIfAnyAttributeProfileIsFunded := func()(bool, error){ + + if (len(reportedHash) != 27){ + reportedHashHex := encoding.EncodeBytesToHexString(reportedHash) + return false, errors.New("ReadReport returning invalid length reportedHash for Attribute report: " + reportedHashHex) + } + + reportedAttributeHash := [27]byte(reportedHash) + + anyExist, attributeProfileHashesList, err := badgerDatabase.GetAttributeProfilesList(reportedAttributeHash) + if (err != nil) { return false, err } + if (anyExist == false){ + return false, nil + } + + for _, profileHash := range attributeProfileHashesList{ + + statusIsKnown, isFunded, _, _, err := verifiedFundedStatus.GetVerifiedMateProfileIsFundedStatus(profileHash) + if (err != nil) { return false, err } + if (statusIsKnown == true && isFunded == true){ + return true, nil + } + } + + return false, nil + } + + anyAttributeProfileIsFunded, err := checkIfAnyAttributeProfileIsFunded() + if (err != nil) { return nil, err } + if (anyAttributeProfileIsFunded == false){ + continue + } + } + + reportsInfoMap[reportHash] = reportedHash + } + + return reportsInfoMap, nil + } + + reportsInfoMap, err := getReportsInfoMap() + if (err != nil) { return nil, err } + + responseBytes, err := serverResponse.CreateServerResponse_GetIdentityReportsInfo(myHostPublicIdentityKey, myHostPrivateIdentityKey, connectionKey, requestIdentifier, reportsInfoMap) + if (err != nil) { return nil, err } + + return responseBytes, nil + } + if (requestType == "GetMessageReportsInfo"){ + + acceptableVersionsList, requestRangeStart, requestRangeEnd, requestedReportedMessageHashesList, err := serverRequest.ReadDecryptedServerRequest_GetMessageReportsInfo(decryptedRequestBytes) + if (err != nil){ + return invalidRequestResponse, nil + } + + getReportsInfoMap := func()(map[[30]byte][26]byte, error){ + + hostModeEnabled, hostingAnyInboxes, myInboxRangeStart, myInboxRangeEnd, err := myRanges.GetMyInboxesToHostRange() + if (err != nil) { return nil, err } + if (hostModeEnabled == false){ + // We must have disabled host mode recently + reportsInfoMap := make(map[[30]byte][26]byte) + return reportsInfoMap, nil + } + if (hostingAnyInboxes == false){ + // We are not hosting any messages/ message reports + reportsInfoMap := make(map[[30]byte][26]byte) + return reportsInfoMap, nil + } + + getRelevantMessageHashesList := func()([][26]byte, error){ + + if (len(requestedReportedMessageHashesList) != 0){ + + // We will get reports from requested message hashes list + + return requestedReportedMessageHashesList, nil + } + + allReportedMessageHashesList, err := badgerDatabase.GetAllReportedMessageHashes() + if (err != nil) { return nil, err } + + return allReportedMessageHashesList, nil + } + + relevantMessageHashesList, err := getRelevantMessageHashesList() + if (err != nil) { return nil, err } + + relevantReportHashesList := make([][30]byte, 0) + + for _, messageHash := range relevantMessageHashesList{ + + anyExist, reportHashesList, err := badgerDatabase.GetMessageReportsList(messageHash) + if (err != nil) { return nil, err } + if (anyExist == true){ + relevantReportHashesList = append(relevantReportHashesList, reportHashesList...) + } + } + + // Map Structure: Report Hash -> Reported Message Hash + reportsInfoMap := make(map[[30]byte][26]byte) + + for _, reportHash := range relevantReportHashesList{ + + statusIsKnown, reportIsFunded, _, _, err := verifiedFundedStatus.GetVerifiedReportIsFundedStatus(reportHash) + if (err != nil) { return nil, err } + if (statusIsKnown == false || reportIsFunded == false){ + continue + } + + reportExists, reportBytes, err := badgerDatabase.GetReport(reportHash) + if (err != nil) { return nil, err } + if (reportExists == false){ + continue + } + + ableToRead, currentReportHash, reportVersion, reportNetworkType, _, reportType, reportedHash, _, err := readReports.ReadReportAndHash(false, reportBytes) + if (err != nil) { return nil, err } + if (ableToRead == false){ + return nil, errors.New("Database corrupt: Contains invalid report.") + } + if (reportHash != currentReportHash){ + return nil, errors.New("Database corrupt: Report entry key does not match report hash") + } + + if (reportType != "Message"){ + return nil, errors.New("Database corrupt: Message reports list contains non-message report") + } + + if (len(reportedHash) != 26){ + reportedHashHex := encoding.EncodeBytesToHexString(reportedHash) + return nil, errors.New("ReadReport returning invalid length reportedHash for Message report: " + reportedHashHex) + } + + reportedMessageHash := [26]byte(reportedHash) + + isAcceptableVersion := slices.Contains(acceptableVersionsList, reportVersion) + if (isAcceptableVersion == false){ + // This report is of a version that the requestor cannot accept + // We must be running a different version of Seekia than them. + continue + } + if (reportNetworkType != networkType){ + // Report belongs to a different networkType + continue + } + + metadataExists, _, messageNetworkType, _, messageInbox, _, err := contentMetadata.GetMessageMetadata(reportedMessageHash) + if (err != nil) { return nil, err } + if (metadataExists == false){ + // We cannot seed reports for messages whose metadata we do not know + continue + } + if (messageNetworkType != networkType){ + // Report author must be malicious + // They have created a report for a message belonging to a different networkType + continue + } + + isWithinMyRange, err := byteRange.CheckIfInboxIsWithinRange(myInboxRangeStart, myInboxRangeEnd, messageInbox) + if (err != nil) { return nil, err } + if (isWithinMyRange == false){ + continue + } + + // We reduce the requested reports based on requested inbox range + + isWithinRequestRange, err := byteRange.CheckIfInboxIsWithinRange(requestRangeStart, requestRangeEnd, messageInbox) + if (err != nil) { return nil, err } + if (isWithinRequestRange == false){ + continue + } + + // We make sure message is funded + + statusIsKnown, messageIsFunded, _, _, err := verifiedFundedStatus.GetVerifiedMessageIsFundedStatus(reportedMessageHash) + if (err != nil) { return nil, err } + if (statusIsKnown == false || messageIsFunded == false){ + continue + } + + reportsInfoMap[reportHash] = reportedMessageHash + } + + return reportsInfoMap, nil + } + + reportsInfoMap, err := getReportsInfoMap() + if (err != nil) { return nil, err } + + responseBytes, err := serverResponse.CreateServerResponse_GetMessageReportsInfo(myHostPublicIdentityKey, myHostPrivateIdentityKey, connectionKey, requestIdentifier, reportsInfoMap) + if (err != nil) { return nil, err } + + return responseBytes, nil + } + if (requestType == "GetReports"){ + + requestedReportHashesList, err := serverRequest.ReadDecryptedServerRequest_GetReports(decryptedRequestBytes) + if (err != nil) { return nil, err } + + getReportsListForResponse := func()([][]byte, error){ + + // We will reduce report list to only include reports within our hosted range + // This is done to prevent fingerprinting + + // We also make sure reported identities/profiles/messages are funded + + reportsListForResponse := make([][]byte, 0) + + for _, reportHash := range requestedReportHashesList{ + + reportExists, reportBytes, err := badgerDatabase.GetReport(reportHash) + if (err != nil) { return nil, err } + if (reportExists == false){ + continue + } + + ableToRead, currentReportHash, _, reportNetworkType, _, reportType, reportedHash, _, err := readReports.ReadReportAndHash(false, reportBytes) + if (err != nil) { return nil, err } + if (ableToRead == false){ + return nil, errors.New("Database corrupt: Contains invalid report.") + } + if (reportHash != currentReportHash){ + return nil, errors.New("Database corrupt: Contains report with mismatched reportHash entry key") + } + if (reportNetworkType != networkType){ + // Requestor must be malicious + // They should only request reports which we offered them + // We will only have offered them reports on the current networkType + continue + } + + if (reportType == "Message"){ + + if (len(reportedHash) != 26){ + reportedHashHex := encoding.EncodeBytesToHexString(reportedHash) + return nil, errors.New("ReadReport returning invalid length reportedHash for Message report: " + reportedHashHex) + } + + reportedMessageHash := [26]byte(reportedHash) + + metadataExists, _, messageNetworkType, _, messageInbox, _, err := contentMetadata.GetMessageMetadata(reportedMessageHash) + if (err != nil){ return nil, err } + if (metadataExists == false){ + // We cannot host reports for messages whose metadata we do not have. + continue + } + if (messageNetworkType != networkType){ + // This should not happen because we would have already checked for this before serving report info to requestor + // Requestor must be maliciously requesting reports we never offered them + continue + } + + hostModeEnabled, isWithinMyRange, err := myRanges.CheckIfMessageInboxIsWithinMyHostedRange(messageInbox) + if (err != nil) { return nil, err } + if (hostModeEnabled == false){ + // Host mode must have been disabled after earlier check. Return empty list. + emptyList := make([][]byte, 0) + return emptyList, nil + } + if (isWithinMyRange == false){ + continue + } + + // We make sure message is funded + + statusIsKnown, messageIsFunded, _, _, err := verifiedFundedStatus.GetVerifiedMessageIsFundedStatus(reportedMessageHash) + if (err != nil) { return nil, err } + if (statusIsKnown == false || messageIsFunded == false){ + continue + } + + } else { + + // reportType == "Identity" or "Profile" or "Attribute" + + //Outputs: + // -bool: Report is malicious + // -bool: Reported identity hash found + // -[16]byte: Reported identity hash + // -error + getReportedIdentityHash := func()(bool, bool, [16]byte, error){ + + if (reportType == "Identity"){ + + if (len(reportedHash) != 16){ + reportedHashHex := encoding.EncodeBytesToHexString(reportedHash) + return false, false, [16]byte{}, errors.New("ReadReport returning invalid length reportedHash for Identity report: " + reportedHashHex) + } + + reportedIdentityHash := [16]byte(reportedHash) + + return false, true, reportedIdentityHash, nil + } + if (reportType == "Profile"){ + + if (len(reportedHash) != 28){ + reportedHashHex := encoding.EncodeBytesToHexString(reportedHash) + return false, false, [16]byte{}, errors.New("ReadReport returning invalid length reportedHash for Profile report: " + reportedHashHex) + } + + reportedProfileHash := [28]byte(reportedHash) + + metadataExists, _, profileNetworkType, profileAuthor, _, _, _, _, err := contentMetadata.GetProfileMetadata(reportedProfileHash) + if (err != nil) { return false, false, [16]byte{}, err } + if (metadataExists == false){ + return false, false, [16]byte{}, nil + } + if (profileNetworkType != networkType){ + return true, false, [16]byte{}, nil + } + + return false, true, profileAuthor, nil + } + if (reportType == "Attribute"){ + + if (len(reportedHash) != 27){ + reportedHashHex := encoding.EncodeBytesToHexString(reportedHash) + return false, false, [16]byte{}, errors.New("ReadReport returning invalid length reportedHash for Attribute report: " + reportedHashHex) + } + + reportedAttributeHash := [27]byte(reportedHash) + + metadataExists, _, authorIdentityHash, attributeNetworkType, _, err := profileStorage.GetProfileAttributeMetadata(reportedAttributeHash) + if (err != nil){ return false, false, [16]byte{}, err } + if (metadataExists == false){ + return false, false, [16]byte{}, nil + } + if (attributeNetworkType != networkType){ + return true, false, [16]byte{}, nil + } + + return false, true, authorIdentityHash, nil + } + + return false, false, [16]byte{}, errors.New("ReadReport returning invalid reportType: " + reportType) + } + + reportIsMalicious, metadataExists, reportedIdentityHash, err := getReportedIdentityHash() + if (err != nil) { return nil, err } + if (reportIsMalicious == true){ + // The requestor must be malicious, because we should have already checked for this condition + // before serving this report hash in our Get...ReportsInfo responses. + continue + } + if (metadataExists == false){ + // We cannot seed a report whose reported identity hash we do not know + continue + } + + // We make sure identity is within our hosted range + + hostModeEnabled, isInMyRange, err := myRanges.CheckIfIdentityHashIsWithinMyHostedRange(reportedIdentityHash) + if (err != nil) { return nil, err } + if (hostModeEnabled == false){ + // Host mode must have been disabled after earlier check. Return empty list. + emptyList := make([][]byte, 0) + return emptyList, nil + } + + if (isInMyRange == false){ + continue + } + + // We make sure identity is funded + + reportedIdentityType, err := identity.GetIdentityTypeFromIdentityHash(reportedIdentityHash) + if (err != nil){ return nil, err } + + if (reportedIdentityType == "Moderator"){ + + statusIsKnown, _, scoreIsSufficient, _, _, err := moderatorScores.GetModeratorIdentityScore(reportedIdentityHash) + if (err != nil) { return nil, err } + if (statusIsKnown == false || scoreIsSufficient == false){ + continue + } + + } else { + + // reportedIdentityType == "Mate" or "Host" + + statusIsKnown, isFunded, _, _, err := verifiedFundedStatus.GetVerifiedIdentityIsFundedStatus(reportedIdentityHash, networkType) + if (err != nil) { return nil, err } + if (statusIsKnown == false || isFunded == false){ + continue + } + } + + if (reportedIdentityType == "Mate" && reportType == "Profile"){ + + // We make sure profile is funded + + if (len(reportedHash) != 28){ + reportedHashHex := encoding.EncodeBytesToHexString(reportedHash) + return nil, errors.New("ReadReport returning invalid length reportedHash for Profile report: " + reportedHashHex) + } + + reportedProfileHash := [28]byte(reportedHash) + + statusIsKnown, isFunded, _, _, err := verifiedFundedStatus.GetVerifiedMateProfileIsFundedStatus(reportedProfileHash) + if (err != nil) { return nil, err } + if (statusIsKnown == false || isFunded == false){ + continue + } + } else if (reportedIdentityType == "Mate" && reportType == "Attribute"){ + + // We make sure at least 1 profile with this attribute is funded + + if (len(reportedHash) != 27){ + reportedHashHex := encoding.EncodeBytesToHexString(reportedHash) + return nil, errors.New("ReadReport returning invalid length reportedHash for Attribute report: " + reportedHashHex) + } + + reportedAttributeHash := [27]byte(reportedHash) + + checkIfAnyAttributeProfileIsFunded := func()(bool, error){ + + anyExist, attributeProfileHashesList, err := badgerDatabase.GetAttributeProfilesList(reportedAttributeHash) + if (err != nil) { return false, err } + if (anyExist == false){ + return false, nil + } + + for _, profileHash := range attributeProfileHashesList{ + + statusIsKnown, isFunded, _, _, err := verifiedFundedStatus.GetVerifiedMateProfileIsFundedStatus(profileHash) + if (err != nil) { return false, err } + if (statusIsKnown == true && isFunded == true){ + return true, nil + } + } + + return false, nil + } + + anyAttributeProfileIsFunded, err := checkIfAnyAttributeProfileIsFunded() + if (err != nil) { return nil, err } + if (anyAttributeProfileIsFunded == false){ + continue + } + } + } + + reportsListForResponse = append(reportsListForResponse, reportBytes) + } + + return reportsListForResponse, nil + } + + reportsListForResponse, err := getReportsListForResponse() + if (err != nil) { return nil, err } + + responseBytes, err := serverResponse.CreateServerResponse_GetReports(myHostPublicIdentityKey, myHostPrivateIdentityKey, connectionKey, requestIdentifier, reportsListForResponse) + if (err != nil) { return nil, err } + + return responseBytes, nil + } + if (requestType == "GetViewableStatuses"){ + + requestedIdentityHashesList, requestedProfileHashesList, err := serverRequest.ReadDecryptedServerRequest_GetViewableStatuses(decryptedRequestBytes) + if (err != nil){ + return invalidRequestResponse, nil + } + + getIdentityHashStatusesMap := func()(map[[16]byte]bool, error){ + + // Map Structure: Identity Hash -> Is Viewable status + identityHashStatusesMap := make(map[[16]byte]bool) + + for _, userIdentityHash := range requestedIdentityHashesList{ + + hostModeEnabled, isWithinMyRange, err := myRanges.CheckIfIdentityHashIsWithinMyHostedRange(userIdentityHash) + if (err != nil) { return nil, err } + if (hostModeEnabled == false){ + // Host mode has been disabled. We return empty response. + emptyMap := make(map[[16]byte]bool) + return emptyMap, nil + } + if (isWithinMyRange == false){ + continue + } + + // We make sure identity is funded + + userIdentityType, err := identity.GetIdentityTypeFromIdentityHash(userIdentityHash) + if (err != nil) { return nil, err } + + if (userIdentityType == "Moderator"){ + + statusIsKnown, _, scoreIsSufficient, _, _, err := moderatorScores.GetModeratorIdentityScore(userIdentityHash) + if (err != nil) { return nil, err } + if (statusIsKnown == false || scoreIsSufficient == false){ + continue + } + + } else { + + // userIdentityType == "Mate" or "Host" + + statusIsKnown, isFunded, _, _, err := verifiedFundedStatus.GetVerifiedIdentityIsFundedStatus(userIdentityHash, networkType) + if (err != nil) { return nil, err } + if (statusIsKnown == false || isFunded == false){ + continue + } + } + + downloadingRequiredReviews, parametersExist, stickyStatusEstablished, identityIsViewableStatus, err := verifiedStickyStatus.GetVerifiedIdentityIsViewableStickyStatus(userIdentityHash, networkType) + if (err != nil) { return nil, err } + if (downloadingRequiredReviews == false){ + continue + } + if (parametersExist == false){ + // Host needs to download required moderation parameters to know sticky consensuses. + // We skip identities. + break + } + if (stickyStatusEstablished == false){ + continue + } + + identityHashStatusesMap[userIdentityHash] = identityIsViewableStatus + } + + return identityHashStatusesMap, nil + } + + identityHashStatusesMap, err := getIdentityHashStatusesMap() + if (err != nil) { return nil, err } + + getProfileHashStatusesMap := func()(map[[28]byte]bool, error){ + + // Map Structure: Profile Hash -> Is Viewable status + profileHashStatusesMap := make(map[[28]byte]bool) + + for _, profileHash := range requestedProfileHashesList{ + + profileIsDisabled, profileMetadataIsKnown, profileNetworkType, profileAuthor, downloadingRequiredReviews, parametersFound, stickyStatusEstablished, profileIsViewableStatus, err := verifiedStickyStatus.GetVerifiedProfileIsViewableStickyStatus(profileHash) + if (err != nil) { return nil, err } + if (profileMetadataIsKnown == false){ + // We don't know profile author, so we cannot check if author is banned, or if profile is banned. + // We don't know sticky status. We skip profile. + continue + } + if (profileNetworkType != networkType){ + // Requestor must be malicious + // They should only request viewable statuses for profiles on our networkType + continue + } + + if (profileIsDisabled == false){ + if (downloadingRequiredReviews == false){ + continue + } + if (parametersFound == false){ + // Host needs to download required network parameters to host sticky consensuses. + // We skip all profiles. + break + } + if (stickyStatusEstablished == false){ + continue + } + } + + hostModeEnabled, isWithinMyRange, err := myRanges.CheckIfIdentityHashIsWithinMyHostedRange(profileAuthor) + if (err != nil) { return nil, err } + if (hostModeEnabled == false){ + // Host mode has been disabled. Return empty response. + emptyMap := make(map[[28]byte]bool) + return emptyMap, nil + } + if (isWithinMyRange == false){ + continue + } + + // We make sure identity is funded + + profileIdentityType, err := identity.GetIdentityTypeFromIdentityHash(profileAuthor) + if (err != nil){ return nil, err } + + if (profileIdentityType == "Moderator"){ + + statusIsKnown, _, scoreIsSufficient, _, _, err := moderatorScores.GetModeratorIdentityScore(profileAuthor) + if (err != nil) { return nil, err } + if (statusIsKnown == false || scoreIsSufficient == false){ + continue + } + + } else { + + // profileIdentityType == "Mate" or "Host" + + statusIsKnown, isFunded, _, _, err := verifiedFundedStatus.GetVerifiedIdentityIsFundedStatus(profileAuthor, networkType) + if (err != nil) { return nil, err } + if (statusIsKnown == false || isFunded == false){ + continue + } + } + + // If profile is Mate, we make sure it is funded + + if (profileIdentityType == "Mate"){ + + statusIsKnown, isFunded, _, _, err := verifiedFundedStatus.GetVerifiedMateProfileIsFundedStatus(profileHash) + if (err != nil) { return nil, err } + if (statusIsKnown == false || isFunded == false){ + continue + } + } + + if (profileIsDisabled == false && profileIsViewableStatus == false){ + // Profile is unviewable + profileHashStatusesMap[profileHash] = false + continue + } + + // Profile is viewable + // Now we check to make sure identity is not banned + + downloadingRequiredReviews, parametersExist, stickyStatusEstablished, identityIsViewableStatus, err := verifiedStickyStatus.GetVerifiedIdentityIsViewableStickyStatus(profileAuthor, networkType) + if (err != nil) { return nil, err } + if (downloadingRequiredReviews == false){ + continue + } + if (parametersExist == false){ + // Host needs to download required moderation parameters to host sticky consensuses. + // We skip identity hashes for now. + break + } + + if (stickyStatusEstablished == false){ + continue + } + + profileHashStatusesMap[profileHash] = identityIsViewableStatus + } + + return profileHashStatusesMap, nil + } + + profileHashStatusesMap, err := getProfileHashStatusesMap() + if (err != nil) { return nil, err } + + responseBytes, err := serverResponse.CreateServerResponse_GetViewableStatuses(myHostPublicIdentityKey, myHostPrivateIdentityKey, connectionKey, requestIdentifier, identityHashStatusesMap, profileHashStatusesMap) + if (err != nil) { return nil, err } + + return responseBytes, nil + } + if (requestType == "BroadcastContent"){ + + contentTypeToBroadcast, contentBytesList, err := serverRequest.ReadDecryptedServerRequest_BroadcastContent(decryptedRequestBytes) + if (err != nil){ + return invalidRequestResponse, nil + } + + // Map Structure: Content Hash -> We will accept and host + contentAcceptedInfoMap := make(map[string]bool) + + // If host mode is disabled during this process, we will set this bool to true + // If true, we will return an response which has contentAccepted = "No" for all content hashes + hostModeDisabled := false + + for _, contentBytes := range contentBytesList{ + + if (contentTypeToBroadcast == "Parameters"){ + + ableToRead, parametersHash, _, parametersNetworkType, _, _, _, _, err := readParameters.ReadParametersAndHash(false, contentBytes) + if (err != nil) { return nil, err } + if (ableToRead == false){ + return nil, errors.New("ReadDecryptedServerRequest_BroadcastContent not verifying parameters are valid") + } + if (parametersNetworkType != networkType){ + // Requestor must be malicious + // They should only broadcast content which belongs to our networkType. + contentAcceptedInfoMap[string(parametersHash[:])] = false + continue + } + + //TODO: Check if we are hosting parameters + + contentAcceptedInfoMap[string(parametersHash[:])] = true + + _, _, err = parametersStorage.AddParameters(contentBytes) + if (err != nil) { return nil, err } + + continue + } + if (contentTypeToBroadcast == "Message"){ + + ableToRead, messageHash, _, messageNetworkType, messageInbox, _, _, _, _, _, _, err := readMessages.ReadChatMessagePublicDataAndHash(false, contentBytes) + if (err != nil){ + return nil, errors.New("ReadDecryptedServerRequest_BroadcastContent not verifying message is valid") + } + if (ableToRead == false){ + return nil, errors.New("ReadDecryptedServerRequest_BroadcastContent not verifying message is valid") + } + if (messageNetworkType != networkType){ + // Requestor must be malicious + // They should only broadcast content which belongs to our networkType. + contentAcceptedInfoMap[string(messageHash[:])] = false + continue + } + + hostModeEnabled, isWithinMyRange, err := myRanges.CheckIfMessageInboxIsWithinMyHostedRange(messageInbox) + if (err != nil) { return nil, err } + if (hostModeEnabled == false){ + hostModeDisabled = true + contentAcceptedInfoMap[string(messageHash[:])] = false + continue + } + + contentAcceptedInfoMap[string(messageHash[:])] = isWithinMyRange + + if (isWithinMyRange == true){ + + // We add message to database + + messageIsWellFormed, err := chatMessageStorage.AddMessage(contentBytes) + if (err != nil) { return nil, err } + if (messageIsWellFormed == false){ + return nil, errors.New("AddMessage not verifying message the same way ReadChatMessagePublicData is") + } + } + + continue + } + if (contentTypeToBroadcast == "Profile"){ + + ableToRead, profileHash, _, profileNetworkType, profileAuthor, _, _, _, err := readProfiles.ReadProfileAndHash(false, contentBytes) + if (err != nil) { return nil, err } + if (ableToRead == false){ + return nil, errors.New("ReadDecryptedServerRequest_BroadcastContent not verifying profile is valid.") + } + if (profileNetworkType != networkType){ + // Requestor must be malicious + // They should only broadcast content which belongs to our networkType. + contentAcceptedInfoMap[string(profileHash[:])] = false + continue + } + + hostModeEnabled, isWithinMyRange, err := myRanges.CheckIfIdentityHashIsWithinMyHostedRange(profileAuthor) + if (err != nil) { return nil, err } + if (hostModeEnabled == false){ + hostModeDisabled = true + contentAcceptedInfoMap[string(profileHash[:])] = false + continue + } + + contentAcceptedInfoMap[string(profileHash[:])] = isWithinMyRange + + if (isWithinMyRange == true){ + + // We add profile to database + + profileIsWellFormed, addedProfileHash, err := profileStorage.AddUserProfile(contentBytes) + if (err != nil){ return nil, err } + if (profileIsWellFormed == false){ + return nil, errors.New("AddUserProfile not verifying profile the same way ReadProfile is") + } + if (addedProfileHash != profileHash){ + return nil, errors.New("AddUserProfile returning mismatched profile hash") + } + } + + continue + } + if (contentTypeToBroadcast == "Review"){ + + ableToRead, reviewHash, _, reviewNetworkType, _, _, reviewType, reviewedHash, _, _, err := readReviews.ReadReviewAndHash(false, contentBytes) + if (err != nil) { return nil, err } + if (ableToRead == false){ + return nil, errors.New("ReadDecryptedServerRequest_BroadcastContent not verifying review is valid.") + } + if (reviewNetworkType != networkType){ + // Requestor must be malicious + // They should only broadcast content which belongs to our networkType. + contentAcceptedInfoMap[string(reviewHash[:])] = false + continue + } + + //Outputs: + // -bool: Host mode is enabled + // -bool: We will host review + // -error + checkIfWeWillHostReview := func()(bool, bool, error){ + + if (reviewType == "Message"){ + + if (len(reviewedHash) != 26){ + reviewedHashHex := encoding.EncodeBytesToHexString(reviewedHash) + return false, false, errors.New("ReadReview returning invalid length reviewedHash for Message review: " + reviewedHashHex) + } + + reviewedMessageHash := [26]byte(reviewedHash) + + metadataExists, _, messageNetworkType, _, messageInbox, _, err := contentMetadata.GetMessageMetadata(reviewedMessageHash) + if (err != nil) { return false, false, err } + if (metadataExists == false){ + // We cannot accept reviews for messages whose metadata we do not know + // This is because we cannot seed reviews for messages whose metadata we do not know + return true, false, nil + } + if (messageNetworkType != reviewNetworkType){ + // The review is reviewing a message from a different network type + // It is a malicious review. We will not host it. + return true, false, nil + } + + hostModeEnabled, isWithinMyRange, err := myRanges.CheckIfMessageInboxIsWithinMyHostedRange(messageInbox) + if (err != nil) { return false, false, err } + + return hostModeEnabled, isWithinMyRange, nil + } + // reviewType == "Identity" or "Profile" or "Attribute" + + //Outputs: + // -bool: Reviewed identity hash is known + // -bool: Review is malicious + // -[16]byte: Reviewed identity hash + // -error + getReviewedIdentityHash := func()(bool, bool, [16]byte, error){ + + if (reviewType == "Identity"){ + + if (len(reviewedHash) != 16){ + reviewedHashHex := encoding.EncodeBytesToHexString(reviewedHash) + return false, false, [16]byte{}, errors.New("ReadReview returning invalid length reviewedHash for Identity review: " + reviewedHashHex) + } + + reviewedIdentityHash := [16]byte(reviewedHash) + + return true, false, reviewedIdentityHash, nil + } + if (reviewType == "Profile"){ + + if (len(reviewedHash) != 28){ + reviewedHashHex := encoding.EncodeBytesToHexString(reviewedHash) + return false, false, [16]byte{}, errors.New("ReadReview returning invalid length reviewedHash for Profile review: " + reviewedHashHex) + } + + reviewedProfileHash := [28]byte(reviewedHash) + + metadataExists, _, profileNetworkType, reviewedProfileAuthor, _, _, _, _, err := contentMetadata.GetProfileMetadata(reviewedProfileHash) + if (err != nil) { return false, false, [16]byte{}, err } + if (metadataExists == false){ + return false, false, [16]byte{}, nil + } + if (profileNetworkType != reviewNetworkType){ + // Review is reviewing a profile from a different network type + // Review and author of review are malicious + return true, true, [16]byte{}, nil + } + + return true, false, reviewedProfileAuthor, nil + } + if (reviewType == "Attribute"){ + + if (len(reviewedHash) != 27){ + reviewedHashHex := encoding.EncodeBytesToHexString(reviewedHash) + return false, false, [16]byte{}, errors.New("ReadReview returning invalid length reviewedHash for Attribute review: " + reviewedHashHex) + } + + reviewedAttributeHash := [27]byte(reviewedHash) + + metadataExists, _, authorIdentityHash, attributeNetworkType, _, err := profileStorage.GetProfileAttributeMetadata(reviewedAttributeHash) + if (err != nil) { return false, false, [16]byte{}, err } + if (metadataExists == false){ + return false, false, [16]byte{}, nil + } + if (attributeNetworkType != reviewNetworkType){ + // Review is reviewing an attribute from a different network type + // Review and author of review are malicious + return true, true, [16]byte{}, nil + } + + return true, false, authorIdentityHash, nil + } + + return false, false, [16]byte{}, errors.New("ReadReview returning invalid reviewType: " + reviewType) + } + + reviewedIdentityHashIsKnown, reviewIsMalicious, reviewedIdentityHash, err := getReviewedIdentityHash() + if (err != nil) { return false, false, err } + if (reviewedIdentityHashIsKnown == false){ + return true, false, nil + } + if (reviewIsMalicious == true){ + return true, false, nil + } + + hostModeEnabled, isWithinMyRange, err := myRanges.CheckIfIdentityHashIsWithinMyHostedRange(reviewedIdentityHash) + if (err != nil) { return false, false, err } + + return hostModeEnabled, isWithinMyRange, nil + } + + hostModeIsEnabled, weWillHostReview, err := checkIfWeWillHostReview() + if (err != nil) { return nil, err } + if (hostModeIsEnabled == false){ + hostModeDisabled = true + contentAcceptedInfoMap[string(reviewHash[:])] = false + continue + } + if (weWillHostReview == false){ + contentAcceptedInfoMap[string(reviewHash[:])] = false + continue + } + + contentAcceptedInfoMap[string(reviewHash[:])] = true + + // We add review to database + + reviewIsWellFormed, err := reviewStorage.AddReview(contentBytes) + if (err != nil) { return nil, err } + if (reviewIsWellFormed == false){ + return nil, errors.New("ReadReview not verifying review the same way that AddReview is") + } + + continue + } + if (contentTypeToBroadcast == "Report"){ + + ableToRead, reportHash, _, reportNetworkType, _, reportType, reportedHash, _, err := readReports.ReadReportAndHash(false, contentBytes) + if (err != nil) { return nil, err } + if (ableToRead == false){ + return nil, errors.New("ReadDecryptedServerRequest_BroadcastContent not verifying report") + } + if (reportNetworkType != networkType){ + // Requestor must be malicious + // They should only broadcast content which belongs to our networkType. + contentAcceptedInfoMap[string(reportHash[:])] = false + continue + } + + //Outputs: + // -bool: Host mode enabled + // -bool: We will host Report + // -error + checkIfWeWillHostReport := func()(bool, bool, error){ + + if (reportType == "Message"){ + + // We see if we are hosting the reported message + + if (len(reportedHash) != 26){ + reportedHashHex := encoding.EncodeBytesToHexString(reportedHash) + return false, false, errors.New("ReadReport returning invalid length reportedHash for Message report: " + reportedHashHex) + } + + reportedMessageHash := [26]byte(reportedHash) + + metadataExists, _, messageNetworkType, _, messageInbox, _, err := contentMetadata.GetMessageMetadata(reportedMessageHash) + if (err != nil) { return false, false, err } + if (metadataExists == false){ + // We cannot accept reports for messages whose metadata we do not know + // This is because we cannot seed reports for messages whose metadata we do not know + return true, false, nil + } + if (messageNetworkType != reportNetworkType){ + // Report is reporting a message from a different networkType + // Report and report author must be malicious + return true, false, nil + } + + hostModeEnabled, isWithinMyRange, err := myRanges.CheckIfMessageInboxIsWithinMyHostedRange(messageInbox) + if (err != nil) { return false, false, err } + + return hostModeEnabled, isWithinMyRange, nil + } + // reportType = "Identity" or "Profile" or "Attribute" + + //Outputs: + // -bool: Reported identity hash is known + // -bool: Report is malicious + // -[16]byte: Reported identity hash + // -error + getReportedIdentityHash := func()(bool, bool, [16]byte, error){ + + if (reportType == "Identity"){ + + if (len(reportedHash) != 16){ + reportedHashHex := encoding.EncodeBytesToHexString(reportedHash) + return false, false, [16]byte{}, errors.New("ReadReport returning invalid length reportedHash for Identity report: " + reportedHashHex) + } + + reportedIdentityHash := [16]byte(reportedHash) + + return true, false, reportedIdentityHash, nil + } + if (reportType == "Profile"){ + + if (len(reportedHash) != 28){ + reportedHashHex := encoding.EncodeBytesToHexString(reportedHash) + return false, false, [16]byte{}, errors.New("ReadReport returning invalid length reportedHash for Profile report: " + reportedHashHex) + } + + reportedProfileHash := [28]byte(reportedHash) + + metadataExists, _, profileNetworkType, reportedProfileAuthor, _, _, _, _, err := contentMetadata.GetProfileMetadata(reportedProfileHash) + if (err != nil) { return false, false, [16]byte{}, err } + if (metadataExists == false){ + return false, false, [16]byte{}, nil + } + if (profileNetworkType != reportNetworkType){ + // Report is reporting a profile from a different networkType + // Report and report author must be malicious + return true, true, [16]byte{}, nil + } + + return true, false, reportedProfileAuthor, nil + } + if (reportType == "Attribute"){ + + if (len(reportedHash) != 27){ + reportedHashHex := encoding.EncodeBytesToHexString(reportedHash) + return false, false, [16]byte{}, errors.New("ReadReport returning invalid length reportedHash for Attribute report: " + reportedHashHex) + } + + reportedAttributeHash := [27]byte(reportedHash) + + metadataExists, _, authorIdentityHash, attributeNetworkType, _, err := profileStorage.GetProfileAttributeMetadata(reportedAttributeHash) + if (err != nil) { return false, false, [16]byte{}, err } + if (metadataExists == false){ + return false, false, [16]byte{}, nil + } + if (attributeNetworkType != reportNetworkType){ + // Report is reporting an attribute from a different networkType + // Report and report author must be malicious + return true, true, [16]byte{}, nil + } + + return true, false, authorIdentityHash, nil + } + + return false, false, [16]byte{}, errors.New("ReadReport returning invalid reportType: " + reportType) + } + + reportedIdentityHashIsKnown, reportIsMalicious, reportedIdentityHash, err := getReportedIdentityHash() + if (err != nil) { return false, false, err } + if (reportedIdentityHashIsKnown == false){ + return true, false, nil + } + if (reportIsMalicious == true){ + return true, false, nil + } + + hostModeEnabled, isWithinMyRange, err := myRanges.CheckIfIdentityHashIsWithinMyHostedRange(reportedIdentityHash) + if (err != nil) { return false, false, err } + + return hostModeEnabled, isWithinMyRange, nil + } + + hostModeIsEnabled, weWillHostReport, err := checkIfWeWillHostReport() + if (err != nil) { return nil, err } + if (hostModeIsEnabled == false){ + hostModeDisabled = true + contentAcceptedInfoMap[string(reportHash[:])] = false + continue + } + if (weWillHostReport == false){ + contentAcceptedInfoMap[string(reportHash[:])] = false + continue + } + + contentAcceptedInfoMap[string(reportHash[:])] = true + + // We add report to database + + reportIsWellFormed, err := reportStorage.AddReport(contentBytes) + if (err != nil) { return nil, err } + if (reportIsWellFormed == false){ + return nil, errors.New("ReadReport not verifying report the same way as AddReport is") + } + + continue + } + + return nil, errors.New("ReadDecryptedServerRequest_BroadcastContent returning invalid contentTypeToBroadcast: " + contentTypeToBroadcast) + } + + if (hostModeDisabled == true){ + + // Host mode is disabled, so we cannot accept any content + + for contentHash, _ := range contentAcceptedInfoMap{ + contentAcceptedInfoMap[contentHash] = false + } + } + + responseBytes, err := serverResponse.CreateServerResponse_BroadcastContent(myHostPublicIdentityKey, myHostPrivateIdentityKey, connectionKey, requestIdentifier, contentAcceptedInfoMap) + if (err != nil) { return nil, err } + + return responseBytes, nil + } + + //TODO: GetAddressDeposits, GetFundedStatuses + + // Request contains unknown requestType + return invalidRequestResponse, nil +} + + + diff --git a/internal/network/eligibleHosts/eligibleHosts.go b/internal/network/eligibleHosts/eligibleHosts.go new file mode 100644 index 0000000..9654d56 --- /dev/null +++ b/internal/network/eligibleHosts/eligibleHosts.go @@ -0,0 +1,235 @@ + +// eligibleHosts provides a function to get a list of eligible Seekia hosts +// Eligible = not banned, not unreachable, not malicious, not disabled, funded, not blocked, and whose profiles are not expired + +package eligibleHosts + +// The list of hosts we return might not fulfill some of the eligibility requirements if not enough hosts are available who do. +// For example, if a user has not opened their Seekia client since the host profile expiration time has passed, +// all of the stored host profiles will be expired. +// Thus, expired hosts will be included in the eligible hosts list when not enough hosts are available. +// This is necessary to be able to download newer unexpired host profiles + +// If there are insufficient eligible-but-expired hosts, the function will return at least some blocked/banned/unreachable/malicious hosts. +// This is because a malicious moderator could ban all hosts, or we could have falsely marked hosts as being unreachable + +// TODO: The network entry hosts, which are coded into the client and provided in the parameters +// These hosts will always be eligible +// They are the hosts that the client initially connects to, to discover other hosts + +//TODO: Store eligible hosts list in memory and refresh automatically using backgroundJobs? + +import "seekia/internal/helpers" +import "seekia/internal/myBlockedUsers" +import "seekia/internal/network/enabledHosts" +import "seekia/internal/network/unreachableHosts" +import "seekia/internal/network/maliciousHosts" +import "seekia/internal/network/fundedStatus" +import "seekia/internal/profiles/viewableProfiles" +import "seekia/internal/parameters/getParameters" + +import "time" +import "slices" +import "errors" + +func GetEligibleHostsList(networkType byte)([][16]byte, error){ + + isValid := helpers.VerifyNetworkType(networkType) + if (isValid == false){ + networkTypeString := helpers.ConvertByteToString(networkType) + return nil, errors.New("GetEligibleHostsList called with invalid networkType: " + networkTypeString) + } + + enabledHostsList, err := enabledHosts.GetEnabledHostsList(true, networkType) + if (err != nil) { return nil, err } + + helpers.RandomizeListOrder(enabledHostsList) + + // We start by only removing hosts who are blocked + unblockedHostsList := make([][16]byte, 0) + + for _, hostIdentityHash := range enabledHostsList{ + + hostIsBlocked, _, _, _, err := myBlockedUsers.CheckIfUserIsBlocked(hostIdentityHash) + if (err != nil) { return nil, err } + if (hostIsBlocked == true){ + continue + } + + unblockedHostsList = append(unblockedHostsList, hostIdentityHash) + } + + if (len(unblockedHostsList) < 10){ + // We don't have enough hosts to be any more selective + // We will never serve a blocked host, even if no hosts are available. + // This is because a blocked host will always be manually blocked by the user + + return unblockedHostsList, nil + } + + //Outputs: + // -[]string: New list + // -error + fillHostsListWithLessDesireableHosts := func(inputList [][16]byte, lessDesireableHostsList [][16]byte)([][16]byte, error){ + + for _, hostIdentityHash := range lessDesireableHostsList{ + + alreadyAdded := slices.Contains(inputList, hostIdentityHash) + if (alreadyAdded == true){ + continue + } + + inputList = append(inputList, hostIdentityHash) + + if (len(inputList) >= 10){ + return inputList, nil + } + } + + // This is only reached if not enough hosts were available in the lessDesireableHostsList + return nil, errors.New("fillHostsListWithLessDesireableHosts called with lessDesireableHostsList with insufficient hosts.") + } + + nonmaliciousHostsList := make([][16]byte, 0) + + for _, hostIdentityHash := range unblockedHostsList{ + + hostIsMalicious, err := maliciousHosts.CheckIfHostIsMalicious(hostIdentityHash) + if (err != nil) { return nil, err } + if (hostIsMalicious == true){ + continue + } + + nonmaliciousHostsList = append(nonmaliciousHostsList, hostIdentityHash) + } + + if (len(nonmaliciousHostsList) < 10){ + + // We don't have enough hosts to be any more selective + + resultList, err := fillHostsListWithLessDesireableHosts(nonmaliciousHostsList, unblockedHostsList) + if (err != nil) { return nil, err } + + return resultList, nil + } + + reachableHostsList := make([][16]byte, 0) + + for _, hostIdentityHash := range nonmaliciousHostsList{ + + hostIsUnreachableClearnet, err := unreachableHosts.CheckIfHostIsUnreachable(hostIdentityHash, networkType, "Clearnet") + if (err != nil) { return nil, err } + + hostIsUnreachableTor, err := unreachableHosts.CheckIfHostIsUnreachable(hostIdentityHash, networkType, "Tor") + if (err != nil) { return nil, err } + + if (hostIsUnreachableClearnet == true && hostIsUnreachableTor == true){ + continue + } + + reachableHostsList = append(reachableHostsList, hostIdentityHash) + } + + if (len(reachableHostsList) < 10){ + + resultList, err := fillHostsListWithLessDesireableHosts(reachableHostsList, nonmaliciousHostsList) + if (err != nil) { return nil, err } + + return resultList, nil + } + + fundedHostsList := make([][16]byte, 0) + + for _, hostIdentityHash := range reachableHostsList{ + + statusIsKnown, identityIsFunded, err := fundedStatus.GetIdentityIsFundedStatus(hostIdentityHash, networkType) + if (err != nil) { return nil, err } + if (statusIsKnown == true && identityIsFunded == false){ + continue + } + + fundedHostsList = append(fundedHostsList, hostIdentityHash) + } + + if (len(fundedHostsList) < 10){ + + resultList, err := fillHostsListWithLessDesireableHosts(fundedHostsList, reachableHostsList) + if (err != nil) { return nil, err } + + return resultList, nil + } + + viewableHostsList := make([][16]byte, 0) + + // Map structure: Host identity hash -> Newest viewable profile broadcast time + hostBroadcastTimesMap := make(map[[16]byte]int64) + + for _, hostIdentityHash := range fundedHostsList{ + + // Now we check to see if identity is not banned, and has a profile that is viewable + // If the consensus status is unknown, we will allow the user to request from the host + // This is necessary, because otherwise new users and hosts would not be able to connect to any hosts, because + // they would not know the sticky consensus status of any hosts + + //TODO: Check if host is a network entry host, in which case, they cannot be banned + + exists, _, _, _, profileBroadcastTime, _, err := viewableProfiles.GetNewestViewableUserProfile(hostIdentityHash, networkType, true, true, true) + if (err != nil) { return nil, err } + if (exists == false){ + continue + } + + hostBroadcastTimesMap[hostIdentityHash] = profileBroadcastTime + + viewableHostsList = append(viewableHostsList, hostIdentityHash) + } + + if (len(viewableHostsList) < 10){ + + resultList, err := fillHostsListWithLessDesireableHosts(viewableHostsList, fundedHostsList) + if (err != nil) { return nil, err } + + return resultList, nil + } + + // Now we check for hosts whose profiles are active + // If the user has not turned Seekia on and downloaded profiles since the host inactivity expiration duration, + // all host profiles will be expired + + unexpiredHostsList := make([][16]byte, 0) + + for _, hostIdentityHash := range viewableHostsList{ + + hostProfileBroadcastTime, exists := hostBroadcastTimesMap[hostIdentityHash] + if (exists == false){ + return nil, errors.New("hostBroadcastTimesMap missing host identity hash.") + } + + _, maximumExistenceDuration, err := getParameters.GetHostProfileMaximumExistenceDuration(networkType) + if (err != nil) { return nil, err } + + currentTime := time.Now().Unix() + + profileAge := currentTime - hostProfileBroadcastTime + + if (profileAge > maximumExistenceDuration){ + // Host's profile has expired + continue + } + + unexpiredHostsList = append(unexpiredHostsList, hostIdentityHash) + } + + if (len(unexpiredHostsList) < 10){ + + resultList, err := fillHostsListWithLessDesireableHosts(unexpiredHostsList, viewableHostsList) + if (err != nil) { return nil, err } + + return resultList, nil + } + + return unexpiredHostsList, nil +} + + + diff --git a/internal/network/enabledHosts/enabledHosts.go b/internal/network/enabledHosts/enabledHosts.go new file mode 100644 index 0000000..f079189 --- /dev/null +++ b/internal/network/enabledHosts/enabledHosts.go @@ -0,0 +1,144 @@ + +// enabledHosts provides functions to retrieve and refresh a list of all enabled hosts +// An enabled host is a host whose profile is not disabled + +package enabledHosts + +import "seekia/internal/badgerDatabase" +import "seekia/internal/helpers" +import "seekia/internal/profiles/profileStorage" +import "seekia/internal/profiles/readProfiles" + +import "errors" +import "sync" +import "slices" + +var enabledHostsListMutex sync.RWMutex + +// This list stores all enabled hosts +// It will not necessarily be up to date +var enabledHostsList [][16]byte + +// This variable stores the network type that the enabledHostsList belongs to +var enabledHostsListNetworkType byte + +// This function must be called upon application startup, and then again on a regular automatic interval +// It is also used when refreshing viewedHosts +// It should also be called whenever the application network type is changed +func UpdateEnabledHostsList(networkType byte)error{ + + isValid := helpers.VerifyNetworkType(networkType) + if (isValid == false){ + networkTypeString := helpers.ConvertByteToString(networkType) + return errors.New("UpdateEnabledHostsList called with invalid networkType: " + networkTypeString) + } + + _, err := GetEnabledHostsList(false, networkType) + if (err != nil){ return err } + + return nil +} + + +func GetNumberOfEnabledHosts(allowCache bool, networkType byte)(int, error){ + + isValid := helpers.VerifyNetworkType(networkType) + if (isValid == false){ + networkTypeString := helpers.ConvertByteToString(networkType) + return 0, errors.New("GetNumberOfEnabledHosts called with invalid networkType: " + networkTypeString) + } + + currentEnabledHostsList, err := GetEnabledHostsList(allowCache, networkType) + if (err != nil){ return 0, err } + + numberOfHosts := len(currentEnabledHostsList) + + return numberOfHosts, nil +} + +func CheckIfHostIsEnabled(allowCache bool, hostIdentityHash [16]byte, networkType byte)(bool, error){ + + isValid := helpers.VerifyNetworkType(networkType) + if (isValid == false){ + networkTypeString := helpers.ConvertByteToString(networkType) + return false, errors.New("CheckIfHostIsEnabled called with invalid networkType: " + networkTypeString) + } + + currentEnabledHostsList, err := GetEnabledHostsList(allowCache, networkType) + if (err != nil){ return false, err } + + isEnabled := slices.Contains(currentEnabledHostsList, hostIdentityHash) + + return isEnabled, nil +} + +//Outputs: +// -[][16]byte: List of enabled host identity hashes +// -error +func GetEnabledHostsList(allowCache bool, networkType byte)([][16]byte, error){ + + isValid := helpers.VerifyNetworkType(networkType) + if (isValid == false){ + networkTypeString := helpers.ConvertByteToString(networkType) + return nil, errors.New("GetEnabledHostsList called with invalid networkType: " + networkTypeString) + } + + if (allowCache == true){ + + enabledHostsListMutex.RLock() + + if (enabledHostsList != nil && enabledHostsListNetworkType == networkType){ + + // A cache list is generated which belongs to the specified networkType + + listCopy := slices.Clone(enabledHostsList) + + enabledHostsListMutex.RUnlock() + + return listCopy, nil + } + + // We must generate a new enabled hosts cache list + + enabledHostsListMutex.RUnlock() + } + + hostIdentityHashesList, err := badgerDatabase.GetAllProfileIdentityHashes("Host") + if (err != nil) { return nil, err } + + newEnabledHostsList := make([][16]byte, 0) + + for _, identityHash := range hostIdentityHashesList{ + + profileExists, _, _, _, _, newestRawProfileMap, err := profileStorage.GetNewestUserProfile(identityHash, networkType) + if (err != nil) { return nil, err } + if (profileExists == false){ + // Profile does not exist anymore. + // Missing entries from Identity Profiles list will be removed automatically by backgroundJobs + continue + } + + profileIsDisabled, _, err := readProfiles.GetFormattedProfileAttributeFromRawProfileMap(newestRawProfileMap, "Disabled") + if (err != nil) { return nil, err } + if (profileIsDisabled == true){ + continue + } + + newEnabledHostsList = append(newEnabledHostsList, identityHash) + } + + enabledHostsListMutex.Lock() + + enabledHostsList = newEnabledHostsList + enabledHostsListNetworkType = networkType + + listCopy := slices.Clone(enabledHostsList) + + enabledHostsListMutex.Unlock() + + return listCopy, nil +} + + + + diff --git a/internal/network/fundedStatus/fundedStatus.go b/internal/network/fundedStatus/fundedStatus.go new file mode 100644 index 0000000..54bf6c0 --- /dev/null +++ b/internal/network/fundedStatus/fundedStatus.go @@ -0,0 +1,122 @@ + +// fundedStatus provides functions to check the funded status of identities, mate profiles, messages, and reports +// Some content must be funded via the account credit servers +// This package will retrieve from the verified and trusted funded status packages. + +package fundedStatus + +// Be aware that Funded means that the content/identity will be hosted by the network +// It may have been funded at one point in the past, but if it has expired, it is no longer considered Funded + +import "seekia/internal/helpers" +import "seekia/internal/network/verifiedFundedStatus" +import "seekia/internal/network/trustedFundedStatus" + +import "errors" + +//Outputs: +// -bool: Status is known (it may not be fully up to date, but it at least has been checked once) +// -bool: Identity is funded status +// -error +func GetIdentityIsFundedStatus(identityHash [16]byte, networkType byte)(bool, bool, error){ + + isValid := helpers.VerifyNetworkType(networkType) + if (isValid == false){ + networkTypeString := helpers.ConvertByteToString(networkType) + return false, false, errors.New("GetIdentityIsFundedStatus called with invalid networkType: " + networkTypeString) + } + + verifiedStatusIsKnown, verifiedIdentityIsFundedStatus, _, timeOfCheckingVerified, err := verifiedFundedStatus.GetVerifiedIdentityIsFundedStatus(identityHash, networkType) + if (err != nil) { return false, false, err } + + trustedStatusIsKnown, trustedIdentityIsFundedStatus, timeOfCheckingTrusted, err := trustedFundedStatus.GetTrustedIdentityIsFundedStatus(identityHash, networkType) + if (err != nil) { return false, false, err } + + if (verifiedStatusIsKnown == false && trustedStatusIsKnown == false){ + return false, false, nil + } + + if (verifiedStatusIsKnown == true && trustedStatusIsKnown == false){ + return true, verifiedIdentityIsFundedStatus, nil + } + if (verifiedStatusIsKnown == false && trustedStatusIsKnown == true){ + return true, trustedIdentityIsFundedStatus, nil + } + + // Both statuses are known and are the same + if (verifiedIdentityIsFundedStatus == trustedIdentityIsFundedStatus){ + return true, verifiedIdentityIsFundedStatus, nil + } + + // Both statuses are known, and they disagree. + // We rely on the verified status more, unless it is older than the trusted status by at least 1 day + + if (timeOfCheckingTrusted > (timeOfCheckingVerified + 86400)){ + return true, trustedIdentityIsFundedStatus, nil + } + + return true, verifiedIdentityIsFundedStatus, nil +} + + +// Outputs: +// -bool: Status is known +// -bool: Profile is funded status +// -error +func GetMateProfileIsFundedStatus(profileHash [28]byte)(bool, bool, error){ + + statusIsKnown, profileIsFunded, _, _, err := verifiedFundedStatus.GetVerifiedMateProfileIsFundedStatus(profileHash) + if (err != nil) { return false, false, err } + if (statusIsKnown == true){ + return true, profileIsFunded, nil + } + + statusIsKnown, profileIsFunded, err = trustedFundedStatus.GetTrustedMateProfileIsFundedStatus(profileHash) + if (err != nil) { return false, false, err } + if (statusIsKnown == true){ + return true, profileIsFunded, nil + } + + return false, false, nil +} + + +//Outputs: +// -bool: Status is known +// -bool: Message is funded +// -error +func GetMessageIsFundedStatus(messageHash [26]byte)(bool, bool, error){ + + statusIsKnown, messageIsFunded, _, _, err := verifiedFundedStatus.GetVerifiedMessageIsFundedStatus(messageHash) + if (err != nil) { return false, false, err } + if (statusIsKnown == true){ + return true, messageIsFunded, nil + } + + statusIsKnown, messageIsFunded, err = trustedFundedStatus.GetTrustedMessageIsFundedStatus(messageHash) + if (err != nil) { return false, false, err } + if (statusIsKnown == true){ + return true, messageIsFunded, nil + } + + return false, false, nil +} + +func GetReportIsFundedStatus(reportHash [30]byte)(bool, bool, error){ + + statusIsKnown, reportIsFunded, _, _, err := verifiedFundedStatus.GetVerifiedReportIsFundedStatus(reportHash) + if (err != nil) { return false, false, err } + if (statusIsKnown == true){ + return true, reportIsFunded, nil + } + + statusIsKnown, reportIsFunded, err = trustedFundedStatus.GetTrustedReportIsFundedStatus(reportHash) + if (err != nil) { return false, false, err } + if (statusIsKnown == true){ + return true, reportIsFunded, nil + } + + return false, false, nil +} + + diff --git a/internal/network/hostRanges/hostRanges.go b/internal/network/hostRanges/hostRanges.go new file mode 100644 index 0000000..1ff5140 --- /dev/null +++ b/internal/network/hostRanges/hostRanges.go @@ -0,0 +1,135 @@ + +// hostRanges provides functions to retrieve a host's hosted ranges from their profile map + +package hostRanges + +import "seekia/internal/byteRange" +import "seekia/internal/encoding" +import "seekia/internal/messaging/inbox" +import "seekia/internal/profiles/readProfiles" + +import messagepack "github.com/vmihailenco/msgpack/v5" + +import "errors" + +//Outputs: +// -bool: Host is hosting any provided identityType content +// -[16]byte: Hosted Identity hash range start +// -[16]byte: Hosted Identity hash range end +// -error +func GetHostedIdentityHashRangeFromHostRawProfileMap(hostRawProfileMap map[int]messagepack.RawMessage, identityType string)(bool, [16]byte, [16]byte, error){ + + if (identityType != "Mate" && identityType != "Host" && identityType != "Moderator"){ + return false, [16]byte{}, [16]byte{}, errors.New("GetHostedIdentityHashRangeFromHostRawProfileMap called with invalid identityType: " + identityType) + } + + hostIsDisabled, _, err := readProfiles.GetFormattedProfileAttributeFromRawProfileMap(hostRawProfileMap, "Disabled") + if (err != nil) { return false, [16]byte{}, [16]byte{}, err } + if (hostIsDisabled == true){ + return false, [16]byte{}, [16]byte{}, nil + } + + exists, hostingIdentityTypeContent, err := readProfiles.GetFormattedProfileAttributeFromRawProfileMap(hostRawProfileMap, "Hosting" + identityType + "Content") + if (err != nil) { return false, [16]byte{}, [16]byte{}, err } + if (exists == false){ + return false, [16]byte{}, [16]byte{}, errors.New("Database corrupt: Contains Host profile map missing Hosting" + identityType + "Content") + } + + if (hostingIdentityTypeContent != "Yes"){ + return false, [16]byte{}, [16]byte{}, nil + } + if (identityType != "Mate"){ + // Hosts must host either all or none of Host/Moderator content + + minimumIdentityBound, maximumIdentityBound := byteRange.GetMinimumMaximumIdentityHashBounds() + + return true, minimumIdentityBound, maximumIdentityBound, nil + } + + exists, theirMateHostRangeStartString, err := readProfiles.GetFormattedProfileAttributeFromRawProfileMap(hostRawProfileMap, "MateIdentitiesRangeStart") + if (err != nil) { return false, [16]byte{}, [16]byte{}, err } + if (exists == false){ + return false, [16]byte{}, [16]byte{}, errors.New("Database corrupt: Contains host profile missing MateIdentitiesRangeStart") + } + + exists, theirMateHostRangeEndString, err := readProfiles.GetFormattedProfileAttributeFromRawProfileMap(hostRawProfileMap, "MateIdentitiesRangeEnd") + if (err != nil) { return false, [16]byte{}, [16]byte{}, err } + if (exists == false){ + return false, [16]byte{}, [16]byte{}, errors.New("Database corrupt: Contains host profile missing MateIdentitiesRangeEnd") + } + + theirMateHostRangeStartBytes, err := encoding.DecodeHexStringToBytes(theirMateHostRangeStartString) + if (err != nil){ + return false, [16]byte{}, [16]byte{}, errors.New("Database corrupt: Contains host profile containing invalid MateIdentitiesRangeStart: Not Hex: " + theirMateHostRangeStartString) + } + + if (len(theirMateHostRangeStartBytes) != 16){ + return false, [16]byte{}, [16]byte{}, errors.New("Database corrupt: Contains host profile containing invalid MateIdentitiesRangeStart: Invalid length: " + theirMateHostRangeStartString) + } + + theirMateHostRangeStart := [16]byte(theirMateHostRangeStartBytes) + + theirMateHostRangeEndBytes, err := encoding.DecodeHexStringToBytes(theirMateHostRangeEndString) + if (err != nil){ + return false, [16]byte{}, [16]byte{}, errors.New("Database corrupt: Contains host profile containing invalid MateIdentitiesRangeEnd: Not Hex: " + theirMateHostRangeEndString) + } + + if (len(theirMateHostRangeEndBytes) != 16){ + return false, [16]byte{}, [16]byte{}, errors.New("Database corrupt: Contains host profile containing invalid MateIdentitiesRangeEnd: Invalid length: " + theirMateHostRangeEndString) + } + + theirMateHostRangeEnd := [16]byte(theirMateHostRangeEndBytes) + + return true, theirMateHostRangeStart, theirMateHostRangeEnd, nil +} + + +//Outputs: +// -bool: Host is hosting any messages +// -[10]byte: Hosted message Inbox range start +// -[10]byte: Hosted message inbox range end +// -error +func GetHostedMessageInboxesRangeFromHostRawProfileMap(hostRawProfileMap map[int]messagepack.RawMessage)(bool, [10]byte, [10]byte, error){ + + hostIsDisabled, _, err := readProfiles.GetFormattedProfileAttributeFromRawProfileMap(hostRawProfileMap, "Disabled") + if (err != nil) { return false, [10]byte{}, [10]byte{}, err } + if (hostIsDisabled == true){ + return false, [10]byte{}, [10]byte{}, nil + } + + exists, hostingMessages, err := readProfiles.GetFormattedProfileAttributeFromRawProfileMap(hostRawProfileMap, "HostingMessages") + if (err != nil) { return false, [10]byte{}, [10]byte{}, err } + if (exists == false) { + return false, [10]byte{}, [10]byte{}, errors.New("Database corrupt: Contains host profile missing HostingMessages") + } + if (hostingMessages != "Yes"){ + return false, [10]byte{}, [10]byte{}, nil + } + + exists, theirInboxHostRangeStartString, err := readProfiles.GetFormattedProfileAttributeFromRawProfileMap(hostRawProfileMap, "MessageInboxesRangeStart") + if (err != nil) { return false, [10]byte{}, [10]byte{}, err } + if (exists == false){ + return false, [10]byte{}, [10]byte{}, errors.New("Database corrupt: Contains host profile missing MessageInboxesRangeStart") + } + + exists, theirInboxHostRangeEndString, err := readProfiles.GetFormattedProfileAttributeFromRawProfileMap(hostRawProfileMap, "MessageInboxesRangeEnd") + if (err != nil) { return false, [10]byte{}, [10]byte{}, err } + if (exists == false){ + return false, [10]byte{}, [10]byte{}, errors.New("Database corrupt: Contains host profile missing MessageInboxesRangeEnd") + } + + theirInboxHostRangeStart, err := inbox.ReadInboxString(theirInboxHostRangeStartString) + if (err != nil){ + return false, [10]byte{}, [10]byte{}, errors.New("Database corrupt: Contains host profile with invalid MessageInboxesRangeStart: " + theirInboxHostRangeStartString) + } + + theirInboxHostRangeEnd, err := inbox.ReadInboxString(theirInboxHostRangeEndString) + if (err != nil){ + return false, [10]byte{}, [10]byte{}, errors.New("Database corrupt: Contains host profile with invalid MessageInboxesRangeEnd: " + theirInboxHostRangeEndString) + } + + return true, theirInboxHostRangeStart, theirInboxHostRangeEnd, nil +} + + + diff --git a/internal/network/maliciousHosts/maliciousHosts.go b/internal/network/maliciousHosts/maliciousHosts.go new file mode 100644 index 0000000..2abee4d --- /dev/null +++ b/internal/network/maliciousHosts/maliciousHosts.go @@ -0,0 +1,20 @@ + +// maliciousHosts provides functions to keep track of hosts who are malicious +// A host is malicious if they are providing invalid responses in a way that the Seekia client would never do. + +package maliciousHosts + +//TODO: Build package +// A host who is malicious on one networkType is considered malicious on all networks. + + +func CheckIfHostIsMalicious(hostIdentityHash [16]byte)(bool, error){ + + return false, nil +} + + +func AddHostToMaliciousHostsList(hostIdentityHash [16]byte)error{ + + return nil +} diff --git a/internal/network/manualBroadcasts/manualBroadcasts.go b/internal/network/manualBroadcasts/manualBroadcasts.go new file mode 100644 index 0000000..23b3f21 --- /dev/null +++ b/internal/network/manualBroadcasts/manualBroadcasts.go @@ -0,0 +1,1097 @@ + +// manualBroadcasts provides functions to initiate and monitor manual broadcasts +// Manual broadcasts are tasks which run in the background to broadcast profiles, messages, reviews, reports, and parameters +// These broadcasts are initiated by the user through the GUI, and their status can be monitored by the user +// An example of its use is Mate profiles, will will always be manually broadcasted +// This package provides the ability to get the status of each broadcast + +// All broadcasted content will be continually broadcasted in the background, regardless of whether it was broadcasted manually or not +// networkJobs.go provides functions to broadcast content automatically in the background to make sure the content is on the network + +package manualBroadcasts + +//TODO: Call the myReviews.DeleteMyReviewsListCache() function whenever broadcasting a review, or just remove the ability to use +// this package to broadcast reviews. + +import "seekia/internal/byteRange" +import "seekia/internal/contentMetadata" +import "seekia/internal/encoding" +import "seekia/internal/helpers" +import "seekia/internal/identity" +import "seekia/internal/logger" +import "seekia/internal/messaging/readMessages" +import "seekia/internal/moderation/readReports" +import "seekia/internal/moderation/readReviews" +import "seekia/internal/network/eligibleHosts" +import "seekia/internal/network/hostRanges" +import "seekia/internal/network/peerClient" +import "seekia/internal/network/sendRequests" +import "seekia/internal/parameters/readParameters" +import "seekia/internal/profiles/profileStorage" +import "seekia/internal/profiles/readProfiles" +import "seekia/internal/readContent" + +import messagepack "github.com/vmihailenco/msgpack/v5" + +import "time" +import "sync" +import "errors" +import "slices" + +// This object stores data about active processes +type processObject struct{ + + // Set to true once the process is complete. Will be true if we encounter an error + isComplete bool + + // Set to true if we encounter an error + encounteredError bool + + // The error we encountered + errorEncountered error + + // Stores the number of hosts that have been successfully broadcasted to + numberOfSuccessfulBroadcasts int + + // Stores details about process progress, which are shown to user in gui + progressDetails string +} + +var processObjectsMapMutex sync.RWMutex + +var processObjectsMap map[[22]byte]processObject = make(map[[22]byte]processObject) + +//Outputs: +// -[22]byte: New process identifier +// -error +func initializeNewProcessObject()([22]byte, error){ + + processIdentifierBytes, err := helpers.GetNewRandomBytes(22) + if (err != nil) { return [22]byte{}, err } + + processIdentifier := [22]byte(processIdentifierBytes) + + newProcessObject := processObject{ + isComplete: false, + encounteredError: false, + errorEncountered: nil, + numberOfSuccessfulBroadcasts: 0, + progressDetails: "", + } + + processObjectsMapMutex.Lock() + processObjectsMap[processIdentifier] = newProcessObject + processObjectsMapMutex.Unlock() + + return processIdentifier, nil +} + +func increaseProcessSuccessfulBroadcastsCount(processIdentifier [22]byte)error{ + + processObjectsMapMutex.Lock() + defer processObjectsMapMutex.Unlock() + + processObject, exists := processObjectsMap[processIdentifier] + if (exists == false){ + processIdentifierHex := encoding.EncodeBytesToHexString(processIdentifier[:]) + return errors.New("increaseProcessSuccessfulBroadcastsCount called with uninitialized process: " + processIdentifierHex) + } + + processObject.numberOfSuccessfulBroadcasts += 1 + + processObjectsMap[processIdentifier] = processObject + + return nil +} + +func setProcessProgressDetails(processIdentifier [22]byte, newProgressDetails string)error{ + + processObjectsMapMutex.Lock() + defer processObjectsMapMutex.Unlock() + + processObject, exists := processObjectsMap[processIdentifier] + if (exists == false){ + processIdentifierHex := encoding.EncodeBytesToHexString(processIdentifier[:]) + return errors.New("setProcessProgressDetails called with uninitialized process: " + processIdentifierHex) + } + + processObject.progressDetails = newProgressDetails + + processObjectsMap[processIdentifier] = processObject + + return nil +} + +func setProcessEncounteredError(processIdentifier [22]byte, errorEncountered error)error{ + + processObjectsMapMutex.Lock() + defer processObjectsMapMutex.Unlock() + + processObject, exists := processObjectsMap[processIdentifier] + if (exists == false){ + processIdentifierHex := encoding.EncodeBytesToHexString(processIdentifier[:]) + return errors.New("setProcessEncounteredError called with uninitialized process: " + processIdentifierHex) + } + + processObject.isComplete = true + processObject.encounteredError = true + processObject.errorEncountered = errorEncountered + + processObjectsMap[processIdentifier] = processObject + + return nil +} + +func setProcessIsComplete(processIdentifier [22]byte)error{ + + processObjectsMapMutex.Lock() + defer processObjectsMapMutex.Unlock() + + processObject, exists := processObjectsMap[processIdentifier] + if (exists == false){ + processIdentifierHex := encoding.EncodeBytesToHexString(processIdentifier[:]) + return errors.New("setProcessIsComplete called with uninitialized process: " + processIdentifierHex) + } + + processObject.isComplete = true + + processObjectsMap[processIdentifier] = processObject + + return nil +} + + +//Outputs: +// -bool: Process found +// -bool: Process is complete status +// -bool: Process encountered error +// -error: Error that process encountered +// -int: Number of hosts successfully broadcasted to +// -string: Process progress details +func GetProcessInfo(processIdentifier [22]byte)(bool, bool, bool, error, int, string){ + + processObjectsMapMutex.RLock() + defer processObjectsMapMutex.RUnlock() + + processObject, exists := processObjectsMap[processIdentifier] + if (exists == false) { + return false, false, false, nil, 0, "" + } + + processIsComplete := processObject.isComplete + processEncounteredError := processObject.encounteredError + processError := processObject.errorEncountered + processNumberOfSuccessfulBroadcasts := processObject.numberOfSuccessfulBroadcasts + processProgressDetails := processObject.progressDetails + + return true, processIsComplete, processEncounteredError, processError, processNumberOfSuccessfulBroadcasts, processProgressDetails +} + +// A broadcast is considered complete when a specified number of hosts have been contacted and have responded with WillHostContent=Yes +// +// Non-Parameters content provided to this function must be of the same profileIdentityHash/messageInbox +// This is because they must be acceptable by the same hosts +// +// Multiple reviews/reports are not allowed. We will only need to broadcast 1 review/report at a time. +// +//Outputs: +// -bool: At least 1 host found to contact +// -[22]byte: Process Identifier +// -error +func StartContentBroadcast(contentType string, contentNetworkType byte, contentList [][]byte, numberOfHostsToContact int)(bool, [22]byte, error){ + + if (len(contentList) == 0){ + return false, [22]byte{}, errors.New("StartContentBroadcast called with empty contentList") + } + + if (contentType == "Review" || contentType == "Report"){ + if (len(contentList) != 1){ + return false, [22]byte{}, errors.New("StartContentBroadcast does not accept more than 1 review or report") + } + } + + isValid := helpers.VerifyNetworkType(contentNetworkType) + if (isValid == false){ + contentNetworkTypeString := helpers.ConvertByteToString(contentNetworkType) + return false, [22]byte{}, errors.New("StartContentBroadcast called with invalid contentNetworkType: " + contentNetworkTypeString) + } + + hostsToContactList, err := GetAvailableHostsToAcceptBroadcastList(contentType, contentNetworkType, contentList) + if (err != nil) { return false, [22]byte{}, err } + + if (len(hostsToContactList) == 0){ + // No eligible hosts to contact exist. Broadcast is impossible. + // User should wait for their client to download more hosts and then try again. + return false, [22]byte{}, nil + } + + /// Now we get content hashes. + + contentHashesList := make([][]byte, 0, len(contentList)) + + for _, contentBytes := range contentList{ + + ableToRead, contentHash, err := readContent.GetContentHashFromContentBytes(true, contentType, contentBytes) + if (err != nil) { return false, [22]byte{}, err } + if (ableToRead == false){ + return false, [22]byte{}, errors.New("StartContentBroadcast called with invalid " + contentType + " content to broadcast.") + } + + contentHashesList = append(contentHashesList, contentHash) + } + + processIdentifier, err := initializeNewProcessObject() + if (err != nil) { return false, [22]byte{}, err } + + performBroadcasts := func(){ + + var contactedHostsListMutex sync.RWMutex + + // We use this list to prevent broadcasting to the same host twice + contactedHostIdentityHashesList := make([][16]byte, 0) + + checkIfHostHasBeenContacted := func(hostIdentityHash [16]byte)bool{ + + contactedHostsListMutex.RLock() + isContacted := slices.Contains(contactedHostIdentityHashesList, hostIdentityHash) + contactedHostsListMutex.RUnlock() + + return isContacted + } + + addContactedHostIdentityHashToList := func(contactedHostIdentityHash [16]byte){ + contactedHostsListMutex.Lock() + contactedHostIdentityHashesList = append(contactedHostIdentityHashesList, contactedHostIdentityHash) + contactedHostsListMutex.Unlock() + } + + var activeBroadcastsMutex sync.RWMutex + numberOfActiveBroadcasts := 0 + + increaseActiveBroadcasts := func(){ + activeBroadcastsMutex.Lock() + numberOfActiveBroadcasts += 1 + activeBroadcastsMutex.Unlock() + } + decreaseActiveBroadcasts := func(){ + activeBroadcastsMutex.Lock() + numberOfActiveBroadcasts -= 1 + activeBroadcastsMutex.Unlock() + } + getNumberOfActiveBroadcasts := func()int{ + activeBroadcastsMutex.RLock() + result := numberOfActiveBroadcasts + activeBroadcastsMutex.RUnlock() + return result + } + + executeBroadcastToHost := func(hostIdentityHash [16]byte){ + + executeBroadcastToHostFunction := func()error{ + + processFound, processIsComplete, errorEncountered, _, _, _ := GetProcessInfo(processIdentifier) + if (processFound == false){ + processIdentifierHex := encoding.EncodeBytesToHexString(processIdentifier[:]) + return errors.New("executeBroadcastToHostFunction called with uninitialized process: " + processIdentifierHex) + } + if (processIsComplete == true || errorEncountered == true){ + // Error may have been encountered from a different broadcast goroutine + return nil + } + + hostIdentityHashString, identityType, err := identity.EncodeIdentityHashBytesToString(hostIdentityHash) + if (err != nil) { return err } + if (identityType != "Moderator"){ + return errors.New("executeBroadcastToHost called with non-moderator identity hash.") + } + + hostIdentityHashTrimmed, _, err := helpers.TrimAndFlattenString(hostIdentityHashString, 6) + if (err != nil) { return err } + + hostProfileFound, connectionEstablished, connectionIdentifier, err := peerClient.EstablishNewConnectionToHost(false, hostIdentityHash, contentNetworkType) + if (err != nil) { return err } + if (hostProfileFound == false){ + return nil + } + if (connectionEstablished == false){ + err := setProcessProgressDetails(processIdentifier, "Failed to connect to host " + hostIdentityHashTrimmed) + if (err != nil) { return err } + + return nil + } + + err = setProcessProgressDetails(processIdentifier, "Broadcasting to host " + hostIdentityHashTrimmed) + if (err != nil) { return err } + + processFound, processIsComplete, errorEncountered, _, _, _ = GetProcessInfo(processIdentifier) + if (processFound == false){ + // This should not happen + return errors.New("Process not found after being found already.") + } + if (processIsComplete == true || errorEncountered == true){ + // Error may have been encountered from a different broadcast goroutine + return nil + } + + successfulDownload, contentAcceptedInfoMap, err := sendRequests.BroadcastContentToHost(connectionIdentifier, hostIdentityHash, contentNetworkType, contentType, contentList) + if (err != nil) { return err } + if (successfulDownload == false){ + + err := setProcessProgressDetails(processIdentifier, "Failed to broadcast to host " + hostIdentityHashTrimmed) + if (err != nil) { return err } + + return nil + } + + getAllContentAcceptedStatus := func()(bool, error){ + + for _, contentHash := range contentHashesList{ + contentAcceptedStatus, exists := contentAcceptedInfoMap[string(contentHash)] + if (exists == false){ + return false, errors.New("BroadcastContentToHost not verifying contentAcceptedInfoMap contains content hash") + } + if (contentAcceptedStatus == false){ + return false, nil + } + } + return true, nil + } + contentAcceptedStatus, err := getAllContentAcceptedStatus() + if (err != nil) { return err } + if (contentAcceptedStatus == true){ + + err := increaseProcessSuccessfulBroadcastsCount(processIdentifier) + if (err != nil) { return err } + + err = setProcessProgressDetails(processIdentifier, "Successful broadcast to host " + hostIdentityHashTrimmed) + if (err != nil) { return err } + } + + return nil + } + + err := executeBroadcastToHostFunction() + if (err != nil){ + logger.AddLogError("General", err) + err := setProcessEncounteredError(processIdentifier, err) + if (err != nil){ + logger.AddLogError("General", err) + } + } + + decreaseActiveBroadcasts() + } + + startTime := time.Now().Unix() + + for { + + processFound, processIsComplete, errorEncountered, _, numberOfSuccessfulBroadcasts, _ := GetProcessInfo(processIdentifier) + if (processFound == false){ + // This should not happen + logger.AddLogError("General", errors.New("Process not found during manualBroadcasts loop 1.")) + return + } + if (processIsComplete == true || errorEncountered == true){ + return + } + if (numberOfSuccessfulBroadcasts >= numberOfHostsToContact){ + // We have completed the required number of broadcasts + // Nothing left to do + break + } + + activeBroadcasts := getNumberOfActiveBroadcasts() + + pendingAndCompletedBroadcasts := numberOfSuccessfulBroadcasts + activeBroadcasts + + if (pendingAndCompletedBroadcasts >= numberOfHostsToContact){ + + // We are actively broadcasting to the required number of hosts + // We wait to see if we need to retry to another host + + currentTime := time.Now().Unix() + secondsElapsed := currentTime - startTime + + if (secondsElapsed > 150){ + // Something has gone wrong. Broadcasts should timeout before this. + err := setProcessEncounteredError(processIdentifier, errors.New("Broadcast failed: Reached timeout.")) + if (err != nil){ + logger.AddLogError("General", err) + } + return + } + + time.Sleep(time.Second) + + continue + } + + // We need to start another broadcast + // We find a host we have not contacted yet + + //Output: + // -bool: We found a host + startNewBroadcast := func()bool{ + + for _, hostIdentityHash := range hostsToContactList{ + + isContacted := checkIfHostHasBeenContacted(hostIdentityHash) + if (isContacted == false){ + + addContactedHostIdentityHashToList(hostIdentityHash) + + increaseActiveBroadcasts() + + go executeBroadcastToHost(hostIdentityHash) + + return true + } + } + return false + } + + foundHost := startNewBroadcast() + if (foundHost == false){ + // There are not enough hosts to contact the requested numberOfHostsToContact + break + } + } + + // We wait for broadcasts to complete + secondsElapsed := 0 + for { + + processFound, processIsComplete, errorEncountered, _, _, _ := GetProcessInfo(processIdentifier) + if (processFound == false){ + // This should not happen + logger.AddLogError("General", errors.New("Process not found while waiting for manualBroadcasts to complete.")) + return + } + if (processIsComplete == true || errorEncountered == true){ + // Error may have been encountered from a different broadcast goroutine + return + } + + activeBroadcasts := getNumberOfActiveBroadcasts() + if (activeBroadcasts <= 0){ + // We are done waiting for broadcasts to complete + break + } + + time.Sleep(time.Second) + + secondsElapsed += 1 + + if (secondsElapsed > 100){ + // Something has gone wrong. + err := setProcessEncounteredError(processIdentifier, errors.New("Broadcast failed: Reached timeout.")) + if (err != nil){ + logger.AddLogError("General", err) + } + return + } + } + + err := setProcessIsComplete(processIdentifier) + if (err != nil){ + logger.AddLogError("General", err) + return + } + + processFound, processIsComplete, errorEncountered, _, numberOfSuccessfulBroadcasts, _ := GetProcessInfo(processIdentifier) + if (processFound == false){ + // This should not happen + return + } + if (processIsComplete == false){ + // This should not happen + err := setProcessEncounteredError(processIdentifier, errors.New("setProcessIsComplete is not working.")) + if (err != nil) { + logger.AddLogError("General", err) + } + return + } + if (errorEncountered == true){ + return + } + + getFinalBroadcastStatus := func()string{ + + if (numberOfSuccessfulBroadcasts != 0){ + + return "Broadcast complete!" + } + return "Broadcast failed: Hosts unavailable." + } + + finalBroadcastStatus := getFinalBroadcastStatus() + + err = setProcessProgressDetails(processIdentifier, finalBroadcastStatus) + if (err != nil){ + logger.AddLogError("General", err) + return + } + } + + go performBroadcasts() + + return true, processIdentifier, nil +} + + +// This function will return a list of all hosts who are available to accept our content, and will host our content +// Non-Parameters content provided to this function must be of the same profileIdentityHash/messageInbox +// This is because they must be acceptable by the same hosts +// Multiple contents are not allowed for reviews and reports + +func GetAvailableHostsToAcceptBroadcastList(contentType string, contentNetworkType byte, contentList [][]byte)([][16]byte, error){ + + if (contentType != "Parameters" && contentType != "Profile" && contentType != "Message" && contentType != "Review" && contentType != "Report"){ + return nil, errors.New("GetAvailableHostsToAcceptBroadcastList called with invalid contentType: " + contentType) + } + + isValid := helpers.VerifyNetworkType(contentNetworkType) + if (isValid == false){ + contentNetworkTypeString := helpers.ConvertByteToString(contentNetworkType) + return nil, errors.New("GetAvailableHostsToAcceptBroadcastList called with invalid contentNetworkType: " + contentNetworkTypeString) + } + + if (len(contentList) == 0){ + return nil, errors.New("GetAvailableHostsToAcceptBroadcastList called with empty contentList") + } + + if (contentType == "Review" || contentType == "Report"){ + if (len(contentList) != 1){ + return nil, errors.New("GetAvailableHostsToAcceptBroadcastList does not accept more than 1 review or report") + } + } + + // We use this function to determine if host will host the content we are broadcasting + //Outputs: + // -func(hostRawProfileMap map[int]messagepack.RawMessage)(bool, error): Host is hosting my content range + // -error + getCheckIfHostWillHostContentFunction := func()(func(map[int]messagepack.RawMessage)(bool, error), error){ + + if (contentType == "Parameters"){ + + for _, parametersBytes := range contentList{ + + ableToRead, _, parametersNetworkType, _, _, _, _, err := readParameters.ReadParameters(true, parametersBytes) + if (err != nil) { return nil, err } + if (ableToRead == false){ + return nil, errors.New("GetAvailableHostsToAcceptBroadcastList called with invalid parameters.") + } + if (parametersNetworkType != contentNetworkType){ + return nil, errors.New("GetAvailableHostsToAcceptBroadcastList called with parameters of different networkType.") + } + } + + checkIfHostWillHostParametersFunction := func(hostRawProfileMap map[int]messagepack.RawMessage)(bool, error){ + + exists, isHostingParameters, err := readProfiles.GetFormattedProfileAttributeFromRawProfileMap(hostRawProfileMap, "HostingParameters") + if (err != nil) { return false, err } + if (exists == false) { + return false, errors.New("Database corrupt: Contains host profile missing HostingParameters") + } + if (isHostingParameters != "Yes"){ + return false, nil + } + + return true, nil + } + + return checkIfHostWillHostParametersFunction, nil + } + + if (contentType == "Profile"){ + + var profilesAuthor [16]byte + + for index, profileBytes := range contentList{ + + ableToRead, _, profileNetworkType, profileAuthor, _, _, _, err := readProfiles.ReadProfile(true, profileBytes) + if (err != nil) { return nil, err } + if (ableToRead == false){ + return nil, errors.New("Trying to broadcast invalid profile.") + } + if (profileNetworkType != contentNetworkType){ + return nil, errors.New("GetAvailableHostsToAcceptBroadcastList called with profile of different networkType.") + } + + if (index == 0){ + profilesAuthor = profileAuthor + } else { + if (profilesAuthor != profileAuthor){ + return nil, errors.New("Trying to broadcast profiles with different profile authors") + } + } + } + + identityType, err := identity.GetIdentityTypeFromIdentityHash(profilesAuthor) + if (err != nil) { return nil, err } + + checkIfHostWillHostProfileFunction := func(hostRawProfileMap map[int]messagepack.RawMessage)(bool, error){ + + //TODO: Make sure host is hosting unviewable profiles + + hostIsHostingIdentityType, hostRangeStart, hostRangeEnd, err := hostRanges.GetHostedIdentityHashRangeFromHostRawProfileMap(hostRawProfileMap, identityType) + if (err != nil) { return false, err } + if (hostIsHostingIdentityType == false){ + return false, nil + } + if (identityType != "Mate"){ + // Hosts must host either all or none of Host/Moderator profiles + return true, nil + } + + identityIsWithinTheirRange, err := byteRange.CheckIfIdentityHashIsWithinRange(hostRangeStart, hostRangeEnd, profilesAuthor) + if (err != nil) { return false, err } + if (identityIsWithinTheirRange == false){ + return false, nil + } + return true, nil + } + + return checkIfHostWillHostProfileFunction, nil + } + + if (contentType == "Message"){ + + var messagesInbox [10]byte + + for index, messageBytes := range contentList{ + + ableToRead, _, messageNetworkType, messageInbox, _, _, _, _, _, _, err := readMessages.ReadChatMessagePublicData(true, messageBytes) + if (err != nil){ return nil, err } + if (ableToRead == false){ + return nil, errors.New("Trying to broadcast invalid message.") + } + if (messageNetworkType != contentNetworkType){ + return nil, errors.New("GetAvailableHostsToAcceptBroadcastList called with message of different networkType.") + } + + if (index == 0){ + messagesInbox = messageInbox + } else { + if (messagesInbox != messageInbox){ + return nil, errors.New("Trying to broadcast messages with different inbox") + } + } + } + + checkIfHostWillHostMessageFunction := func(hostRawProfileMap map[int]messagepack.RawMessage)(bool, error){ + + hostIsHostingMessages, theirInboxRangeStart, theirInboxRangeEnd, err := hostRanges.GetHostedMessageInboxesRangeFromHostRawProfileMap(hostRawProfileMap) + if (err != nil) { return false, err } + if (hostIsHostingMessages == false){ + return false, nil + } + + inboxIsWithinTheirRange, err := byteRange.CheckIfInboxIsWithinRange(theirInboxRangeStart, theirInboxRangeEnd, messagesInbox) + if (err != nil) { return false, err } + if (inboxIsWithinTheirRange == false){ + return false, nil + } + + return true, nil + } + + return checkIfHostWillHostMessageFunction, nil + } + if (contentType == "Review"){ + + reviewBytes := contentList[0] + + ableToRead, _, reviewNetworkType, _, _, reviewType, reviewedHash, _, _, err := readReviews.ReadReview(true, reviewBytes) + if (err != nil) { return nil, err } + if (ableToRead == false){ + return nil, errors.New("GetAvailableHostsToAcceptBroadcastList called with invalid review") + } + if (reviewNetworkType != contentNetworkType){ + return nil, errors.New("GetAvailableHostsToAcceptBroadcastList called with review of different networkType.") + } + + if (reviewType == "Message"){ + + if (len(reviewedHash) != 26){ + reviewedHashHex := encoding.EncodeBytesToHexString(reviewedHash) + return nil, errors.New("ReadReview returning invalid message reviewedHash: " + reviewedHashHex) + } + + reviewedMessageHash := [26]byte(reviewedHash) + + messageMetadataExists, _, messageNetworkType, _, reviewedMessageInbox, _, err := contentMetadata.GetMessageMetadata(reviewedMessageHash) + if (err != nil) { return nil, err } + + if (messageMetadataExists == true && messageNetworkType != contentNetworkType){ + return nil, errors.New("GetAvailableHostsToAcceptBroadcastList called with review which reviews a message on a different networkType.") + } + + checkIfHostWillHostReviewFunction := func(hostRawProfileMap map[int]messagepack.RawMessage)(bool, error){ + + hostIsHostingMessages, theirInboxRangeStart, theirInboxRangeEnd, err := hostRanges.GetHostedMessageInboxesRangeFromHostRawProfileMap(hostRawProfileMap) + if (err != nil){ return false, err } + if (hostIsHostingMessages == false){ + return false, nil + } + + if (messageMetadataExists == true){ + + inboxIsWithinTheirRange, err := byteRange.CheckIfInboxIsWithinRange(theirInboxRangeStart, theirInboxRangeEnd, reviewedMessageInbox) + if (err != nil) { return false, err } + if (inboxIsWithinTheirRange == false){ + return false, nil + } + return true, nil + } + // messageMetadataExists == false + + // We don't know what inbox the message was sent to + // This should not happen because we presumably just reviewed this message + // We will only contact hosts who are hosting all inboxes + + minimumInboxBound, maximumInboxBound := byteRange.GetMinimumMaximumInboxBounds() + if (theirInboxRangeStart == minimumInboxBound && theirInboxRangeEnd == maximumInboxBound){ + return true, nil + } + return false, nil + } + + return checkIfHostWillHostReviewFunction, nil + } + + // reviewType = "Identity" or "Profile" or "Attribute" + + //Outputs: + // -string: Identity type + // -bool: Identity hash is known + // -[16]byte: Identity hash + // -error + getReviewedIdentityHash := func()(string, bool, [16]byte, error){ + + if (reviewType == "Identity"){ + + if (len(reviewedHash) != 16){ + reviewedHashHex := encoding.EncodeBytesToHexString(reviewedHash) + return "", false, [16]byte{}, errors.New("ReadReview returning invalid Identity reviewedHash: " + reviewedHashHex) + } + + reviewedIdentityHash := [16]byte(reviewedHash) + + reviewedIdentityType, err := identity.GetIdentityTypeFromIdentityHash(reviewedIdentityHash) + if (err != nil) { return "", false, [16]byte{}, err } + + return reviewedIdentityType, true, reviewedIdentityHash, nil + } + if (reviewType == "Profile"){ + + if (len(reviewedHash) != 28){ + reviewedHashHex := encoding.EncodeBytesToHexString(reviewedHash) + return "", false, [16]byte{}, errors.New("ReadReview returning invalid Profile reviewedHash: " + reviewedHashHex) + } + + reviewedProfileHash := [28]byte(reviewedHash) + + reviewedIdentityType, isDisabled, err := readProfiles.ReadProfileHashMetadata(reviewedProfileHash) + if (err != nil) { + reviewedHashHex := encoding.EncodeBytesToHexString(reviewedHash) + return "", false, [16]byte{}, errors.New("ReadReview returning invalid profile review reviewedHash: " + reviewedHashHex) + } + if (isDisabled == true){ + return "", false, [16]byte{}, errors.New("ReadReview returning disabled profile as reviewedHash") + } + + metadataExists, _, profileNetworkType, profileAuthorIdentityHash, _, profileIsDisabled, _, _, err := contentMetadata.GetProfileMetadata(reviewedProfileHash) + if (err != nil) { return "", false, [16]byte{}, err } + if (metadataExists == false){ + return reviewedIdentityType, false, [16]byte{}, nil + } + if (profileNetworkType != contentNetworkType){ + return "", false, [16]byte{}, errors.New("GetAvailableHostsToAcceptBroadcastList called with review reviewing a different network type profile.") + } + if (profileIsDisabled == true){ + return "", false, [16]byte{}, errors.New("GetAvailableHostsToAcceptBroadcastList called with review reviewing disabled profile.") + } + + return reviewedIdentityType, true, profileAuthorIdentityHash, nil + } + // reviewType == "Attribute" + + if (len(reviewedHash) != 27){ + reviewedHashHex := encoding.EncodeBytesToHexString(reviewedHash) + return "", false, [16]byte{}, errors.New("ReadReview returning invalid Attribute reviewedHash: " + reviewedHashHex) + } + + reviewedAttributeHash := [27]byte(reviewedHash) + + authorIdentityType, _, err := readProfiles.ReadAttributeHashMetadata(reviewedAttributeHash) + if (err != nil){ + reviewedHashHex := encoding.EncodeBytesToHexString(reviewedHash) + return "", false, [16]byte{}, errors.New("ReadReview returning invalid Attribute reviewedHash: " + reviewedHashHex) + } + + metadataExists, _, authorIdentityHash, attributeNetworkType, _, err := profileStorage.GetProfileAttributeMetadata(reviewedAttributeHash) + if (err != nil) { return "", false, [16]byte{}, err } + if (metadataExists == false){ + return authorIdentityType, false, [16]byte{}, nil + } + if (attributeNetworkType != contentNetworkType){ + return "", false, [16]byte{}, errors.New("GetAvailableHostsToAcceptBroadcastList called with review reviewing a different network type attribute.") + } + + return authorIdentityType, true, authorIdentityHash, nil + } + + reviewedIdentityType, reviewedIdentityHashKnown, reviewedIdentityHash, err := getReviewedIdentityHash() + if (err != nil) { return nil, err } + + checkIfHostWillHostReviewFunction := func(hostRawProfileMap map[int]messagepack.RawMessage)(bool, error){ + + hostIsHostingIdentityType, hostRangeStart, hostRangeEnd, err := hostRanges.GetHostedIdentityHashRangeFromHostRawProfileMap(hostRawProfileMap, reviewedIdentityType) + if (err != nil) { return false, err } + if (hostIsHostingIdentityType == false){ + return false, nil + } + if (reviewedIdentityType != "Mate"){ + // Hosts must host either all or none of Host/Moderator profiles. They should host this profile. + return true, nil + } + + if (reviewedIdentityHashKnown == true){ + + identityIsWithinTheirRange, err := byteRange.CheckIfIdentityHashIsWithinRange(hostRangeStart, hostRangeEnd, reviewedIdentityHash) + if (err != nil) { return false, err } + if (identityIsWithinTheirRange == false){ + return false, nil + } + return true, nil + } + // reviewedIdentityHashKnown == false + + // This should not happen, because we have presumably just reviewed this identity's profile + // We will require the host to be hosting all identities + + minimumIdentityBound, maximumIdentityBound := byteRange.GetMinimumMaximumIdentityHashBounds() + if (hostRangeStart == minimumIdentityBound && hostRangeEnd == maximumIdentityBound){ + return true, nil + } + + return false, nil + } + + return checkIfHostWillHostReviewFunction, nil + } + // contentType == "Report" + + reportBytes := contentList[0] + + ableToRead, _, reportNetworkType, _, reportType, reportedHash, _, err := readReports.ReadReport(true, reportBytes) + if (err != nil) { return nil, err } + if (ableToRead == false){ + return nil, errors.New("GetAvailableHostsToAcceptBroadcastList called with invalid report.") + } + if (reportNetworkType != contentNetworkType){ + return nil, errors.New("GetAvailableHostsToAcceptBroadcastList called with report of different networkType.") + } + + if (reportType == "Message"){ + + if (len(reportedHash) != 26){ + reportedHashHex := encoding.EncodeBytesToHexString(reportedHash) + return nil, errors.New("ReadReport returning invalid length Message reportedHash: " + reportedHashHex) + } + + reportedMessageHash := [26]byte(reportedHash) + + messageMetadataExists, _, reportedMessageNetworkType, _, reportedMessageInbox, _, err := contentMetadata.GetMessageMetadata(reportedMessageHash) + if (err != nil) { return nil, err } + + if (messageMetadataExists == true && reportedMessageNetworkType != contentNetworkType){ + return nil, errors.New("GetAvailableHostsToAcceptBroadcastList called with report which reports a message from a different networkType.") + } + + checkIfHostWillHostReportFunction := func(hostRawProfileMap map[int]messagepack.RawMessage)(bool, error){ + + hostIsHostingMessages, theirInboxRangeStart, theirInboxRangeEnd, err := hostRanges.GetHostedMessageInboxesRangeFromHostRawProfileMap(hostRawProfileMap) + if (err != nil) { return false, err } + if (hostIsHostingMessages == false){ + return false, nil + } + + if (messageMetadataExists == true){ + + inboxIsWithinTheirRange, err := byteRange.CheckIfInboxIsWithinRange(theirInboxRangeStart, theirInboxRangeEnd, reportedMessageInbox) + if (err != nil) { return false, err } + if (inboxIsWithinTheirRange == false){ + return false, nil + } + return true, nil + } + + // messageMetadataExists == false + + // This should not happen, because we presumably just reported this message + // We will only contact hosts who are hosting all inboxes + + minimumInboxBound, maximumInboxBound := byteRange.GetMinimumMaximumInboxBounds() + + if (theirInboxRangeStart == minimumInboxBound && theirInboxRangeEnd == maximumInboxBound){ + return true, nil + } + return false, nil + } + + return checkIfHostWillHostReportFunction, nil + } + + // reportType = "Identity" or "Profile" or "Attribute" + + //Outputs: + // -string: Identity type + // -bool: Identity hash is known + // -[16]byte: Identity hash + // -error + getReportedIdentityHash := func()(string, bool, [16]byte, error){ + + if (reportType == "Identity"){ + + if (len(reportedHash) != 16){ + reportedHashHex := encoding.EncodeBytesToHexString(reportedHash) + return "", false, [16]byte{}, errors.New("ReadReport returning invalid length Identity reportedHash: " + reportedHashHex) + } + + reportedIdentityHash := [16]byte(reportedHash) + + reportedIdentityType, err := identity.GetIdentityTypeFromIdentityHash(reportedIdentityHash) + if (err != nil) { return "", false, [16]byte{}, err } + + return reportedIdentityType, true, reportedIdentityHash, nil + } + if (reportType == "Profile"){ + + if (len(reportedHash) != 28){ + reportedHashHex := encoding.EncodeBytesToHexString(reportedHash) + return "", false, [16]byte{}, errors.New("ReadReport returning invalid length Identity reportedHash: " + reportedHashHex) + } + + reportedProfileHash := [28]byte(reportedHash) + + reportedIdentityType, profileIsDisabled, err := readProfiles.ReadProfileHashMetadata(reportedProfileHash) + if (err != nil) { + reportedHashHex := encoding.EncodeBytesToHexString(reportedHash) + return "", false, [16]byte{}, errors.New("ReadReport returning invalid profile report reportedHash: " + reportedHashHex) + } + if (profileIsDisabled == true){ + return "", false, [16]byte{}, errors.New("ReadReport returning invalid reportedHash: Profile is disabled.") + } + + metadataExists, _, profileNetworkType, profileAuthorIdentityHash, _, profileIsDisabled, _, _, err := contentMetadata.GetProfileMetadata(reportedProfileHash) + if (err != nil) { return "", false, [16]byte{}, err } + if (metadataExists == false){ + return reportedIdentityType, false, [16]byte{}, nil + } + if (profileNetworkType != contentNetworkType){ + return "", false, [16]byte{}, errors.New("GetAvailableHostsToAcceptBroadcastList called with report reporting a profile from a different networkType.") + } + if (profileIsDisabled == true){ + return "", false, [16]byte{}, errors.New("GetAvailableHostsToAcceptBroadcastList called with report reporting disabled profile.") + } + + return reportedIdentityType, true, profileAuthorIdentityHash, nil + } + // reportType == "Attribute" + + if (len(reportedHash) != 27){ + reportedHashHex := encoding.EncodeBytesToHexString(reportedHash) + return "", false, [16]byte{}, errors.New("ReadReport returning invalid length Attribute reportedHash: " + reportedHashHex) + } + + reportedAttributeHash := [27]byte(reportedHash) + + authorIdentityType, _, err := readProfiles.ReadAttributeHashMetadata(reportedAttributeHash) + if (err != nil) { + reportedHashHex := encoding.EncodeBytesToHexString(reportedHash) + return "", false, [16]byte{}, errors.New("ReadReport returning invalid Attribute reportedHash: " + reportedHashHex) + } + + metadataExists, _, authorIdentityHash, attributeNetworkType, _, err := profileStorage.GetProfileAttributeMetadata(reportedAttributeHash) + if (err != nil) { return "", false, [16]byte{}, err } + if (metadataExists == false){ + return authorIdentityType, false, [16]byte{}, nil + } + if (attributeNetworkType != contentNetworkType){ + return "", false, [16]byte{}, errors.New("GetAvailableHostsToAcceptBroadcastList called with report reporting an attribute from a different networkType.") + } + + return authorIdentityType, true, authorIdentityHash, nil + } + + reportedIdentityType, reportedIdentityHashKnown, reportedIdentityHash, err := getReportedIdentityHash() + if (err != nil) { return nil, err } + + checkIfHostWillHostReportFunction := func(hostRawProfileMap map[int]messagepack.RawMessage)(bool, error){ + + hostIsHostingIdentityType, hostRangeStart, hostRangeEnd, err := hostRanges.GetHostedIdentityHashRangeFromHostRawProfileMap(hostRawProfileMap, reportedIdentityType) + if (err != nil) { return false, err } + if (hostIsHostingIdentityType == false){ + return false, nil + } + if (reportedIdentityType != "Mate"){ + // Hosts must host either all or none of Host/Moderator profiles. They should host this profile. + return true, nil + } + + if (reportedIdentityHashKnown == true){ + + identityIsWithinTheirRange, err := byteRange.CheckIfIdentityHashIsWithinRange(hostRangeStart, hostRangeEnd, reportedIdentityHash) + if (err != nil) { return false, err } + if (identityIsWithinTheirRange == false){ + return false, nil + } + return true, nil + } + // reportedIdentityHashKnown == false + + // This should not happen, because we presumably just reported the profile + // We will only broadcast to hosts whom are hosting all identities + + minimumIdentityBound, maximumIdentityBound := byteRange.GetMinimumMaximumIdentityHashBounds() + if (hostRangeStart == minimumIdentityBound && hostRangeEnd == maximumIdentityBound){ + return true, nil + } + return false, nil + } + + return checkIfHostWillHostReportFunction, nil + } + + checkIfHostWillHostContentFunction, err := getCheckIfHostWillHostContentFunction() + if (err != nil) { return nil, err } + + allEligibleHostIdentityHashesList, err := eligibleHosts.GetEligibleHostsList(contentNetworkType) + if (err != nil) { return nil, err } + + // We use this list to store hosts who are hosting the content we are broadcasting + availableHostsList := make([][16]byte, 0) + + for _, hostIdentityHash := range allEligibleHostIdentityHashesList{ + + exists, _, _, _, _, hostRawProfileMap, err := profileStorage.GetNewestUserProfile(hostIdentityHash, contentNetworkType) + if (err != nil) { return nil, err } + if (exists == false){ + continue + } + + isHostingMyContent, err := checkIfHostWillHostContentFunction(hostRawProfileMap) + if (err != nil) { return nil, err } + if (isHostingMyContent == true){ + availableHostsList = append(availableHostsList, hostIdentityHash) + } + } + + return availableHostsList, nil +} + + + diff --git a/internal/network/manualDownloads/manualDownloads.go b/internal/network/manualDownloads/manualDownloads.go new file mode 100644 index 0000000..ca0e1b1 --- /dev/null +++ b/internal/network/manualDownloads/manualDownloads.go @@ -0,0 +1,684 @@ + +// manualDownloads provides functions to start and monitor manual downloads +// These are downloads that the user initiates through the GUI, such as downloading a profile/review/report +// The user is able to monitor the status of these downloads through the GUI +// These are different from the downloads that are called automatically in the background (see networkJobs.go) + +package manualDownloads + +// Types of downloads: +// -Download a user's profile +// -Download a specific message +// -Download a review by reviewHash or reviewedHash +// -Download a report by reportedHash +// -Download all reviews by a reviewer +// -Download all reviews for a reviewedHash +// -Download all reports for a reportedHash +// -Update a user's identity score/balance + +import "seekia/internal/byteRange" +import "seekia/internal/encoding" +import "seekia/internal/helpers" +import "seekia/internal/identity" +import "seekia/internal/logger" +import "seekia/internal/moderation/trustedViewableStatus" +import "seekia/internal/network/eligibleHosts" +import "seekia/internal/network/hostRanges" +import "seekia/internal/network/peerClient" +import "seekia/internal/network/sendRequests" +import "seekia/internal/profiles/profileStorage" + +import "errors" +import "time" +import "sync" +import "slices" + +// This object stores data about active processes +type processObject struct{ + + // Set to true once process is complete. Will be true if we encounter an error + isComplete bool + + // Set to true if we encounter an error + encounteredError bool + + // The error we encountered + errorEncountered error + + // Stores the number of hosts that have been successfully downloaded from + numberOfSuccessfulDownloads int + + // Stores the number of hosts we tried to download from whom did not have the content + numberOfHostsMissingContent int + + // Stores details about progress status, which are shown to user in GUI + progressDetails string +} + +var processObjectsMapMutex sync.RWMutex + +var processObjectsMap map[[23]byte]processObject = make(map[[23]byte]processObject) + + +//Outputs: +// -[23]byte: New process identifier +// -error +func initializeNewProcessObject()([23]byte, error){ + + processIdentifierBytes, err := helpers.GetNewRandomBytes(23) + if (err != nil) { return [23]byte{}, err } + + processIdentifier := [23]byte(processIdentifierBytes) + + newProcessObject := processObject{ + isComplete: false, + encounteredError: false, + errorEncountered: nil, + numberOfSuccessfulDownloads: 0, + numberOfHostsMissingContent: 0, + progressDetails: "", + } + + processObjectsMapMutex.Lock() + + processObjectsMap[processIdentifier] = newProcessObject + + processObjectsMapMutex.Unlock() + + return processIdentifier, nil +} + +func increaseProcessSuccessfulDownloadsCount(processIdentifier [23]byte)error{ + + processObjectsMapMutex.Lock() + defer processObjectsMapMutex.Unlock() + + processObject, exists := processObjectsMap[processIdentifier] + if (exists == false){ + processIdentifierHex := encoding.EncodeBytesToHexString(processIdentifier[:]) + return errors.New("increaseProcessSuccessfulDownloadsCount called with uninitialized process: " + processIdentifierHex) + } + + processObject.numberOfSuccessfulDownloads += 1 + + processObjectsMap[processIdentifier] = processObject + + return nil +} + +func increaseProcessHostsMissingContentCount(processIdentifier [23]byte)error{ + + processObjectsMapMutex.Lock() + defer processObjectsMapMutex.Unlock() + + processObject, exists := processObjectsMap[processIdentifier] + if (exists == false){ + processIdentifierHex := encoding.EncodeBytesToHexString(processIdentifier[:]) + return errors.New("increaseProcessHostsMissingContentCount called with uninitialized process: " + processIdentifierHex) + } + + processObject.numberOfHostsMissingContent += 1 + + processObjectsMap[processIdentifier] = processObject + + return nil +} + +func setProcessProgressDetails(processIdentifier [23]byte, newProgressDetails string)error{ + + processObjectsMapMutex.Lock() + defer processObjectsMapMutex.Unlock() + + processObject, exists := processObjectsMap[processIdentifier] + if (exists == false){ + processIdentifierHex := encoding.EncodeBytesToHexString(processIdentifier[:]) + return errors.New("setProcessProgressDetails called with uninitialized process: " + processIdentifierHex) + } + + processObject.progressDetails = newProgressDetails + + processObjectsMap[processIdentifier] = processObject + + return nil +} + + +func setProcessEncounteredError(processIdentifier [23]byte, errorEncountered error)error{ + + processObjectsMapMutex.Lock() + defer processObjectsMapMutex.Unlock() + + processObject, exists := processObjectsMap[processIdentifier] + if (exists == false){ + processIdentifierHex := encoding.EncodeBytesToHexString(processIdentifier[:]) + return errors.New("setProcessEncounteredError called with uninitialized process: " + processIdentifierHex) + } + + processObject.isComplete = true + processObject.encounteredError = true + processObject.errorEncountered = errorEncountered + + processObjectsMap[processIdentifier] = processObject + + return nil +} + +func setProcessIsComplete(processIdentifier [23]byte)error{ + + processObjectsMapMutex.Lock() + defer processObjectsMapMutex.Unlock() + + processObject, exists := processObjectsMap[processIdentifier] + if (exists == false){ + processIdentifierHex := encoding.EncodeBytesToHexString(processIdentifier[:]) + return errors.New("setProcessIsComplete called with uninitialized process: " + processIdentifierHex) + } + + processObject.isComplete = true + + processObjectsMap[processIdentifier] = processObject + + return nil +} + +//Outputs: +// -bool: Process found +// -bool: Process is complete status +// -bool: Process encountered error +// -error: Error encountered by process +// -int: Number of hosts successfully downloaded from (These are hosts who gave us the content we are trying to download) +// -int: Number of hosts missing content (These are hosts that have said they do not have the content) +// -string: Process progress details +func GetProcessInfo(processIdentifier [23]byte)(bool, bool, bool, error, int, int, string){ + + processObjectsMapMutex.RLock() + defer processObjectsMapMutex.RUnlock() + + processObject, exists := processObjectsMap[processIdentifier] + if (exists == false) { + return false, false, false, nil, 0, 0, "" + } + + processIsComplete := processObject.isComplete + processEncounteredError := processObject.encounteredError + processError := processObject.errorEncountered + processNumberOfSuccessfulDownloads := processObject.numberOfSuccessfulDownloads + processNumberOfHostsMissingContent := processObject.numberOfHostsMissingContent + processProgressDetails := processObject.progressDetails + + return true, processIsComplete, processEncounteredError, processError, processNumberOfSuccessfulDownloads, processNumberOfHostsMissingContent, processProgressDetails +} + +func EndProcess(processIdentifier [23]byte)error{ + + err := setProcessIsComplete(processIdentifier) + if (err != nil){ return err } + + err = setProcessProgressDetails(processIdentifier, "Download manually stopped.") + if (err != nil) { return err } + + return nil +} + +//Inputs: +// -[16]byte: User identity hash whose profile we are downloading +// -byte: Network type to retrieve from +// -bool: Get viewable profiles only +// -int: Number of hosts to query (This is the number of hosts to request a successful download from) +// -A successful download is one where the host either offered a profile we already have, or we downloaded a new profile +// -If they say they have no profile, the download is not successful +// -int: Maximum number of hosts to query +//Outputs: +// -bool: Any eligible hosts found to download from +// -[23]byte: Process identifier +// -error +func StartNewestUserProfileDownload(userIdentityHash [16]byte, networkType byte, getViewableOnly bool, numberOfHostsToDownloadFrom int, maximumHostsToContact int)(bool, [23]byte, error){ + + //TODO: Add to temporary downloads range so the profile doesn't get deleted automatically by backgroundJobs + + userIdentityType, err := identity.GetIdentityTypeFromIdentityHash(userIdentityHash) + if (err != nil) { + userIdentityHashHex := encoding.EncodeBytesToHexString(userIdentityHash[:]) + return false, [23]byte{}, errors.New("StartNewestUserProfileDownload called with invalid user identity hash: " + userIdentityHashHex) + } + + isValid := helpers.VerifyNetworkType(networkType) + if (isValid == false){ + networkTypeString := helpers.ConvertByteToString(networkType) + return false, [23]byte{}, errors.New("StartNewestUserProfileDownload called with invalid networkType: " + networkTypeString) + } + + allEligibleHostsList, err := eligibleHosts.GetEligibleHostsList(networkType) + if (err != nil) { return false, [23]byte{}, err } + + hostsToContactList := make([][16]byte, 0) + + for _, hostIdentityHash := range allEligibleHostsList{ + + exists, _, _, _, _, hostRawProfileMap, err := profileStorage.GetNewestUserProfile(hostIdentityHash, networkType) + if (err != nil) { return false, [23]byte{}, err } + if (exists == false){ + // Host profile was deleted, skip host + continue + } + + hostIsHostingIdentityType, theirHostRangeStart, theirHostRangeEnd, err := hostRanges.GetHostedIdentityHashRangeFromHostRawProfileMap(hostRawProfileMap, userIdentityType) + if (err != nil) { return false, [23]byte{}, err } + if (hostIsHostingIdentityType == false){ + // Host is not hosting any profiles of this profileType. Skip to next host + continue + } + + isWithinRange, err := byteRange.CheckIfIdentityHashIsWithinRange(theirHostRangeStart, theirHostRangeEnd, userIdentityHash) + if (err != nil) { return false, [23]byte{}, err } + if (isWithinRange == true){ + hostsToContactList = append(hostsToContactList, hostIdentityHash) + } + } + + if (len(hostsToContactList) == 0){ + // No hosts exist who we can download this user's profile from + // User needs to wait for Seekia to download more hosts + return false, [23]byte{}, nil + } + + processIdentifier, err := initializeNewProcessObject() + if (err != nil) { return false, [23]byte{}, err } + + performDownloads := func(){ + + var contactedHostsListMutex sync.RWMutex + + //We use this list to prevent downloading from the same host twice + contactedHostIdentityHashesList := make([][16]byte, 0) + + checkIfHostHasBeenContacted := func(hostIdentityHash [16]byte)bool{ + + contactedHostsListMutex.RLock() + isContacted := slices.Contains(contactedHostIdentityHashesList, hostIdentityHash) + contactedHostsListMutex.RUnlock() + + return isContacted + } + + addContactedHostIdentityHashToList := func(contactedHostIdentityHash [16]byte){ + contactedHostsListMutex.Lock() + contactedHostIdentityHashesList = append(contactedHostIdentityHashesList, contactedHostIdentityHash) + contactedHostsListMutex.Unlock() + } + + var activeDownloadsMutex sync.RWMutex + numberOfActiveDownloads := 0 + + increaseActiveDownloads := func(){ + activeDownloadsMutex.Lock() + numberOfActiveDownloads += 1 + activeDownloadsMutex.Unlock() + } + decreaseActiveDownloads := func(){ + activeDownloadsMutex.Lock() + numberOfActiveDownloads -= 1 + activeDownloadsMutex.Unlock() + } + getNumberOfActiveDownloads := func()int{ + activeDownloadsMutex.RLock() + result := numberOfActiveDownloads + activeDownloadsMutex.RUnlock() + return result + } + + executeDownloadFromHost := func(hostIdentityHash [16]byte){ + + executeDownloadFromHostFunction := func()error{ + + processFound, processIsComplete, errorEncountered, _, _, _, _ := GetProcessInfo(processIdentifier) + if (processFound == false){ + return errors.New("executeDownloadFromHostFunction called with uninitialized process.") + } + if (processIsComplete == true || errorEncountered == true){ + // Error may have been encountered from a different broadcast goroutine + return nil + } + + hostIdentityHashString, identityType, err := identity.EncodeIdentityHashBytesToString(hostIdentityHash) + if (err != nil) { return err } + if (identityType != "Moderator"){ + return errors.New("executeDownloadFromHost called with non-moderator identity hash.") + } + + hostIdentityHashTrimmed, _, err := helpers.TrimAndFlattenString(hostIdentityHashString, 6) + if (err != nil) { return err } + + hostProfileFound, connectionEstablished, connectionIdentifier, err := peerClient.EstablishNewConnectionToHost(false, hostIdentityHash, networkType) + if (err != nil) { return err } + if (hostProfileFound == false){ + return nil + } + if (connectionEstablished == false){ + + err := setProcessProgressDetails(processIdentifier, "Failed to connect to host " + hostIdentityHashTrimmed) + if (err != nil) { return err } + + return nil + } + + err = setProcessProgressDetails(processIdentifier, "Downloading from host " + hostIdentityHashTrimmed) + if (err != nil) { return err } + + minimumRange, maximumRange := byteRange.GetMinimumMaximumIdentityHashBounds() + + identityList := [][16]byte{userIdentityHash} + + successfulDownload, profilesInfoObjectsList, err := sendRequests.GetProfilesInfoFromHost(connectionIdentifier, hostIdentityHash, networkType, userIdentityType, minimumRange, maximumRange, identityList, nil, true, getViewableOnly) + if (err != nil) { return err } + if (successfulDownload == false){ + + err := setProcessProgressDetails(processIdentifier, "Failed to download profile from host " + hostIdentityHashTrimmed) + if (err != nil) { return err } + + return nil + } + + if (len(profilesInfoObjectsList) == 0){ + // Host does not have profiles for this user. + // We don't consider this a successful download + // User profile we are trying to get may not exist anywhere + // We need to make sure user is aware that they may never retrieve user's profile, and all downloads will fail + + err := increaseProcessHostsMissingContentCount(processIdentifier) + if (err != nil) { return err } + + err = setProcessProgressDetails(processIdentifier, "Host " + hostIdentityHashTrimmed + " does not have the user's profile.") + if (err != nil) { return err } + + return nil + } + + if (len(profilesInfoObjectsList) != 1){ + return errors.New("GetProfilesInfoFromHost not validating profilesInfoObjectsList properly") + } + + profilesInfoObject := profilesInfoObjectsList[0] + + profileHash := profilesInfoObject.ProfileHash + profileBroadcastTime := profilesInfoObject.ProfileBroadcastTime + + // We check if we already have this profile + alreadyExists, _, err := profileStorage.GetStoredProfile(profileHash) + if (err != nil){ return err } + if (alreadyExists == true){ + + if (getViewableOnly == true){ + err := trustedViewableStatus.AddTrustedProfileIsViewableStatus(profileHash, hostIdentityHash, true) + if (err != nil) { return err } + err = trustedViewableStatus.AddTrustedIdentityIsViewableStatus(userIdentityHash, hostIdentityHash, networkType, true) + if (err != nil) { return err } + } + + err := increaseProcessSuccessfulDownloadsCount(processIdentifier) + if (err != nil){ return err } + + err = setProcessProgressDetails(processIdentifier, "Host offered profile we already have.") + if (err != nil){ return err } + + return nil + } + + profileHashesToDownloadList := [][28]byte{profileHash} + + expectedProfileIdentityHashesMap := make(map[[28]byte][16]byte) + expectedProfileIdentityHashesMap[profileHash] = userIdentityHash + + expectedProfileBroadcastTimesMap := make(map[[28]byte]int64) + expectedProfileBroadcastTimesMap[profileHash] = profileBroadcastTime + + processFound, processIsComplete, errorEncountered, _, _, _, _ = GetProcessInfo(processIdentifier) + if (processFound == false){ + // This should not happen + return errors.New("Process not found after being found already.") + } + if (processIsComplete == true || errorEncountered == true){ + // Error may have been encountered from a different download goroutine + return nil + } + + downloadSuccessful, listOfProfiles, err := sendRequests.GetProfilesFromHost(connectionIdentifier, hostIdentityHash, networkType, userIdentityType, profileHashesToDownloadList, expectedProfileIdentityHashesMap, expectedProfileBroadcastTimesMap, nil) + if (err != nil) { return err } + if (downloadSuccessful == false){ + + err := setProcessProgressDetails(processIdentifier, "Failed to download profile from host " + hostIdentityHashTrimmed) + if (err != nil){ return err } + + return nil + } + + if (len(listOfProfiles) == 0){ + // Host does not have profiles for this user (after saying they did) + // We don't consider this a successful download + // User profile we are trying to get may not exist anywhere + // We need to make sure user is aware that they may never retrieve user profile, and all downloads will fail + + err := increaseProcessHostsMissingContentCount(processIdentifier) + if (err != nil) { return err } + + err = setProcessProgressDetails(processIdentifier, "Host " + hostIdentityHashTrimmed + " does not have the user's profile.") + if (err != nil) { return err } + + return nil + } + + if (len(listOfProfiles) != 1){ + return errors.New("sendRequests.GetProfilesFromHost not verifying list of profiles matches requested profile hashes list.") + } + + profileBytes := listOfProfiles[0] + + profileIsWellFormed, _, err := profileStorage.AddUserProfile(profileBytes) + if (err != nil) { return err } + if (profileIsWellFormed == false){ + return errors.New("GetProfilesFromHost not verifying profile is well formed") + } + if (getViewableOnly == true){ + + err := trustedViewableStatus.AddTrustedProfileIsViewableStatus(profileHash, hostIdentityHash, true) + if (err != nil) { return err } + + err = trustedViewableStatus.AddTrustedIdentityIsViewableStatus(userIdentityHash, hostIdentityHash, networkType, true) + if (err != nil) { return err } + } + + err = increaseProcessSuccessfulDownloadsCount(processIdentifier) + if (err != nil){ return err } + + err = setProcessProgressDetails(processIdentifier, "Successfully downloaded profile from " + hostIdentityHashTrimmed) + if (err != nil) { return err } + + return nil + } + + err := executeDownloadFromHostFunction() + if (err != nil){ + logger.AddLogError("General", err) + err := setProcessEncounteredError(processIdentifier, err) + if (err != nil){ + logger.AddLogError("General", err) + } + } + + decreaseActiveDownloads() + } + + startTime := time.Now().Unix() + + for { + + processFound, processIsComplete, errorEncountered, _, numberOfSuccessfulDownloads, numberOfHostsMissingContent, _ := GetProcessInfo(processIdentifier) + if (processFound == false){ + // This should not happen + logger.AddLogError("General", errors.New("Process not found during manualDownloads loop 1")) + return + } + if (processIsComplete == true || errorEncountered == true){ + return + } + + if (numberOfSuccessfulDownloads >= numberOfHostsToDownloadFrom){ + // We have completed the required number of downloads + // Nothing left to do + break + } + + totalAttemptedDownloads := numberOfSuccessfulDownloads + numberOfHostsMissingContent + if (totalAttemptedDownloads >= maximumHostsToContact){ + // We have contacted enough hosts + // We are done + break + } + + activeDownloads := getNumberOfActiveDownloads() + + pendingAndCompletedDownloads := numberOfSuccessfulDownloads + activeDownloads + + if (pendingAndCompletedDownloads >= numberOfHostsToDownloadFrom || (numberOfHostsMissingContent + pendingAndCompletedDownloads) >= maximumHostsToContact){ + + // We are actively downloading from the required/maximum number of hosts + // Wait to see if we need to retry to another host + + currentTime := time.Now().Unix() + secondsElapsed := currentTime - startTime + + if (secondsElapsed > 150){ + // Something has gone wrong. Downloads should timeout before this. + err := setProcessEncounteredError(processIdentifier, errors.New("Profile download failed: Reached timeout.")) + if (err != nil) { + logger.AddLogError("General", err) + } + return + } + + time.Sleep(time.Second) + + continue + } + + // We need to start another download + // We find a host we have not contacted yet + + //Outputs: + // -bool: We found a host + startNewDownload := func()bool{ + + for _, hostIdentityHash := range hostsToContactList{ + + isContacted := checkIfHostHasBeenContacted(hostIdentityHash) + if (isContacted == false){ + + addContactedHostIdentityHashToList(hostIdentityHash) + + increaseActiveDownloads() + + go executeDownloadFromHost(hostIdentityHash) + + return true + } + } + return false + } + + foundHost := startNewDownload() + if (foundHost == false){ + // There are not enough hosts to contact the requested numberOfHostsToDownloadFrom + break + } + } + + // We wait for downloads to complete + secondsElapsed := 0 + for { + + processFound, processIsComplete, errorEncountered, _, _, _, _ := GetProcessInfo(processIdentifier) + if (processFound == false){ + // This should not happen + logger.AddLogError("General", errors.New("Process not found while waiting for manual download to complete")) + return + } + if (processIsComplete == true || errorEncountered == true){ + // Error may have been encountered from a different broadcast goroutine + return + } + + activeDownloads := getNumberOfActiveDownloads() + if (activeDownloads <= 0){ + // We are done waiting for downloads to complete + break + } + + time.Sleep(time.Second) + + secondsElapsed += 1 + + if (secondsElapsed > 100){ + // Something has gone wrong. + err := setProcessEncounteredError(processIdentifier, errors.New("Download failed: reached timeout.")) + if (err != nil){ + logger.AddLogError("General", err) + } + return + } + } + + err := setProcessIsComplete(processIdentifier) + if (err != nil){ + logger.AddLogError("General", err) + return + } + + processFound, processIsComplete, errorEncountered, _, numberOfSuccessfulDownloads, numberOfHostsMissingContent, _ := GetProcessInfo(processIdentifier) + if (processFound == false){ + // This should not happen + return + } + if (processIsComplete == false){ + // This should not happen + err := setProcessEncounteredError(processIdentifier, errors.New("setProcessIsComplete is not working.")) + if (err != nil) { + logger.AddLogError("General", err) + } + return + } + if (errorEncountered == true){ + return + } + + getFinalDownloadStatus := func()string{ + + if (numberOfSuccessfulDownloads != 0){ + + numberOfSuccessfulDownloadsString := helpers.ConvertIntToString(numberOfSuccessfulDownloads) + + return "Downloaded profile from " + numberOfSuccessfulDownloadsString + " hosts." + } + if (numberOfHostsMissingContent == 0){ + // We were unable to download from any hosts + return "Download failed: Hosts unavailable." + } + // We downloaded from hosts, but none of them had the user's profile + return "Download failed: Content unavailable." + } + + finalDownloadStatus := getFinalDownloadStatus() + + err = setProcessProgressDetails(processIdentifier, finalDownloadStatus) + if (err != nil){ + logger.AddLogError("General", err) + return + } + } + + go performDownloads() + + return true, processIdentifier, nil +} + + + diff --git a/internal/network/mateCriteria/mateCriteria.go b/internal/network/mateCriteria/mateCriteria.go new file mode 100644 index 0000000..c7cfc4f --- /dev/null +++ b/internal/network/mateCriteria/mateCriteria.go @@ -0,0 +1,171 @@ + +// mateCriteria provides functions to read/create profile criterias for server requests +// These are used to filter mate profiles from a host using the user's desires +// They have their own encoding + +package mateCriteria + +import "seekia/internal/desires/mateDesires" +import "seekia/internal/encoding" +import "seekia/internal/profiles/calculatedAttributes" +import "seekia/internal/profiles/readProfiles" + +import messagepack "github.com/vmihailenco/msgpack/v5" + +import "strings" +import "errors" + +//Outputs: +// -bool: Criteria is valid +// -bool: Profile fulfills criteria +// -error +func CheckIfMateProfileFulfillsCriteria(allowParameters bool, profileVersion int, rawProfileMap map[int]messagepack.RawMessage, criteriaBytes []byte)(bool, bool, error){ + + profileIsDisabled, _, err := readProfiles.GetFormattedProfileAttributeFromRawProfileMap(rawProfileMap, "Disabled") + if (err != nil) { return false, false, err } + if (profileIsDisabled == true){ + return true, false, nil + } + + exists, profileType, err := readProfiles.GetFormattedProfileAttributeFromRawProfileMap(rawProfileMap, "ProfileType") + if (err != nil) { return false, false, err } + if (exists == false){ + return false, false, errors.New("CheckIfMateProfileFulfillsCriteria called profile missing ProfileType.") + } + if (profileType != "Mate"){ + return false, false, errors.New("CheckIfMateProfileFulfillsCriteria called with non-mate profileType: " + profileType) + } + + getAnyProfileAttributeFunction, err := calculatedAttributes.GetRetrieveAnyProfileAttributeIncludingCalculatedFunction(profileVersion, rawProfileMap) + if (err != nil) { return false, false, err } + + criteriaMap := make(map[string]string) + + err = encoding.DecodeMessagePackBytes(false, criteriaBytes, &criteriaMap) + if (err != nil) { return false, false, err } + + getAnyDesireFunction := func(desireName string)(bool, string, error){ + + desireValue, exists := criteriaMap[desireName] + if (exists == false){ + return false, "", nil + } + return true, desireValue, nil + } + + allDesiresList := mateDesires.GetAllDesiresList(false) + + for _, desireName := range allDesiresList{ + + if (desireName == "HasMessagedMe" || + desireName == "IHaveMessaged" || + desireName == "HasRejectedMe" || + desireName == "IsLiked" || + desireName == "IsIgnored" || + desireName == "IsMyContact" || + desireName == "Distance" || + desireName == "OffspringProbabilityOfAnyMonogenicDisease"){ + // These desires are not used in criteria + continue + } + if (desireName == "Wealth"){ + // Criteria does not use the Wealth desire. It instead uses the WealthInGold desire + continue + } + + desireIsFilterAll := strings.HasSuffix(desireName, "_FilterAll") + if (desireIsFilterAll == true){ + // We use these to convey if a desire is a FilterAll desire + continue + } + + desireIsRequireResponse := strings.HasSuffix(desireName, "_RequireResponse") + if (desireIsRequireResponse == true){ + // We use these to convey if a desire is a ResponseRequired desire + continue + } + + if (allowParameters == false){ + + // We do not allow reliance on parameters to determine if the profile passes criteria + // This will only be false when we are trying to determine if a host is malicious + // A host's parameters might be different than our own + // Without this bool, we could falsely believe that a host was malicious + // The host could serve us a profile that fulfills our criteria when using their parameters, + // while our parameters would result in the profile not fulfilling our criteria + + if (desireName == "WealthInGold"){ + // Calculating this requires reliance on the currency exchange rate parameters + continue + } + } + + desireAllowsResponseRequired, attributeNameToCheck := mateDesires.CheckIfDesireAllowsRequireResponse(desireName) + if (desireAllowsResponseRequired == true){ + + // We check to see if the user requires a response + + getRequireResponseBool := func()bool{ + requireResponseStatus, exists := criteriaMap[desireName + "_RequireResponse"] + if (exists == true && requireResponseStatus == "Yes"){ + return true + } + return false + } + + requireResponseBool := getRequireResponseBool() + + profileContainsAttribute, _, _, err := getAnyProfileAttributeFunction(attributeNameToCheck) + if (err != nil) { return false, false, err } + if (profileContainsAttribute == false){ + + // Profile is missing a response + + if (requireResponseBool == true){ + // Criteria requires a response + return true, false, nil + } + // Criteria does not require a response + continue + } + } + + getFilterAllBool := func()bool{ + filterAllStatus, exists := criteriaMap[desireName + "_FilterAll"] + if (exists == true && filterAllStatus == "Yes"){ + return true + } + return false + } + + filterAllBool := getFilterAllBool() + if (filterAllBool == false){ + // User passes this desire + continue + } + + anyDesireExists, desireIsValid, userResponseExists, userFulfillsDesire, err := mateDesires.CheckIfMateProfileFulfillsDesire(desireName, getAnyDesireFunction, getAnyProfileAttributeFunction) + if (err != nil) { return false, false, err } + if (anyDesireExists == false){ + continue + } + if (desireIsValid == false){ + // Criteria is invalid. Requestor must be malicious (or a bug in Seekia). + return false, false, nil + } + if (userResponseExists == false){ + // We already checked for this earlier + return false, false, errors.New("CheckIfMateProfileFulfillsDesire says user has no response, but we already checked.") + } + if (userFulfillsDesire == false){ + return true, false, nil + } + } + + // Profile passed all desires + + return true, true, nil +} + + + diff --git a/internal/network/myAccountCredit/myAccountCredit.go b/internal/network/myAccountCredit/myAccountCredit.go new file mode 100644 index 0000000..a1e067c --- /dev/null +++ b/internal/network/myAccountCredit/myAccountCredit.go @@ -0,0 +1,163 @@ + +// myAccountCredit provides functions to manage a user's credit account +// An account exists inside of the account credit servers +// It is used to fund messages, mate/host identities, profiles, and reports on the network. +// Users interface with their accounts using their account private keys. + +package myAccountCredit + +//TODO: Complete this package +// It must function similarly to a cryptocurrency wallet +// Each key corresponds to an identifier or a cryptocurrency address +// We keep track of the balance of each account +// We add the balance of all accounts to get the total balance +// We must be able to send to other accounts via their identifier. +// -We may have to merge all credit to a single account, unless we implement the ability to spend from multiple in the same transaction +// We must show a fresh receiving address every time the user wants to add credit or share their identifier +// -A fresh address is one that has never received credit +// We do this because address/identifier reuse is bad for privacy. +// For example, if a user shared their identifier to receive funds, they should be able to +// share a different, fresh identifier in the future to add more funds + +import "seekia/internal/convertCurrencies" +import "seekia/internal/helpers" +import "seekia/internal/mySeedPhrases" +import "seekia/internal/network/accountKeys" + +import "errors" + +//Outputs: +// -bool: Parameters exist +// -float64: Specified currency balance +// -error +func GetMyCreditAccountBalanceInAnyCurrency(myIdentityType string, networkType byte, currencyCode string)(bool, float64, error){ + + isValid := helpers.VerifyNetworkType(networkType) + if (isValid == false){ + networkTypeString := helpers.ConvertByteToString(networkType) + return false, 0, errors.New("GetMyCreditAccountBalanceInAnyCurrency called with invalid networkType: " + networkTypeString) + } + + //TODO + totalAppCurrencyBalance := float64(0) + + cryptocurrencyNamesList := []string{"Ethereum", "Cardano"} + + for _, cryptocurrencyName := range cryptocurrencyNamesList{ + + currentCryptoUnitsBalance := int64(100) + + parametersExist, accountCreditAppCurrencyBalance, err := convertCurrencies.ConvertCryptoAtomicUnitsToAnyCurrency(networkType, cryptocurrencyName, currentCryptoUnitsBalance, currencyCode) + if (err != nil){ return false, 0, err } + if (parametersExist == false){ + return false, 0, nil + } + + totalAppCurrencyBalance += accountCreditAppCurrencyBalance + } + + return true, totalAppCurrencyBalance, nil +} + +//Outputs: +// -bool: Identity exists +// -string: Unused account identifier +// -error +func GetAnUnusedCreditAccountIdentifier(myIdentityType string, networkType byte)(bool, string, error){ + + if (myIdentityType != "Mate" && myIdentityType != "Host" && myIdentityType != "Moderator"){ + return false, "", errors.New("GetAnUnusedCreditAccountIdentifier called with invalid identityType: " + myIdentityType) + } + + isValid := helpers.VerifyNetworkType(networkType) + if (isValid == false){ + networkTypeString := helpers.ConvertByteToString(networkType) + return false, "", errors.New("GetAnUnusedCreditAccountIdentifier called with invalid networkType: " + networkTypeString) + } + + identityFound, mySeedPhraseHash, err := mySeedPhrases.GetMySeedPhraseHash(myIdentityType) + if (err != nil) { return false, "", err } + if (identityFound == false){ + return false, "", nil + } + + //TODO: Get unused keys index + // We must keep track of which identifiers are used, and skip those indexes. + keysIndex := 1 + + accountPublicKey, _, err := accountKeys.GetCreditAccountPublicPrivateKeys(mySeedPhraseHash, networkType, "Identifier", keysIndex) + if (err != nil) { return false, "", err } + + accountIdentifier, err := accountKeys.GetAccountIdentifierFromAccountPublicKey(accountPublicKey) + if (err != nil) { return false, "", err } + + return true, accountIdentifier, nil +} + +// This will return a fresh account credit crypto address +// We must avoid address reuse +//Outputs: +// -bool: Identity exists +// -string: Credit account crypto receiving address +// -error +func GetAnUnusedCreditAccountCryptoAddress(myIdentityType string, networkType byte, cryptocurrencyName string)(bool, string, error){ + + if (myIdentityType != "Mate" && myIdentityType != "Host" && myIdentityType != "Moderator"){ + return false, "", errors.New("GetAnUnusedCreditAccountCryptoAddress called with invalid identityType: " + myIdentityType) + } + + isValid := helpers.VerifyNetworkType(networkType) + if (isValid == false){ + networkTypeString := helpers.ConvertByteToString(networkType) + return false, "", errors.New("GetAnUnusedCreditAccountCryptoAddress called with invalid networkType: " + networkTypeString) + } + + if (cryptocurrencyName != "Ethereum" && cryptocurrencyName != "Cardano"){ + return false, "", errors.New("GetAnUnusedCreditAccountCryptoAddress called with invalid cryptocurrencyName: " + cryptocurrencyName) + } + + identityFound, mySeedPhraseHash, err := mySeedPhrases.GetMySeedPhraseHash(myIdentityType) + if (err != nil) { return false, "", err } + if (identityFound == false){ + return false, "", nil + } + + //TODO: Get unused keys index + keysIndex := 1 + + accountPublicKey, _, err := accountKeys.GetCreditAccountPublicPrivateKeys(mySeedPhraseHash, networkType, cryptocurrencyName, keysIndex) + if (err != nil) { return false, "", err } + + _, err = accountKeys.GetCryptocurrencyAddressFromAccountPublicKey(cryptocurrencyName, accountPublicKey) + if (err != nil) { return false, "", err } + + //TODO: Return address once Seekia is complete + + return true, "SeekiaIsNotCompleteYet", nil +} + +//Outputs: +// -bool: Parameters exist +// -bool: Sufficient credit exist +// -error +func FreezeCreditForMessage(messageIdentifier [20]byte, messageNetworkType byte, messageDuration int, messageSize int)(bool, bool, error){ + + //TODO + // Adjust amount of funds frozen as the cost of messages changes? + + return true, true, nil +} + +//Outputs: +// -bool: Frozen credit found +// -error +func ReleaseFrozenCreditForMessage(messageIdentifier [20]byte)(bool, error){ + + //TODO + + return true, nil +} + + + + diff --git a/internal/network/myBroadcastStatus/myBroadcastStatus.go b/internal/network/myBroadcastStatus/myBroadcastStatus.go new file mode 100644 index 0000000..3e7a106 --- /dev/null +++ b/internal/network/myBroadcastStatus/myBroadcastStatus.go @@ -0,0 +1,10 @@ + + +// myBroadcastStatus provides functions to track how many times a user's content has been broadcast, when it was broadcast, and to whom. +// This helps to track which content is the least broadcasted and would benefit the most from a rebroadcast. +// For example, we want to prioritize broadcasting messages we have not broadcasted yet over ones which we have already broadcasted many times. + +package myBroadcastStatus + +//TODO: Build this package +// We can also stop broadcasting messages once the recipient has acknowledged that they have seen the message. diff --git a/internal/network/myBroadcasts/myBroadcasts.go b/internal/network/myBroadcasts/myBroadcasts.go new file mode 100644 index 0000000..a9faa9c --- /dev/null +++ b/internal/network/myBroadcasts/myBroadcasts.go @@ -0,0 +1,601 @@ + +// myBroadcasts provides functions to manage a user's broadcast content +// This is content that will be broadcast or has already been broadcast +// The app will automatically broadcast the content in the background + +package myBroadcasts + +//TODO: Add reports and parameters +//TODO: Keep track of how many times each piece of content has been broadcast, and use that information to +// broadcast older content less over time +//TODO: Add functions to prune old broadcasted profiles, messages, reports, and parameters + +import "seekia/internal/cryptography/nacl" +import "seekia/internal/cryptography/kyber" +import "seekia/internal/encoding" +import "seekia/internal/helpers" +import "seekia/internal/identity" +import "seekia/internal/localFilesystem" +import "seekia/internal/messaging/myChatKeys" +import "seekia/internal/messaging/readMessages" +import "seekia/internal/moderation/mySkippedContent" +import "seekia/internal/moderation/readReviews" +import "seekia/internal/moderation/reviewStorage" +import "seekia/internal/myIdentity" +import "seekia/internal/network/appNetworkType/getAppNetworkType" +import "seekia/internal/profiles/calculatedAttributes" +import "seekia/internal/profiles/myProfileExports" +import "seekia/internal/profiles/profileStorage" +import "seekia/internal/profiles/readProfiles" + +import messagepack "github.com/vmihailenco/msgpack/v5" + +import goFilepath "path/filepath" +import "os" +import "errors" + +func InitializeMyBroadcastsFolders()error{ + + userDirectory, err := localFilesystem.GetAppUserFolderPath() + if (err != nil) { return err } + + myBroadcastsFolderpath := goFilepath.Join(userDirectory, "MyBroadcasts") + + _, err = localFilesystem.CreateFolder(myBroadcastsFolderpath) + if (err != nil) { return err } + + profilesFolderpath := goFilepath.Join(userDirectory, "MyBroadcasts", "Profiles") + messagesFolderpath := goFilepath.Join(userDirectory, "MyBroadcasts", "Messages") + reportsFolderpath := goFilepath.Join(userDirectory, "MyBroadcasts", "Reports") + parametersFolderpath := goFilepath.Join(userDirectory, "MyBroadcasts", "Parameters") + + folderpathsList := []string{profilesFolderpath, messagesFolderpath, reportsFolderpath, parametersFolderpath} + + for _, folderpath := range folderpathsList{ + + _, err := localFilesystem.CreateFolder(folderpath) + if (err != nil) { return err } + + network1Folderpath := goFilepath.Join(folderpath, "Network1") + network2Folderpath := goFilepath.Join(folderpath, "Network2") + + _, err = localFilesystem.CreateFolder(network1Folderpath) + if (err != nil) { return err } + + _, err = localFilesystem.CreateFolder(network2Folderpath) + if (err != nil) { return err } + } + + reviewsFolderpath := goFilepath.Join(userDirectory, "MyBroadcasts", "Reviews") + + _, err = localFilesystem.CreateFolder(reviewsFolderpath) + if (err != nil) { return err } + + // We create reviews subfolders for each reviewType + // We use subfolders so retrieval is faster + // The speedup will be significant for moderators with tens of thousands of reviews + // Reports do not need subfolders because users will typically create very few reports + + identityReviewsFolderpath := goFilepath.Join(userDirectory, "MyBroadcasts", "Reviews", "Identity") + profileReviewsFolderpath := goFilepath.Join(userDirectory, "MyBroadcasts", "Reviews", "Profile") + attributeReviewsFolderpath := goFilepath.Join(userDirectory, "MyBroadcasts", "Reviews", "Attribute") + messageReviewsFolderpath := goFilepath.Join(userDirectory, "MyBroadcasts", "Reviews", "Message") + + folderpathsList = []string{identityReviewsFolderpath, profileReviewsFolderpath, attributeReviewsFolderpath, messageReviewsFolderpath} + + for _, folderpath := range folderpathsList{ + + _, err := localFilesystem.CreateFolder(folderpath) + if (err != nil) { return err } + + network1Folderpath := goFilepath.Join(folderpath, "Network1") + network2Folderpath := goFilepath.Join(folderpath, "Network2") + + _, err = localFilesystem.CreateFolder(network1Folderpath) + if (err != nil) { return err } + + _, err = localFilesystem.CreateFolder(network2Folderpath) + if (err != nil) { return err } + } + + return nil +} + +//Outputs: +// -bool: My Identity exists +// -bool: Profile exists +// -int: Profile version +// -bool: Attribute exists +// -string: Attribute value +// -error +func GetAnyAttributeFromMyBroadcastProfile(myIdentityHash [16]byte, networkType byte, attribute string)(bool, bool, int, bool, string, error){ + + isValid := helpers.VerifyNetworkType(networkType) + if (isValid == false){ + networkTypeString := helpers.ConvertByteToString(networkType) + return false, false, 0, false, "", errors.New("GetAnyAttributeFromMyBroadcastProfile called with invalid networkType: " + networkTypeString) + } + + identityExists, profileExists, _, getAnyAttributeFunction, err := GetRetrieveAnyAttributeFromMyBroadcastProfileFunction(myIdentityHash, networkType) + if (err != nil) { return false, false, 0, false, "", err } + if (identityExists == false){ + return false, false, 0, false, "", nil + } + if (profileExists == false){ + return true, false, 0, false, "", nil + } + + attributeExists, profileVersion, attributeValue, err := getAnyAttributeFunction(attribute) + if (err != nil) { return false, false, 0, false, "", err } + if (attributeExists == false){ + return true, true, 0, false, "", nil + } + + return true, true, profileVersion, true, attributeValue, nil +} + +//Outputs: +// -bool: My identity exists +// -bool: Profile exists +// -[28]byte: Profile hash +// -func(attributeName string)(bool, int, string, error) +// -error +func GetRetrieveAnyAttributeFromMyBroadcastProfileFunction(myIdentityHash [16]byte, networkType byte)(bool, bool, [28]byte, func(string)(bool, int, string, error), error){ + + isValid := helpers.VerifyNetworkType(networkType) + if (isValid == false){ + networkTypeString := helpers.ConvertByteToString(networkType) + return false, false, [28]byte{}, nil, errors.New("GetRetrieveAnyAttributeFromMyBroadcastProfileFunction called with invalid networkType: " + networkTypeString) + } + + identityExists, _, err := myIdentity.CheckIfIdentityHashIsMine(myIdentityHash) + if (err != nil) { return false, false, [28]byte{}, nil, err } + if (identityExists == false){ + return false, false, [28]byte{}, nil, nil + } + + profileExists, profileVersion, profileHash, _, rawProfileMap, err := GetMyNewestBroadcastProfile(myIdentityHash, networkType) + if (err != nil) { return false, false, [28]byte{}, nil, err } + if (profileExists == false){ + return true, false, [28]byte{}, nil, nil + } + + getAnyAttributeFunction, err := calculatedAttributes.GetRetrieveAnyProfileAttributeIncludingCalculatedFunction(profileVersion, rawProfileMap) + if (err != nil) { return false, false, [28]byte{}, nil, err } + + return true, true, profileHash, getAnyAttributeFunction, nil +} + +//Outputs: +// -bool: Profile found +// -int: Profile version +// -[28]byte: Profile hash +// -[]byte: Profile bytes +// -map[int]messagepack.RawMessage: Raw profile map +// -error +func GetMyNewestBroadcastProfile(myIdentityHash [16]byte, networkType byte)(bool, int, [28]byte, []byte, map[int]messagepack.RawMessage, error){ + + identityExists, _, err := myIdentity.CheckIfIdentityHashIsMine(myIdentityHash) + if (err != nil) { return false, 0, [28]byte{}, nil, nil, err } + if (identityExists == false){ + return false, 0, [28]byte{}, nil, nil, errors.New("GetMyNewestBroadcastProfile called with identity that is not mine.") + } + + isValid := helpers.VerifyNetworkType(networkType) + if (isValid == false){ + networkTypeString := helpers.ConvertByteToString(networkType) + return false, 0, [28]byte{}, nil, nil, errors.New("GetMyNewestBroadcastProfile called with invalid networkType: " + networkTypeString) + } + + userDirectory, err := localFilesystem.GetAppUserFolderPath() + if (err != nil) { return false, 0, [28]byte{}, nil, nil, err } + + networkTypeString := helpers.ConvertByteToString(networkType) + + networkTypeFoldername := "Network" + networkTypeString + + profilesFolderPath := goFilepath.Join(userDirectory, "MyBroadcasts", "Profiles", networkTypeFoldername) + + broadcastProfilesList, err := localFilesystem.GetAllFilesInFolderAsList(profilesFolderPath) + if (err != nil) { return false, 0, [28]byte{}, nil, nil, err } + + anyProfileFound := false + newestProfileVersion := 0 + var newestProfileHash [28]byte + newestProfileBytes := make([]byte, 0) + newestProfileRawProfileMap := make(map[int]messagepack.RawMessage) + newestProfileBroadcastTime := int64(0) + + for _, profileBytes := range broadcastProfilesList{ + + ableToRead, profileHash, profileVersion, profileNetworkType, profileIdentityHash, profileBroadcastTime, _, rawProfileMap, err := readProfiles.ReadProfileAndHash(true, profileBytes) + if (err != nil) { return false, 0, [28]byte{}, nil, nil, err } + if (ableToRead == false){ + return false, 0, [28]byte{}, nil, nil, errors.New("MyBroadcasts contains invalid profile.") + } + if (profileNetworkType != networkType){ + return false, 0, [28]byte{}, nil, nil, errors.New("MyBroadcasts contains profile for different networkType.") + } + + if (profileIdentityHash != myIdentityHash){ + continue + } + + if (anyProfileFound == false || profileBroadcastTime > newestProfileBroadcastTime){ + + anyProfileFound = true + newestProfileVersion = profileVersion + newestProfileHash = profileHash + newestProfileBytes = profileBytes + newestProfileRawProfileMap = rawProfileMap + newestProfileBroadcastTime = profileBroadcastTime + } + } + + if (anyProfileFound == false){ + return false, 0, [28]byte{}, nil, nil, nil + } + + return true, newestProfileVersion, newestProfileHash, newestProfileBytes, newestProfileRawProfileMap, nil +} + +// This function overwrites an identity's existing broadcast profile with current exported profile + +//Outputs: +// -bool: My identity exists +// -[28]byte: Profile hash of new broadcast profile +// -error +func UpdateMyBroadcastProfile(myIdentityType string, networkType byte)(bool, [28]byte, error){ + + isValid := helpers.VerifyNetworkType(networkType) + if (isValid == false){ + networkTypeString := helpers.ConvertByteToString(networkType) + return false, [28]byte{}, errors.New("UpdateMyBroadcastProfile called with invalid networkType: " + networkTypeString) + } + + myIdentityExists, myIdentityHash, err := myIdentity.GetMyIdentityHash(myIdentityType) + if (err != nil) { return false, [28]byte{}, err } + if (myIdentityExists == false){ + return false, [28]byte{}, nil + } + + newProfileFound, exportProfileHash, newProfileBytes, _, err := myProfileExports.GetMyExportedProfile(myIdentityType, networkType) + if (err != nil) { return false, [28]byte{}, err } + if (newProfileFound == false){ + return false, [28]byte{}, errors.New("UpdateMyBroadcastProfile called when export profile is missing.") + } + + ableToRead, newProfileHash, _, newProfileNetworkType, profileIdentityHash, newProfileBroadcastTime, _, newProfileRawProfileMap, err := readProfiles.ReadProfileAndHash(true, newProfileBytes) + if (err != nil) { return false, [28]byte{}, err } + if (ableToRead == false){ + return false, [28]byte{}, errors.New("MyExports contains invalid profile.") + } + if (newProfileNetworkType != networkType){ + return false, [28]byte{}, errors.New("GetMyExportedProfile returning profile with different networkType.") + } + if (exportProfileHash != newProfileHash){ + return false, [28]byte{}, errors.New("GetMyExportedProfile returning different profileHash than the profileBytes") + } + if (profileIdentityHash != myIdentityHash){ + return false, [28]byte{}, errors.New("New profile identity hash is not mine.") + } + + checkIfProfileHasChatKeys := func()(bool, error){ + + profileIsDisabled, _, err := readProfiles.GetFormattedProfileAttributeFromRawProfileMap(newProfileRawProfileMap, "Disabled") + if (err != nil) { return false, err } + if (profileIsDisabled == true){ + return false, nil + } + if (myIdentityType == "Mate" || myIdentityType == "Moderator"){ + return true, nil + } + return false, nil + } + + profileHasChatKeys, err := checkIfProfileHasChatKeys() + if (err != nil) { return false, [28]byte{}, err } + if (profileHasChatKeys == true){ + + // We deal with updating the user's latest chat keys update time + // We keep track of this locally and send it within all of our chat messages + // The exported profile's chat keys latest update time will be accurate, based on our existing chat keys + + exists, newProfileChatKeysLatestUpdateTimeString, err := readProfiles.GetFormattedProfileAttributeFromRawProfileMap(newProfileRawProfileMap, "ChatKeysLatestUpdateTime") + if (err != nil) { return false, [28]byte{}, err } + if (exists == false){ + return false, [28]byte{}, errors.New("Invalid exported profile: Missing ChatKeysLatestUpdateTime") + } + + newProfileChatKeysLatestUpdateTime, err := helpers.ConvertStringToInt64(newProfileChatKeysLatestUpdateTimeString) + if (err != nil) { + return false, [28]byte{}, errors.New("Invalid exported profile: Contains invalid ChatKeysLatestUpdateTime: " + newProfileChatKeysLatestUpdateTimeString) + } + + getLatestChatKeysTimeNeedsUpdateBool := func()(bool, error){ + + latestUpdateTimeExists, existingChatKeysLatestUpdateTime, err := myChatKeys.GetMyChatKeysLatestUpdateTime(myIdentityHash, networkType) + if (err != nil) { return false, err } + if (latestUpdateTimeExists == false){ + // No time exists, we need to update it. + return true, nil + } + if (newProfileChatKeysLatestUpdateTime > existingChatKeysLatestUpdateTime){ + // The profile we are broadcasting has new chat keys. + return true, nil + } + return false, nil + } + + latestChatKeysTimeNeedsUpdate, err := getLatestChatKeysTimeNeedsUpdateBool() + if (err != nil) { return false, [28]byte{}, err } + if (latestChatKeysTimeNeedsUpdate == true){ + + err := myChatKeys.SetMyChatKeysLatestUpdateTime(myIdentityHash, networkType, newProfileBroadcastTime) + if (err != nil) { return false, [28]byte{}, err } + } + + exists, newProfileNaclKeyString, err := readProfiles.GetFormattedProfileAttributeFromRawProfileMap(newProfileRawProfileMap, "NaclKey") + if (err != nil) { return false, [28]byte{}, err } + if (exists == false) { + return false, [28]byte{}, errors.New("Invalid export profile: Missing NaclKey") + } + + exists, newProfileKyberKeyString, err := readProfiles.GetFormattedProfileAttributeFromRawProfileMap(newProfileRawProfileMap, "KyberKey") + if (err != nil) { return false, [28]byte{}, err } + if (exists == false) { + return false, [28]byte{}, errors.New("Invalid export profile: Missing KyberKey") + } + + newProfileNaclKey, err := nacl.ReadNaclPublicKeyString(newProfileNaclKeyString) + if (err != nil){ + return false, [28]byte{}, errors.New("Invalid export profile: Contains invalid NaclKey: " + newProfileNaclKeyString) + } + + newProfileKyberKey, err := kyber.ReadKyberPublicKeyString(newProfileKyberKeyString) + if (err != nil){ + return false, [28]byte{}, errors.New("Invalid export profile: Contains invalid KyberKey: " + newProfileKyberKeyString) + } + + // These keys may not be different from the existing keys we have saved + // We dont have to check if they are different, we will set them anyway + + err = myChatKeys.SetMyNewestBroadcastPublicChatKeys(myIdentityHash, networkType, newProfileNaclKey, newProfileKyberKey) + if (err != nil) { return false, [28]byte{}, err } + } + + err = DeleteMyBroadcastProfiles(myIdentityHash) + if (err != nil) { return false, [28]byte{}, err } + + userDirectory, err := localFilesystem.GetAppUserFolderPath() + if (err != nil) { return false, [28]byte{}, err } + + networkTypeString := helpers.ConvertByteToString(networkType) + + networkTypeFoldername := "Network" + networkTypeString + + profilesFolderPath := goFilepath.Join(userDirectory, "MyBroadcasts", "Profiles", networkTypeFoldername) + + newProfileHashHex := encoding.EncodeBytesToHexString(newProfileHash[:]) + + filename := newProfileHashHex + ".messagepack" + + err = localFilesystem.CreateOrOverwriteFile(newProfileBytes, profilesFolderPath, filename) + if (err != nil) { return false, [28]byte{}, err } + + // We add the profile to our database + // For moderators, this will help with determining moderation details, such as what is banned/approved + // For hosts, this will broadcast our profile during our standard host profile seeding tasks + // + // TODO: Skip/delay this step for Mate users? + // For Mate users, this could reveal their identity if the user fulfills their own criteria + // For example, a user who requests to download profiles after being offline for a while could reveal their identity. + // 1. User makes a request to download profiles which fulfill their criteria within the host's range + // 2. Host offers many profiles, some of which have been updated since the user last connected to network + // 3. User requests to download all profiles which are newer than a given time EXCEPT for their own profile + // The user has a newer version of their own profile which the user has recently broadcasted + // The host has not received the profile yet via network propagation. + + wellFormed, _, err := profileStorage.AddUserProfile(newProfileBytes) + if (err != nil) { return false, [28]byte{}, err } + if (wellFormed == false){ + return false, [28]byte{}, errors.New("Profile to broadcast is not well formed after being well formed already.") + } + + return true, newProfileHash, nil +} + + +// This function will delete all broadcast profiles for a provided identity hash +func DeleteMyBroadcastProfiles(myIdentityHash [16]byte)error{ + + isValid, err := identity.VerifyIdentityHash(myIdentityHash, false, "") + if (err != nil) { return err } + if (isValid == false){ + myIdentityHashHex := encoding.EncodeBytesToHexString(myIdentityHash[:]) + return errors.New("DeleteMyBroadcastProfiles called with invalid identityHash: " + myIdentityHashHex) + } + + userDirectory, err := localFilesystem.GetAppUserFolderPath() + if (err != nil) { return err } + + profilesFolderPath := goFilepath.Join(userDirectory, "MyBroadcasts", "Profiles") + + network1ProfilesFolderpath := goFilepath.Join(profilesFolderPath, "Network1") + network2ProfilesFolderpath := goFilepath.Join(profilesFolderPath, "Network2") + + deleteMyProfilesInFolder := func(folderPath string, networkType byte)error{ + + fileList, err := os.ReadDir(folderPath) + if (err != nil) { return err } + + for _, fileObject := range fileList{ + + fileName := fileObject.Name() + + filePath := goFilepath.Join(folderPath, fileName) + + fileBytes, err := os.ReadFile(filePath) + if (err != nil){ return err } + + ableToRead, _, profileNetworkType, profileAuthor, _, _, _, err := readProfiles.ReadProfile(true, fileBytes) + if (err != nil) { return err } + if (ableToRead == false){ + return errors.New("MyBroadcasts malformed: Contains invalid profile.") + } + if (profileNetworkType != networkType){ + return errors.New("MyBroadcasts malformed: Contains profile from different networkType.") + } + + if (profileAuthor != myIdentityHash){ + continue + } + + err = os.Remove(filePath) + if (err != nil) { return err } + } + + return nil + } + + err = deleteMyProfilesInFolder(network1ProfilesFolderpath, 1) + if (err != nil) { return err } + + err = deleteMyProfilesInFolder(network2ProfilesFolderpath, 2) + if (err != nil) { return err } + + return nil +} + +func BroadcastMyMessage(messageBytes []byte)error{ + + ableToRead, messageHash, _, messageNetworkType, _, _, _, _, _, _, _, err := readMessages.ReadChatMessagePublicDataAndHash(true, messageBytes) + if (err != nil) { return err } + if (ableToRead == false){ + return errors.New("BroadcastMyMessage called with invalid message.") + } + + userDirectory, err := localFilesystem.GetAppUserFolderPath() + if (err != nil) { return err } + + appNetworkType, err := getAppNetworkType.GetAppNetworkType() + if (err != nil) { return err } + if (appNetworkType != messageNetworkType){ + return errors.New("BroadcastMyMessage called with message for different networkType than application.") + } + + messageNetworkTypeString := helpers.ConvertByteToString(messageNetworkType) + + networkTypeFoldername := "Network" + messageNetworkTypeString + + messagesFolderPath := goFilepath.Join(userDirectory, "MyBroadcasts", "Messages", networkTypeFoldername) + + messageHashHex := encoding.EncodeBytesToHexString(messageHash[:]) + + filename := messageHashHex + ".messagepack" + + err = localFilesystem.CreateOrOverwriteFile(messageBytes, messagesFolderPath, filename) + if (err != nil) { return err } + + return nil +} + +// This function should only be called via the myReviews.CreateAndBroadcastMyReview function +func BroadcastMyReview(newReview []byte)error{ + + myIdentityExists, myIdentityHash, err := myIdentity.GetMyIdentityHash("Moderator") + if (err != nil) { return err } + if (myIdentityExists == false) { + return errors.New("Trying to broadcast review when my moderator identity does not exist.") + } + + ableToRead, newReviewHash, _, newReviewNetworkType, reviewerIdentityHash, _, newReviewType, newReviewedHash, _, _, err := readReviews.ReadReviewAndHash(true, newReview) + if (err != nil) { return err } + if (ableToRead == false){ + return errors.New("Trying to broadcast invalid review.") + } + + if (myIdentityHash != reviewerIdentityHash){ + return errors.New("Trying to broadcast review not created by current moderator identity.") + } + + userDirectory, err := localFilesystem.GetAppUserFolderPath() + if (err != nil) { return err } + + newReviewNetworkTypeString := helpers.ConvertByteToString(newReviewNetworkType) + + networkTypeFoldername := "Network" + newReviewNetworkTypeString + + reviewTypeFolderPath := goFilepath.Join(userDirectory, "MyBroadcasts", "Reviews", newReviewType, networkTypeFoldername) + + newReviewHashHex := encoding.EncodeBytesToHexString(newReviewHash[:]) + + newReviewFilename := newReviewHashHex + ".messagepack" + + err = localFilesystem.CreateOrOverwriteFile(newReview, reviewTypeFolderPath, newReviewFilename) + if (err != nil) { return err } + + wellFormed, err := reviewStorage.AddReview(newReview) + if (err != nil) { return err } + if (wellFormed == false){ + return errors.New("New review to broadcast is not well formed after being verified already.") + } + + if (newReviewType == "Profile"){ + + if (len(newReviewedHash) != 28){ + reviewedHashHex := encoding.EncodeBytesToHexString(newReviewedHash) + return errors.New("ReadReview returning invalid length reviewedHash for profile review: " + reviewedHashHex) + } + + newReviewedProfileHash := [28]byte(newReviewedHash) + + err = mySkippedContent.DeleteProfileFromMySkippedProfilesMap(newReviewedProfileHash) + if (err != nil) { return err } + + } else if (newReviewType == "Attribute"){ + + if (len(newReviewedHash) != 27){ + reviewedHashHex := encoding.EncodeBytesToHexString(newReviewedHash) + return errors.New("ReadReview returning invalid length reviewedHash for Attribute review: " + reviewedHashHex) + } + + newReviewedAttributeHash := [27]byte(newReviewedHash) + + err = mySkippedContent.DeleteAttributeFromMySkippedAttributesMap(newReviewedAttributeHash) + if (err != nil) { return err } + + } else if (newReviewType == "Message"){ + + if (len(newReviewedHash) != 26){ + reviewedHashHex := encoding.EncodeBytesToHexString(newReviewedHash) + return errors.New("ReadReview returning invalid length reviewedHash for Message review: " + reviewedHashHex) + } + + newReviewedMessageHash := [26]byte(newReviewedHash) + + err = mySkippedContent.DeleteMessageFromMySkippedMessagesMap(newReviewedMessageHash) + if (err != nil) { return err } + } + + return nil +} + + +// This function will prune old reviews that have been replaced by new reviews +// For example, if we approved a message, then banned the same message later, we need to prune the approve review from our broadcasts +func PruneMyBroadcastedReviews()error{ + + //TODO + // If our moderator identity does not exist, delete all of our revies + // We also need to deal with the reality that full profile approve verdicts replace attribute ban verdicts + // We also need to deal with the reality that attribute ban verdicts replace full profile approve verdicts + // We have to delete all reviews that are not the newest. + // None reviews must be kept, when they are not replaced by other reviews + + return nil +} + + + + diff --git a/internal/network/myFundedStatus/myFundedStatus.go b/internal/network/myFundedStatus/myFundedStatus.go new file mode 100644 index 0000000..20651f3 --- /dev/null +++ b/internal/network/myFundedStatus/myFundedStatus.go @@ -0,0 +1,70 @@ + +// myFundedStatus provides functions to store and retrieve a user's message/profile/report funded statuses. + +package myFundedStatus + +// Each message, mate profile and report must be funded with the user's account credit +// This package will keep track of which messages are funded +// This package will save the information in myDatastores, whereas verifiedFundedStatus stores statuses in badgerDatabase +// We use myIdentityBalance to keep track of our identity funded status + +//TODO: Build this package + +func SetMyMessageIsFundedStatus(messageHash [26]byte, messageIsFunded bool, expirationTime int64)error{ + + //TODO + + return nil +} + +func SetMyMateProfileIsFundedStatus(profileHash [28]byte, profileIsFunded bool, expirationTime int64)error{ + + //TODO + + return nil +} + +func SetMyReportIsFundedStatus(reportHash [30]byte, reportIsFunded bool, expirationTime int64)error{ + + //TODO + + return nil +} + +// Outputs: +// -bool: Status is known +// -bool: Message is funded +// -int64: Expiration time +// -error +func CheckIfMyMessageIsFunded(messageHash [26]byte)(bool, bool, int64, error){ + + //TODO + + return true, true, 1000000000000, nil +} + +//Outputs: +// -bool: Status is known +// -bool: Profile is funded +// -int64: Profile expiration time +// -error +func CheckIfMyMateProfileIsFunded(profileHash [28]byte)(bool, bool, int64, error){ + + //TODO + + return true, true, 1000000000000, nil +} + +//Outputs: +// -bool: Status is known +// -bool: Report is funded +// -int64: Report expiration time +// -error +func CheckIfMyReportIsFunded(reportHash [30]byte)(bool, bool, int64, error){ + + //TODO + + return true, true, 1000000000000, nil +} + + diff --git a/internal/network/myIdentityBalance/myIdentityBalance.go b/internal/network/myIdentityBalance/myIdentityBalance.go new file mode 100644 index 0000000..3b3e48f --- /dev/null +++ b/internal/network/myIdentityBalance/myIdentityBalance.go @@ -0,0 +1,65 @@ + +// myIdentityBalance provides functions to store a user's identity balance, and to update the balance through the account credit servers + +package myIdentityBalance + +// Only Mate/Host identities use identity balances. Moderator identities use Identity Scores. + +// This package is useful because a user's identity balance will be retained, even if the database if deleted +// We also query directly from the account credit servers, instead of relying on trusted statuses from other hosts +// See trustedFundedStatus.go, verifiedFundedStatus.go, and fundedStatus.go to see how peer identity balances are retrieved and stored + +// Each identity needs to be funded with a minimum amount of gold to activate it +// The account credits servers keep track of each identity's Activated status +// Before it is activated, each transaction has to send at least a parameters-specified minimum amount of gold to activate it +// Once an identity is activated, any amount of funds can be sent to it to extend its expiration time + +//TODO: Complete this package +// We need to retrieve this information from the account funds interface servers + +import "seekia/internal/helpers" +import "seekia/internal/myIdentity" + +import "time" +import "errors" + +// Start time and end time describe the current uninterrupted period of the balance being sufficient +// The start time is recorded locally whenever we successfully fund the balance +//Outputs: +// -bool: My identity found +// -bool: Identity is activated +// -If this bool is true, the identity was funded enough to be activated at one point in the past +// -Balance can still be insufficient, even if this bool is true +// -bool: Balance is sufficient +// -int64: Balance is sufficient start time (Unix) +// -int64: Balance is sufficient end time (Expiration time) (Unix) +// -error +func GetMyIdentityBalanceStatus(myIdentityHash [16]byte, networkType byte)(bool, bool, bool, int64, int64, error){ + + isValid := helpers.VerifyNetworkType(networkType) + if (isValid == false){ + networkTypeString := helpers.ConvertByteToString(networkType) + return false, false, false, 0, 0, errors.New("GetMyIdentityBalanceStatus called with invalid networkType: " + networkTypeString) + } + + isMine, identityType, err := myIdentity.CheckIfIdentityHashIsMine(myIdentityHash) + if (err != nil) { return false, false, false, 0, 0, err } + if (isMine == false){ + return false, false, false, 0, 0, nil + } + if (identityType == "Moderator"){ + return false, false, false, 0, 0, errors.New("GetMyIdentityBalanceStatus called with Moderator identity") + } + + //TODO: Build function + + fakeStartTime := time.Now().Unix() - 31556926 + fakeExpirationTime := time.Now().Unix() + 10000000 + + return true, true, true, fakeStartTime, fakeExpirationTime, nil +} + + + + + diff --git a/internal/network/myMateCriteria/myMateCriteria.go b/internal/network/myMateCriteria/myMateCriteria.go new file mode 100644 index 0000000..b24a8f3 --- /dev/null +++ b/internal/network/myMateCriteria/myMateCriteria.go @@ -0,0 +1,563 @@ + +// myMateCriteria provides functions to retrieve a user's downloads criteria and check if users fulfill their criteria + +package myMateCriteria + +// Criteria is a way of formatting mate desires for hosts +// This is used when downloading mate profiles from hosts +// The user can choose which desires to share with hosts +// The less information they share, the more profiles they will have to download +// It is a tradeoff between speed and privacy + +// Most users should share desires which they would not mind being leaked to the public +// These include things like desired distance, age, and sex +// Malicious nodes could track each requestor's criteria, link the requestor to their user identity, and leak the information online + +// TODO: Desires could be altered to make them less specific +// For example, Age desires can be rounded to multiples of 5. Example: 18 -> 20, 26 -> 25 +// They should be altered in a different way each time to make it harder to fingerprint the requestor +// I'm unsure if this would increase the requestor anonymity set by much +// Most fingerprints will be very unique, unless the user has few desires and/or shares few desires in their criteria + +import "seekia/resources/geneticReferences/monogenicDiseases" + +import "seekia/internal/convertCurrencies" +import "seekia/internal/desires/mateDesires" +import "seekia/internal/desires/myLocalDesires" +import "seekia/internal/desires/myMateDesires" +import "seekia/internal/encoding" +import "seekia/internal/genetics/readGeneticAnalysis" +import "seekia/internal/genetics/myChosenAnalysis" +import "seekia/internal/helpers" +import "seekia/internal/myDatastores/myMap" +import "seekia/internal/network/appNetworkType/getAppNetworkType" +import "seekia/internal/profiles/myLocalProfiles" + +import "strings" +import "errors" + + +// This function returns a list of desires that are used in criteria +// This is used when a user is choosing their download desires +// Each desire will have a ShareDesire value associated with it, stored in the myCriteriaMapDatastore +func GetAllMyMateDownloadDesiresList()[]string{ + + allMyDesiresList := myMateDesires.GetAllMyDesiresList(false) + + downloadDesiresList := make([]string, 0) + + for _, desireName := range allMyDesiresList{ + + if (desireName == "HasMessagedMe" || desireName == "IHaveMessaged" || desireName == "HasRejectedMe" || desireName == "IsLiked" || desireName == "IsIgnored" || desireName == "IsMyContact"){ + // These desires are not used in criteria + continue + } + + downloadDesiresList = append(downloadDesiresList, desireName) + } + + return downloadDesiresList +} + +// Outputs: +// -bool: Any criteria exists +// -[]byte: My mate downloads criteria +// -error +func GetMyMateDownloadsCriteria()(bool, []byte, error){ + + allDesiresList := mateDesires.GetAllDesiresList(false) + + myCriteriaMap := make(map[string]string) + + for _, desireName := range allDesiresList{ + + if (desireName == "HasMessagedMe" || desireName == "IHaveMessaged" || desireName == "HasRejectedMe" || desireName == "IsLiked" || desireName == "IsIgnored" || desireName == "IsMyContact"){ + // These desires are not used in criteria + // We cannot share these desires with hosts + continue + } + if (desireName == "WealthInGold" || desireName == "DistanceFrom"){ + // These are desires that we will create using different desires + // Example: Wealth -> WealthInGold, Distance -> DistanceFrom + continue + } + + 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 check it twice + continue + } + + desireIsMonogenicDisease := strings.HasPrefix(desireName, "MonogenicDisease_") + if (desireIsMonogenicDisease == true){ + // These are desires that we create from our OffspringHasAnyDiseaseProbability desire + continue + } + + shareDesireBool, err := GetShareMyDesireStatus(desireName) + if (err != nil) { return false, nil, err } + if (shareDesireBool == false){ + // We will not share this desire with hosts. + continue + } + + // We use this function to add the RequireResponse option to the criteria + // Inputs: + // -string: The name of the desire as it appears in myLocalDesires + // -string: The name of the desire as it appears in the newly created Criteria + // Outputs: + // -error + addDesireRequireResponseOptionToCriteriaMap := func(inputDesireName string, criteriaDesireName string)error{ + + desireAllowsResponseRequired, _ := mateDesires.CheckIfDesireAllowsRequireResponse(inputDesireName) + if (desireAllowsResponseRequired == false){ + // RequireResponse is not allowed for this attribute + return nil + } + + getRequireResponseBool := func()(bool, error){ + + exists, currentResponseRequired, err := myLocalDesires.GetDesire(inputDesireName + "_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 err } + if (requireResponseBool == true){ + myCriteriaMap[criteriaDesireName + "_RequireResponse"] = "Yes" + } + + return nil + } + + // We use this function to add the FilterAll option to the criteriaMap + // Inputs: + // -string: The name of the desire as it appears in myLocalDesires + // -string: The name of the desire as it appears in the newly created Criteria + // Outputs: + // -bool: FilterAll is enabled (if true, we need to add the desire value to the criteria map) + // -error + addDesireFilterAllOptionToCriteriaMap := func(inputDesireName string, criteriaDesireName string)(bool, error){ + + getFilterAllBool := func()(bool, error){ + + exists, filterAllStatus, err := myLocalDesires.GetDesire(inputDesireName + "_FilterAll") + if (err != nil) { return false, err } + if (exists == true && filterAllStatus == "Yes"){ + return true, nil + } + return false, nil + } + + filterAllBool, err := getFilterAllBool() + if (err != nil) { return false, err } + if (filterAllBool == false){ + // We do not have filterAll enabled + // All users will pass the desire (excluding those without a response, if RequireResponse == true) + return false, nil + } + // FilterAll is enabled + myCriteriaMap[criteriaDesireName + "_FilterAll"] = "Yes" + + return true, nil + } + + // For some of the desires, we need to convert them to a different form + // This is because some of them are calculated from data within our profile + + if (desireName == "Wealth"){ + + err := addDesireRequireResponseOptionToCriteriaMap("Wealth", "Wealth") + if (err != nil) { return false, nil, err } + + // We must convert our currency to gold + // This way we will not leak our currency to any hosts + // Hosts will user a user's WealthInGold attribute for the comparison + + currencyExists, currencyCode, err := myLocalDesires.GetDesire("WealthCurrency") + if (err != nil) { return false, nil, err } + if (currencyExists == false){ + // We have not selected our desired wealth currency + continue + } + + wealthExists, desiredWealthAmount, err := myLocalDesires.GetDesire("Wealth") + if (err != nil) { return false, nil, err } + if (wealthExists == true){ + // We have not selected our desired wealth amount + continue + } + + filterAllIsEnabled, err := addDesireFilterAllOptionToCriteriaMap("Wealth", "Wealth") + if (err != nil) { return false, nil, err } + if (filterAllIsEnabled == false){ + continue + } + + desiredWealthAmountFloat64, err := helpers.ConvertStringToFloat64(desiredWealthAmount) + if (err != nil) { return false, nil, err } + + appNetworkType, err := getAppNetworkType.GetAppNetworkType() + if (err != nil) { return false, nil, err } + + _, convertedDesiredWealth, err := convertCurrencies.ConvertKilogramsOfGoldToAnyCurrency(appNetworkType, desiredWealthAmountFloat64, currencyCode) + if (err != nil) { return false, nil, err } + + convertedDesiredWealthString := helpers.ConvertFloat64ToString(convertedDesiredWealth) + + myCriteriaMap["WealthInGold"] = convertedDesiredWealthString + + continue + } + + if (desireName == "Distance"){ + + err := addDesireRequireResponseOptionToCriteriaMap("Distance", "DistanceFrom") + if (err != nil) { return false, nil, err } + + //TODO: Randomize location + // We do not want to share our exact location + // This would make it easier to link requestor to user profile + // We want to choose a random distance from our true location, and create a radius that includes our true desired distance + // We must figure out the best way to do this that maximizes privacy + // + // We might want to use only a few random locations that are generated using entropy from the device seed + // This means that, even after making hundreds of requests, hosts would not be able to derive true location + // Otherwise, they could find the average of all requested locations to find the true location of requestor + // This strategy could be combined by using an origin location that is offset from the true location, + // using entropy from the device seed + + // We convert Distance to DistanceFrom + + minimumExists, desiredDistanceMinimum, err := myLocalDesires.GetDesire("Distance_Minimum") + if (err != nil) { return false, nil, err } + + maximumExists, desiredDistanceMaximum, err := myLocalDesires.GetDesire("Distance_Maximum") + if (err != nil) { return false, nil, err } + + if (minimumExists == false && maximumExists == false){ + // We do not have a desired distance + continue + } + + getMinimumBound := func()string{ + + if (minimumExists == false){ + return "0" + } + return desiredDistanceMinimum + } + + minimumBound := getMinimumBound() + + getMaximumBound := func()string{ + + if (maximumExists == false){ + return "20000" + } + return desiredDistanceMaximum + } + + maximumBound := getMaximumBound() + + desiredDistanceMinimumFloat64, err := helpers.ConvertStringToFloat64(minimumBound) + if (err != nil){ + return false, nil, errors.New("MyLocalDesires contains invalid Distance_Minimum: " + desiredDistanceMinimum) + } + + desiredDistanceMaximumFloat64, err := helpers.ConvertStringToFloat64(maximumBound) + if (err != nil){ + return false, nil, errors.New("MyLocalDesires contains invalid Distance_Maximum: " + desiredDistanceMaximum) + } + + if (desiredDistanceMinimumFloat64 < 0){ + return false, nil, errors.New("MyLocalDesires contains invalid Distance_Minimum: " + desiredDistanceMinimum) + } + if (desiredDistanceMaximumFloat64 < desiredDistanceMinimumFloat64){ + return false, nil, errors.New("MyLocalDesires contains Distance_Minimum that is larger than Distance_Maximum.") + } + + exists, myLocationLatitudeString, err := myLocalProfiles.GetProfileData("Mate", "PrimaryLocationLatitude") + if (err != nil) { return false, nil, err } + if (exists == false){ + // We have not added a location, we cannot calculate distance. + continue + } + + filterAllIsEnabled, err := addDesireFilterAllOptionToCriteriaMap("Distance", "DistanceFrom") + if (err != nil) { return false, nil, err } + if (filterAllIsEnabled == false){ + continue + } + + exists, myLocationLongitudeString, err := myLocalProfiles.GetProfileData("Mate", "PrimaryLocationLongitude") + if (err != nil) { return false, nil, err } + if (exists == false){ + return false, nil, errors.New("MyLocalProfiles contains PrimaryLocationLatitude but not PrimaryLocationLongitude") + } + + myLocationLatitudeFloat64, err := helpers.ConvertStringToFloat64(myLocationLatitudeString) + if (err != nil) { + return false, nil, errors.New("MyLocalProfiles contains invalid PrimaryLocationLatitude: " + myLocationLatitudeString) + } + myLocationLongitudeFloat64, err := helpers.ConvertStringToFloat64(myLocationLongitudeString) + if (err != nil) { + return false, nil, errors.New("MyLocalProfiles contains invalid PrimaryLocationLongitude: " + myLocationLongitudeString) + } + + isValid := helpers.VerifyLatitude(myLocationLatitudeFloat64) + if (isValid == false){ + return false, nil, errors.New("MyLocalProfiles contains invalid PrimaryLocationLatitude: " + myLocationLatitudeString) + } + + isValid = helpers.VerifyLongitude(myLocationLongitudeFloat64) + if (isValid == false){ + return false, nil, errors.New("MyLocalProfiles contains invalid PrimaryLocationLongitude: " + myLocationLongitudeString) + } + + distanceFromValue := minimumBound + "$" + maximumBound + "@" + myLocationLatitudeString + "+" + myLocationLongitudeString + + myCriteriaMap["DistanceFrom"] = distanceFromValue + + continue + } + + if (desireName == "23andMe_AncestryComposition"){ + + getDesireToInclude := func()(string, error){ + + 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 + } + + desireToInclude, err := getDesireToInclude() + if (err != nil) { return false, nil, err } + + err = addDesireRequireResponseOptionToCriteriaMap(desireToInclude, desireToInclude) + if (err != nil) { return false, nil, err } + + exists, desireValue, err := myLocalDesires.GetDesire(desireToInclude) + if (err != nil) { return false, nil, err } + if (exists == false){ + continue + } + + filterAllIsEnabled, err := addDesireFilterAllOptionToCriteriaMap(desireToInclude, desireToInclude) + if (err != nil) { return false, nil, err } + if (filterAllIsEnabled == false){ + continue + } + + myCriteriaMap[desireToInclude] = desireValue + + continue + } + + if (desireName == "OffspringProbabilityOfAnyMonogenicDisease"){ + + // We have to determine which mongenic diseases we have a non-zero risk of passing a variant for + + desireExists, desiredMaximumRisk, err := myLocalDesires.GetDesire("OffspringProbabilityOfAnyMonogenicDisease_Maximum") + if (err != nil) { return false, nil, err } + if (desireExists == false){ + // We have no OffspringProbabilityOfAnyMonogenicDisease desire. + continue + } + + myPersonChosen, myGenomesExist, myAnalysisIsReady, myAnalysisMapList, myGenomeIdentifier, iHaveMultipleGenomes, err := myChosenAnalysis.GetMyChosenMateGeneticAnalysis() + if (err != nil) { return false, nil, err } + if (myPersonChosen == false || myGenomesExist == false || myAnalysisIsReady == false){ + // We have not linked our genome to our profile + continue + } + + monogenicDiseaseObjectsList, err := monogenicDiseases.GetMonogenicDiseaseObjectsList() + if (err != nil) { return false, nil, err } + + for _, diseaseObject := range monogenicDiseaseObjectsList{ + + diseaseName := diseaseObject.DiseaseName + diseaseIsDominantOrRecessive := diseaseObject.DominantOrRecessive + + myProbabilityIsKnown, _, _, myProbabilityOfPassingAMonogenicDiseaseVariant, _, _, _, err := readGeneticAnalysis.GetPersonMonogenicDiseaseInfoFromGeneticAnalysis(myAnalysisMapList, diseaseName, myGenomeIdentifier, iHaveMultipleGenomes) + if (err != nil) { return false, nil, err } + if (myProbabilityIsKnown == false){ + continue + } + + // Outputs: + // -bool: Desire is needed + // -string: Maximum bound of user we desire + // -error + getDiseaseProbabilityMaximumDesiredBound := func()(bool, string, error){ + + if (diseaseIsDominantOrRecessive == "Dominant"){ + + if (desiredMaximumRisk == "0"){ + + // Because disease is dominant, the only way to have a 0% risk is if both users have a 0% risk + return true, "0", nil + } + + // desiredMaximumRisk == 99 + + if (myProbabilityOfPassingAMonogenicDiseaseVariant == 100){ + // All users will be filtered, regardless of their probability + // We tried to warn user about this + return true, "0", nil + } + + // We might have some probability of passing the disease, but not a 100% probability + // We must make sure the other user does not have a 100% probability of passing the disease + return true, "99", nil + } + + // diseaseIsDominantOrRecessive == "Recessive" + + if (desiredMaximumRisk == "0"){ + + if (myProbabilityOfPassingAMonogenicDiseaseVariant == 0){ + // Risk is always 0%, because we are 0% + // No desire is needed + return false, "", nil + } + // We have >0% risk + // We must make sure that the other user has a 0% probability of passing a variant + return true, "0", nil + } + // desiredMaximumRisk == "99" + + if (myProbabilityOfPassingAMonogenicDiseaseVariant == 100){ + + // We must make sure other user has a <100% probability of passing a variant + return true, "99", nil + } + + // We may have some risk, but not 100% risk + // Thus, offspring risk will never be 100% + // No desire is needed + return false, "", nil + } + + desireNeeded, maximumBound, err := getDiseaseProbabilityMaximumDesiredBound() + if (err != nil){ return false, nil, err } + if (desireNeeded == false){ + continue + } + + diseaseNameWithUnderscores := strings.ReplaceAll(diseaseName, " ", "_") + diseaseDesireName := "MonogenicDisease_" + diseaseNameWithUnderscores + "_ProbabilityOfPassingAVariant_Maximum" + + myCriteriaMap[diseaseDesireName] = maximumBound + } + } + + err = addDesireRequireResponseOptionToCriteriaMap(desireName, desireName) + if (err != nil) { return false, nil, err } + + desireIsNumerical := mateDesires.CheckIfDesireIsNumerical(desireName) + if (desireIsNumerical == true){ + + minimumExists, minimumValue, err := myLocalDesires.GetDesire(desireName + "_Minimum") + if (err != nil) { return false, nil, err } + + maximumExists, maximumValue, err := myLocalDesires.GetDesire(desireName + "_Maximum") + if (err != nil) { return false, nil, err } + + if (minimumExists == false && maximumExists == false){ + continue + } + + filterAllIsEnabled, err := addDesireFilterAllOptionToCriteriaMap(desireName, desireName) + if (err != nil) { return false, nil, err } + if (filterAllIsEnabled == false){ + continue + } + + myCriteriaMap[desireName + "_Minimum"] = minimumValue + myCriteriaMap[desireName + "_Maximum"] = maximumValue + + continue + } + + // Desire is not numerical + + exists, desireValue, err := myLocalDesires.GetDesire(desireName) + if (err != nil) { return false, nil, err } + if (exists == false){ + continue + } + + filterAllIsEnabled, err := addDesireFilterAllOptionToCriteriaMap(desireName, desireName) + if (err != nil) { return false, nil, err } + if (filterAllIsEnabled == false){ + continue + } + + myCriteriaMap[desireName] = desireValue + } + + if (len(myCriteriaMap) == 0){ + return false, nil, nil + } + + myCriteriaBytes, err := encoding.EncodeMessagePackBytes(myCriteriaMap) + if (err != nil) { return false, nil, err } + + return true, myCriteriaBytes, nil +} + + +// This will store the share status of each desire +// This status determines if the user wants to share their desire or not +var myCriteriaMapDatastore *myMap.MyMap + +// This function must be called whenever we sign in to an app user +func InitializeMyCriteriaDatastore()error{ + + newMyCriteriaMapDatastore, err := myMap.CreateNewMap("MyCriteria") + if (err != nil) { return err } + + myCriteriaMapDatastore = newMyCriteriaMapDatastore + + return nil +} + +// This will return whether the user has decided to share this desire with hosts +func GetShareMyDesireStatus(desireName string)(bool, error){ + + exists, statusValue, err := myCriteriaMapDatastore.GetMapEntry(desireName) + if (err != nil) { return false, err } + if (exists == false){ + return false, nil + } + + statusValueBool, err := helpers.ConvertYesOrNoStringToBool(statusValue) + if (err != nil) { + return false, errors.New("Invalid myCriteria desire share status: " + statusValue) + } + + return statusValueBool, nil +} + +func SetShareMyDesireStatus(desireName string, shareStatus bool)error{ + + newStatusString := helpers.ConvertBoolToYesOrNoString(shareStatus) + + err := myCriteriaMapDatastore.SetMapEntry(desireName, newStatusString) + if (err != nil) { return err } + + return nil +} + + diff --git a/internal/network/networkJobs/networkJobs.go b/internal/network/networkJobs/networkJobs.go new file mode 100644 index 0000000..f68d53d --- /dev/null +++ b/internal/network/networkJobs/networkJobs.go @@ -0,0 +1,1777 @@ + +// networkJobs provides functions to perform network jobs +// An example is downloading a user's inbox messages +// All of these jobs are run in the background by backgroundJobs (see backgroundJobs.go) + +package networkJobs + +//TODO: Add all of these jobs to backgroundJobs.go + +//TODO: Implement these network jobs: + +// Download message funded statuses for hosting +// Download message funded statuses for moderation +// Download message funded statuses for reading (this is to make sure messages we have received are funded) +// Download Host identity funded statuses +// Download Mate identity/profile funded statuses for hosting +// Download Mate identity/profile funded statuses for moderation +// Download Mate identity/profile funded statuses for browsing +// Download Mate identity/profile funded statuses for contacts/chat recipients +// Download report funded statuses for hosting +// Download report funded statuses for moderation +// Broadcast my Mate profile +// Broadcast my Host profile +// Broadcast my Moderator profile/reviews +// Broadcast my Mate messages +// Broadcast my Moderator messages + +// Update Existing Mate User Profiles +// -We need a way for a client to user know if another user has changed their profile to no longer fulfill our downloads criteria +// -We need to download updated profiles for specific identities, without providing a Criteria in the request +// -We should download the newest viewable profile for all Mate users whose stored profile fulfills our criteria. +// -If the user's newer downloaded profile does not fulfill our criteria, we should delete their old profiles. + +//TODO: More jobs + +//TODO: Add limits to the number of hosts per job, and the number of items per request +// Otherwise, some of these jobs could take a very long time to complete, due to the number of hosts being contacted. + +//TODO: The Mate outlier lists can be reduced by removing all of the mate outliers whose profiles/viewable statuses will be downloaded by other jobs. + +import "seekia/internal/byteRange" +import "seekia/internal/badgerDatabase" +import "seekia/internal/contentMetadata" +import "seekia/internal/desires/myMateDesires" +import "seekia/internal/helpers" +import "seekia/internal/logger" +import "seekia/internal/messaging/myChatMessages" +import "seekia/internal/messaging/myInbox" +import "seekia/internal/messaging/peerChatKeys" +import "seekia/internal/moderation/myReviews" +import "seekia/internal/myBlockedUsers" +import "seekia/internal/myContacts" +import "seekia/internal/myIdentity" +import "seekia/internal/myLikedUsers" +import "seekia/internal/myRanges" +import "seekia/internal/mySettings" +import "seekia/internal/network/fundedStatus" +import "seekia/internal/network/mateCriteria" +import "seekia/internal/network/myMateCriteria" +import "seekia/internal/network/queryHosts" +import "seekia/internal/profiles/calculatedAttributes" +import "seekia/internal/profiles/profileStorage" +import "seekia/internal/profiles/readProfiles" +import "seekia/internal/profiles/viewableProfiles" + +import "errors" + +// This function will download and update the network-wide parameters files +func DownloadParameters(networkType byte)error{ + + isValid := helpers.VerifyNetworkType(networkType) + if (isValid == false){ + networkTypeString := helpers.ConvertByteToString(networkType) + return errors.New("DownloadParameters called with invalid networkType: " + networkTypeString) + } + + err := queryHosts.DownloadParametersFromHosts(true, networkType, 5) + if (err != nil) { return err } + + return nil +} + +// This function will download all of the network's newest viewable host/moderator profiles +// -This is used by all users to download host profiles +// -Users will only connect to Viewable hosts, whom are not banned +// -This is used by moderators/hosts to download all moderator profiles +// All hosts/moderators must download all newest viewable Moderator profiles +func DownloadAllNewestViewableUserProfiles(profileType string, networkType byte)error{ + + isValid := helpers.VerifyNetworkType(networkType) + if (isValid == false){ + networkTypeString := helpers.ConvertByteToString(networkType) + return errors.New("DownloadAllNewestViewableUserProfiles called with invalid networkType: " + networkTypeString) + } + + // This functions is not used for Mate profiles + // Mate profiles are always downloaded based on a range, whereas in this function, we download all newest viewable profiles. + + if (profileType != "Host" && profileType != "Moderator"){ + return errors.New("DownloadAllNewestViewableUserProfiles called with invalid profileType: " + profileType) + } + + checkIfProfileShouldBeDownloaded := func(_ [28]byte, profileAuthor [16]byte, profileBroadcastTime int64)(bool, error){ + + // Check to see if our newest profile is older than the received profile + + newestViewableProfileExists, _, _, _, storedProfileBroadcastTime, _, err := viewableProfiles.GetNewestViewableUserProfile(profileAuthor, networkType, true, false, true) + if (err != nil) { return false, err } + if (newestViewableProfileExists == false){ + // We do not have any viewable profiles for this host + // Download this profile + return true, nil + } + + if (profileBroadcastTime <= storedProfileBroadcastTime){ + // Host we are retrieving from has a profile that is not newer than our existing profile + // We can skip this profile + return false, nil + } + + // This profile is newer than our existing newest viewable profile + // We download it. + return true, nil + } + + minimumRange, maximumRange := byteRange.GetMinimumMaximumIdentityHashBounds() + + emptyList := make([][16]byte, 0) + + err := queryHosts.DownloadProfilesFromHosts(false, networkType, profileType, minimumRange, maximumRange, emptyList, nil, true, true, false, checkIfProfileShouldBeDownloaded, 10) + if (err != nil) { return err } + + return nil +} + +// This function will download newest viewable mate profiles that fulfill user's downloads criteria +// It is used by users who are downloading profiles to find matches. +func DownloadMateProfilesToBrowse(networkType byte)error{ + + isValid := helpers.VerifyNetworkType(networkType) + if (isValid == false){ + networkTypeString := helpers.ConvertByteToString(networkType) + return errors.New("DownloadMateProfilesToBrowse called with invalid networkType: " + networkTypeString) + } + + minimumRange, maximumRange := byteRange.GetMinimumMaximumIdentityHashBounds() + + getMyCriteria := func()([]byte, error){ + + exists, myCriteria, err := myMateCriteria.GetMyMateDownloadsCriteria() + if (err != nil) { return nil, err } + if (exists == false){ + return nil, nil + } + return myCriteria, nil + } + + myCriteria, err := getMyCriteria() + if (err != nil) { return err } + + getDownloadAllBool := func()(bool, error){ + + //TODO: Fix below + desiresPruningMode := false + + if (desiresPruningMode == true){ + return true, nil + } + + return false, nil + } + + downloadAllBool, err := getDownloadAllBool() + if (err != nil) { return err } + + emptyList := make([][16]byte, 0) + + checkIfProfileShouldBeDownloaded := func(_ [28]byte, profileAuthor [16]byte, profileBroadcastTime int64)(bool, error){ + // This function is only called if profile hash is not already downloaded + // We check if we have a profile that is newer (and viewable) + // If we have a newer viewable profile, we will skip downloading this profile + + // We wont check, but if we have a newer profile that is not known to be viewable, we will still download this profile + // We will delete this older profile if and when the newer profile becomes viewable + + profileExists, _, _, _, storedProfileBroadcastTime, _, err := viewableProfiles.GetNewestViewableUserProfile(profileAuthor, networkType, true, false, true) + if (err != nil) { return false, err } + if (profileExists == false){ + return true, nil + } + + if (storedProfileBroadcastTime >= profileBroadcastTime){ + // The profile the host is offering is not newer than our existing + // We will skip it + + return false, nil + } + + // Profile is newer than the profile we already have, or we dont have any profile for this identity + // We will download it + return true, nil + } + + err = queryHosts.DownloadProfilesFromHosts(false, networkType, "Mate", minimumRange, maximumRange, emptyList, myCriteria, true, true, downloadAllBool, checkIfProfileShouldBeDownloaded, 10) + if (err != nil) { return err } + + return nil +} + +// This function will download the newest viewable profiles for a user's mate outliers +// Outliers are either contacts, likes, or chat recipients. +// We download them so the user's client will have their profiles downloaded, even if the users do not fulfill our criteria +// This is done so that a user's client has the profiles for users within their contacts and users whom they are chatting with +// These are downloaded one-by-one to prevent hosts from learning a user's contacts/likes/chat recipients +func DownloadMateOutlierProfiles(outlierType string, networkType byte, missingOrExisting string)error{ + + if (outlierType != "Contacts" && outlierType != "Likes" && outlierType != "ChatRecipients"){ + return errors.New("DownloadMateOutlierProfiles called with invalid outlierType: " + outlierType) + } + + isValid := helpers.VerifyNetworkType(networkType) + if (isValid == false){ + networkTypeString := helpers.ConvertByteToString(networkType) + return errors.New("DownloadMateOutlierProfiles called with invalid networkType: " + networkTypeString) + } + + if (missingOrExisting != "Missing" && missingOrExisting != "Existing"){ + return errors.New("DownloadMateOutlierProfiles called with invalid missingOrExisting: " + missingOrExisting) + } + + //Outputs: + // -[][16]byte: List of identities + // -error + getMyOutlierIdentitiesList := func()([][16]byte, error){ + + if (outlierType == "Contacts"){ + + myContactIdentityHashesList, err := myContacts.GetMyContactsList("Mate") + if (err != nil) { return nil, err } + + return myContactIdentityHashesList, nil + } + if (outlierType == "Likes"){ + + myLikedUsersList, err := myLikedUsers.GetMyLikedUsersList() + if (err != nil) { return nil, err } + + return myLikedUsersList, nil + } + + myIdentityExists, chatRecipientIdentityHashesList, err := myChatMessages.GetAllMyNonBlockedChatRecipients("Mate", networkType) + if (err != nil) { return nil, err } + if (myIdentityExists == false){ + // Nothing to do, mate identity does not exist + emptyList := make([][16]byte, 0) + return emptyList, nil + } + + return chatRecipientIdentityHashesList, nil + } + + myOutlierIdentitiesList, err := getMyOutlierIdentitiesList() + if (err != nil) { return err } + if (len(myOutlierIdentitiesList) == 0){ + return nil + } + + helpers.RandomizeListOrder(myOutlierIdentitiesList) + + // We will attempt to retrieve the newest viewable profiles for the identities + + for _, userIdentityHash := range myOutlierIdentitiesList{ + + statusIsKnown, identityIsFunded, err := fundedStatus.GetIdentityIsFundedStatus(userIdentityHash, networkType) + if (err != nil) { return err } + if (statusIsKnown == true && identityIsFunded == false){ + + // The user's identity not funded + // This means that none of their profiles will be hosted by the network + // We will not try to download their profile, because it will not exist on the network + continue + } + + // We check to see if the user's active chat keys exist + // We do this because even if a user's profile exists, it may not contain the user's newest chat keys + + keysAreMissing, err := peerChatKeys.CheckIfUsersChatKeysAreMissing(userIdentityHash, networkType) + if (err != nil) { return err } + if (keysAreMissing == true){ + if (missingOrExisting == "Existing"){ + continue + } + } + + profileExists, _, _, _, _, _, err := viewableProfiles.GetNewestViewableUserProfile(userIdentityHash, networkType, true, false, true) + if (err != nil) { return err } + if (profileExists == false){ + + if (missingOrExisting == "Existing"){ + continue + } + } else { + if (missingOrExisting == "Missing"){ + continue + } + } + + checkIfProfileShouldBeDownloaded := func(_ [28]byte, profileAuthor [16]byte, profileBroadcastTime int64)(bool, error){ + + // We check to see if this profile is newer than our stored newest viewable profile for the identity + // If it is, we download the profile + + profileExists, _, _, _, existingProfileBroadcastTime, _, err := viewableProfiles.GetNewestViewableUserProfile(profileAuthor, networkType, true, false, true) + if (err != nil) { return false, err } + if (profileExists == false){ + return true, nil + } + + if (profileBroadcastTime <= existingProfileBroadcastTime){ + return false, nil + } + + return true, nil + } + + identitiesList := [][16]byte{userIdentityHash} + + minimumRange, maximumRange := byteRange.GetMinimumMaximumIdentityHashBounds() + + err = queryHosts.DownloadProfilesFromHosts(true, networkType, "Mate", minimumRange, maximumRange, identitiesList, nil, true, true, false, checkIfProfileShouldBeDownloaded, 1) + if (err != nil) { return err } + } + + return nil +} + +// This function will download profiles within our host range +// This should be called if a user is in Host mode. +func DownloadProfilesToHost(profileType string, networkType byte)error{ + + if (profileType != "Mate" && profileType != "Host" && profileType != "Moderator"){ + return errors.New("DownloadProfilesToHost called with invalid profileType: " + profileType) + } + + isValid := helpers.VerifyNetworkType(networkType) + if (isValid == false){ + networkTypeString := helpers.ConvertByteToString(networkType) + return errors.New("DownloadProfilesToHost called with invalid networkType: " + networkTypeString) + } + + hostModeEnabled, hostingAny, myIdentitiesToHostRangeStart, myIdentitiesToHostRangeEnd, err := myRanges.GetMyIdentitiesToHostRange(profileType) + if (err != nil) { return err } + if (hostModeEnabled == false){ + + // Host mode must have been turned off recently. + + err := logger.AddLogEntry("Network", "DownloadProfilesToHost called when Host mode is disabled.") + if (err != nil) { return err } + + return nil + } + if (hostingAny == false) { + // No profiles to host of this profileType. Nothing to do. + return nil + } + + checkIfProfileShouldBeDownloaded := func(profileHash [28]byte, profileAuthor [16]byte, profileBroadcastTime int64)(bool, error){ + + //TODO + + // First we check the moderator consensus of the identity + // If consensus is ban, we will only download profile if time it has been banned is not older than maximum time to store banned content + + // Then we check to see if we have profile metadata + // If we don't, we download the profile. + + // At this point, we have the profile metadata but not the profile + // That means we thought it was worth deleting at one point (or it was downloaded for mate/moderator reasons) + + // Then we check the moderator consensus of profile + // If consensus is ban, we will only download profile if time it has been banned is not older than maximum time to store banned content + // Otherwise, we will not download profile + + // At this point, consensus for profile is approve/none and identity is not banned + // We then check to see if we have a newer profile by the same identity + // If there is no newer profile, we download the profile + + // At this point, there is a newer profile for the identity + // We only want to download this older profile if it can be used by moderators to determine if the identity should be banned + // We dont want this to be abused, if an attacker created a report for many old profiles that existed on the network, they could cause hosts to download all those old profiles + // So, if profile has been approved for a long enough period of time, it does not need to be hosted anymore + // We cannot trust the broadcastTimes for reviews, reports, or profiles, so each host keeps track of the time that a consensus has existed based on the reviews it has downloaded + // We use verdictHistory for this + + // If offered profile is approved, we check to see if profile has been approved for at least the maximum time to store old approved profiles + // If it has been approved for at least that long, we will not download the profile + // If it has been approved but not for that long, we will download the profile, just so that moderators can determine if consensus is wrong before profile is dropped from network + + // At this point, consensus for profile is none, and it is not the user's newest profile + // We check to see if any ban reviews and reports exist for the profile + + // If there are any ban reviews or reports, we check to see how long time that consensus has been none. + // If that time is older than maximum time to store banned and reported no consensus old content, we will not download it + // If that time has not passed, we will download the profile + + // At this point, the profile has no ban reviews/reports and consensus is none + // We will check if profile's no consensus time is older than maximum time to store unreported and unbanned no consensus old content + // If the profile is older than that time, we will not download it + // If the profile is newer than that time, we will download it + + // This is quite complicated and could be simplified + // Basically, we want to profiles on the network for long enough so the moderators can review them and ban any moderators who wrongly banned/approved them + + return true, nil + } + + getGetViewableProfilesOnlyBool := func()(bool, error){ + + if (profileType == "Host"){ + // Hosts must download all peer host profiles, regardless of viewable status + // This is because (a) malicious moderator(s) could ban all Host profiles and cripple the network + // Host profiles have no images, so they are much less legally risky to download anyway. + //TODO: Make it clear in the GUI that hosts will host all unviewable host profiles, even if HostUnviewableProfiles is disabled. + return false, nil + } + + exists, hostUnviewableStatus, err := mySettings.GetSetting("HostUnviewableProfilesOnOffStatus") + if (err != nil) { return false, err } + if (exists == true && hostUnviewableStatus == "On") { + return false, nil + } + return true, nil + } + + getViewableProfilesOnlyBool, err := getGetViewableProfilesOnlyBool() + if (err != nil) { return err } + + emptyList := make([][16]byte, 0) + + err = queryHosts.DownloadProfilesFromHosts(true, networkType, profileType, myIdentitiesToHostRangeStart, myIdentitiesToHostRangeEnd, emptyList, nil, false, getViewableProfilesOnlyBool, false, checkIfProfileShouldBeDownloaded, 10) + if (err != nil) { return err } + + return nil +} + + +// This function will download profiles within our moderation range +func DownloadProfilesToModerate(profileType string, networkType byte)error{ + + if (profileType != "Mate" && profileType != "Host" && profileType != "Moderator"){ + return errors.New("DownloadProfilesToModerate called with invalid profileType: " + profileType) + } + + isValid := helpers.VerifyNetworkType(networkType) + if (isValid == false){ + networkTypeString := helpers.ConvertByteToString(networkType) + return errors.New("DownloadProfilesToModerate called with invalid networkType: " + networkTypeString) + } + + moderatorModeEnabled, moderatingAny, myIdentitiesToModerateRangeStart, myIdentitiesToModerateRangeEnd, err := myRanges.GetMyIdentitiesToModerateRange(profileType) + if (err != nil) { return err } + if (moderatorModeEnabled == false){ + + // This should not happen unless moderator mode was turned off recently + err := logger.AddLogEntry("Network", "DownloadProfilesToModerate called when moderator mode is disabled.") + if (err != nil) { return err } + + return nil + } + if (moderatingAny == false){ + // We are not moderating any profiles + return nil + } + + checkIfProfileShouldBeDownloaded := func(profileHash [28]byte, profileIdentityHash [16]byte, profileBroadcastTime int64)(bool, error){ + + //TODO: See if we have already reviewed the profile, if yes, then we don't need to download it. + // Also, if we have the profile's metadata, and it is not the user's newest profile, and it has no ban reviews, we don't need to download it. + // We must have the profile metadata downloaded because it is the only way we can be sure the profile has no ban reviews + + return true, nil + } + + emptyList := make([][16]byte, 0) + + err = queryHosts.DownloadProfilesFromHosts(true, networkType, profileType, myIdentitiesToModerateRangeStart, myIdentitiesToModerateRangeEnd, emptyList, nil, false, false, false, checkIfProfileShouldBeDownloaded, 10) + if (err != nil) { return err } + + return nil +} + + +// This function will download messages in our inboxes +// Each inbox has to be queried seperately from a different host to prevent linking the inboxes together +func DownloadMyInboxMessages(identityType string, networkType byte)error{ + + if (identityType != "Mate" && identityType != "Moderator"){ + return errors.New("DownloadMyInboxMessages called with invalid identityType: " + identityType) + } + + isValid := helpers.VerifyNetworkType(networkType) + if (isValid == false){ + networkTypeString := helpers.ConvertByteToString(networkType) + return errors.New("DownloadMyInboxMessages called with invalid networkType: " + networkTypeString) + } + + myIdentityFound, myIdentityHash, err := myIdentity.GetMyIdentityHash(identityType) + if (err != nil) { return err } + if (myIdentityFound == false){ + return nil + } + + myInboxesList, err := myInbox.GetAllMyActiveInboxes(myIdentityHash, networkType) + if (err != nil) { return err } + + helpers.RandomizeListOrder(myInboxesList) + + minimumInboxRange, maximumInboxRange := byteRange.GetMinimumMaximumInboxBounds() + + for _, inbox := range myInboxesList{ + + inboxAsList := [][10]byte{inbox} + + checkIfMessageShouldBeDownloaded := func(messageHash [26]byte)(bool, error){ + + metadataExists, _, messageNetworkType, _, messageInbox, _, err := contentMetadata.GetMessageMetadata(messageHash) + if (err != nil) { return false, err } + if (metadataExists == false){ + return true, nil + } + // We already downloaded the message at some point + // First we see if the message inbox/networkType matches what we requested + + if (messageNetworkType != networkType || messageInbox != inbox){ + // The host is offering us a message we know does not belong to the inbox/networkType we requested + // The host is malicious. + + // We download the message so they do not learn that we downloaded this message at one point (to prevent fingerprinting) + + // We should have already checked for this within queryHosts.DownloadMessagesFromHosts + // The only way we would not have known is if we downloaded the message recently + err := logger.AddLogEntry("Network", "checkIfMessageShouldBeDownloaded called when inbox/networkType does not match message we have metadata for.") + if (err != nil) { return false, err } + + return true, nil + } + + // Now we check to see if we have already imported, deleted, or failed to decrypt the message + + isDeleted, err := myChatMessages.CheckIfMessageIsDeleted(messageHash) + if (err != nil) { return false, err } + if (isDeleted == true){ + return false, nil + } + + isUndecryptable, err := myChatMessages.CheckIfMessageIsUndecryptable(messageHash) + if (err != nil) { return false, err } + if (isUndecryptable == true){ + return false, nil + } + + isImported, err := myChatMessages.CheckIfMessageIsImported(messageHash, identityType, messageNetworkType) + if (err != nil) { return false, err } + if (isImported == true){ + return false, nil + } + + return true, nil + } + + err := queryHosts.DownloadMessagesFromHosts(false, networkType, minimumInboxRange, maximumInboxRange, inboxAsList, false, false, checkIfMessageShouldBeDownloaded, 1) + if (err != nil) { return err } + } + + return nil +} + +// This function will download messages within our hosting range +// This should be called periodically if we are in Host mode +func DownloadMessagesToHost(networkType byte)error{ + + isValid := helpers.VerifyNetworkType(networkType) + if (isValid == false){ + networkTypeString := helpers.ConvertByteToString(networkType) + return errors.New("DownloadMessagesToHost called with invalid networkType: " + networkTypeString) + } + + hostModeEnabled, hostingAny, myInboxRangeStart, myInboxRangeEnd, err := myRanges.GetMyInboxesToHostRange() + if (err != nil) { return err } + if (hostModeEnabled == false){ + // Host mode is disabled. Nothing to do. + // This should not happen unless host mode was turned off recently + err := logger.AddLogEntry("Network", "DownloadMessagesToHost called when Host mode is disabled.") + if (err != nil) { return err } + + return nil + } + if (hostingAny == false){ + // Not hosting any messages. Nothing to do. + return nil + } + + // We are not retrieving based on a list of inboxes + inboxesToRetrieveList := make([][10]byte, 0) + + getGetViewableMessagesOnlyBool := func()(bool, error){ + + exists, hostUnviewableStatus, err := mySettings.GetSetting("HostUnviewableMessagesOnOffStatus") + if (err != nil) { return false, err } + if (exists == true && hostUnviewableStatus == "On") { + return false, nil + } + + return true, nil + } + + getViewableMessagesOnlyBool, err := getGetViewableMessagesOnlyBool() + if (err != nil) { return err } + + checkIfMessageShouldBeDownloaded := func(inputMessageHash [26]byte)(bool, error){ + + //TODO: + // This should be similar to the function in DownloadProfilesToHost + + return true, nil + } + + err = queryHosts.DownloadMessagesFromHosts(true, networkType, myInboxRangeStart, myInboxRangeEnd, inboxesToRetrieveList, getViewableMessagesOnlyBool, false, checkIfMessageShouldBeDownloaded, 5) + if (err != nil) { return err } + + return nil +} + + +// This function will download messages within our moderation range +// This should be called periodically if the user is in Moderator mode. +func DownloadMessagesToModerate(networkType byte)error{ + + isValid := helpers.VerifyNetworkType(networkType) + if (isValid == false){ + networkTypeString := helpers.ConvertByteToString(networkType) + return errors.New("DownloadMessagesToModerate called with invalid networkType: " + networkTypeString) + } + + moderatorModeEnabled, moderatingAny, myInboxRangeStart, myInboxRangeEnd, err := myRanges.GetMyInboxesToModerateRange() + if (err != nil) { return err } + if (moderatorModeEnabled == false){ + // User must have disabled moderator mode recently + err := logger.AddLogEntry("Network", "DownloadMessagesToModerate called when user is not in Moderator mode.") + if (err != nil) { return err } + + return nil + } + if (moderatingAny == false){ + // We are not moderating any messages. + + return nil + } + + checkIfMessageShouldBeDownloaded := func(inputMessageHash [26]byte)(bool, error){ + + // This function is only called if we don't already have the message downloaded + + metadataExists, _, messageNetworkType, _, messageInbox, _, err := contentMetadata.GetMessageMetadata(inputMessageHash) + if (err != nil) { return false, err } + if (metadataExists == false){ + return true, nil + } + if (messageNetworkType != networkType){ + // The host is offering us a message we know does not belong to the networkType we requested + // The host is malicious. + + // We download the message so they do not learn that we downloaded this message at one point (to prevent fingerprinting) + + return true, nil + } + + isWithinRequestRange, err := byteRange.CheckIfInboxIsWithinRange(myInboxRangeStart, myInboxRangeEnd, messageInbox) + if (err != nil) { return false, err } + if (isWithinRequestRange == false){ + // The host is offering us a message we know does not belong to the inbox range we requested + // The host is malicious. + + // We download the message so they do not learn that we downloaded this message at one point (to prevent fingerprinting) + + return true, nil + } + + myIdentityExists, messageMetadataIsKnown, iHaveReviewed, _, err := myReviews.GetMyNewestMessageModerationVerdict(inputMessageHash) + if (err != nil) { return false, err } + if (myIdentityExists == false){ + // Without an identity, we would only use moderator mode to browse content + // We should therefore download the message + return true, nil + } + if (messageMetadataIsKnown == false){ + // We must have just deleted our message metadata + //TODO: Log this + return true, nil + } + if (iHaveReviewed == true){ + + // We can skip messages that the user has already reviewed + //TODO: Create a mode where this behavior can be disabled + // Some moderators may want to keep those messages so they can reference them again, until they + // expire from the network, or they have been sufficiently banned + + return false, nil + } + + return true, nil + } + + emptyList := make([][10]byte, 0) + + err = queryHosts.DownloadMessagesFromHosts(true, networkType, myInboxRangeStart, myInboxRangeEnd, emptyList, false, true, checkIfMessageShouldBeDownloaded, 5) + if (err != nil) { return err } + + return nil +} + + +// This function will download all reviews banning moderator identities. +// This is necessary if Moderator/Host mode is enabled. +// These reviews determine which moderators are banned, so they are needed to determine the moderation verdict for all identities/content. +// Thus, they must be downloaded by all hosts and moderators. +func DownloadModeratorIdentityBanningReviews(networkType byte)error{ + + isValid := helpers.VerifyNetworkType(networkType) + if (isValid == false){ + networkTypeString := helpers.ConvertByteToString(networkType) + return errors.New("DownloadModeratorIdentityBanningReviews called with invalid networkType: " + networkTypeString) + } + + minimumRange, maximumRange := byteRange.GetMinimumMaximumIdentityHashBounds() + + emptyListA := make([][16]byte, 0) + emptyListB := make([][16]byte, 0) + + checkIfReviewShouldBeDownloadedFunction := func(_ [29]byte, reviewedHash []byte)(bool, error){ + + reviewedType, err := helpers.GetReviewedTypeFromReviewedHash(reviewedHash) + if (err != nil) { return false, err } + if (reviewedType != "Identity" && reviewedType != "Profile" && reviewedType != "Attribute"){ + return false, errors.New("DownloadIdentityReviewsFromHosts not checking that identity reviewed hash is valid.") + } + if (reviewedType == "Identity"){ + return true, nil + } + + return false, nil + } + + err := queryHosts.DownloadIdentityReviewsFromHosts(false, networkType, "Moderator", minimumRange, maximumRange, emptyListA, emptyListB, checkIfReviewShouldBeDownloadedFunction, 5) + if (err != nil) { return err } + + return nil +} + + +// This function will download reviews for identities/profiles/attributes within our Host range +// It is called periodically if Host mode is enabled. +func DownloadIdentityReviewsToHost(identityTypeToRetrieve string, networkType byte)error{ + + if (identityTypeToRetrieve != "Mate" && identityTypeToRetrieve != "Host" && identityTypeToRetrieve != "Moderator"){ + return errors.New("DownloadIdentityReviewsToHost called with invalid identityTypeToRetrieve: " + identityTypeToRetrieve) + } + + isValid := helpers.VerifyNetworkType(networkType) + if (isValid == false){ + networkTypeString := helpers.ConvertByteToString(networkType) + return errors.New("DownloadIdentityReviewsToHost called with invalid networkType: " + networkTypeString) + } + + hostModeEnabled, hostingAnyIdentities, myIdentityRangeStart, myIdentityRangeEnd, err := myRanges.GetMyIdentitiesToHostRange(identityTypeToRetrieve) + if (err != nil) { return err } + if (hostModeEnabled == false){ + // Host mode is disabled. Nothing to do. + // This should not happen unless host mode was turned off recently + err := logger.AddLogEntry("Network", "DownloadIdentityReviewsToHost called when Host mode is disabled.") + if (err != nil) { return err } + + return nil + } + if (hostingAnyIdentities == false){ + + // We are not hosting any identity reviews of the provided identityType + + return nil + } + + emptyListA := make([][16]byte, 0) + emptyListB := make([][16]byte, 0) + + checkIfReviewShouldBeDownloadedFunction := func(_ [29]byte, _ []byte)(bool, error){ + + //TODO: + // We want to defend against downloading reviews that we will delete immediately. + // Don't download reviews by moderators who have been banned for a long enough period of time + // Don't download reviews for content/identities that have been expired for long enough + // Any hosts who offers us content that should no longer exist on the network would be a malicious host. + + return true, nil + } + + err = queryHosts.DownloadIdentityReviewsFromHosts(false, networkType, identityTypeToRetrieve, myIdentityRangeStart, myIdentityRangeEnd, emptyListA, emptyListB, checkIfReviewShouldBeDownloadedFunction, 5) + if (err != nil) { return err } + + return nil +} + + +// This function will download reviews for messages within our Host range +// It is called periodically if Host mode is enabled. +func DownloadMessageReviewsToHost(networkType byte)error{ + + isValid := helpers.VerifyNetworkType(networkType) + if (isValid == false){ + networkTypeString := helpers.ConvertByteToString(networkType) + return errors.New("DownloadMessageReviewsToHost called with invalid networkType: " + networkTypeString) + } + + hostModeEnabled, hostingAnyInboxes, myInboxRangeStart, myInboxRangeEnd, err := myRanges.GetMyInboxesToHostRange() + if (err != nil) { return err } + if (hostModeEnabled == false){ + // Host mode is disabled. Nothing to do. + // This should not happen unless host mode was turned off recently + err := logger.AddLogEntry("Network", "DownloadMessageReviewsToHost called when Host mode is disabled.") + if (err != nil) { return err } + + return nil + } + if (hostingAnyInboxes == false){ + + // We are not hosting any message reviews + + return nil + } + + emptyListA := make([][26]byte, 0) + emptyListB := make([][16]byte, 0) + emptyMap := make(map[[26]byte][10]byte) + + checkIfReviewShouldBeDownloadedFunction := func(reviewHash [29]byte, messageHash [26]byte)(bool, error){ + + //TODO: + // We want to defend against downloading reviews that we will delete immediately. + // Don't download reviews by moderators who have been banned for a long enough period of time + // Don't download reviews for messages that have been expired for long enough + // Any hosts who offers us content that should no longer exist on the network would be a malicious host. + + return true, nil + } + + err = queryHosts.DownloadMessageReviewsFromHosts(false, networkType, myInboxRangeStart, myInboxRangeEnd, emptyListA, emptyListB, emptyMap, checkIfReviewShouldBeDownloadedFunction, 5) + if (err != nil) { return err } + + return nil +} + + +// This function will download reviews for identities/profiles/attributes within our moderation range +// This will be called periodically if Moderator mode is enabled. +func DownloadIdentityReviewsForModeration(identityTypeToRetrieve string, networkType byte)error{ + + if (identityTypeToRetrieve != "Mate" && identityTypeToRetrieve != "Host" && identityTypeToRetrieve != "Moderator"){ + return errors.New("DownloadIdentityReviewsForModeration called with invalid identityTypeToRetrieve: " + identityTypeToRetrieve) + } + + isValid := helpers.VerifyNetworkType(networkType) + if (isValid == false){ + networkTypeString := helpers.ConvertByteToString(networkType) + return errors.New("DownloadIdentityReviewsForModeration called with invalid networkType: " + networkTypeString) + } + + moderatorModeEnabled, moderatingAnyIdentities, myIdentityRangeStart, myIdentityRangeEnd, err := myRanges.GetMyIdentitiesToModerateRange(identityTypeToRetrieve) + if (err != nil) { return err } + if (moderatorModeEnabled == false){ + // Moderator mode must have been disabled recently + + err := logger.AddLogEntry("Network", "DownloadIdentityReviewsForModeration called when user is not in Moderator mode.") + if (err != nil) { return err } + + return nil + } + if (moderatingAnyIdentities == false){ + // We are not moderating any identities of the provided identityType + // We don't need to download any reviews for the identities + return nil + } + + emptyListA := make([][16]byte, 0) + emptyListB := make([][16]byte, 0) + + checkIfReviewShouldBeDownloadedFunction := func(reviewHash [29]byte, reviewedHash []byte)(bool, error){ + + //TODO + // see DownloadIdentityReviewsToHost for some ideas on what restrictions to add + + return true, nil + } + + err = queryHosts.DownloadIdentityReviewsFromHosts(false, networkType, identityTypeToRetrieve, myIdentityRangeStart, myIdentityRangeEnd, emptyListA, emptyListB, checkIfReviewShouldBeDownloadedFunction, 5) + if (err != nil) { return err } + + return nil +} + + +// This function will download reviews for messages within our moderation range +// This will be called periodically if Moderator mode is enabled. +func DownloadMessageReviewsForModeration(networkType byte)error{ + + isValid := helpers.VerifyNetworkType(networkType) + if (isValid == false){ + networkTypeString := helpers.ConvertByteToString(networkType) + return errors.New("DownloadMessageReviewsForModeration called with invalid networkType: " + networkTypeString) + } + + moderatorModeEnabled, moderatingAnyInboxes, myInboxRangeStart, myInboxRangeEnd, err := myRanges.GetMyInboxesToModerateRange() + if (err != nil) { return err } + if (moderatorModeEnabled == false){ + // Moderator mode must have been disabled recently + + err := logger.AddLogEntry("Network", "DownloadMessageReviewsForModeration called when user is not in Moderator mode.") + if (err != nil) { return err } + + return nil + } + if (moderatingAnyInboxes == false){ + // We are not moderating any message inboxes + // We don't need to download any message reviews + return nil + } + + emptyListA := make([][26]byte, 0) + emptyListB := make([][16]byte, 0) + emptyMap := make(map[[26]byte][10]byte) + + checkIfReviewShouldBeDownloadedFunction := func(reviewHash [29]byte, reviewedMessageHash [26]byte)(bool, error){ + + //TODO + // see DownloadMessageReviewsToHost for some ideas on what restrictions to add + + return true, nil + } + + err = queryHosts.DownloadMessageReviewsFromHosts(false, networkType, myInboxRangeStart, myInboxRangeEnd, emptyListA, emptyListB, emptyMap, checkIfReviewShouldBeDownloadedFunction, 5) + if (err != nil) { return err } + + return nil +} + + +// This function will download reports for identities/profiles/attributes within our host range +// It must be called if Host mode is enabled +func DownloadIdentityReportsToHost(identityTypeToRetrieve string, networkType byte)error{ + + if (identityTypeToRetrieve != "Mate" && identityTypeToRetrieve != "Host" && identityTypeToRetrieve != "Moderator"){ + + return errors.New("DownloadIdentityReportsToHost called with invalid identityTypeToRetrieve: " + identityTypeToRetrieve) + } + + isValid := helpers.VerifyNetworkType(networkType) + if (isValid == false){ + networkTypeString := helpers.ConvertByteToString(networkType) + return errors.New("DownloadIdentityReportsToHost called with invalid networkType: " + networkTypeString) + } + + hostModeEnabled, hostingAnyIdentities, myIdentityRangeStart, myIdentityRangeEnd, err := myRanges.GetMyIdentitiesToHostRange(identityTypeToRetrieve) + if (err != nil) { return err } + if (hostModeEnabled == false){ + // Host mode is disabled. Nothing to do. + // This should not happen unless host mode was turned off recently + err := logger.AddLogEntry("Network", "DownloadIdentityReportsToHost called when Host mode is disabled.") + if (err != nil) { return err } + + return nil + } + if (hostingAnyIdentities == false){ + return nil + } + + checkIfReportShouldBeDownloaded := func(reportHash [30]byte, reportedHash []byte)(bool, error){ + + //TODO + // see DownloadReviewsToHost for some ideas on what restrictions to add + + return true, nil + } + + emptyList := make([][16]byte, 0) + + err = queryHosts.DownloadIdentityReportsFromHosts(true, networkType, identityTypeToRetrieve, myIdentityRangeStart, myIdentityRangeEnd, emptyList, checkIfReportShouldBeDownloaded, 5) + if (err != nil) { return err } + + return nil +} + + +// This function will download reports for messages within our host range +// It must be called if Host mode is enabled +func DownloadMessageReportsToHost(networkType byte)error{ + + isValid := helpers.VerifyNetworkType(networkType) + if (isValid == false){ + networkTypeString := helpers.ConvertByteToString(networkType) + return errors.New("DownloadMessageReportsToHost called with invalid networkType: " + networkTypeString) + } + + hostModeEnabled, hostingAnyInboxes, myInboxRangeStart, myInboxRangeEnd, err := myRanges.GetMyInboxesToHostRange() + if (err != nil) { return err } + if (hostModeEnabled == false){ + // Host mode is disabled. Nothing to do. + // This should not happen unless host mode was turned off recently + err := logger.AddLogEntry("Network", "DownloadMessageReportsToHost called when Host mode is disabled.") + if (err != nil) { return err } + + return nil + } + if (hostingAnyInboxes == false){ + return nil + } + + checkIfReportShouldBeDownloaded := func(reportHash [30]byte, reportedMessageHash [26]byte)(bool, error){ + + //TODO + // see DownloadMessageReportsToHost for some ideas on what restrictions to add + + return true, nil + } + + emptyList := make([][26]byte, 0) + emptyMap := make(map[[26]byte][10]byte) + + err = queryHosts.DownloadMessageReportsFromHosts(true, networkType, myInboxRangeStart, myInboxRangeEnd, emptyList, emptyMap, checkIfReportShouldBeDownloaded, 5) + if (err != nil) { return err } + + return nil +} + +// This function will download reports for identities/profiles/attributes within our moderation range +// It is called periodically if Moderator mode is enabled +func DownloadIdentityReportsForModeration(identityTypeToRetrieve string, networkType byte)error{ + + if (identityTypeToRetrieve != "Mate" && identityTypeToRetrieve != "Host" && identityTypeToRetrieve != "Moderator"){ + + return errors.New("DownloadIdentityReportsForModeration called with invalid identityTypeToRetrieve: " + identityTypeToRetrieve) + } + + isValid := helpers.VerifyNetworkType(networkType) + if (isValid == false){ + networkTypeString := helpers.ConvertByteToString(networkType) + return errors.New("DownloadIdentityReportsForModeration called with invalid networkType: " + networkTypeString) + } + + moderatorModeEnabled, moderatingAny, myRangeStart, myRangeEnd, err := myRanges.GetMyIdentitiesToModerateRange(identityTypeToRetrieve) + if (err != nil) { return err } + if (moderatorModeEnabled == false){ + + // Moderator mode must have been disabled recently + + err := logger.AddLogEntry("Network", "DownloadIdentityReportsForModeration called when user is not in Moderator mode.") + if (err != nil) { return err } + + return nil + } + if (moderatingAny == false){ + + // Not moderating any identities of provided identityType + // We don't need to download reports + return nil + } + + checkIfReportShouldBeDownloaded := func(reportHash [30]byte, reportedHash []byte)(bool, error){ + + //TODO + // see DownloadIdentityReviewsToHost for some ideas on what restrictions to add + + return true, nil + } + + emptyList := make([][16]byte, 0) + + err = queryHosts.DownloadIdentityReportsFromHosts(true, networkType, identityTypeToRetrieve, myRangeStart, myRangeEnd, emptyList, checkIfReportShouldBeDownloaded, 5) + if (err != nil) { return err } + + return nil +} + + +// This function will download reports for messages/profiles/identities within our moderation range +// It is called periodically if Moderator mode is enabled +func DownloadMessageReportsForModeration(networkType byte)error{ + + isValid := helpers.VerifyNetworkType(networkType) + if (isValid == false){ + networkTypeString := helpers.ConvertByteToString(networkType) + return errors.New("DownloadMessageReportsForModeration called with invalid networkType: " + networkTypeString) + } + + moderatorModeEnabled, moderatingAny, myInboxRangeStart, myInboxRangeEnd, err := myRanges.GetMyInboxesToModerateRange() + if (err != nil) { return err } + if (moderatorModeEnabled == false){ + + // Moderator mode must have been disabled recently + + err := logger.AddLogEntry("Network", "DownloadMessageReportsForModeration called when user is not in Moderator mode.") + if (err != nil) { return err } + + return nil + } + if (moderatingAny == false){ + // Not moderating any messages + // We don't need to download any message reports + return nil + } + + checkIfReportShouldBeDownloaded := func(reportHash [30]byte, reportedMessageHash [26]byte)(bool, error){ + + //TODO + // see DownloadMessageReviewsToHost for some ideas on what restrictions to add + + return true, nil + } + + emptyList := make([][26]byte, 0) + emptyMap := make(map[[26]byte][10]byte) + + err = queryHosts.DownloadMessageReportsFromHosts(true, networkType, myInboxRangeStart, myInboxRangeEnd, emptyList, emptyMap, checkIfReportShouldBeDownloaded, 5) + if (err != nil) { return err } + + return nil +} + + +// This function will download trusted viewable statuses for downloaded host identities/profiles. +// Downloading these statuses is not needed if a user is able to determine these statuses themselves +// +// Hosts who are hosting host identities will download all host profile reviews, so they can calculate the viewable status themselves +// Moderators who are moderating Host identities can also calculate the viewable status for host identities/profiles within their moderation range +func DownloadHostViewableStatuses(networkType byte, knownOrUnknown string)error{ + + isValid := helpers.VerifyNetworkType(networkType) + if (isValid == false){ + networkTypeString := helpers.ConvertByteToString(networkType) + return errors.New("DownloadHostViewableStatuses called with invalid networkType: " + networkTypeString) + } + + if (knownOrUnknown != "Known" && knownOrUnknown != "Unknown"){ + return errors.New("DownloadHostViewableStatuses called with invalid knownOrUnknown: " + knownOrUnknown) + } + + hostModeEnabled, hostingAny, _, _, err := myRanges.GetMyIdentitiesToHostRange("Host") + if (err != nil) { return err } + if (hostModeEnabled == true && hostingAny == true){ + // We are already hosting all reviews for host identities, so we can calculate the viewable status of host profiles ourselves + // Thus, we don't need to download any trusted viewable statuses for host identitites/profiles + return nil + } + + allHostProfileIdentityHashesList, err := badgerDatabase.GetAllProfileIdentityHashes("Host") + if (err != nil) { return err } + + getRelevantHostProfileIdentityHashesList := func()([][16]byte, error){ + + moderatorModeEnabled, moderatingAny, identityRangeStart, identityRangeEnd, err := myRanges.GetMyIdentitiesToModerateRange("Host") + if (err != nil) { return nil, err } + if (moderatorModeEnabled == false || moderatingAny == false){ + // We are not moderating any host identities + // We do not need to reduce the list at all + return allHostProfileIdentityHashesList, nil + } + + relevantHostProfileIdentityHashesList := make([][16]byte, 0) + + for _, hostIdentityHash := range allHostProfileIdentityHashesList{ + + isWithinMyRange, err := byteRange.CheckIfIdentityHashIsWithinRange(identityRangeStart, identityRangeEnd, hostIdentityHash) + if (err != nil) { return nil, err } + if (isWithinMyRange == true){ + // We don't need to download this host's viewable status, because we can calculate it ourselves. + continue + } + + relevantHostProfileIdentityHashesList = append(relevantHostProfileIdentityHashesList, hostIdentityHash) + } + + return relevantHostProfileIdentityHashesList, nil + } + + relevantHostProfileIdentityHashesList, err := getRelevantHostProfileIdentityHashesList() + if (err != nil) { return err } + + // Map Structure: Profile Hash -> Author identity hash + profileHashesToRetrieveMap := make(map[[28]byte][16]byte) + + for _, identityHash := range relevantHostProfileIdentityHashesList{ + + exists, profileHashesList, err := badgerDatabase.GetIdentityProfileHashesList(identityHash) + if (err != nil) { return err } + if (exists == false){ + // Entry must have been deleted. Will be fixed automatically + continue + } + if (len(profileHashesList) == 0){ + continue + } + + for _, profileHash := range profileHashesList{ + + profileMetadataExists, _, profileNetworkType, profileAuthor, _, profileIsDisabled, _, _, err := contentMetadata.GetProfileMetadata(profileHash) + if (err != nil) { return err } + if (profileMetadataExists == false){ + // Profile is not downloaded, skip it. + continue + } + if (profileNetworkType != networkType){ + continue + } + if (profileAuthor != identityHash){ + return errors.New("Database corrupt: Host identity profile hashes list contains profile of different identity") + } + + if (profileIsDisabled == true){ + continue + } + + profileHashesToRetrieveMap[profileHash] = identityHash + } + } + + err = queryHosts.DownloadViewableStatusesFromHosts(false, networkType, knownOrUnknown, relevantHostProfileIdentityHashesList, profileHashesToRetrieveMap, 5) + if (err != nil) { return err } + + return nil +} + +// This function will download trusted viewable statuses for downloaded moderator profiles whose viewable statuses we cannot calculate ourselves +// +// We only need to do this is Moderator mode is enabled. +// This is only needed so moderators can view Moderator profiles whose viewable status we are not aware of and be sure they are viewable +// If Host mode is enabled, all of the moderator profiles we download, we will be able to determine the viewable status of. +// Mate mode will not download any moderator profiles. +// +// We don't need to download viewable statuses for moderator identities +// This is because in Moderator/Host mode, we should be downloading all moderator identity reviews +// Thus, we should be able to determine their viewable statuses ourselves +// +func DownloadModeratorProfileViewableStatuses(networkType byte, knownOrUnknown string)error{ + + isValid := helpers.VerifyNetworkType(networkType) + if (isValid == false){ + networkTypeString := helpers.ConvertByteToString(networkType) + return errors.New("DownloadModeratorProfileViewableStatuses called with invalid networkType: " + networkTypeString) + } + + if (knownOrUnknown != "Known" && knownOrUnknown != "Unknown"){ + return errors.New("DownloadModeratorProfileViewableStatuses called with invalid knownOrUnknown: " + knownOrUnknown) + } + + moderatorModeEnabled, moderatingAnyModeratorIdentities, myIdentityRangeStart, myIdentityRangeEnd, err := myRanges.GetMyIdentitiesToModerateRange("Moderator") + if (err != nil) { return err } + if (moderatorModeEnabled == false){ + // Moderator mode must have been disabled recently + + err := logger.AddLogEntry("Network", "DownloadModeratorProfileViewableStatuses called when user is not in Moderator mode.") + if (err != nil) { return err } + + return nil + } + + if (moderatingAnyModeratorIdentities == true){ + + minimumIdentityBound, maximumIdentityBound := byteRange.GetMinimumMaximumIdentityHashBounds() + + if (myIdentityRangeStart == minimumIdentityBound && myIdentityRangeEnd == maximumIdentityBound){ + + // We are moderating all Moderator identities, so we should be able to determine the viewable status for all moderator profiles + // We don't need to download any moderator profile trusted viewable statuses + return nil + } + } + + // Now we see if we are hosting all moderator identities + + hostModeEnabled, hostingAny, _, _, err := myRanges.GetMyIdentitiesToHostRange("Moderator") + if (err != nil) { return err } + if (hostModeEnabled == true && hostingAny == true){ + // We are hosting all moderator identities + // Thus, we are downloading all reviews for all moderator profiles + // We don't need to download the viewable statuses for any moderator profiles + return nil + } + + allModeratorProfileIdentityHashesList, err := badgerDatabase.GetAllProfileIdentityHashes("Moderator") + if (err != nil) { return err } + + // Map Structure: Profile Hash -> Author identity hash + profileHashesToRetrieveMap := make(map[[28]byte][16]byte) + + for _, moderatorIdentityHash := range allModeratorProfileIdentityHashesList{ + + if (moderatingAnyModeratorIdentities == true){ + + // We see if moderator's identity is within our range + // If it is, we don't have to get the status for profiles authored by this identity, because we can determine the viewable statuses ourselves + + isWithinRange, err := byteRange.CheckIfIdentityHashIsWithinRange(myIdentityRangeStart, myIdentityRangeEnd, moderatorIdentityHash) + if (err != nil) { return err } + if (isWithinRange == true){ + + continue + } + } + + exists, profileHashesList, err := badgerDatabase.GetIdentityProfileHashesList(moderatorIdentityHash) + if (err != nil) { return err } + if (exists == false){ + // Entry must have been deleted. Will be fixed automatically + continue + } + if (len(profileHashesList) == 0){ + continue + } + + for _, profileHash := range profileHashesList{ + + profileMetadataExists, _, profileNetworkType, profileAuthor, _, profileIsDisabled, _, _, err := contentMetadata.GetProfileMetadata(profileHash) + if (err != nil) { return err } + if (profileMetadataExists == false){ + // Profile is not downloaded, skip it. + continue + } + if (profileNetworkType != networkType){ + continue + } + if (profileAuthor != moderatorIdentityHash){ + return errors.New("Database corrupt: Moderator identity profile hashes list contains profile of different identity") + } + + if (profileIsDisabled == true){ + continue + } + + profileHashesToRetrieveMap[profileHash] = moderatorIdentityHash + } + } + + emptyList := make([][16]byte, 0) + + err = queryHosts.DownloadViewableStatusesFromHosts(false, networkType, knownOrUnknown, emptyList, profileHashesToRetrieveMap, 5) + if (err != nil) { return err } + + return nil +} + +// This function will download viewable statuses for mate identities/profiles we are browsing +// If we are not in DesiresPruningMode, we will download the statuses for all downloaded profiles/identities which fulfill our criteria +// If we are in DesiresPruningMode, we have to download the statuses for our matches profiles/identities one-at-a-time +func DownloadMateViewableStatusesForBrowsing(networkType byte, knownOrUnknown string)error{ + + isValid := helpers.VerifyNetworkType(networkType) + if (isValid == false){ + networkTypeString := helpers.ConvertByteToString(networkType) + return errors.New("DownloadMateViewableStatusesForBrowsing called with invalid networkType: " + networkTypeString) + } + + if (knownOrUnknown != "Known" && knownOrUnknown != "Unknown"){ + return errors.New("DownloadMateViewableStatusesForBrowsing called with invalid knownOrUnknown: " + knownOrUnknown) + } + + //TODO: Fix + desiresPruningMode := false + + if (desiresPruningMode == true){ + + // We will retrieve consensus statuses for all users who could potentially be matches one-by-one + // A user could potentially be a match if their newest profile fulfills our desires + // Their newest profile may not be viewable, so we still need to retrieve statuses for all of their profiles which fulfill our desires + + myIdentityExists, myIdentityHash, err := myIdentity.GetMyIdentityHash("Mate") + if (err != nil) { return err } + + mateIdentityHashesList, err := badgerDatabase.GetAllProfileIdentityHashes("Mate") + if (err != nil) { return err } + + for _, userIdentityHash := range mateIdentityHashesList{ + + if (myIdentityExists == true && userIdentityHash == myIdentityHash){ + continue + } + + userIsBlocked, _, _, _, err := myBlockedUsers.CheckIfUserIsBlocked(userIdentityHash) + if (err != nil) { return err } + if (userIsBlocked == true){ + // We don't need to get the viewable statuses for blocked users + continue + } + + profileExists, profileVersion, newestProfileHash, _, _, newestRawProfileMap, err := profileStorage.GetNewestUserProfile(userIdentityHash, networkType) + if (err != nil) { return err } + if (profileExists == false){ + // This user has no profiles, so we do not need to retrieve any viewable statuses for this user + // The user's profile was probably deleted in the background + continue + } + + userIsDisabled, _, err := readProfiles.GetFormattedProfileAttributeFromRawProfileMap(newestRawProfileMap, "Disabled") + if (err != nil) { return err } + if (userIsDisabled == true){ + // The user will never be a match, because their newest profile is disabled + // Thus, we do not need to retrieve viewable statuses for the user. + continue + } + + getAnyProfileAttributeFunction, err := calculatedAttributes.GetRetrieveAnyProfileAttributeIncludingCalculatedFunction(profileVersion, newestRawProfileMap) + if (err != nil){ return err } + + profilePassesMyDesires, err := myMateDesires.CheckIfMateProfilePassesAllMyDesires(false, "", getAnyProfileAttributeFunction) + if (err != nil) { return err } + if (profilePassesMyDesires == false){ + // The user's newest profile does not fulfill our desires + // We don't need to get this profile's viewable status, because it does not fulfill our desires + // We don't need to get any viewable statuses for this user, unless they are an outlier + continue + } + + anyExist, profileHashesList, err := badgerDatabase.GetIdentityProfileHashesList(userIdentityHash) + if (err != nil) { return err } + if (anyExist == false){ + // This user has no profiles + continue + } + + profileHashesToGetStatusesOfList := [][28]byte{newestProfileHash} + + for _, profileHash := range profileHashesList{ + + if (profileHash == newestProfileHash){ + // We are already getting this profile's status + continue + } + + profileExists, profileBytes, err := badgerDatabase.GetUserProfile("Mate", profileHash) + if (err != nil) { return err } + if (profileExists == false){ + continue + } + + ableToRead, profileVersion, profileNetworkType, profileAuthor, _, profileIsDisabled, rawProfileMap, err := readProfiles.ReadProfile(false, profileBytes) + if (err != nil) { return err } + if (ableToRead == false){ + return errors.New("Database corrupt: Contains malformed profile.") + } + if (profileNetworkType != networkType){ + // Profile belongs to a different network type. + continue + } + if (profileAuthor != userIdentityHash){ + return errors.New("Database corrupt: Mate identity profile hashes list contains profile of different identity") + } + if (profileIsDisabled == true){ + // We don't need to retrieve status for disabled profiles. + // These profiles will always be viewable, unless their author is banned. + continue + } + + getAnyProfileAttributeFunction, err := calculatedAttributes.GetRetrieveAnyProfileAttributeIncludingCalculatedFunction(profileVersion, rawProfileMap) + if (err != nil){ return err } + + profilePassesMyDesires, err := myMateDesires.CheckIfMateProfilePassesAllMyDesires(false, "", getAnyProfileAttributeFunction) + if (err != nil) { return err } + if (profilePassesMyDesires == false){ + // We don't need to get this profile's viewable status, because it does not fulfill our desires + // Thus, we will not show it to the user unless the author is an outlier + // Outlier viewable statuses are retrieved in a different job + continue + } + + profileHashesToGetStatusesOfList = append(profileHashesToGetStatusesOfList, profileHash) + } + + // Map Structure: Profile Hash -> User Identity Hash + profileHashesToRetrieveMap := make(map[[28]byte][16]byte) + + for _, profileHash := range profileHashesToGetStatusesOfList{ + + profileHashesToRetrieveMap[profileHash] = userIdentityHash + } + + identityHashList := [][16]byte{userIdentityHash} + + err = queryHosts.DownloadViewableStatusesFromHosts(false, networkType, knownOrUnknown, identityHashList, profileHashesToRetrieveMap, 1) + if (err != nil) { return err } + } + + return nil + } + + // We will retrieve statuses for all identities whose newest profile fulfills our downloads criteria, and their profiles that fulfill our criteria + + myCriteriaExists, myCriteria, err := myMateCriteria.GetMyMateDownloadsCriteria() + if (err != nil) { return err } + + allMateProfileIdentityHashesList, err := badgerDatabase.GetAllProfileIdentityHashes("Mate") + if (err != nil) { return err } + + helpers.RandomizeListOrder(allMateProfileIdentityHashesList) + + identityHashesToRetrieveList := make([][16]byte, 0) + + // Map Structure: Profile Hash -> User Identity Hash + profileHashesToRetrieveMap := make(map[[28]byte][16]byte) + + for _, userIdentityHash := range allMateProfileIdentityHashesList{ + + statusIsKnown, identityIsFunded, err := fundedStatus.GetIdentityIsFundedStatus(userIdentityHash, networkType) + if (err != nil) { return err } + if (statusIsKnown == true && identityIsFunded == false){ + + // The user's identity is not funded + // This means that none of their profiles will be hosted by the network + // We won't try to retrieve their viewable status, because their profile/identity will not exist on the network + continue + } + + profileExists, profileVersion, newestProfileHash, _, _, newestRawProfileMap, err := profileStorage.GetNewestUserProfile(userIdentityHash, networkType) + if (err != nil) { return err } + if (profileExists == false){ + continue + } + + userIsDisabled, _, err := readProfiles.GetFormattedProfileAttributeFromRawProfileMap(newestRawProfileMap, "Disabled") + if (err != nil) { return err } + if (userIsDisabled == true){ + // The user will never be a match, because their newest profile is disabled + // Thus, we do not need to retrieve viewable statuses for the user. + continue + } + + if (myCriteriaExists == true){ + + criteriaIsValid, fulfillsCriteria, err := mateCriteria.CheckIfMateProfileFulfillsCriteria(true, profileVersion, newestRawProfileMap, myCriteria) + if (err != nil) { return err } + if (criteriaIsValid == false){ + return errors.New("GetMyMateDownloadsCriteria returning invalid criteria.") + } + + if (fulfillsCriteria == false){ + // This user's newest profile does not fulfill our criteria. Skip them. + // DatabaseJobs will delete all of their profiles (unless they are an outlier) + continue + } + } + + anyExist, profileHashesList, err := badgerDatabase.GetIdentityProfileHashesList(userIdentityHash) + if (err != nil) { return err } + if (anyExist == false){ + // User has no profiles + // They must have been deleted after earlier check + continue + } + + for _, profileHash := range profileHashesList{ + + if (profileHash == newestProfileHash){ + // We already checked and know that this profile fulfills our criteria + profileHashesToRetrieveMap[profileHash] = userIdentityHash + continue + } + + profileExists, profileBytes, err := badgerDatabase.GetUserProfile("Mate", profileHash) + if (err != nil) { return err } + if (profileExists == false){ + continue + } + + ableToRead, profileVersion, profileNetworkType, profileAuthor, _, profileIsDisabled, rawProfileMap, err := readProfiles.ReadProfile(false, profileBytes) + if (err != nil) { return err } + if (ableToRead == false){ + return errors.New("Database corrupt: Contains malformed profile.") + } + if (profileNetworkType != networkType){ + // Profile belongs to a different network type. + continue + } + if (profileAuthor != userIdentityHash){ + return errors.New("Database corrupt: Mate identity profile hashes list contains profile of different identity") + } + if (profileIsDisabled == true){ + // We don't need to retrieve status for disabled profiles. They are always approved, unless identity is banned. + continue + } + + if (myCriteriaExists == true){ + + criteriaIsValid, fulfillsCriteria, err := mateCriteria.CheckIfMateProfileFulfillsCriteria(true, profileVersion, rawProfileMap, myCriteria) + if (err != nil) { return err } + if (criteriaIsValid == false){ + return errors.New("GetMyMateDownloadsCriteria returning invalid criteria.") + } + + if (fulfillsCriteria == false){ + // This profile does not fulfill our criteria + // We don't need to get its viewable status, because it will never be shown to the user + // The exception is if the profile is an outlier, in which case we will retrieve its status using DownloadMateOutlierViewableStatuses + // Another reason we skip the profile is that requesting its status would pose a risk of exposing our older criteria to the host + continue + } + } + + profileHashesToRetrieveMap[profileHash] = userIdentityHash + } + + identityHashesToRetrieveList = append(identityHashesToRetrieveList, userIdentityHash) + } + + if (len(identityHashesToRetrieveList) == 0){ + // No users fulfill our criteria. Nothing to do. + return nil + } + + err = queryHosts.DownloadViewableStatusesFromHosts(false, networkType, knownOrUnknown, identityHashesToRetrieveList, profileHashesToRetrieveMap, 5) + if (err != nil) { return err } + + return nil +} + +// This function will download trusted viewable statuses for Mate users whom are our outliers +// Outliers are either contacts, likes, or chat recipients. +// We download them so the user's client will have their downloaded profiles viewable, even if the users do not fulfill our criteria +// These need to be downloaded one-by-one to prevent any host from learning our contacts/likes/chat recipients +func DownloadMateOutlierViewableStatuses(outlierType string, networkType byte, knownOrUnknown string)error{ + + if (outlierType != "Contacts" && outlierType != "Likes" && outlierType != "ChatRecipients"){ + return errors.New("DownloadMateOutlierViewableStatuses called with invalid outlierType: " + outlierType) + } + isValid := helpers.VerifyNetworkType(networkType) + if (isValid == false){ + networkTypeString := helpers.ConvertByteToString(networkType) + return errors.New("DownloadMateOutlierViewableStatuses called with invalid networkType: " + networkTypeString) + } + + if (knownOrUnknown != "Known" && knownOrUnknown != "Unknown"){ + return errors.New("DownloadMateOutlierViewableStatuses called with invalid knownOrUnknown: " + knownOrUnknown) + } + + //Outputs: + // -[]string: List of identities + // -error + getMyOutlierIdentitiesList := func()([][16]byte, error){ + + if (outlierType == "Contacts"){ + + myContactIdentityHashesList, err := myContacts.GetMyContactsList("Mate") + if (err != nil) { return nil, err } + + return myContactIdentityHashesList, nil + } + if (outlierType == "Likes"){ + + myLikedUsersList, err := myLikedUsers.GetMyLikedUsersList() + if (err != nil) { return nil, err } + + return myLikedUsersList, nil + } + + myIdentityExists, chatRecipientIdentityHashesList, err := myChatMessages.GetAllMyNonBlockedChatRecipients("Mate", networkType) + if (err != nil) { return nil, err } + if (myIdentityExists == false){ + // Nothing to do, mate identity does not exist + emptyList := make([][16]byte, 0) + return emptyList, nil + } + + return chatRecipientIdentityHashesList, nil + } + + myOutlierIdentityHashesList, err := getMyOutlierIdentitiesList() + if (err != nil) { return err } + if (len(myOutlierIdentityHashesList) == 0){ + return nil + } + + helpers.RandomizeListOrder(myOutlierIdentityHashesList) + + for _, mateIdentityHash := range myOutlierIdentityHashesList{ + + statusIsKnown, identityIsFunded, err := fundedStatus.GetIdentityIsFundedStatus(mateIdentityHash, networkType) + if (err != nil) { return err } + if (statusIsKnown == true && identityIsFunded == false){ + + // The user's identity is not funded + // This means that none of their profiles will be hosted by the network + // We won't try to retrieve their viewable status, because their profile/identity will not exist on the network + continue + } + + identityHashList := [][16]byte{mateIdentityHash} + + // Outputs: + // -map[[28]byte][16]byte: Map of profile hashes whose viewable status we want to retrieve (Profile Hash -> User identity Hash) + // -error + getProfileHashesToRetrieveMap := func()(map[[28]byte][16]byte, error){ + + profileExists, _, newestProfileHash, _, _, newestRawProfileMap, err := profileStorage.GetNewestUserProfile(mateIdentityHash, networkType) + if (err != nil) { return nil, err } + if (profileExists == false){ + // We have no profiles downloaded for this user + emptyMap := make(map[[28]byte][16]byte) + + return emptyMap, nil + } + + isDisabled, _, err := readProfiles.GetFormattedProfileAttributeFromRawProfileMap(newestRawProfileMap, "Disabled") + if (err != nil) { return nil, err } + if (isDisabled == true){ + + // User's newest profile is disabled. + // We don't need to download the profile's viewable status, because it cannot be banned + + emptyMap := make(map[[28]byte][16]byte) + + return emptyMap, nil + } + + // We retrieve the viewable status for all of their downloaded profiles + + profileHashesToRetrieveMap := map[[28]byte][16]byte{ + newestProfileHash: mateIdentityHash, + } + + anyExist, identityProfileHashesList, err := badgerDatabase.GetIdentityProfileHashesList(mateIdentityHash) + if (err != nil) { return nil, err } + if (anyExist == true){ + + for _, profileHash := range identityProfileHashesList{ + profileHashesToRetrieveMap[profileHash] = mateIdentityHash + } + } + + return profileHashesToRetrieveMap, nil + } + + profileHashesToRetrieveMap, err := getProfileHashesToRetrieveMap() + if (err != nil) { return err } + + err = queryHosts.DownloadViewableStatusesFromHosts(false, networkType, knownOrUnknown, identityHashList, profileHashesToRetrieveMap, 1) + if (err != nil) { return err } + } + + return nil +} + +// This function will download the cryptocurrency deposit history for all downloaded moderator identities +// We download deposits for all moderators in bulk +// Because users must download either all or no moderator profiles, there is no fingerprinting risk in downloading them in bulk +// This should be called if Host/Moderator mode is enabled +func DownloadModeratorIdentityDeposits(networkType byte, unknownOrKnown string)error{ + + isValid := helpers.VerifyNetworkType(networkType) + if (isValid == false){ + networkTypeString := helpers.ConvertByteToString(networkType) + return errors.New("DownloadModeratorIdentityDeposits called with invalid networkType: " + networkTypeString) + } + + err := queryHosts.DownloadModeratorAddressDepositsFromHosts(true, networkType, "Ethereum", unknownOrKnown, 6) + if (err != nil) { return err } + + err = queryHosts.DownloadModeratorAddressDepositsFromHosts(true, networkType, "Cardano", unknownOrKnown, 6) + if (err != nil) { return err } + + return nil +} + + diff --git a/internal/network/peerClient/peerClient.go b/internal/network/peerClient/peerClient.go new file mode 100644 index 0000000..30b948f --- /dev/null +++ b/internal/network/peerClient/peerClient.go @@ -0,0 +1,503 @@ + +// peerClient provides functions to create and manage outgoing connections to hosts. +// Each connections has an associated encryption key that is established through a key handshake +// The encryption key encrypts all requests and responses between the host and requestor +// These connections are useful because they negate the need to reconnect and perform a new key handshake with each request to the same host + +package peerClient + +import "seekia/internal/cryptography/kyber" +import "seekia/internal/cryptography/nacl" +import "seekia/internal/helpers" +import "seekia/internal/mySettings" +import "seekia/internal/network/maliciousHosts" +import "seekia/internal/network/peerConnection" +import "seekia/internal/network/serverRequest" +import "seekia/internal/network/serverResponse" +import "seekia/internal/network/unreachableHosts" +import "seekia/internal/profiles/profileStorage" +import "seekia/internal/profiles/readProfiles" +import "seekia/internal/profiles/viewableProfiles" + +import messagepack "github.com/vmihailenco/msgpack/v5" + +import "net" +import "net/url" +import "net/netip" +import "sync" +import "time" +import "errors" + +type connectionMetadata struct{ + + // The host that the connection is made with + HostIdentityHash [16]byte + + // The key to use to encrypt all communications + ConnectionKey [32]byte + + // The NetworkType that the connection exists on + NetworkType byte +} + +var hostConnectionObjectsMapMutex sync.RWMutex + +// Map structure: Connection Identifier -> Connection object +var hostConnectionObjectsMap map[[21]byte]net.Conn = make(map[[21]byte]net.Conn) + +var hostConnectionMetadatasMapMutex sync.RWMutex + +// Map structure: Connection Identifier -> Connection Metadata +var hostConnectionMetadatasMap map[[21]byte]connectionMetadata = make(map[[21]byte]connectionMetadata) + +func addConnectionToConnectionObjectsMap(connectionIdentifier [21]byte, connectionObject net.Conn){ + + hostConnectionObjectsMapMutex.Lock() + hostConnectionObjectsMap[connectionIdentifier] = connectionObject + hostConnectionObjectsMapMutex.Unlock() +} + +func getConnectionFromConnectionObjectsMap(connectionIdentifier [21]byte)(bool, net.Conn){ + + hostConnectionObjectsMapMutex.RLock() + connectionObject, exists := hostConnectionObjectsMap[connectionIdentifier] + hostConnectionObjectsMapMutex.RUnlock() + + if (exists == false){ + return false, nil + } + + return true, connectionObject +} + +func deleteConnectionFromConnectionObjectsMap(connectionIdentifier [21]byte){ + + hostConnectionObjectsMapMutex.Lock() + delete(hostConnectionObjectsMap, connectionIdentifier) + hostConnectionObjectsMapMutex.Unlock() +} + +func addMetadataToConnectionMetadatasMap(connectionIdentifier [21]byte, hostIdentityHash [16]byte, connectionKey [32]byte, networkType byte)error{ + + connectionMetadataObject := connectionMetadata{ + HostIdentityHash: hostIdentityHash, + ConnectionKey: connectionKey, + NetworkType: networkType, + } + + hostConnectionMetadatasMapMutex.Lock() + hostConnectionMetadatasMap[connectionIdentifier] = connectionMetadataObject + hostConnectionMetadatasMapMutex.Unlock() + + return nil +} + +func deleteMetadataFromConnectionMetadatasMap(connectionIdentifier [21]byte){ + + hostConnectionMetadatasMapMutex.Lock() + delete(hostConnectionMetadatasMap, connectionIdentifier) + hostConnectionMetadatasMapMutex.Unlock() +} + +//Outputs: +// -bool: Connection found +// -[16]byte: Host identity hash +// -[32]byte: Connection key +// -byte: Network type +// -error +func GetConnectionMetadata(connectionIdentifier [21]byte)(bool, [16]byte, [32]byte, byte, error){ + + hostConnectionMetadatasMapMutex.RLock() + connectionMetadataObject, exists := hostConnectionMetadatasMap[connectionIdentifier] + hostConnectionMetadatasMapMutex.RUnlock() + if (exists == false){ + return false, [16]byte{}, [32]byte{}, 0, nil + } + + hostIdentityHash := connectionMetadataObject.HostIdentityHash + connectionKey := connectionMetadataObject.ConnectionKey + networkType := connectionMetadataObject.NetworkType + + return true, hostIdentityHash, connectionKey, networkType, nil +} + +//Outputs: +// -bool: Connection successful +// -[]byte: Response received +// -error +func SendRequestThroughConnection(connectionIdentifier [21]byte, contentToSend []byte)(bool, []byte, error){ + + connectionExists, connectionObject := getConnectionFromConnectionObjectsMap(connectionIdentifier) + if (connectionExists == false){ + return false, nil, nil + } + + //TODO: Fix max response size + requestSuccessful, responseBytes, err := peerConnection.SendRequestThroughConnection(connectionObject, contentToSend, 100000) + if (err != nil) { return false, nil, err } + if (requestSuccessful == false){ + return false, nil, nil + } + + return true, responseBytes, nil +} + +func CloseConnection(connectionIdentifier [21]byte)error{ + + connectionExists, connectionObject := getConnectionFromConnectionObjectsMap(connectionIdentifier) + if (connectionExists == false){ + deleteMetadataFromConnectionMetadatasMap(connectionIdentifier) + return nil + } + + _ = connectionObject.Close() + //TODO: Deal with error + + deleteConnectionFromConnectionObjectsMap(connectionIdentifier) + deleteMetadataFromConnectionMetadatasMap(connectionIdentifier) + + return nil +} + +//Outputs: +// -bool: Host profile found +// -bool: Connection established +// -[21]byte: The connection identifier +// -error +func EstablishNewConnectionToHost(allowClearnet bool, hostIdentityHash [16]byte, networkType byte)(bool, bool, [21]byte, error){ + + isValid := helpers.VerifyNetworkType(networkType) + if (isValid == false){ + networkTypeString := helpers.ConvertByteToString(networkType) + return false, false, [21]byte{}, errors.New("EstablishNewConnectionToHost called with invalid networkType: " + networkTypeString) + } + + getHostRawProfileMap := func()(bool, map[int]messagepack.RawMessage, error){ + + profileExists, _, _, _, _, hostRawProfileMap, err := viewableProfiles.GetNewestViewableUserProfile(hostIdentityHash, networkType, true, true, true) + if (err != nil) { return false, nil, err } + if (profileExists == true){ + return true, hostRawProfileMap, nil + } + + // Now we check to see if any host profile exists. + // This would be true if host's profile is banned. + // We will query banned hosts if there are not enough viewable hosts available + + profileExists, _, _, _, _, hostRawProfileMap, err = profileStorage.GetNewestUserProfile(hostIdentityHash, networkType) + if (err != nil) { return false, nil, err } + if (profileExists == true){ + return true, hostRawProfileMap, nil + } + + return false, nil, nil + } + + hostProfileExists, hostRawProfileMap, err := getHostRawProfileMap() + if (err != nil) { return false, false, [21]byte{}, err } + if (hostProfileExists == false){ + // This should only happen if host profile was deleted after eligible hosts list was created + return false, false, [21]byte{}, nil + } + + + profileIsDisabled, _, err := readProfiles.GetFormattedProfileAttributeFromRawProfileMap(hostRawProfileMap, "Disabled") + if (err != nil) { return false, false, [21]byte{}, err } + if (profileIsDisabled == true){ + // Profile is disabled. + return false, false, [21]byte{}, nil + } + + // This function will return the address and method we will use to contact the host + // + // If allowClearnet is false, function will only send request over Tor + // If allowClearnet is true, function will determine if we are in HostOverClearnet mode + // If we are, function will see if Host has a clearnet address and send request to that + // Otherwise, function will send request to host's .onion address over tor + // + // Sending over Tor prefers .onion address, but will connect to host's clearnet address over Tor exit node if onion address is offline or not provided + // Onion address could be offline due to DDOS attack, too many requests, or host network is blocking Tor access + // A host's client should automatically disable the .onion address if it is not reachable + // We will probably want to prefer connecting to clearnet URLs over Tor because .onion domains have become much slower due to recent spam attacks + // + //Outputs: + // -string: Method to use (Tor/Clearnet) + // -string: Address type (Tor/Clearnet) + // -string: Address to contact + // -int: Address port (if addressType is clearnet) + // -error + getHostAddressAndMethod := func()(string, string, string, int, error){ + + //Outputs: + // -bool: Host has tor address + // -string: Host tor address + // -error: Host + getHostTorAddress := func()(bool, string, error){ + + exists, hostTorAddress, err := readProfiles.GetFormattedProfileAttributeFromRawProfileMap(hostRawProfileMap, "TorAddress") + if (err != nil) { return false, "", err } + if (exists == false) { + return false, "", nil + } + return true, hostTorAddress, nil + } + + //Outputs: + // -bool: Host clearnet address exists + // -string: Host clearnet address + // -int: Host clearnet port + // -error + getHostClearnetAddress := func()(bool, string, int, error){ + + exists, clearnetAddress, err := readProfiles.GetFormattedProfileAttributeFromRawProfileMap(hostRawProfileMap, "ClearnetAddress") + if (err != nil) { return false, "", 0, err } + if (exists == false) { + return false, "", 0, nil + } + + exists, clearnetPort, err := readProfiles.GetFormattedProfileAttributeFromRawProfileMap(hostRawProfileMap, "ClearnetPort") + if (err != nil) { return false, "", 0, err } + if (exists == false) { + return false, "", 0, errors.New("Host profile with ClearnetAddress missing ClearnetPort") + } + + clearnetPortInt, err := helpers.ConvertStringToInt(clearnetPort) + if (err != nil) { + return false, "", 0, errors.New("Database corrupt: Contains host profile with invalid ClearnetPort: " + clearnetPort) + } + + return true, clearnetAddress, clearnetPortInt, nil + } + + hostHasTorAddress, hostTorAddress, err := getHostTorAddress() + if (err != nil) { return "", "", "", 0, err } + + hostHasClearnetAddress, hostClearnetAddress, hostClearnetPort, err := getHostClearnetAddress() + if (err != nil) { return "", "", "", 0, err } + + if (hostHasTorAddress == false && hostHasClearnetAddress == false){ + return "", "", "", 0, errors.New("Database corrupt: Contains host profile missing Tor and Clearnet addresses.") + } + + hostIsUnreachableClearnet, err := unreachableHosts.CheckIfHostIsUnreachable(hostIdentityHash, networkType, "Clearnet") + if (err != nil) { return "", "", "", 0, err } + + hostIsUnreachableTor, err := unreachableHosts.CheckIfHostIsUnreachable(hostIdentityHash, networkType, "Tor") + if (err != nil) { return "", "", "", 0, err } + + // If host is unreachable from both addresses, we will assume that eligibleHosts provided the host because there were not enough reachable hosts available + + getHostIsFullyUnreachableStatus := func()bool{ + + if (hostIsUnreachableClearnet == true && hostIsUnreachableTor == true){ + return true + } + return false + } + + hostIsFullyUnreachable := getHostIsFullyUnreachableStatus() + + if (allowClearnet == true && hostHasClearnetAddress == true){ + + checkIfHostOverClearnetModeIsEnabled := func()(bool, error){ + + exists, hostModeOnOffStatus, err := mySettings.GetSetting("HostModeOnOffStatus") + if (err != nil) { return false, err } + if (exists == false){ + return false, nil + } + + if (hostModeOnOffStatus != "On"){ + return false, nil + } + + exists, hostOverClearnetStatus, err := mySettings.GetSetting("HostOverClearnetOnOffStatus") + if (err != nil) { return false, err } + if (exists == false){ + return false, nil + } + if (hostOverClearnetStatus != "On"){ + return false, nil + } + + //TODO: Check for ModerateOverClearnet mode + + return true, nil + } + + hostOverClearnetModeEnabledStatus, err := checkIfHostOverClearnetModeIsEnabled() + if (err != nil) { return "", "", "", 0, err } + + if (hostOverClearnetModeEnabledStatus == true){ + + if (hostIsUnreachableClearnet == false || hostIsFullyUnreachable == true){ + + return "Clearnet", "Clearnet", hostClearnetAddress, hostClearnetPort, nil + } + } + } + + // Either hostOverClearnet is disabled or recipient does not have reachable clearnet address + // We will send request over tor + // We prefer .onion address but will also try to access their clearnet address over tor if .onion is unreachable/nonexistent + + if (hostHasTorAddress == true){ + + if (hostIsUnreachableTor == false || hostIsFullyUnreachable == true){ + + return "Tor", "Tor", hostTorAddress, 0, nil + } + } + + return "Tor", "Clearnet", hostClearnetAddress, hostClearnetPort, nil + } + + methodToUse, hostAddressType, hostAddress, addressPort, err := getHostAddressAndMethod() + if (err != nil) { return false, false, [21]byte{}, err } + + getFormattedAddressToContact := func()(string, error){ + + if (hostAddressType == "Tor"){ + return hostAddress, nil + } + hostIPAddressObject, err := netip.ParseAddr(hostAddress) + if (err == nil){ + + portString := helpers.ConvertIntToString(addressPort) + + addressIsIpv6 := hostIPAddressObject.Is6() + if (addressIsIpv6 == false){ + formattedAddress := hostAddress + ":" + portString + return formattedAddress, nil + } + formattedAddress := "[" + hostAddress + "]" + ":" + portString + return formattedAddress, nil + } + + // Address must be a clearnet URL, rather than an IP address + addressObject, err := url.Parse(hostAddress) + if (err != nil){ + // Address is not IP or clearnet. The profile is invalid. + return "", errors.New("Database corrupt: Contains host profile with invalid clearnetAddress: " + hostAddress) + } + + addressHost := addressObject.Host + formattedAddress := addressHost + ":http" + + return formattedAddress, nil + } + + formattedAddressToContact, err := getFormattedAddressToContact() + if (err != nil){ return false, false, [21]byte{}, err } + + //Outputs: + // -bool: Connection successful + // -net.Conn: Connection object + // -error + getConnectionObject := func()(bool, net.Conn, error){ + + if (methodToUse == "Tor"){ + //TODO + return false, nil, nil + } + // methodToUse == "Clearnet" + + newDialer := &net.Dialer{ + Timeout: time.Minute, + } + connectionObject, err := newDialer.Dial("tcp", formattedAddressToContact) + if (err != nil) { + //TODO: Distinguish between network error and other types of errors + return false, nil, nil + } + return true, connectionObject, nil + } + + connectionSuccessful, connectionObject, err := getConnectionObject() + if (err != nil) { return false, false, [21]byte{}, err } + if (connectionSuccessful == false){ + return true, false, [21]byte{}, nil + } + + //Outputs: + // -bool: Handshake successful + // -[32]byte: Connection key + // -error + performKeyHandshake := func()(bool, [32]byte, error){ + + myNaclPublicKey, myNaclPrivateKey, err := nacl.GetNewRandomPublicPrivateNaclKeys() + if (err != nil) { return false, [32]byte{}, err } + + myKyberPublicKey, myKyberPrivateKey, err := kyber.GetNewRandomPublicPrivateKyberKeys() + if (err != nil) { return false, [32]byte{}, err } + + requestBytes, requestIdentifier, err := serverRequest.CreateServerRequest_EstablishConnectionKey(hostIdentityHash, networkType, myNaclPublicKey, myKyberPublicKey) + if (err != nil) { return false, [32]byte{}, err } + + //TODO: Fix max response size + requestSuccessful, responseBytes, err := peerConnection.SendRequestThroughConnection(connectionObject, requestBytes, 100000) + if (err != nil) { return false, [32]byte{}, err } + if (requestSuccessful == false){ + return false, [32]byte{}, nil + } + + //TODO: Add ability to handle busy response/different network type response + // If host is busy, we will keep track of that in a new package and not try to recontact them for a while + + ableToRead, requestIdentifier_Received, hostIdentityHash_Received, connectionKey, err := serverResponse.ReadServerResponse_EstablishConnectionKey(responseBytes, myNaclPublicKey, myNaclPrivateKey, myKyberPrivateKey) + if (err != nil) { return false, [32]byte{}, err } + if (ableToRead == false){ + // Peer sent invalid response, must be malicious + + err := maliciousHosts.AddHostToMaliciousHostsList(hostIdentityHash) + if (err != nil){ return false, [32]byte{}, err } + + return false, [32]byte{}, nil + } + + checkIfResponseIsValid := func()bool{ + + if (requestIdentifier_Received != requestIdentifier){ + return false + } + if (hostIdentityHash_Received != hostIdentityHash){ + return false + } + return true + } + + responseIsValid := checkIfResponseIsValid() + if (responseIsValid == false){ + + err := maliciousHosts.AddHostToMaliciousHostsList(hostIdentityHash) + if (err != nil){ return false, [32]byte{}, err } + } + + return true, connectionKey, nil + } + + handshakeSuccessful, connectionKey, err := performKeyHandshake() + if (err != nil){ + _ = connectionObject.Close() + //TODO: Deal with possible errors + return false, false, [21]byte{}, err + } + if (handshakeSuccessful == false){ + _ = connectionObject.Close() + //TODO: Deal with possible errors + return true, false, [21]byte{}, nil + } + + newConnectionIdentifier, err := helpers.GetNewRandomBytes(21) + if (err != nil) { return false, false, [21]byte{}, err } + + newConnectionIdentifierArray := [21]byte(newConnectionIdentifier) + + addConnectionToConnectionObjectsMap(newConnectionIdentifierArray, connectionObject) + addMetadataToConnectionMetadatasMap(newConnectionIdentifierArray, hostIdentityHash, connectionKey, networkType) + + return true, true, newConnectionIdentifierArray, nil +} + + + diff --git a/internal/network/peerConnection/peerConnection.go b/internal/network/peerConnection/peerConnection.go new file mode 100644 index 0000000..0db5ad5 --- /dev/null +++ b/internal/network/peerConnection/peerConnection.go @@ -0,0 +1,125 @@ + +// peerConnection provides a function to send data through a net.Conn connection +// We need this package because we cannot send raw data through a connection object. +// Data must be sent with a prefix that describes the amount of data being sent + +package peerConnection + +//TODO: Catch any errors from connectionObject.Read and connectionObject.Write that +/// would be returned if something has gone wrong on our end + +//TODO: There might be a better way to make connections than this. Websockets? + +import "encoding/binary" +import "net" +import "errors" + +//Outputs: +// -bool: Request successful +// -[]byte: Response received +// -error +func SendRequestThroughConnection(connectionObject net.Conn, requestToSend []byte, maxResponseSize int)(bool, []byte, error){ + + requestSize := len(requestToSend) + + if (requestSize > 4294967295){ + return false, nil, errors.New("SendRequestThroughConnectionObject called with request that is too large.") + } + + requestSizeUint32 := uint32(requestSize) + + headerBytes := make([]byte, 4) + + binary.LittleEndian.PutUint32(headerBytes, requestSizeUint32) + + _, err := connectionObject.Write(headerBytes) + if (err != nil) { + return false, nil, nil + } + + _, err = connectionObject.Write(requestToSend) + if (err != nil) { + return false, nil, nil + } + + responseHeader := make([]byte, 4) + + _, err = connectionObject.Read(responseHeader) + if (err != nil) { + return false, nil, nil + } + + responseSizeUint32 := binary.LittleEndian.Uint32(responseHeader[:]) + + responseSizeInt := int(responseSizeUint32) + + responseBytes := make([]byte, responseSizeInt) + + _, err = connectionObject.Read(responseBytes) + if (err != nil) { + return false, nil, nil + } + + if (len(responseBytes) > maxResponseSize){ + return false, nil, nil + } + + return true, responseBytes, nil +} + + +//Outputs: +// -bool: Request received successfully +// -[]byte: Request bytes +// -error +func GetRequestThroughConnection(connectionObject net.Conn)(bool, []byte, error){ + + requestHeader := make([]byte, 4) + + _, err := connectionObject.Read(requestHeader) + if (err != nil){ + return false, nil, nil + } + + requestSizeUint32 := binary.LittleEndian.Uint32(requestHeader[:]) + + requestSizeInt := int(requestSizeUint32) + + requestBytes := make([]byte, requestSizeInt) + + _, err = connectionObject.Read(requestBytes) + if (err != nil) { + return false, nil, nil + } + + return true, requestBytes, nil +} + + +//Outputs: +// -bool: Response sent successfully +// -error +func SendResponseThroughConnection(connectionObject net.Conn, responseToSend []byte)(bool, error){ + + responseSize := len(responseToSend) + responseSizeUint32 := uint32(responseSize) + + headerBytes := make([]byte, 4) + + binary.LittleEndian.PutUint32(headerBytes, responseSizeUint32) + + _, err := connectionObject.Write(headerBytes) + if (err != nil) { + return false, nil + } + + _, err = connectionObject.Write(responseToSend) + if (err != nil) { + return false, nil + } + + return true, nil +} + + + diff --git a/internal/network/peerServer/peerServer.go b/internal/network/peerServer/peerServer.go new file mode 100644 index 0000000..6e4671e --- /dev/null +++ b/internal/network/peerServer/peerServer.go @@ -0,0 +1,330 @@ + +// peerServer provides functions to start and stop a host's server. + +package peerServer + +//TODO: Make sure this package works + +import "seekia/internal/logger" +import "seekia/internal/helpers" +import "seekia/internal/mySettings" +import "seekia/internal/network/appNetworkType/getAppNetworkType" +import "seekia/internal/network/peerConnection" +import "seekia/internal/network/craftResponses" + +import "sync" +import "net" +import "time" +import "errors" + +// We lock this mutex whenever we start or stop the server +var startOrStopServerMutex sync.Mutex + +var torServerIsRunningBoolMutex sync.RWMutex +var torServerIsRunningBool bool + +var clearnetServerIsRunningBoolMutex sync.RWMutex +var clearnetServerIsRunningBool bool + +func getTorServerIsRunningBool()bool{ + + torServerIsRunningBoolMutex.RLock() + currentStatus := torServerIsRunningBool + torServerIsRunningBoolMutex.RUnlock() + + return currentStatus +} + +func getClearnetServerIsRunningBool()bool{ + + clearnetServerIsRunningBoolMutex.RLock() + currentStatus := clearnetServerIsRunningBool + clearnetServerIsRunningBoolMutex.RUnlock() + + return currentStatus +} + +func setClearnetServerIsRunningBool(newStatus bool){ + + clearnetServerIsRunningBoolMutex.Lock() + clearnetServerIsRunningBool = newStatus + clearnetServerIsRunningBoolMutex.Unlock() +} + +func setTorServerIsRunningBool(newStatus bool){ + + torServerIsRunningBoolMutex.Lock() + torServerIsRunningBool = newStatus + torServerIsRunningBoolMutex.Unlock() +} + +// If host mode is enabled, this will be called automatically every 3 seconds by backgroundJobs +// If the server is already running, we return nil +func StartPeerServer()error{ + + startOrStopServerMutex.Lock() + defer startOrStopServerMutex.Unlock() + + exists, hostModeStatus, err := mySettings.GetSetting("HostModeOnOffStatus") + if (err != nil) { return err } + if (exists == false || hostModeStatus != "On"){ + return nil + } + + //TODO: Make sure Host identity exists, and connectivity checks (example: check.torproject.org) + + //TODO: Remove this return once this is ready + return nil + + startServerFunction := func(torOrClearnet string)error{ + + if (torOrClearnet != "Tor" && torOrClearnet != "Clearnet"){ + return errors.New("startServerFunction called with invalid torOrClearnet: " + torOrClearnet) + } + + getCurrentServerIsRunningStatus := func()bool{ + + if (torOrClearnet == "Tor"){ + currentStatus := getTorServerIsRunningBool() + return currentStatus + } + + currentStatus := getClearnetServerIsRunningBool() + return currentStatus + } + + currentRunningStatus := getCurrentServerIsRunningStatus() + if (currentRunningStatus == true){ + return nil + } + + getServerListenerObject := func()(net.Listener, error){ + + //if (torOrClearnet == "Tor"){ + // //TODO + // return nil, nil + //} + + getMyClearnetPort := func()(string, error){ + + exists, myPort, err := mySettings.GetSetting("HostOverClearnetPort") + if (err != nil) { return "", err } + if (exists == false){ + return "10321", nil + } + myPortInt, err := helpers.ConvertStringToInt(myPort) + if (err != nil) { + return "", errors.New("MySettings corrupt: Contains invalid HostOverClearnetPort: " + myPort) + } + + if (myPortInt < 1 || myPortInt > 49151){ + return "", errors.New("MySettings corrupt: Contains invalid HostOverClearnetPort: " + myPort) + } + + return myPort, nil + } + + myClearnetPort, err := getMyClearnetPort() + if (err != nil) { return nil, err } + + newListenerObject, err := net.Listen("tcp", "127.0.0.1:" + myClearnetPort) + if (err != nil) { return nil, err } + + return newListenerObject, nil + } + + serverListenerObject, err := getServerListenerObject() + if (err != nil){ + return errors.New("Server Error: " + err.Error()) + } + + // This loop will wait for new connections, and handle them when they arrive + runServerLoop := func(){ + + for { + + newConnectionObject, err := serverListenerObject.Accept() + if (err != nil) { + logger.AddLogEntry("Network", "Server Error:" + err.Error()) + break + } + + go handleServerConnection(newConnectionObject) + } + + serverListenerObject.Close() + + if (torOrClearnet == "Tor"){ + setTorServerIsRunningBool(false) + } else { + setClearnetServerIsRunningBool(false) + } + } + + // This loop will check if we have shutdown the server + // If we have, it will shutdown the serverListenerObject + runServerShutdownLoop := func(){ + + for { + + if (torOrClearnet == "Tor"){ + torServerIsRunning := getTorServerIsRunningBool() + if (torServerIsRunning == false){ + break + } + } else { + clearnetServerIsRunning := getClearnetServerIsRunningBool() + if (clearnetServerIsRunning == false){ + break + } + } + + time.Sleep(time.Millisecond * 500) + } + + serverListenerObject.Close() + } + + if (torOrClearnet == "Tor"){ + setTorServerIsRunningBool(true) + } else { + setClearnetServerIsRunningBool(true) + } + + go runServerLoop() + go runServerShutdownLoop() + + return nil + } + + settingExists, hostOverClearnetStatus, err := mySettings.GetSetting("HostOverClearnetOnOffStatus") + if (err != nil) { return err } + if (settingExists == true && hostOverClearnetStatus == "On"){ + err := startServerFunction("Clearnet") + if (err != nil){ return err } + } + + settingExists, hostOverTorStatus, err := mySettings.GetSetting("HostOverTorOnOffStatus") + if (err != nil) { return err } + if (settingExists == false || hostOverTorStatus != "Off"){ + err := startServerFunction("Tor") + if (err != nil){ return err } + } + + return nil +} + +// If host mode is disabled, this will be called automatically every 3 seconds by backgroundJobs +// If the server is not running, we return nil +// We will also call this when the application is shutting down. Background jobs will stop calling it at that point. +func StopPeerServer()error{ + + //TODO: Graceful shutdown of current connections + // Finish sending data, and then close connection. + // We just want to avoid the requestor from designating us as a malicious host + + startOrStopServerMutex.Lock() + defer startOrStopServerMutex.Unlock() + + setClearnetServerIsRunningBool(false) + setTorServerIsRunningBool(false) + + return nil +} + + +func handleServerConnection(inputConnection net.Conn){ + + handleConnection := func()error{ + + // First request should always be an EstablishConnectionKey request + + downloadSuccessful, requestBytes, err := peerConnection.GetRequestThroughConnection(inputConnection) + if (err != nil){ return err } + if (downloadSuccessful == false){ + return nil + } + + appNetworkType, err := getAppNetworkType.GetAppNetworkType() + if (err != nil) { return err } + + requestIsWellFormed, identityHashMatches, networkTypeMatches, establishKeyResponse, connectionKey, err := craftResponses.GetEstablishConnectionKeyResponse(requestBytes, appNetworkType) + if (err != nil) { return err } + if (requestIsWellFormed == false){ + + responseToSend := []byte("Invalid request.") + + _, err := peerConnection.SendResponseThroughConnection(inputConnection, responseToSend) + if (err != nil){ return err } + + return nil + } + if (identityHashMatches == false){ + + // The requestor believes we have a different identity hash than we currently do. + // This could be happening because we recently switched our identity + // It could also be happening because someone else is sharing our IP address on their own host profile + + responseToSend := []byte("Contacted wrong host.") + + _, err := peerConnection.SendResponseThroughConnection(inputConnection, responseToSend) + if (err != nil){ return err } + + return nil + } + if (networkTypeMatches == false){ + // The requestor believes we are on a different network type than we are + // This may be because we recently switched network types. + + responseToSend := []byte("Invalid network type.") + + _, err := peerConnection.SendResponseThroughConnection(inputConnection, responseToSend) + if (err != nil){ return err } + + return nil + } + + responseSentSuccessfully, err := peerConnection.SendResponseThroughConnection(inputConnection, establishKeyResponse) + if (err != nil){ return err } + if (responseSentSuccessfully == false){ + return nil + } + + // Connection is established + // Now we wait for new data to be sent + + for { + + downloadSuccessful, requestBytes, err := peerConnection.GetRequestThroughConnection(inputConnection) + if (err != nil){ return err } + if (downloadSuccessful == false){ + return nil + } + + responseBytes, err := craftResponses.GetServerResponseForRequest(requestBytes, appNetworkType, connectionKey) + if (err != nil) { return err } + + responseSuccessful, err := peerConnection.SendResponseThroughConnection(inputConnection, responseBytes) + if (err != nil) { return err } + if (responseSuccessful == false){ + return nil + } + + //TODO: Deal with pruning old connections + // We should keep a count of all active connections + // If it exceeds a certain limit, we should stop accepting new connections and reply with a Busy message + // We should also start dropping connections, starting with the ones that have been idle for the longest + } + } + + err := handleConnection() + if (err != nil){ + logger.AddLogEntry("Network", "Server Error: " + err.Error()) + } + + inputConnection.Close() +} + + + + diff --git a/internal/network/queryHosts/queryHosts.go b/internal/network/queryHosts/queryHosts.go new file mode 100644 index 0000000..a0e4133 --- /dev/null +++ b/internal/network/queryHosts/queryHosts.go @@ -0,0 +1,3372 @@ + +// queryHosts provides functions to download information from hosts +// These include parameters, profiles, messages, reports, reviews, identity deposits, and trusted moderation viewable statuses +// Each function will query a specified number of eligible hosts + +package queryHosts + +//TODO: If you download a user's identity, then the host is saying that the user's identity is funded. Add it to trustedFundedStatus + +//TODO: Split requests up so that memory does not have to store all of the requested hashes +//TODO: Set a maximum limit, so that only a certain amount of data will be requested from each host + +//TODO: Choose hosts more intelligently based on what ranges we are requesting +// Right now, we are sending requests to randomly chosen hosts until we reach the numberOfHostsToQuery +// This is a bad strategy, because, if we are requesting range 20-100, we might request from a host that is only hosting from range 10-25 +// We should use numberOfHostsToQuery to choose a set of hosts that will host the entire range +// We must make sure that we sometimes request from hosts that only host a small number of total profiles. + +// TODO: Add BroadcastToHosts, GetFundedStatuses, and more + +//TODO: If only Host/Moderator mode is enabled, we can skip all of the downloaded identity/inbox fingerprinting prevention logic +// This logic is designed to prevent hosts from learning the user's moderator/host identity/behavior from their downloaded content +// For example, a user is moderating and hosting. requestors could determine a host's moderator identity by querying for +// profiles outside of their host range and seeing which mate profiles they have downloaded. +// Using this information, they could learn their moderator range and link it to their moderator identity/profile. +// +// We cannot disable fingerprinting if Mate mode is enabled, because a user's Mate criteria will change, so we cannot allow the host to know what their criteria was previously +// This is also true if a Host or Moderator's ranges change, but in those cases, the leak of privacy is fine +// This is because the information (previous host/moderator ranges) is not sensitive, and not a problem if it is leaked + +import "seekia/internal/byteRange" +import "seekia/internal/badgerDatabase" +import "seekia/internal/contentMetadata" +import "seekia/internal/encoding" +import "seekia/internal/helpers" +import "seekia/internal/identity" +import "seekia/internal/logger" +import "seekia/internal/messaging/chatMessageStorage" +import "seekia/internal/moderation/moderatorScores" +import "seekia/internal/moderation/readReports" +import "seekia/internal/moderation/readReviews" +import "seekia/internal/moderation/reportStorage" +import "seekia/internal/moderation/reviewStorage" +import "seekia/internal/moderation/trustedAddressDeposits" +import "seekia/internal/moderation/trustedViewableStatus" +import "seekia/internal/network/eligibleHosts" +import "seekia/internal/network/hostRanges" +import "seekia/internal/network/maliciousHosts" +import "seekia/internal/network/mateCriteria" +import "seekia/internal/network/peerClient" +import "seekia/internal/network/sendRequests" +import "seekia/internal/network/serverResponse" +import "seekia/internal/parameters/parametersStorage" +import "seekia/internal/profiles/profileStorage" +import "seekia/internal/profiles/readProfiles" + +import "bytes" +import "errors" +import "slices" + +func DownloadParametersFromHosts(allowClearnet bool, networkType byte, numberOfHostsToQuery int)error{ + + isValid := helpers.VerifyNetworkType(networkType) + if (isValid == false){ + networkTypeString := helpers.ConvertByteToString(networkType) + return errors.New("DownloadParametersFromHosts called with invalid networkType: " + networkTypeString) + } + + // Outputs: + // -bool: Successful download from host + // -error + downloadParametersFromHost := func(hostIdentityHash [16]byte)(bool, error){ + + exists, _, _, _, _, hostRawProfileMap, err := profileStorage.GetNewestUserProfile(hostIdentityHash, networkType) + if (err != nil) { return false, err } + if (exists == false){ + // Host profile was deleted, skip host + return false, nil + } + + exists, hostIsHostingParameters, err := readProfiles.GetFormattedProfileAttributeFromRawProfileMap(hostRawProfileMap, "HostingParameters") + if (err != nil) { return false, err } + if (exists == false){ + return false, errors.New("Database corrupt: Contains host Profile missing HostingParameters") + } + if (hostIsHostingParameters != "Yes"){ + // This host is not hosting parameters, skip + return false, nil + } + + hostProfileExists, connectionEstablished, connectionIdentifier, err := peerClient.EstablishNewConnectionToHost(allowClearnet, hostIdentityHash, networkType) + if (err != nil){ return false, err } + if (hostProfileExists == false){ + return false, nil + } + if (connectionEstablished == false){ + return false, nil + } + defer peerClient.CloseConnection(connectionIdentifier) + + downloadSuccessful, parametersInfoMap, err := sendRequests.GetParametersInfoFromHost(connectionIdentifier, hostIdentityHash, networkType) + if (err != nil) { return false, err } + if (downloadSuccessful == false){ + return false, nil + } + + if (len(parametersInfoMap) == 0){ + // The host has provided no parameters. We will not consider this a successful download + return false, nil + } + + // We will check to see if our parameters broadcast time is older than the one the host is offering + // If it is, we will download it and continue + + parametersTypesToRetrieveList := make([]string, 0) + + for parametersType, parametersBroadcastTime := range parametersInfoMap{ + + storedParametersFound, _, _, storedParametersBroadcastTime, err := parametersStorage.GetAuthorizedParameters(parametersType, networkType) + if (err != nil) { return false, err } + if (storedParametersFound == true){ + if (storedParametersBroadcastTime < parametersBroadcastTime){ + // The offered parameters are not newer than our existing parameters + // Skip downloading them. + continue + } + } + parametersTypesToRetrieveList = append(parametersTypesToRetrieveList, parametersType) + } + + if (len(parametersTypesToRetrieveList) == 0){ + // The host has no parameters that we need. Nothing to download. + return true, nil + } + + // Now we download parameters + + downloadSuccessful, receivedParametersList, err := sendRequests.GetParametersFromHost(connectionIdentifier, hostIdentityHash, networkType, parametersTypesToRetrieveList) + if (err != nil) { return false, err } + if (downloadSuccessful == false){ + // Unable to download parameters. Nothing left to do with this host. + return false, nil + } + + if (len(receivedParametersList) == 0){ + // Host is probably malicious + // Host could have received new admin permissions after we got parameters info + // Either way, we do not count this as a successful download + return false, nil + } + + for _, parametersBytes := range receivedParametersList{ + + parametersAreValid, _, err := parametersStorage.AddParameters(parametersBytes) + if (err != nil) { return false, err } + if (parametersAreValid == false){ + return false, errors.New("GetParametersFromHost not verifying parameters.") + } + } + + // These parameters were downloaded, not necessarily authorized or added + + numberOfDownloadedParametersString := helpers.ConvertIntToString(len(receivedParametersList)) + + err = logger.AddLogEntry("Network", "Successfully downloaded " + numberOfDownloadedParametersString + " network parameters from host.") + if (err != nil) { return false, err } + + return true, nil + } + + eligibleHostsList, err := eligibleHosts.GetEligibleHostsList(networkType) + if (err != nil) { return err } + + queriedHosts := 0 + + for _, hostIdentityHash := range eligibleHostsList{ + + successfulDownload, err := downloadParametersFromHost(hostIdentityHash) + if (err != nil) { return err } + if (successfulDownload == true){ + queriedHosts += 1 + } + if (queriedHosts >= numberOfHostsToQuery){ + return nil + } + } + + return nil +} + + +func DownloadProfilesFromHosts( + allowClearnet bool, + networkType byte, + profileTypeToRetrieve string, + rangeToRetrieveStart [16]byte, + rangeToRetrieveEnd [16]byte, + identityHashesToRetrieveList [][16]byte, + criteria []byte, + getNewestProfilesOnly bool, + getViewableProfilesOnly bool, + downloadAllMode bool, + checkIfProfileShouldBeDownloaded func([28]byte, [16]byte, int64)(bool, error), + numberOfHostsToQuery int)error{ + + isValid := helpers.VerifyNetworkType(networkType) + if (isValid == false){ + networkTypeString := helpers.ConvertByteToString(networkType) + return errors.New("DownloadProfilesFromHosts called with invalid networkType: " + networkTypeString) + } + + // We cycle through each host + // We get the profiles info for profiles that are within our requested criteria/range/identitites list + // We see if any are knowingly outside of criteria, and download anyway if they are (to resist fingerprinting) + // We download the profiles. + // We don't download any profiles that we already have (if desiresPruningMode == false) + + //TODO: Add better selection of hosts if getViewableOnly is off. Prioritize hosts hosting non-viewable profiles + + //Outputs: + // -bool: Successfully downloaded from host (false if host does not have any profiles we need) + // -error + downloadProfilesFromHost := func(hostIdentityHash [16]byte)(bool, error){ + + exists, _, _, _, _, hostRawProfileMap, err := profileStorage.GetNewestUserProfile(hostIdentityHash, networkType) + if (err != nil) { return false, err } + if (exists == false){ + // Host profile was deleted, skip host + return false, nil + } + + hostIsHostingProfileType, theirHostRangeStart, theirHostRangeEnd, err := hostRanges.GetHostedIdentityHashRangeFromHostRawProfileMap(hostRawProfileMap, profileTypeToRetrieve) + if (err != nil) { return false, err } + if (hostIsHostingProfileType == false){ + // Host is not hosting any profiles of this profileType. Skip to next host + return false, nil + } + + //Outputs: + // -bool: Download successful + // -[21]byte: Connection identifier + // -[]serverResponse.ProfileInfoStruct: Retrieved profiles info objects list + // -error + getRetrievedProfileInfoObjectsList := func()(bool, [21]byte, []serverResponse.ProfileInfoStruct, error){ + + // Our request will either be based on identity hash range or identity hash list + // We will split up the request into either ranges or identity hashes + + maximumProfilesInGetProfilesInfoRequest := serverResponse.MaximumProfilesInResponse_GetProfilesInfo - 100 + + if (len(identityHashesToRetrieveList) != 0){ + + // We will retrieve identities lists, not identity ranges + + anyValuesExist, identitiesInMyRangeList, err := byteRange.GetAllIdentityHashesInListWithinRange(rangeToRetrieveStart, rangeToRetrieveEnd, identityHashesToRetrieveList) + if (err != nil) { return false, [21]byte{}, nil, err } + if (anyValuesExist == false){ + // This should not happen + return false, [21]byte{}, nil, errors.New("Identity hashes list to retrieve contains no identities within request range.") + } + + anyValuesExist, identitiesInTheirRangeList, err := byteRange.GetAllIdentityHashesInListWithinRange(theirHostRangeStart, theirHostRangeEnd, identitiesInMyRangeList) + if (err != nil) { return false, [21]byte{}, nil, err } + if (anyValuesExist == false){ + // This host is not hosting any identities we desire. We will skip them. + return false, [21]byte{}, nil, nil + } + + // Now we split request into sublists if needed + + getMaximumIdentitiesPerRequest := func()int{ + + if (getNewestProfilesOnly == true){ + return maximumProfilesInGetProfilesInfoRequest + } + + // We assume each identity has approximately 3 profiles + + maximumIdentitiesPerRequest := maximumProfilesInGetProfilesInfoRequest / 3 + + return maximumIdentitiesPerRequest + } + + maximumIdentitiesPerRequest := getMaximumIdentitiesPerRequest() + + sublistsToQueryList, err := helpers.SplitListIntoSublists(identitiesInTheirRangeList, maximumIdentitiesPerRequest) + if (err != nil) { return false, [21]byte{}, nil, err } + + hostProfileExists, connectionEstablished, connectionIdentifier, err := peerClient.EstablishNewConnectionToHost(allowClearnet, hostIdentityHash, networkType) + if (err != nil){ return false, [21]byte{}, nil, err } + if (hostProfileExists == false){ + return false, [21]byte{}, nil, nil + } + if (connectionEstablished == false){ + return false, [21]byte{}, nil, nil + } + + downloadProfileInfoObjectsList := func()(bool, []serverResponse.ProfileInfoStruct, error){ + + retrievedProfileInfoObjectsList := make([]serverResponse.ProfileInfoStruct, 0) + + minimumRange, maximumRange := byteRange.GetMinimumMaximumIdentityHashBounds() + + for _, identityHashesSublist := range sublistsToQueryList{ + + downloadSuccessful, profilesInfoObjectsList, err := sendRequests.GetProfilesInfoFromHost(connectionIdentifier, hostIdentityHash, networkType, profileTypeToRetrieve, minimumRange, maximumRange, identityHashesSublist, criteria, getNewestProfilesOnly, getViewableProfilesOnly) + if (err != nil) { return false, nil, err } + if (downloadSuccessful == false){ + // Failed to download this subrange. + // Could be that host went offline permanently or just this connection failed. + // Either way, cease connection to host + err := logger.AddLogEntry("Network", "Failed to download profiles info from host.") + if (err != nil) { return false, nil, err } + + err = peerClient.CloseConnection(connectionIdentifier) + if (err != nil) { return false, nil, err } + + return false, nil, nil + } + + retrievedProfileInfoObjectsList = append(retrievedProfileInfoObjectsList, profilesInfoObjectsList...) + } + + return true, retrievedProfileInfoObjectsList, nil + } + + downloadSuccessful, retrievedProfileInfoObjectsList, err := downloadProfileInfoObjectsList() + if (err != nil){ + + errB := peerClient.CloseConnection(connectionIdentifier) + if (errB != nil) { return false, [21]byte{}, nil, errB } + + return false, [21]byte{}, nil, err + } + if (downloadSuccessful == false){ + + err = peerClient.CloseConnection(connectionIdentifier) + if (err != nil) { return false, [21]byte{}, nil, err } + + return false, [21]byte{}, nil, nil + } + + return true, connectionIdentifier, retrievedProfileInfoObjectsList, nil + } + + // We will retrieve identity ranges, not identity lists + + //Outputs: + // -Any intersection found + // -[16]byte: Intersection range start + // -[16]byte: Intersection range end + // -error + getIntersectionRange := func()(bool, [16]byte, [16]byte, error){ + + if (profileTypeToRetrieve != "Mate"){ + // Hosts must host all or none of host/moderator identities, so intersection is always all our requested identities + + return true, rangeToRetrieveStart, rangeToRetrieveEnd, nil + } + + anyIntersectionFound, intersectionRangeStart, intersectionRangeEnd, err := byteRange.GetIdentityIntersectionRangeFromTwoRanges(rangeToRetrieveStart, rangeToRetrieveEnd, theirHostRangeStart, theirHostRangeEnd) + if (err != nil) { return false, [16]byte{}, [16]byte{}, err } + + if (anyIntersectionFound == false){ + + return false, [16]byte{}, [16]byte{}, nil + } + + return true, intersectionRangeStart, intersectionRangeEnd, nil + } + + anyIntersectionFound, intersectionRangeStart, intersectionRangeEnd, err := getIntersectionRange() + if (err != nil) { return false, [21]byte{}, nil, err } + if (anyIntersectionFound == false){ + // Host is not hosting any profiles within the range that we are requesting + // Skip this host + return false, [21]byte{}, nil, nil + } + + exists, hostedProfilesQuantity, err := readProfiles.GetFormattedProfileAttributeFromRawProfileMap(hostRawProfileMap, profileTypeToRetrieve + "ProfilesQuantity") + if (err != nil) { return false, [21]byte{}, nil, err } + if (exists == false){ + return false, [21]byte{}, nil, errors.New("Database corrupt: Contains host profile missing profilesQuantity") + } + + hostedProfilesQuantityInt64, err := helpers.ConvertStringToInt64(hostedProfilesQuantity) + if (err != nil) { return false, [21]byte{}, nil, err } + + if (hostedProfilesQuantityInt64 == 0){ + // Host must have just started their node. Skip them. + return false, [21]byte{}, nil, nil + } + + estimatedNumItemsInSubrange, err := byteRange.GetEstimatedIdentitySubrangeQuantity(theirHostRangeStart, theirHostRangeEnd, hostedProfilesQuantityInt64, intersectionRangeStart, intersectionRangeEnd) + if (err != nil) { return false, [21]byte{}, nil, err } + + subrangesToQueryList, err := byteRange.SplitIdentityRangeIntoEqualSubranges(intersectionRangeStart, intersectionRangeEnd, estimatedNumItemsInSubrange, int64(maximumProfilesInGetProfilesInfoRequest)) + if (err != nil) { return false, [21]byte{}, nil, err } + + + hostProfileExists, connectionEstablished, connectionIdentifier, err := peerClient.EstablishNewConnectionToHost(allowClearnet, hostIdentityHash, networkType) + if (err != nil){ return false, [21]byte{}, nil, err } + if (hostProfileExists == false){ + return false, [21]byte{}, nil, nil + } + if (connectionEstablished == false){ + return false, [21]byte{}, nil, nil + } + + //Outputs: + // -bool: Download successful + // -[]serverResponse.ProfileInfoStruct: Retrieved profiles info map list + // -error + downloadProfileInfoObjectsList := func()(bool, []serverResponse.ProfileInfoStruct, error){ + + retrievedProfileInfoObjectsList := make([]serverResponse.ProfileInfoStruct, 0) + + for _, subrangeObject := range subrangesToQueryList{ + + subrangeStart := subrangeObject.SubrangeStart + subrangeEnd := subrangeObject.SubrangeEnd + + downloadSuccessful, profileInfoObjectsList, err := sendRequests.GetProfilesInfoFromHost(connectionIdentifier, hostIdentityHash, networkType, profileTypeToRetrieve, subrangeStart, subrangeEnd, identityHashesToRetrieveList, criteria, getNewestProfilesOnly, getViewableProfilesOnly) + if (err != nil) { return false, nil, err } + if (downloadSuccessful == false){ + // Failed to download this subrange. + // Could be that host went offline permanently or just this connection failed. + // Either way, close connection + err := logger.AddLogEntry("Network", "Failed to download profiles info from host.") + if (err != nil) { return false, nil, err } + + return false, nil, nil + } + + retrievedProfileInfoObjectsList = append(retrievedProfileInfoObjectsList, profileInfoObjectsList...) + } + + return true, retrievedProfileInfoObjectsList, nil + } + + downloadSuccessful, retrievedProfileInfoObjectsList, err := downloadProfileInfoObjectsList() + if (err != nil){ + errB := peerClient.CloseConnection(connectionIdentifier) + if (errB != nil) { return false, [21]byte{}, nil, errB } + + return false, [21]byte{}, nil, err + } + if (downloadSuccessful == false){ + err = peerClient.CloseConnection(connectionIdentifier) + if (err != nil) { return false, [21]byte{}, nil, err } + + return false, [21]byte{}, nil, nil + } + + return true, connectionIdentifier, retrievedProfileInfoObjectsList, nil + } + + downloadSuccessful, connectionIdentifier, retrievedProfilesInfoObjectsList, err := getRetrievedProfileInfoObjectsList() + if (err != nil) { return false, err } + if (downloadSuccessful == false){ + return false, nil + } + + defer peerClient.CloseConnection(connectionIdentifier) + + if (len(retrievedProfilesInfoObjectsList) == 0){ + // Host has no profiles that we need. We are done with this host. + + return true, nil + } + + profileHashesToDownloadList := make([][28]byte, 0) + + // Structure: Profile Hash -> Expected Broadcast Time + profileBroadcastTimesMap := make(map[[28]byte]int64) + + // Structure: Profile Hash -> Identity Hash + profileIdentityHashesMap := make(map[[28]byte][16]byte) + + hostIsMalicious := false + + for _, profileInfoObject := range retrievedProfilesInfoObjectsList{ + + receivedProfileHash := profileInfoObject.ProfileHash + + receivedProfileAuthor := profileInfoObject.ProfileAuthor + + receivedProfileBroadcastTime := profileInfoObject.ProfileBroadcastTime + + // Now we check to see if we already have the profile + // We check to see if it is known to be outside of requested range/identities list/criteria (host is malicious) + // If it is, we still request it in our download, and then set host as malicious afterwards + // Otherwise, host would know that we have this profile stored + //TODO: Disable this step if only host/moderator mode is enabled + + storedProfileExists, storedProfileBytes, err := badgerDatabase.GetUserProfile(profileTypeToRetrieve, receivedProfileHash) + if (err != nil) { return false, err } + + checkIfHostIsOfferingInvalidProfile := func()(bool, error){ + + if (storedProfileExists == false){ + return false, nil + } + + ableToRead, storedProfileHash, profileVersion, profileNetworkType, storedProfileAuthor, storedProfileBroadcastTime, _, storedRawProfileMap, err := readProfiles.ReadProfileAndHash(false, storedProfileBytes) + if (err != nil) { return false, err } + if (ableToRead == false){ + return false, errors.New("Database corrupt: Contains invalid profile.") + } + if (storedProfileHash != receivedProfileHash){ + return false, errors.New("Database corrupt: Contains profile with different hash than entry key.") + } + if (profileNetworkType != networkType){ + return true, nil + } + if (storedProfileAuthor != receivedProfileAuthor){ + return true, nil + } + if (storedProfileBroadcastTime != receivedProfileBroadcastTime){ + return true, nil + } + + if (criteria != nil){ + criteriaIsValid, fulfillsCriteria, err := mateCriteria.CheckIfMateProfileFulfillsCriteria(false, profileVersion, storedRawProfileMap, criteria) + if (err != nil) { return false, err } + if (criteriaIsValid == false){ + return false, errors.New("DownloadProfilesFromHosts called with invalid criteria.") + } + if (fulfillsCriteria == false){ + return true, nil + } + } + + return false, nil + } + + hostIsOfferingInvalidProfile, err := checkIfHostIsOfferingInvalidProfile() + if (err != nil) { return false, err } + + if (hostIsOfferingInvalidProfile == true){ + // Host is malicious, we must still download the profile + hostIsMalicious = true + + profileHashesToDownloadList = append(profileHashesToDownloadList, receivedProfileHash) + profileIdentityHashesMap[receivedProfileHash] = receivedProfileAuthor + profileBroadcastTimesMap[receivedProfileHash] = receivedProfileBroadcastTime + + continue + } + + if (downloadAllMode == true){ + // downloadAllMode is enabled, we must download all profiles regardless of if we already have it + // This is done to prevent hosts learning our private desires + profileHashesToDownloadList = append(profileHashesToDownloadList, receivedProfileHash) + profileIdentityHashesMap[receivedProfileHash] = receivedProfileAuthor + profileBroadcastTimesMap[receivedProfileHash] = receivedProfileBroadcastTime + continue + } + + if (storedProfileExists == true){ + + // We already have profile, skip downloading it + + if (getViewableProfilesOnly == true){ + + err = trustedViewableStatus.AddTrustedProfileIsViewableStatus(receivedProfileHash, hostIdentityHash, true) + if (err != nil) { return false, err } + } + + continue + } + + // Profile does not exist, we must check if we want to download it + // This works differently for Hosting, Moderation, and Browsing + + shouldDownload, err := checkIfProfileShouldBeDownloaded(receivedProfileHash, receivedProfileAuthor, receivedProfileBroadcastTime) + if (err != nil) { return false, err } + + if (shouldDownload == true){ + profileHashesToDownloadList = append(profileHashesToDownloadList, receivedProfileHash) + profileIdentityHashesMap[receivedProfileHash] = receivedProfileAuthor + profileBroadcastTimesMap[receivedProfileHash] = receivedProfileBroadcastTime + continue + } + } + + if (len(profileHashesToDownloadList) == 0){ + // Nothing to download, done with this host. + + if (hostIsMalicious == true){ + err := maliciousHosts.AddHostToMaliciousHostsList(hostIdentityHash) + if (err != nil) { return false, err } + } + + return true, nil + } + + // Now we retrieve profiles from host + + maximumProfileHashesInResponse, err := serverResponse.GetMaximumProfilesInResponse_GetProfiles(profileTypeToRetrieve) + if (err != nil) { return false, err } + + profileHashesSublistsList, err := helpers.SplitListIntoSublists(profileHashesToDownloadList, maximumProfileHashesInResponse) + if (err != nil) { return false, err } + + numberOfDownloadedProfiles := 0 + + for _, profileHashesSublist := range profileHashesSublistsList{ + + downloadSuccessful, profilesList, err := sendRequests.GetProfilesFromHost(connectionIdentifier, hostIdentityHash, networkType, profileTypeToRetrieve, profileHashesSublist, profileIdentityHashesMap, profileBroadcastTimesMap, nil) + if (err != nil) { return false, err } + if (downloadSuccessful == false){ + // Request has failed + // We will stop downloading from this host + break + } + + for _, profileBytes := range profilesList{ + + //TODO: If downloadAll mode is enabled, we need to check to see if profile should be imported or not (desires pruning mode) + + profileIsWellFormed, addedProfileHash, err := profileStorage.AddUserProfile(profileBytes) + if (err != nil) { return false, err } + + if (profileIsWellFormed == false){ + return false, errors.New("GetProfilesFromHost not verifying profiles are well formed.") + } + + if (getViewableProfilesOnly == true){ + + err = trustedViewableStatus.AddTrustedProfileIsViewableStatus(addedProfileHash, hostIdentityHash, true) + if (err != nil) { return false, err } + } + } + + numberOfDownloadedProfiles += len(profilesList) + } + + if (hostIsMalicious == true){ + err := maliciousHosts.AddHostToMaliciousHostsList(hostIdentityHash) + if (err != nil) { return false, err } + } + + if (numberOfDownloadedProfiles != 0){ + + numberOfProfilesDownloadedString := helpers.ConvertIntToString(numberOfDownloadedProfiles) + + err = logger.AddLogEntry("Network", "Successfully downloaded " + numberOfProfilesDownloadedString + " " + profileTypeToRetrieve + " profiles from host.") + if (err != nil) { return false, err } + } + + return true, nil + } + + eligibleHostsList, err := eligibleHosts.GetEligibleHostsList(networkType) + if (err != nil) { return err } + + queriedHosts := 0 + + for _, hostIdentityHash := range eligibleHostsList{ + + successfulDownload, err := downloadProfilesFromHost(hostIdentityHash) + if (err != nil) { return err } + if (successfulDownload == true){ + queriedHosts += 1 + } + if (queriedHosts >= numberOfHostsToQuery){ + return nil + } + } + + return nil +} + + +func DownloadMessagesFromHosts( + allowClearnet bool, + networkType byte, + rangeToRetrieveStart [10]byte, + rangeToRetrieveEnd [10]byte, + inboxesToRetrieveList [][10]byte, + getViewableMessagesOnly bool, + getDecryptableMessagesOnly bool, + checkIfMessageShouldBeDownloaded func([26]byte)(bool, error), + numberOfHostsToQuery int)error{ + + isValid := helpers.VerifyNetworkType(networkType) + if (isValid == false){ + networkTypeString := helpers.ConvertByteToString(networkType) + return errors.New("DownloadMessagesFromHosts called with invalid networkType: " + networkTypeString) + } + + // We cycle through each host + // We get all message hashes that they host that are within our requested range/inboxes list + // We see if any are knowingly outside of inboxes list, and download anyway if they are (to resist fingerprinting) + // We download the messages that we don't already have + + //TODO: Add better selection of hosts if getViewableOnly is off. Prioritize hosts hosting non-viewable messages + + //Outputs: + // -bool: Successfully downloaded from host (false if host does not have any messages we need) + // -error + downloadMessagesFromHost := func(hostIdentityHash [16]byte)(bool, error){ + + exists, _, _, _, _, hostRawProfileMap, err := profileStorage.GetNewestUserProfile(hostIdentityHash, networkType) + if (err != nil) { return false, err } + if (exists == false){ + // Host profile was deleted, skip host + return false, nil + } + + hostIsHostingMessages, theirInboxRangeStart, theirInboxRangeEnd, err := hostRanges.GetHostedMessageInboxesRangeFromHostRawProfileMap(hostRawProfileMap) + if (hostIsHostingMessages == false){ + // Host is not hosting any messages. Skip to next host + return false, nil + } + + //Outputs: + // -bool: Download successful + // -[21]byte: Connection identifier + // -[][26]byte: Retrieved message hashes list + // -[10]byte: Inbox Range start of requested messages + // -[10]byte: Inbox range end of requested messages + // -[][10]byte: Requested inboxes list (nil if none were requested) + // -error + getReceivedMessageHashesList := func()(bool, [21]byte, [][26]byte, [10]byte, [10]byte, [][10]byte, error){ + + // Our request will either be based on inbox range or inbox list + // We will split up the request into either ranges or inboxes + + exists, hostedMessagesQuantity, err := readProfiles.GetFormattedProfileAttributeFromRawProfileMap(hostRawProfileMap, "MessagesQuantity") + if (err != nil) { return false, [21]byte{}, nil, [10]byte{}, [10]byte{}, nil, err } + if (exists == false){ + return false, [21]byte{}, nil, [10]byte{}, [10]byte{}, nil, errors.New("Database corrupt: Contains host profile missing MessagesQuantity") + } + + hostedMessagesQuantityInt64, err := helpers.ConvertStringToInt64(hostedMessagesQuantity) + if (err != nil) { return false, [21]byte{}, nil, [10]byte{}, [10]byte{}, nil, err } + + maximumMessageHashesInResponse := serverResponse.MaximumMessageHashesInResponse_GetMessageHashesList - 100 + + if (len(inboxesToRetrieveList) != 0){ + + // We will split request into inbox lists, not inbox ranges + + anyValuesExist, inboxesInMyRangeList, err := byteRange.GetAllInboxesInListWithinRange(rangeToRetrieveStart, rangeToRetrieveEnd, inboxesToRetrieveList) + if (err != nil) { return false, [21]byte{}, nil, [10]byte{}, [10]byte{}, nil, err } + if (anyValuesExist == false){ + // This should not happen + return false, [21]byte{}, nil, [10]byte{}, [10]byte{}, nil, errors.New("Inboxes list to retrieve contains no inboxes within request range.") + } + + anyValuesExist, inboxesInTheirRangeList, err := byteRange.GetAllInboxesInListWithinRange(theirInboxRangeStart, theirInboxRangeEnd, inboxesInMyRangeList) + if (err != nil) { return false, [21]byte{}, nil, [10]byte{}, [10]byte{}, nil, err } + if (anyValuesExist == false){ + // This host is not hosting any inboxes we desire. We will skip them. + return false, [21]byte{}, nil, [10]byte{}, [10]byte{}, nil, nil + } + + // Now we split request into sublists if needed + + exists, hostedInboxesQuantity, err := readProfiles.GetFormattedProfileAttributeFromRawProfileMap(hostRawProfileMap, "InboxesQuantity") + if (err != nil) { return false, [21]byte{}, nil, [10]byte{}, [10]byte{}, nil, err } + if (exists == false){ + return false, [21]byte{}, nil, [10]byte{}, [10]byte{}, nil, errors.New("Database corrupt: Contains host profile missing InboxesQuantity") + } + + hostedInboxesQuantityInt64, err := helpers.ConvertStringToInt64(hostedInboxesQuantity) + if (err != nil) { + return false, [21]byte{}, nil, [10]byte{}, [10]byte{}, nil, errors.New("Database corrupt: Contains host profile with invalid InboxesQuantity: " + hostedInboxesQuantity) + } + + if (hostedInboxesQuantityInt64 == 0){ + // Host has no inboxes + return false, [21]byte{}, nil, [10]byte{}, [10]byte{}, nil, nil + } + + estimatedNumMessagesPerInbox := hostedMessagesQuantityInt64/hostedInboxesQuantityInt64 + + maximumInboxesPerRequest := int64(maximumMessageHashesInResponse) / estimatedNumMessagesPerInbox + + maximumInboxesPerRequestInt, err := helpers.ConvertInt64ToInt(maximumInboxesPerRequest) + if (err != nil) { return false, [21]byte{}, nil, [10]byte{}, [10]byte{}, nil, err } + + sublistsToQueryList, err := helpers.SplitListIntoSublists(inboxesInTheirRangeList, maximumInboxesPerRequestInt) + if (err != nil) { return false, [21]byte{}, nil, [10]byte{}, [10]byte{}, nil, err } + + hostProfileExists, connectionEstablished, connectionIdentifier, err := peerClient.EstablishNewConnectionToHost(allowClearnet, hostIdentityHash, networkType) + if (err != nil){ return false, [21]byte{}, nil, [10]byte{}, [10]byte{}, nil, err } + if (hostProfileExists == false){ + return false, [21]byte{}, nil, [10]byte{}, [10]byte{}, nil, nil + } + if (connectionEstablished == false){ + return false, [21]byte{}, nil, [10]byte{}, [10]byte{}, nil, nil + } + + minimumRange, maximumRange := byteRange.GetMinimumMaximumInboxBounds() + + //Outputs: + // -bool: Download successful + // -[][26]byte: Message hashes list + // -error + getMessageHashesListFromHost := func()(bool, [][26]byte, error){ + + retrievedMessageHashesList := make([][26]byte, 0) + + for _, inboxesSublist := range sublistsToQueryList{ + + downloadSuccessful, messageHashesList, err := sendRequests.GetMessageHashesListFromHost(connectionIdentifier, hostIdentityHash, networkType, minimumRange, maximumRange, inboxesSublist, getViewableMessagesOnly, getDecryptableMessagesOnly) + if (err != nil) { return false, nil, err } + if (downloadSuccessful == false){ + // Failed to download this sublist. + // Could be that host went offline permanently or just this connection failed. + // We are done with this host + err := logger.AddLogEntry("Network", "Failed to download message hashes from host.") + if (err != nil) { return false, nil, err } + + return false, nil, nil + } + + retrievedMessageHashesList = append(retrievedMessageHashesList, messageHashesList...) + } + + return true, retrievedMessageHashesList, nil + } + + downloadSuccessful, retrievedMessageHashesList, err := getMessageHashesListFromHost() + if (err != nil){ + + errB := peerClient.CloseConnection(connectionIdentifier) + if (errB != nil) { return false, [21]byte{}, nil, [10]byte{}, [10]byte{}, nil, errB } + + return false, [21]byte{}, nil, [10]byte{}, [10]byte{}, nil, err + } + if (downloadSuccessful == false){ + + err := peerClient.CloseConnection(connectionIdentifier) + if (err != nil) { return false, [21]byte{}, nil, [10]byte{}, [10]byte{}, nil, err } + + return false, [21]byte{}, nil, [10]byte{}, [10]byte{}, nil, nil + } + + return true, connectionIdentifier, retrievedMessageHashesList, minimumRange, maximumRange, inboxesInTheirRangeList, nil + } + + // We will retrieve using inbox ranges, not inbox lists + + anyIntersectionFound, intersectionRangeStart, intersectionRangeEnd, err := byteRange.GetInboxIntersectionRangeFromTwoRanges(rangeToRetrieveStart, rangeToRetrieveEnd, theirInboxRangeStart, theirInboxRangeEnd) + if (err != nil) { return false, [21]byte{}, nil, [10]byte{}, [10]byte{}, nil, err } + + if (anyIntersectionFound == false){ + // Host is not hosting any messages within the inboxes range that we are requesting + // Skip this host + return false, [21]byte{}, nil, [10]byte{}, [10]byte{}, nil, nil + } + + estimatedNumItemsInIntersectionRange, err := byteRange.GetEstimatedInboxSubrangeQuantity(theirInboxRangeStart, theirInboxRangeEnd, hostedMessagesQuantityInt64, intersectionRangeStart, intersectionRangeEnd) + if (err != nil) { return false, [21]byte{}, nil, [10]byte{}, [10]byte{}, nil, err } + + subrangesToQueryList, err := byteRange.SplitInboxRangeIntoEqualSubranges(intersectionRangeStart, intersectionRangeEnd, estimatedNumItemsInIntersectionRange, int64(maximumMessageHashesInResponse)) + if (err != nil) { return false, [21]byte{}, nil, [10]byte{}, [10]byte{}, nil, err } + + hostProfileExists, connectionEstablished, connectionIdentifier, err := peerClient.EstablishNewConnectionToHost(allowClearnet, hostIdentityHash, networkType) + if (err != nil){ return false, [21]byte{}, nil, [10]byte{}, [10]byte{}, nil, err } + if (hostProfileExists == false){ + return false, [21]byte{}, nil, [10]byte{}, [10]byte{}, nil, nil + } + if (connectionEstablished == false){ + return false, [21]byte{}, nil, [10]byte{}, [10]byte{}, nil, nil + } + + getMessageHashesListFromHost := func()(bool, [][26]byte, error){ + + retrievedMessageHashesList := make([][26]byte, 0) + + for _, subrangeObject := range subrangesToQueryList{ + + subrangeStart := subrangeObject.SubrangeStart + subrangeEnd := subrangeObject.SubrangeEnd + + downloadSuccessful, messageHashesList, err := sendRequests.GetMessageHashesListFromHost(connectionIdentifier, hostIdentityHash, networkType, subrangeStart, subrangeEnd, inboxesToRetrieveList, getViewableMessagesOnly, getDecryptableMessagesOnly) + if (err != nil) { return false, nil, err } + if (downloadSuccessful == false){ + // Failed to download this subrange. + //Could be that host went offline permanently or just this connection failed. + // Either way, end connection to host + err := logger.AddLogEntry("Network", "Failed to download message hashes from host.") + if (err != nil) { return false, nil, err } + + return false, nil, nil + } + + retrievedMessageHashesList = append(retrievedMessageHashesList, messageHashesList...) + } + + return true, retrievedMessageHashesList, nil + } + + downloadSuccessful, retrievedMessageHashesList, err := getMessageHashesListFromHost() + if (err != nil){ + + errB := peerClient.CloseConnection(connectionIdentifier) + if (errB != nil) { return false, [21]byte{}, nil, [10]byte{}, [10]byte{}, nil, errB } + + return false, [21]byte{}, nil, [10]byte{}, [10]byte{}, nil, err + } + if (downloadSuccessful == false){ + err := peerClient.CloseConnection(connectionIdentifier) + if (err != nil) { return false, [21]byte{}, nil, [10]byte{}, [10]byte{}, nil, err } + + return false, [21]byte{}, nil, [10]byte{}, [10]byte{}, nil, nil + } + + emptyList := make([][10]byte, 0) + + return true, connectionIdentifier, retrievedMessageHashesList, intersectionRangeStart, intersectionRangeEnd, emptyList, nil + } + + downloadSuccessful, connectionIdentifier, retrievedMessageHashesList, requestedInboxesRangeStart, requestedInboxesRangeEnd, requestedInboxesList, err := getReceivedMessageHashesList() + if (err != nil) { return false, err } + if (downloadSuccessful == false){ + + return false, nil + } + defer peerClient.CloseConnection(connectionIdentifier) + + // Now we see if we have any of these messages, and if they do not fulfill the request range/inboxes list + // If they do not fulfill the request, we know host is malicious + // We still download the messages to prevent host from knowing that we have those messages downloaded + // We then mark the host as malicious + //TODO: Immediately stop connection to host if only Host/Moderator mode is enabled, because fingerprinting is not a risk. + + messageHashesToDownloadList := make([][26]byte, 0) + + hostIsMalicious := false + + for _, receivedMessageHash := range retrievedMessageHashesList{ + + checkIfHostIsOfferingInvalidMessage := func()(bool, error){ + + messageMetadataExists, _, messageNetworkType, _, messageInbox, _, err := contentMetadata.GetMessageMetadata(receivedMessageHash) + if (err != nil) { return false, err } + if (messageMetadataExists == false){ + return false, nil + } + if (messageNetworkType != networkType){ + return true, nil + } + + if (len(requestedInboxesList) != 0){ + isInInboxesList := slices.Contains(requestedInboxesList, messageInbox) + if (isInInboxesList == false){ + return true, nil + } + } else { + + inboxIsWithinRange, err := byteRange.CheckIfInboxIsWithinRange(requestedInboxesRangeStart, requestedInboxesRangeEnd, messageInbox) + if (err != nil) { return false, err } + if (inboxIsWithinRange == false){ + return true, nil + } + } + + return false, nil + } + + hostIsOfferingInvalidMessage, err := checkIfHostIsOfferingInvalidMessage() + if (err != nil) { return false, err } + + if (hostIsOfferingInvalidMessage == true){ + // Host is malicious, we must still download the message so they do not learn that we have this message downloaded (or did at one point) + hostIsMalicious = true + + messageHashesToDownloadList = append(messageHashesToDownloadList, receivedMessageHash) + continue + } + + storedMessageExists, _, err := badgerDatabase.GetChatMessage(receivedMessageHash) + if (err != nil) { return false, err } + + if (storedMessageExists == true){ + + // We already have the message, skip downloading it + continue + } + + // Message does not exist, we must check if we want to download it + // This works differently for Hosting, Moderation, and Viewing + // For instance, if the user is downloading messages for viewing, they will not redownload profiles that they have already imported/deleted + + shouldDownload, err := checkIfMessageShouldBeDownloaded(receivedMessageHash) + if (err != nil) { return false, err } + + if (shouldDownload == true){ + messageHashesToDownloadList = append(messageHashesToDownloadList, receivedMessageHash) + continue + } + } + + if (len(messageHashesToDownloadList) == 0){ + // Nothing to download, done with this host. + + if (hostIsMalicious == true){ + err := maliciousHosts.AddHostToMaliciousHostsList(hostIdentityHash) + if (err != nil) { return false, err } + } + return true, nil + } + + // Now we retrieve messages from host + + maximumMessagesInResponse := serverResponse.MaximumMessagesInResponse_GetMessages + + messageHashesSublistsList, err := helpers.SplitListIntoSublists(messageHashesToDownloadList, maximumMessagesInResponse) + if (err != nil) { return false, err } + + numberOfDownloadedMessages := 0 + + for _, messageHashesSublist := range messageHashesSublistsList{ + + downloadSuccessful, messagesList, err := sendRequests.GetMessagesFromHost(connectionIdentifier, hostIdentityHash, networkType, messageHashesSublist, requestedInboxesRangeStart, requestedInboxesRangeEnd, requestedInboxesList) + if (err != nil) { return false, err } + if (downloadSuccessful == false){ + // Request has failed + // We will stop downloading from this host + break + } + + for _, messageBytes := range messagesList{ + + messageIsWellFormed, err := chatMessageStorage.AddMessage(messageBytes) + if (err != nil) { return false, err } + + if (messageIsWellFormed == false){ + return false, errors.New("GetMessagesFromHost not verifying messages are well formed.") + } + } + + numberOfDownloadedMessages += len(messagesList) + } + + if (hostIsMalicious == true){ + err := maliciousHosts.AddHostToMaliciousHostsList(hostIdentityHash) + if (err != nil) { return false, err } + } + + if (numberOfDownloadedMessages != 0){ + numberOfMessagesDownloadedString := helpers.ConvertIntToString(numberOfDownloadedMessages) + + err = logger.AddLogEntry("Network", "Successfully downloaded " + numberOfMessagesDownloadedString + " messages from host.") + if (err != nil) { return false, err } + } + + return true, nil + } + + eligibleHostsList, err := eligibleHosts.GetEligibleHostsList(networkType) + if (err != nil) { return err } + + queriedHosts := 0 + + for _, hostIdentityHash := range eligibleHostsList{ + + successfulDownload, err := downloadMessagesFromHost(hostIdentityHash) + if (err != nil) { return err } + if (successfulDownload == true){ + queriedHosts += 1 + } + if (queriedHosts >= numberOfHostsToQuery){ + return nil + } + } + + return nil +} + + +func DownloadIdentityReviewsFromHosts( + allowClearnet bool, + networkType byte, + identityTypeToRetrieve string, + rangeToRetrieveStart [16]byte, + rangeToRetrieveEnd [16]byte, + reviewedIdentityHashesToRetrieveList [][16]byte, + reviewersToRetrieveList [][16]byte, + checkIfReviewShouldBeDownloaded func([29]byte, []byte)(bool, error), + numberOfHostsToQuery int)error{ + + isValid := helpers.VerifyNetworkType(networkType) + if (isValid == false){ + networkTypeString := helpers.ConvertByteToString(networkType) + return errors.New("DownloadIdentityReviewsFromHosts called with invalid networkType: " + networkTypeString) + } + + //TODO: Verify inputs + + //Outputs: + // -bool: Download successful + // -error + downloadReviewsFromHost := func(hostIdentityHash [16]byte)(bool, error){ + + exists, _, _, _, _, hostRawProfileMap, err := profileStorage.GetNewestUserProfile(hostIdentityHash, networkType) + if (err != nil) { return false, err } + if (exists == false){ + // Host profile was deleted, skip host + return false, nil + } + + // Outputs: + // -bool: Successfully downloaded from host + // -[21]byte: Connection identifier + // -map[[29]byte][]byte: Map of review info: ReviewHash -> ReviewedHash + // -[16]byte: Expected Identity Start Range of retrieved reviews + // -[16]byte: Expected Identity End range of retrieved reviews + // -[][16]byte: Expected retrieved identity hashes list + // -error + getRetrievedReviewsInfoMap := func()(bool, [21]byte, map[[29]byte][]byte, [16]byte, [16]byte, [][16]byte, error){ + + hostIsHostingIdentityType, theirHostedIdentitiesRangeStart, theirHostedIdentitiesRangeEnd, err := hostRanges.GetHostedIdentityHashRangeFromHostRawProfileMap(hostRawProfileMap, identityTypeToRetrieve) + if (err != nil) { return false, [21]byte{}, nil, [16]byte{}, [16]byte{}, nil, err } + if (hostIsHostingIdentityType == false){ + // Host is not hosting any profiles/reviews of this identityType. Skip to next host + return false, [21]byte{}, nil, [16]byte{}, [16]byte{}, nil, nil + } + + exists, identityTypeIdentitiesQuantity, err := readProfiles.GetFormattedProfileAttributeFromRawProfileMap(hostRawProfileMap, identityTypeToRetrieve + "IdentitiesQuantity") + if (err != nil) { return false, [21]byte{}, nil, [16]byte{}, [16]byte{}, nil, err } + if (exists == false){ + return false, [21]byte{}, nil, [16]byte{}, [16]byte{}, nil, errors.New("Database corrupt: Contains host profile missing " + identityTypeToRetrieve + "IdentitiesQuantity") + } + + identityTypeIdentitiesQuantityInt64, err := helpers.ConvertStringToInt64(identityTypeIdentitiesQuantity) + if (err != nil) { + return false, [21]byte{}, nil, [16]byte{}, [16]byte{}, nil, errors.New("Database corrupt: Contains host profile with invalid " + identityTypeToRetrieve + "IdentitiesQuantity: " + identityTypeIdentitiesQuantity) + } + + getMaximumIdentitiesPerRequest := func()(int64, error){ + + maximumReviewsInResponse := serverResponse.MaximumReviewsInResponse_GetIdentityReviewsInfo + + if (len(reviewersToRetrieveList) != 0){ + + // We assume each reviewer has reviewed each identity and all it's profiles + + profilesQuantityAttributeName := identityTypeToRetrieve + "ProfilesQuantity" + + exists, identityTypeProfilesQuantity, err := readProfiles.GetFormattedProfileAttributeFromRawProfileMap(hostRawProfileMap, profilesQuantityAttributeName) + if (err != nil) { return 0, err } + if (exists == false){ + return 0, errors.New("Database corrupt: Contains host profile missing " + identityTypeToRetrieve + "ProfilesQuantity") + } + + identityTypeProfilesQuantityInt64, err := helpers.ConvertStringToInt64(identityTypeProfilesQuantity) + if (err != nil) { return 0, err } + + estimatedNumberOfProfilesPerIdentity := identityTypeProfilesQuantityInt64 / identityTypeIdentitiesQuantityInt64 + + numberOfReviewersToRetrieve := int64(len(reviewersToRetrieveList)) + + estimatedReviewsPerIdentity := numberOfReviewersToRetrieve * (estimatedNumberOfProfilesPerIdentity + 1) + + maximumIdentitiesPerRequest := int64(maximumReviewsInResponse) / estimatedReviewsPerIdentity + + return maximumIdentitiesPerRequest, nil + } + + reviewsQuantityAttributeName := identityTypeToRetrieve + "ReviewsQuantity" + + exists, identityTypeReviewsQuantity, err := readProfiles.GetFormattedProfileAttributeFromRawProfileMap(hostRawProfileMap, reviewsQuantityAttributeName) + if (err != nil) { return 0, err } + if (exists == false){ + return 0, errors.New("Database corrupt: Contains host profile missing " + identityTypeToRetrieve + "ReviewsQuantity") + } + + identityTypeReviewsQuantityInt64, err := helpers.ConvertStringToInt64(identityTypeReviewsQuantity) + if (err != nil) { return 0, err } + + estimatedReviewsPerIdentity := identityTypeIdentitiesQuantityInt64 / identityTypeReviewsQuantityInt64 + + maximumIdentitiesPerRequest := int64(maximumReviewsInResponse) / estimatedReviewsPerIdentity + + return maximumIdentitiesPerRequest, nil + } + + maximumIdentitiesPerRequest, err := getMaximumIdentitiesPerRequest() + if (err != nil) { return false, [21]byte{}, nil, [16]byte{}, [16]byte{}, nil, err } + + // Our request will either be based on identity hash range or identity hashes list + // We will split up the request into either ranges or identity hashes + + if (len(reviewedIdentityHashesToRetrieveList) != 0){ + + // We will split the request into identity hash lists + + hostingAny, identitiesInHostRangeList, err := byteRange.GetAllIdentityHashesInListWithinRange(theirHostedIdentitiesRangeStart, theirHostedIdentitiesRangeEnd, reviewedIdentityHashesToRetrieveList) + if (err != nil) { return false, [21]byte{}, nil, [16]byte{}, [16]byte{}, nil, err } + if (hostingAny == false){ + // Host is not hosting any of our desired identities. Skip host. + return false, [21]byte{}, nil, [16]byte{}, [16]byte{}, nil, nil + } + + // Below step is just being overly cautious. + // The GetIdentityReviewsfromHosts function should request either a list of identities or a range. Not both. + hostingAny, identitiesInRequestRange, err := byteRange.GetAllIdentityHashesInListWithinRange(rangeToRetrieveStart, rangeToRetrieveEnd, identitiesInHostRangeList) + if (err != nil) { return false, [21]byte{}, nil, [16]byte{}, [16]byte{}, nil, err } + if (hostingAny == false){ + return false, [21]byte{}, nil, [16]byte{}, [16]byte{}, nil, errors.New("Requested review identity hashes list outside of request range") + } + + sublistsToQueryList, err := helpers.SplitListIntoSublists(identitiesInRequestRange, int(maximumIdentitiesPerRequest)) + if (err != nil) { return false, [21]byte{}, nil, [16]byte{}, [16]byte{}, nil, err } + + hostProfileExists, connectionEstablished, connectionIdentifier, err := peerClient.EstablishNewConnectionToHost(allowClearnet, hostIdentityHash, networkType) + if (err != nil){ return false, [21]byte{}, nil, [16]byte{}, [16]byte{}, nil, err } + if (hostProfileExists == false){ + return false, [21]byte{}, nil, [16]byte{}, [16]byte{}, nil, nil + } + if (connectionEstablished == false){ + return false, [21]byte{}, nil, [16]byte{}, [16]byte{}, nil, nil + } + + minimumRange, maximumRange := byteRange.GetMinimumMaximumIdentityHashBounds() + + getDownloadedReviewsInfoMap := func()(bool, map[[29]byte][]byte, error){ + + retrievedReviewsInfoMap := make(map[[29]byte][]byte) + + for _, identityHashesSublist := range sublistsToQueryList{ + + downloadSuccessful, currentRetrievedReviewsInfoMap, err := sendRequests.GetIdentityReviewsInfoFromHost(connectionIdentifier, hostIdentityHash, networkType, identityTypeToRetrieve, minimumRange, maximumRange, identityHashesSublist, reviewersToRetrieveList) + if (err != nil) { return false, nil, err } + if (downloadSuccessful == false){ + // Failed to download this sublist. + // Could be that host went offline permanently or just this connection failed. + // Either way, stop download from host. + err := logger.AddLogEntry("Network", "Failed to download identity reviews info from host.") + if (err != nil) { return false, nil, err } + return false, nil, nil + } + + for reviewHash, reviewedHash := range currentRetrievedReviewsInfoMap{ + retrievedReviewsInfoMap[reviewHash] = reviewedHash + } + } + + return true, retrievedReviewsInfoMap, nil + } + + downloadSuccessful, retrievedReviewsInfoMap, err := getDownloadedReviewsInfoMap() + if (err != nil) { + errB := peerClient.CloseConnection(connectionIdentifier) + if (errB != nil) { return false, [21]byte{}, nil, [16]byte{}, [16]byte{}, nil, errB } + + return false, [21]byte{}, nil, [16]byte{}, [16]byte{}, nil, err + } + if (downloadSuccessful == false){ + + err := peerClient.CloseConnection(connectionIdentifier) + if (err != nil) { return false, [21]byte{}, nil, [16]byte{}, [16]byte{}, nil, err } + + return false, [21]byte{}, nil, [16]byte{}, [16]byte{}, nil, nil + } + + return true, connectionIdentifier, retrievedReviewsInfoMap, minimumRange, maximumRange, identitiesInRequestRange, nil + } + + // We will retrieve based on identity range, not identities list + + anyIntersectionFound, intersectionRangeStart, intersectionRangeEnd, err := byteRange.GetIdentityIntersectionRangeFromTwoRanges(rangeToRetrieveStart, rangeToRetrieveEnd, theirHostedIdentitiesRangeStart, theirHostedIdentitiesRangeEnd) + if (err != nil) { return false, [21]byte{}, nil, [16]byte{}, [16]byte{}, nil, err } + if (anyIntersectionFound == false){ + // Host is not hosting any reviews within the identites range that we are requesting + // Skip this host + return false, [21]byte{}, nil, [16]byte{}, [16]byte{}, nil, nil + } + + estimatedNumIdentitiesInIntersectionRange, err := byteRange.GetEstimatedIdentitySubrangeQuantity(theirHostedIdentitiesRangeStart, theirHostedIdentitiesRangeEnd, identityTypeIdentitiesQuantityInt64, intersectionRangeStart, intersectionRangeEnd) + if (err != nil) { return false, [21]byte{}, nil, [16]byte{}, [16]byte{}, nil, err } + + subrangesToQueryList, err := byteRange.SplitIdentityRangeIntoEqualSubranges(intersectionRangeStart, intersectionRangeEnd, estimatedNumIdentitiesInIntersectionRange, maximumIdentitiesPerRequest) + if (err != nil) { return false, [21]byte{}, nil, [16]byte{}, [16]byte{}, nil, err } + + hostProfileExists, connectionEstablished, connectionIdentifier, err := peerClient.EstablishNewConnectionToHost(allowClearnet, hostIdentityHash, networkType) + if (err != nil){ return false, [21]byte{}, nil, [16]byte{}, [16]byte{}, nil, err } + if (hostProfileExists == false){ + return false, [21]byte{}, nil, [16]byte{}, [16]byte{}, nil, nil + } + if (connectionEstablished == false){ + return false, [21]byte{}, nil, [16]byte{}, [16]byte{}, nil, nil + } + + getDownloadedReviewsInfoMap := func()(bool, map[[29]byte][]byte, error){ + + retrievedReviewsInfoMap := make(map[[29]byte][]byte) + + for _, subrangeObject := range subrangesToQueryList{ + + subrangeStart := subrangeObject.SubrangeStart + subrangeEnd := subrangeObject.SubrangeEnd + + emptyList := make([][16]byte, 0) + + downloadSuccessful, currentRetrievedReviewsInfoMap, err := sendRequests.GetIdentityReviewsInfoFromHost(connectionIdentifier, hostIdentityHash, networkType, identityTypeToRetrieve, subrangeStart, subrangeEnd, emptyList, reviewersToRetrieveList) + if (err != nil) { return false, nil, err } + if (downloadSuccessful == false){ + // Failed to download this sublist. + // Could be that host went offline permanently or just this connection failed. + // Either way, stop download. + err := logger.AddLogEntry("Network", "Failed to download identity reviews info from host.") + if (err != nil) { return false, nil, err } + return false, nil, nil + } + + for reviewHash, reviewedHash := range currentRetrievedReviewsInfoMap{ + retrievedReviewsInfoMap[reviewHash] = reviewedHash + } + } + + return true, retrievedReviewsInfoMap, nil + } + + downloadSuccessful, retrievedReviewsInfoMap, err := getDownloadedReviewsInfoMap() + if (err != nil) { + errB := peerClient.CloseConnection(connectionIdentifier) + if (errB != nil) { return false, [21]byte{}, nil, [16]byte{}, [16]byte{}, nil, errB } + + return false, [21]byte{}, nil, [16]byte{}, [16]byte{}, nil, err + } + if (downloadSuccessful == false){ + + err := peerClient.CloseConnection(connectionIdentifier) + if (err != nil) { return false, [21]byte{}, nil, [16]byte{}, [16]byte{}, nil, err } + + return false, [21]byte{}, nil, [16]byte{}, [16]byte{}, nil, nil + } + + return true, connectionIdentifier, retrievedReviewsInfoMap, intersectionRangeStart, intersectionRangeEnd, nil, nil + } + + hostDownloadSuccessful, connectionIdentifier, retrievedReviewsInfoMap, expectedRangeStart, expectedRangeEnd, expectedReviewedIdentityHashesList, err := getRetrievedReviewsInfoMap() + if (err != nil) { return false, err } + if (hostDownloadSuccessful == false){ + // We did not download any reviews from this host. Skip them. + return false, nil + } + + defer peerClient.CloseConnection(connectionIdentifier) + + // Now we see if we have any of these reviews, and if they do not fulfill the requests parameters + // If they do not fulfill the request, we know host is malicious + // This requires checking against content stored in our database + // A host could use this to learn information about what content we are storing + + // We still download the reviews to prevent host from knowing that we have the reviewed content downloaded + // Otherwise they could see that we stopped our request mid-way and become aware of the fact that we detected their malicious response + // Only after the full request is finished do we mark the host as malicious + + // TODO: We shouldn't have to wait until the end of the request if we are only in Host/Moderator mode + // This is because we don't need to worry about leaking what content we have stored + + reviewHashesToDownloadList := make([][29]byte, 0) + + hostIsMalicious := false + + for receivedReviewHash, receivedReviewedHash := range retrievedReviewsInfoMap{ + + storedReviewExists, storedReviewBytes, err := badgerDatabase.GetReview(receivedReviewHash) + if (err != nil) { return false, err } + + checkIfHostIsOfferingInvalidReview := func()(bool, error){ + + if (storedReviewExists == true){ + + ableToRead, storedReviewHash, _, reviewNetworkType, storedReviewReviewerIdentity, _, storedReviewType, storedReviewReviewedHash, _, _, err := readReviews.ReadReviewAndHash(false, storedReviewBytes) + if (err != nil) { return false, err } + if (ableToRead == false){ + return false, errors.New("Database corrupt: Contains invalid review.") + } + if (storedReviewHash != receivedReviewHash){ + return false, errors.New("Database corrupt: Contains review with invalid entry key hash") + } + if (reviewNetworkType != networkType){ + return true, nil + } + + if (len(reviewersToRetrieveList) != 0){ + + isInList := slices.Contains(reviewersToRetrieveList, storedReviewReviewerIdentity) + if (isInList == false){ + return true, nil + } + } + areEqual := bytes.Equal(storedReviewReviewedHash, receivedReviewedHash) + if (areEqual == false){ + return true, nil + } + + if (storedReviewType != "Identity" && storedReviewType != "Profile" && storedReviewType != "Attribute"){ + return true, nil + } + } + + // Now we check to see if we are aware of the reviewed content's metadata, and if it matches our request parameters + + receivedReviewedType, err := helpers.GetReviewedTypeFromReviewedHash(receivedReviewedHash) + if (err != nil) { return false, err } + + if (receivedReviewedType == "Profile"){ + + // We must verify that the profile's identity hash is expected + // We don't have to do this for identities, because we already checked that in the GetIdentityReviewsInfo function + + if (len(receivedReviewedHash) != 28){ + receivedReviewedHashHex := encoding.EncodeBytesToHexString(receivedReviewedHash) + return false, errors.New("GetReviewedTypeFromReviewedHash returning Profile for different length reviewedHash: " + receivedReviewedHashHex) + } + + receivedReviewedProfileHash := [28]byte(receivedReviewedHash) + + metadataIsKnown, _, profileNetworkType, reviewedProfileAuthor, _, profileIsDisabled, _, _, err := contentMetadata.GetProfileMetadata(receivedReviewedProfileHash) + if (err != nil) { return false, err } + if (metadataIsKnown == false){ + return false, nil + } + if (profileIsDisabled == true){ + // Disabled profiles cannot be reviewed. Host must be malicious. + return true, nil + } + if (profileNetworkType != networkType){ + // Review is reviewing profile from different networkType + // Host should have checked this already. Host must be malicious. + return true, nil + } + + if (expectedReviewedIdentityHashesList != nil){ + + identityIsInList := slices.Contains(expectedReviewedIdentityHashesList, reviewedProfileAuthor) + if (identityIsInList == false){ + return true, nil + } + } else { + + identityIsWithinRange, err := byteRange.CheckIfIdentityHashIsWithinRange(expectedRangeStart, expectedRangeEnd, reviewedProfileAuthor) + if (err != nil) { return false, err } + if (identityIsWithinRange == false){ + return true, nil + } + } + } + if (receivedReviewedType == "Attribute"){ + + if (len(receivedReviewedHash) != 27){ + receivedReviewedHashHex := encoding.EncodeBytesToHexString(receivedReviewedHash) + return false, errors.New("GetReviewedTypeFromReviewedHash returning Attribute for different length reviewedHash: " + receivedReviewedHashHex) + } + + receivedReviewedAttributeHash := [27]byte(receivedReviewedHash) + + metadataExists, _, attributeAuthorIdentityHash, attributeNetworkType, _, err := profileStorage.GetProfileAttributeMetadata(receivedReviewedAttributeHash) + if (err != nil){ return false, err } + if (metadataExists == true){ + + if (expectedReviewedIdentityHashesList != nil){ + + identityIsInList := slices.Contains(expectedReviewedIdentityHashesList, attributeAuthorIdentityHash) + if (identityIsInList == false){ + return true, nil + } + } else { + + identityIsWithinRange, err := byteRange.CheckIfIdentityHashIsWithinRange(expectedRangeStart, expectedRangeEnd, attributeAuthorIdentityHash) + if (err != nil) { return false, err } + if (identityIsWithinRange == false){ + return true, nil + } + } + + if (attributeNetworkType != networkType){ + // Review is reviewing attribute from different networkType + // Host should have checked this already. Host must be malicious. + return true, nil + } + + return false, nil + } + } + + // Review is not invalid, as far as we know + + return false, nil + } + + hostIsOfferingInvalidReview, err := checkIfHostIsOfferingInvalidReview() + if (err != nil) { return false, err } + if (hostIsOfferingInvalidReview == true){ + // Host is malicious, we must still download the review + hostIsMalicious = true + + reviewHashesToDownloadList = append(reviewHashesToDownloadList, receivedReviewHash) + continue + } + + if (storedReviewExists == true){ + + // We already have the review, skip downloading it + continue + } + + // Review does not exist, we must check if we want to download it + + shouldDownload, err := checkIfReviewShouldBeDownloaded(receivedReviewHash, receivedReviewedHash) + if (err != nil) { return false, err } + + if (shouldDownload == true){ + reviewHashesToDownloadList = append(reviewHashesToDownloadList, receivedReviewHash) + continue + } + } + + if (len(reviewHashesToDownloadList) == 0){ + // Nothing to download, we are done with this host. + + if (hostIsMalicious == true){ + err := maliciousHosts.AddHostToMaliciousHostsList(hostIdentityHash) + if (err != nil) { return false, err } + } + return true, nil + } + + // Now we download reviews + + maximumReviewsInResponse := serverResponse.MaximumReviewsInResponse_GetReviews + + reviewHashesSublistsList, err := helpers.SplitListIntoSublists(reviewHashesToDownloadList, maximumReviewsInResponse) + if (err != nil) { return false, err } + + numberOfDownloadedReviews := 0 + + for _, reviewHashesSublist := range reviewHashesSublistsList{ + + downloadSuccessful, reviewsList, err := sendRequests.GetReviewsFromHost(connectionIdentifier, hostIdentityHash, networkType, reviewHashesSublist, retrievedReviewsInfoMap, reviewersToRetrieveList) + if (err != nil) { return false, err } + if (downloadSuccessful == false){ + // Request has failed + // We will stop downloading from this host + break + } + if (hostIsMalicious == true){ + // We don't add reviews from malicious hosts + continue + } + + for _, reviewBytes := range reviewsList{ + + reviewIsWellFormed, err := reviewStorage.AddReview(reviewBytes) + if (err != nil) { return false, err } + + if (reviewIsWellFormed == false){ + return false, errors.New("GetReviewsFromHost not verifying reviews are well formed.") + } + } + + numberOfDownloadedReviews += len(reviewsList) + } + + if (hostIsMalicious == true){ + err := maliciousHosts.AddHostToMaliciousHostsList(hostIdentityHash) + if (err != nil) { return false, err } + } + + if (numberOfDownloadedReviews != 0){ + numberOfReviewsDownloadedString := helpers.ConvertIntToString(numberOfDownloadedReviews) + + err = logger.AddLogEntry("Network", "Successfully downloaded " + numberOfReviewsDownloadedString + " identity reviews from host.") + if (err != nil) { return false, err } + } + + return true, nil + } + + eligibleHostsList, err := eligibleHosts.GetEligibleHostsList(networkType) + if (err != nil) { return err } + + queriedHosts := 0 + + for _, hostIdentityHash := range eligibleHostsList{ + + successfulDownload, err := downloadReviewsFromHost(hostIdentityHash) + if (err != nil) { return err } + if (successfulDownload == true){ + queriedHosts += 1 + } + if (queriedHosts >= numberOfHostsToQuery){ + return nil + } + } + + return nil +} + + +func DownloadMessageReviewsFromHosts( + allowClearnet bool, + networkType byte, + rangeToRetrieveStart [10]byte, + rangeToRetrieveEnd [10]byte, + reviewedMessageHashesToRetrieveList [][26]byte, + reviewersToRetrieveList [][16]byte, + reviewedMessageInboxesMap map[[26]byte][10]byte, // Message hash -> Message inbox + checkIfReviewShouldBeDownloaded func([29]byte, [26]byte)(bool, error), + numberOfHostsToQuery int)error{ + + isValid := helpers.VerifyNetworkType(networkType) + if (isValid == false){ + networkTypeString := helpers.ConvertByteToString(networkType) + return errors.New("DownloadMessageReviewsFromHosts called with invalid networkType: " + networkTypeString) + } + + + //TODO: Verify inputs + + //Outputs: + // -bool: Download successful + // -error + downloadReviewsFromHost := func(hostIdentityHash [16]byte)(bool, error){ + + exists, _, _, _, _, hostRawProfileMap, err := profileStorage.GetNewestUserProfile(hostIdentityHash, networkType) + if (err != nil) { return false, err } + if (exists == false){ + // Host profile was deleted, skip host + return false, nil + } + + // Outputs: + // -bool: Successfully downloaded from host + // -[21]byte: Connection identifier + // -map[[29]byte][26]byte: Map of review info: ReviewHash -> ReviewedHash + // -[10]byte: Expected Inbox Start Range of retrieved reviews + // -[10]byte: Expected Inbox End range of retrieved reviews + // -error + getRetrievedReviewsInfoMap := func()(bool, [21]byte, map[[29]byte][26]byte, [10]byte, [10]byte, error){ + + hostIsHostingMessages, theirInboxRangeStart, theirInboxRangeEnd, err := hostRanges.GetHostedMessageInboxesRangeFromHostRawProfileMap(hostRawProfileMap) + if (err != nil) { return false, [21]byte{}, nil, [10]byte{}, [10]byte{}, err } + if (hostIsHostingMessages == false){ + // Host is not hosting messages. Nothing to download. + return false, [21]byte{}, nil, [10]byte{}, [10]byte{}, nil + } + + exists, hostedMessagesQuantity, err := readProfiles.GetFormattedProfileAttributeFromRawProfileMap(hostRawProfileMap, "MessagesQuantity") + if (err != nil) { return false, [21]byte{}, nil, [10]byte{}, [10]byte{}, err } + if (exists == false){ + return false, [21]byte{}, nil, [10]byte{}, [10]byte{}, errors.New("Database corrupt: Contains host profile missing MessagesQuantity") + } + + hostedMessagesQuantityInt64, err := helpers.ConvertStringToInt64(hostedMessagesQuantity) + if (err != nil) { + return false, [21]byte{}, nil, [10]byte{}, [10]byte{}, errors.New("Database corrupt: Contains host profile with invalid MessagesQuantity: " + hostedMessagesQuantity) + } + + exists, hostedMessageReviewsQuantity, err := readProfiles.GetFormattedProfileAttributeFromRawProfileMap(hostRawProfileMap, "MessageReviewsQuantity") + if (err != nil) { return false, [21]byte{}, nil, [10]byte{}, [10]byte{}, err } + if (exists == false){ + return false, [21]byte{}, nil, [10]byte{}, [10]byte{}, errors.New("Database corrupt: Contains host profile missing MessageReviewsQuantity") + } + + hostedMessageReviewsQuantityInt64, err := helpers.ConvertStringToInt64(hostedMessageReviewsQuantity) + if (err != nil) { + return false, [21]byte{}, nil, [10]byte{}, [10]byte{}, errors.New("Database corrupt: Contains host profile with invalid MessageReviewsQuantity: " + hostedMessageReviewsQuantity) + } + + getMaximumMessageHashesPerRequest := func()int{ + + maximumReviewsInResponse := serverResponse.MaximumReviewsInResponse_GetMessageReviewsInfo + + if (len(reviewersToRetrieveList) != 0){ + + // We assume each reviewer has 1 review per message + numberOfReviewersToRetrieve := len(reviewersToRetrieveList) + + maximumMessageHashesPerRequest := maximumReviewsInResponse / numberOfReviewersToRetrieve + + return maximumMessageHashesPerRequest + } + + // We get the estimated number of reviews per message + + estimatedReviewsPerMessage := hostedMessageReviewsQuantityInt64 / hostedMessagesQuantityInt64 + + maximumMessageHashesPerRequest := int64(maximumReviewsInResponse) / estimatedReviewsPerMessage + + return int(maximumMessageHashesPerRequest) + } + + maximumMessageHashesPerRequest := getMaximumMessageHashesPerRequest() + + // We will retrieve reviews from either the reviewed hashes list, or request range + + if (len(reviewedMessageHashesToRetrieveList) != 0){ + + // We will retrieve reviewed hashes list + + messageHashesInTheirRangeList := make([][26]byte, 0) + + for _, messageHash := range reviewedMessageHashesToRetrieveList{ + + messageInbox, exists := reviewedMessageInboxesMap[messageHash] + if (exists == false) { + return false, [21]byte{}, nil, [10]byte{}, [10]byte{}, errors.New("reviewedMessageInboxesMap missing message hash.") + } + + inboxIsWithinTheirRange, err := byteRange.CheckIfInboxIsWithinRange(theirInboxRangeStart, theirInboxRangeEnd, messageInbox) + if (err != nil) { return false, [21]byte{}, nil, [10]byte{}, [10]byte{}, err } + if (inboxIsWithinTheirRange == true){ + messageHashesInTheirRangeList = append(messageHashesInTheirRangeList, messageHash) + } + } + + sublistsToQueryList, err := helpers.SplitListIntoSublists(messageHashesInTheirRangeList, maximumMessageHashesPerRequest) + if (err != nil) { return false, [21]byte{}, nil, [10]byte{}, [10]byte{}, err } + + minimumRange, maximumRange := byteRange.GetMinimumMaximumInboxBounds() + + hostProfileExists, connectionEstablished, connectionIdentifier, err := peerClient.EstablishNewConnectionToHost(allowClearnet, hostIdentityHash, networkType) + if (err != nil){ return false, [21]byte{}, nil, [10]byte{}, [10]byte{}, err } + if (hostProfileExists == false){ + return false, [21]byte{}, nil, [10]byte{}, [10]byte{}, nil + } + if (connectionEstablished == false){ + return false, [21]byte{}, nil, [10]byte{}, [10]byte{}, nil + } + + //Outputs: + // -bool: Download successful + // -map[[29]byte][26]byte: Reviews info map (Review hash -> Reviewed message hash) + // -error + getDownloadedReviewsInfoMap := func()(bool, map[[29]byte][26]byte, error){ + + retrievedReviewsInfoMap := make(map[[29]byte][26]byte) + + for _, messageHashesSublist := range sublistsToQueryList{ + + downloadSuccessful, currentRetrievedReviewsInfoMap, err := sendRequests.GetMessageReviewsInfoFromHost(connectionIdentifier, hostIdentityHash, networkType, minimumRange, maximumRange, messageHashesSublist, reviewersToRetrieveList) + if (err != nil) { return false, nil, err } + if (downloadSuccessful == false){ + // Failed to download this sublist. + // Could be that host went offline permanently or just this connection failed. + // Either way, stop retrieving from this host + err := logger.AddLogEntry("Network", "Failed to download message reviews info from host.") + if (err != nil) { return false, nil, err } + + return false, nil, nil + } + + for reviewHash, reviewedHash := range currentRetrievedReviewsInfoMap{ + retrievedReviewsInfoMap[reviewHash] = reviewedHash + } + } + + return true, retrievedReviewsInfoMap, nil + } + + downloadSuccessful, retrievedReviewsInfoMap, err := getDownloadedReviewsInfoMap() + if (err != nil){ + + errB := peerClient.CloseConnection(connectionIdentifier) + if (errB != nil) { return false, [21]byte{}, nil, [10]byte{}, [10]byte{}, errB } + + return false, [21]byte{}, nil, [10]byte{}, [10]byte{}, err + } + if (downloadSuccessful == false){ + + err := peerClient.CloseConnection(connectionIdentifier) + if (err != nil) { return false, [21]byte{}, nil, [10]byte{}, [10]byte{}, err } + + return false, [21]byte{}, nil, [10]byte{}, [10]byte{}, nil + } + + return true, connectionIdentifier, retrievedReviewsInfoMap, minimumRange, maximumRange, nil + } + + // We will retrieve reviews based on message inbox range + + anyIntersectionFound, intersectionRangeStart, intersectionRangeEnd, err := byteRange.GetInboxIntersectionRangeFromTwoRanges(rangeToRetrieveStart, rangeToRetrieveEnd, theirInboxRangeStart, theirInboxRangeEnd) + if (err != nil) { return false, [21]byte{}, nil, [10]byte{}, [10]byte{}, err } + if (anyIntersectionFound == false){ + // Host is not hosting any reviews within the inboxes range that we are requesting + // Skip this host + return false, [21]byte{}, nil, [10]byte{}, [10]byte{}, nil + } + + estimatedNumItemsInIntersectionRange, err := byteRange.GetEstimatedInboxSubrangeQuantity(theirInboxRangeStart, theirInboxRangeEnd, hostedMessagesQuantityInt64, intersectionRangeStart, intersectionRangeEnd) + if (err != nil) { return false, [21]byte{}, nil, [10]byte{}, [10]byte{}, err } + + subrangesToQueryList, err := byteRange.SplitInboxRangeIntoEqualSubranges(intersectionRangeStart, intersectionRangeEnd, estimatedNumItemsInIntersectionRange, int64(maximumMessageHashesPerRequest)) + if (err != nil) { return false, [21]byte{}, nil, [10]byte{}, [10]byte{}, err } + + hostProfileExists, connectionEstablished, connectionIdentifier, err := peerClient.EstablishNewConnectionToHost(allowClearnet, hostIdentityHash, networkType) + if (err != nil){ return false, [21]byte{}, nil, [10]byte{}, [10]byte{}, err } + if (hostProfileExists == false){ + return false, [21]byte{}, nil, [10]byte{}, [10]byte{}, nil + } + if (connectionEstablished == false){ + return false, [21]byte{}, nil, [10]byte{}, [10]byte{}, nil + } + + //Outputs: + // -bool: Download successful + // -map[[29]byte][26]byte: Downloaded reviews info map (Review Hash -> Reviewed Message Hash) + // -error + getDownloadedReviewsInfoMap := func()(bool, map[[29]byte][26]byte, error){ + + retrievedReviewsInfoMap := make(map[[29]byte][26]byte) + + for _, subrangeObject := range subrangesToQueryList{ + + subrangeStart := subrangeObject.SubrangeStart + subrangeEnd := subrangeObject.SubrangeEnd + + emptyList := make([][26]byte, 0) + + downloadSuccessful, currentRetrievedReviewsInfoMap, err := sendRequests.GetMessageReviewsInfoFromHost(connectionIdentifier, hostIdentityHash, networkType, subrangeStart, subrangeEnd, emptyList, reviewersToRetrieveList) + if (err != nil) { return false, nil, err } + if (downloadSuccessful == false){ + // Failed to download this sublist. + // Could be that host went offline permanently or just this connection failed. + // Either way, we are done with this host + err := logger.AddLogEntry("Network", "Failed to download message reviews info from host.") + if (err != nil) { return false, nil, err } + + return false, nil, nil + } + + for reviewHash, reviewedHash := range currentRetrievedReviewsInfoMap{ + retrievedReviewsInfoMap[reviewHash] = reviewedHash + } + } + + return true, retrievedReviewsInfoMap, nil + } + + downloadSuccessful, retrievedReviewsInfoMap, err := getDownloadedReviewsInfoMap() + if (err != nil) { + errB := peerClient.CloseConnection(connectionIdentifier) + if (errB != nil) { return false, [21]byte{}, nil, [10]byte{}, [10]byte{}, errB } + + return false, [21]byte{}, nil, [10]byte{}, [10]byte{}, err + } + if (downloadSuccessful == false){ + + err := peerClient.CloseConnection(connectionIdentifier) + if (err != nil) { return false, [21]byte{}, nil, [10]byte{}, [10]byte{}, err } + + return false, [21]byte{}, nil, [10]byte{}, [10]byte{}, nil + } + + return true, connectionIdentifier, retrievedReviewsInfoMap, intersectionRangeStart, intersectionRangeEnd, nil + } + + hostDownloadSuccessful, connectionIdentifier, retrievedReviewsInfoMap, expectedRangeStart, expectedRangeEnd, err := getRetrievedReviewsInfoMap() + if (err != nil) { return false, err } + if (hostDownloadSuccessful == false){ + // We did not download any reviews from this host. Skip them. + return false, nil + } + + defer peerClient.CloseConnection(connectionIdentifier) + + // Now we see if we have any of these reviews, and if they do not fulfill the requests parameters + // If they do not fulfill the request, we know host is malicious + // This requires checking against content stored in our database + // A host could use this to learn information about what messages we are storing + + // We still download the reviews to prevent host from knowing that we have the reviewed messages downloaded + // Otherwise they could see that we stopped our request mid-way and become aware of the fact that we detected their malicious response + // Only after the full request is finished do we mark the host as malicious + + // TODO: We shouldn't have to wait until the end of the request if we are only in Host mode + // This is because if we we don't need to worry about leaking what messages we have stored + + reviewHashesToDownloadList := make([][29]byte, 0) + + hostIsMalicious := false + + for receivedReviewHash, receivedReviewedHash := range retrievedReviewsInfoMap{ + + storedReviewExists, storedReviewBytes, err := badgerDatabase.GetReview(receivedReviewHash) + if (err != nil) { return false, err } + + checkIfHostIsOfferingInvalidReview := func()(bool, error){ + + if (storedReviewExists == true){ + + ableToRead, storedReviewHash, _, reviewNetworkType, storedReviewReviewerIdentity, _, storedReviewType, storedReviewReviewedHash, _, _, err := readReviews.ReadReviewAndHash(false, storedReviewBytes) + if (err != nil) { return false, err } + if (ableToRead == false){ + return false, errors.New("Database corrupt: Contains invalid review.") + } + + if (storedReviewHash != receivedReviewHash){ + return false, errors.New("Database corrupt: Contains review with invalid entry key hash") + } + if (reviewNetworkType != networkType){ + return true, nil + } + + if (len(reviewersToRetrieveList) != 0){ + + isInList := slices.Contains(reviewersToRetrieveList, storedReviewReviewerIdentity) + if (isInList == false){ + return true, nil + } + } + + areEqual := bytes.Equal(storedReviewReviewedHash, receivedReviewedHash[:]) + if (areEqual == false){ + return true, nil + } + + if (storedReviewType != "Message"){ + return true, nil + } + } + + // Now we check to see if we are aware of the reviewed message's metadata, and if it matches our request parameters + + metadataIsKnown, _, messageNetworkType, _, reviewedMessageInbox, _, err := contentMetadata.GetMessageMetadata(receivedReviewedHash) + if (err != nil) { return false, err } + if (metadataIsKnown == false){ + return false, nil + } + if (messageNetworkType != networkType){ + // The review is reviewing a message from a different networkType + // The host should have checked for this before serving us the review + // Thus, the host must be malicious + return true, nil + } + + messageInboxIsWithinRange, err := byteRange.CheckIfInboxIsWithinRange(expectedRangeStart, expectedRangeEnd, reviewedMessageInbox) + if (err != nil) { return false, err } + if (messageInboxIsWithinRange == false){ + return false, nil + } + + return false, nil + } + + hostIsOfferingInvalidReview, err := checkIfHostIsOfferingInvalidReview() + if (err != nil) { return false, err } + if (hostIsOfferingInvalidReview == true){ + // Host is malicious, we must still download the review + hostIsMalicious = true + + reviewHashesToDownloadList = append(reviewHashesToDownloadList, receivedReviewHash) + continue + } + + if (storedReviewExists == true){ + + // We already have the review, skip downloading it + continue + } + + // Review does not exist, we must check if we want to download it + + shouldDownload, err := checkIfReviewShouldBeDownloaded(receivedReviewHash, receivedReviewedHash) + if (err != nil) { return false, err } + if (shouldDownload == true){ + reviewHashesToDownloadList = append(reviewHashesToDownloadList, receivedReviewHash) + continue + } + } + + if (len(reviewHashesToDownloadList) == 0){ + // Nothing to download, done with this host. + + if (hostIsMalicious == true){ + err := maliciousHosts.AddHostToMaliciousHostsList(hostIdentityHash) + if (err != nil) { return false, err } + } + + return true, nil + } + + // Now we download reviews + + // We convert the [26]byte values to []byte values to pass to GetReviewsFromHost + reviewsInfoMapForRequest := make(map[[29]byte][]byte) + + for reviewHash, reviewedMessageHash := range retrievedReviewsInfoMap{ + reviewsInfoMapForRequest[reviewHash] = reviewedMessageHash[:] + } + + maximumReviewsInResponse := serverResponse.MaximumReviewsInResponse_GetReviews + + reviewHashesSublistsList, err := helpers.SplitListIntoSublists(reviewHashesToDownloadList, maximumReviewsInResponse) + if (err != nil) { return false, err } + + numberOfDownloadedReviews := 0 + + for _, reviewHashesSublist := range reviewHashesSublistsList{ + + downloadSuccessful, reviewsList, err := sendRequests.GetReviewsFromHost(connectionIdentifier, hostIdentityHash, networkType, reviewHashesSublist, reviewsInfoMapForRequest, reviewersToRetrieveList) + if (err != nil) { return false, err } + if (downloadSuccessful == false){ + // Request has failed + // We will stop downloading from this host + break + } + if (hostIsMalicious == true){ + // We don't add reviews from malicious hosts + continue + } + + for _, reviewBytes := range reviewsList{ + + reviewIsWellFormed, err := reviewStorage.AddReview(reviewBytes) + if (err != nil) { return false, err } + + if (reviewIsWellFormed == false){ + return false, errors.New("GetReviewsFromHost not verifying reviews are well formed.") + } + } + + numberOfDownloadedReviews += len(reviewsList) + } + + if (hostIsMalicious == true){ + err := maliciousHosts.AddHostToMaliciousHostsList(hostIdentityHash) + if (err != nil) { return false, err } + } + + if (numberOfDownloadedReviews != 0){ + numberOfReviewsDownloadedString := helpers.ConvertIntToString(numberOfDownloadedReviews) + + err = logger.AddLogEntry("Network", "Successfully downloaded " + numberOfReviewsDownloadedString + " reviews from host.") + if (err != nil) { return false, err } + } + + return true, nil + } + + eligibleHostsList, err := eligibleHosts.GetEligibleHostsList(networkType) + if (err != nil) { return err } + + queriedHosts := 0 + + for _, hostIdentityHash := range eligibleHostsList{ + + successfulDownload, err := downloadReviewsFromHost(hostIdentityHash) + if (err != nil) { return err } + if (successfulDownload == true){ + queriedHosts += 1 + } + if (queriedHosts >= numberOfHostsToQuery){ + return nil + } + } + + return nil +} + + +func DownloadIdentityReportsFromHosts( + allowClearnet bool, + networkType byte, + identityTypeToRetrieve string, + rangeToRetrieveStart [16]byte, + rangeToRetrieveEnd [16]byte, + reportedIdentityHashesToRetrieveList [][16]byte, + checkIfReportShouldBeDownloaded func([30]byte, []byte)(bool, error), + numberOfHostsToQuery int)error{ + + isValid := helpers.VerifyNetworkType(networkType) + if (isValid == false){ + networkTypeString := helpers.ConvertByteToString(networkType) + return errors.New("DownloadIdentityReportsFromHosts called with invalid networkType: " + networkTypeString) + } + + //TODO: Verify inputs + + //Outputs: + // -bool: Download successful + // -error + downloadReportsFromHost := func(hostIdentityHash [16]byte)(bool, error){ + + exists, _, _, _, _, hostRawProfileMap, err := profileStorage.GetNewestUserProfile(hostIdentityHash, networkType) + if (err != nil) { return false, err } + if (exists == false){ + // Host profile was deleted, skip host + return false, nil + } + + // Outputs: + // -bool: Successfully downloaded from host + // -[21]byte: Connection identifier + // -map[[30]byte][]byte: Map of report info: ReportHash -> ReportedHash + // -[16]byte: Expected Identity Start Range of retrieved reports + // -[16]byte: Expected Identity End range of retrieved reports + // -[][16]byte: Expected retrieved identity hashes list + // -error + getRetrievedReportsInfoMap := func()(bool, [21]byte, map[[30]byte][]byte, [16]byte, [16]byte, [][16]byte, error){ + + hostIsHostingIdentityType, theirHostedIdentitiesRangeStart, theirHostedIdentitiesRangeEnd, err := hostRanges.GetHostedIdentityHashRangeFromHostRawProfileMap(hostRawProfileMap, identityTypeToRetrieve) + if (err != nil) { return false, [21]byte{}, nil, [16]byte{}, [16]byte{}, nil, err } + if (hostIsHostingIdentityType == false){ + // Host is not hosting any profiles/reports of this identityType. Skip to next host + return false, [21]byte{}, nil, [16]byte{}, [16]byte{}, nil, nil + } + + identitiesQuantityAttributeName := identityTypeToRetrieve + "IdentitiesQuantity" + + exists, identityTypeIdentitiesQuantity, err := readProfiles.GetFormattedProfileAttributeFromRawProfileMap(hostRawProfileMap, identitiesQuantityAttributeName) + if (err != nil) { return false, [21]byte{}, nil, [16]byte{}, [16]byte{}, nil, err } + if (exists == false){ + return false, [21]byte{}, nil, [16]byte{}, [16]byte{}, nil, errors.New("Database corrupt: Contains host profile missing " + identitiesQuantityAttributeName) + } + + identityTypeIdentitiesQuantityInt64, err := helpers.ConvertStringToInt64(identityTypeIdentitiesQuantity) + if (err != nil) { + return false, [21]byte{}, nil, [16]byte{}, [16]byte{}, nil, errors.New("Database corrupt: Contains Host profile with invalid " + identityTypeToRetrieve + "IdentitiesQuantity: " + identityTypeIdentitiesQuantity) + } + + reportsQuantityAttributeName := identityTypeToRetrieve + "ReportsQuantity" + + exists, identityTypeReportsQuantity, err := readProfiles.GetFormattedProfileAttributeFromRawProfileMap(hostRawProfileMap, reportsQuantityAttributeName) + if (err != nil) { return false, [21]byte{}, nil, [16]byte{}, [16]byte{}, nil, err } + if (exists == false){ + return false, [21]byte{}, nil, [16]byte{}, [16]byte{}, nil, errors.New("Database corrupt: Contains host profile missing " + reportsQuantityAttributeName) + } + + identityTypeReportsQuantityInt64, err := helpers.ConvertStringToInt64(identityTypeReportsQuantity) + if (err != nil) { + return false, [21]byte{}, nil, [16]byte{}, [16]byte{}, nil, errors.New("Database corrupt: Contains host profile with invalid " + reportsQuantityAttributeName + ": " + identityTypeReportsQuantity) + } + + //TODO: Deal with divide by 0 case here and on all other functions + + estimatedReportsPerIdentity := identityTypeIdentitiesQuantityInt64 / identityTypeReportsQuantityInt64 + + maximumReportsInResponse := serverResponse.MaximumReportsInResponse_GetIdentityReportsInfo + + maximumIdentitiesPerRequest := int64(maximumReportsInResponse) / estimatedReportsPerIdentity + + // Our request will either be based on identity hash range or identity hashes list + // We will split up the request into either ranges or identity hash lists + + if (len(reportedIdentityHashesToRetrieveList) != 0){ + + // We will split the request into identity hash lists + + hostingAny, identitiesInHostRangeList, err := byteRange.GetAllIdentityHashesInListWithinRange(theirHostedIdentitiesRangeStart, theirHostedIdentitiesRangeEnd, reportedIdentityHashesToRetrieveList) + if (err != nil) { return false, [21]byte{}, nil, [16]byte{}, [16]byte{}, nil, err } + if (hostingAny == false){ + // Host is not hosting any of our desired identities. Skip host. + return false, [21]byte{}, nil, [16]byte{}, [16]byte{}, nil, nil + } + + // Below step is just being overly cautious. + // The GetMessageReportsfromHosts function should request either a list of identities or a range. Not both. + hostingAny, identitiesInRequestRange, err := byteRange.GetAllIdentityHashesInListWithinRange(rangeToRetrieveStart, rangeToRetrieveEnd, identitiesInHostRangeList) + if (err != nil) { return false, [21]byte{}, nil, [16]byte{}, [16]byte{}, nil, err } + if (hostingAny == false){ + return false, [21]byte{}, nil, [16]byte{}, [16]byte{}, nil, errors.New("Requested report identity hashes list outside of request range") + } + + sublistsToQueryList, err := helpers.SplitListIntoSublists(identitiesInRequestRange, int(maximumIdentitiesPerRequest)) + if (err != nil) { return false, [21]byte{}, nil, [16]byte{}, [16]byte{}, nil, err } + + hostProfileExists, connectionEstablished, connectionIdentifier, err := peerClient.EstablishNewConnectionToHost(allowClearnet, hostIdentityHash, networkType) + if (err != nil){ return false, [21]byte{}, nil, [16]byte{}, [16]byte{}, nil, err } + if (hostProfileExists == false){ + return false, [21]byte{}, nil, [16]byte{}, [16]byte{}, nil, nil + } + if (connectionEstablished == false){ + return false, [21]byte{}, nil, [16]byte{}, [16]byte{}, nil, nil + } + + minimumRange, maximumRange := byteRange.GetMinimumMaximumIdentityHashBounds() + + //Outputs: + // -bool: Download successful + // -map[[30]byte][]byte: Downloaded reports info map (Report Hash -> Reported Hash) + // -error + getDownloadedReportsInfoMap := func()(bool, map[[30]byte][]byte, error){ + + retrievedReportsInfoMap := make(map[[30]byte][]byte) + + for _, identityHashesSublist := range sublistsToQueryList{ + + downloadSuccessful, currentRetrievedReportsInfoMap, err := sendRequests.GetIdentityReportsInfoFromHost(connectionIdentifier, hostIdentityHash, networkType, identityTypeToRetrieve, minimumRange, maximumRange, identityHashesSublist) + if (err != nil) { return false, nil, err } + if (downloadSuccessful == false){ + // Failed to download this sublist. + // Could be that host went offline permanently or just this connection failed. + // Either way, stop downloading from this host + err := logger.AddLogEntry("Network", "Failed to download identity reports info from host.") + if (err != nil) { return false, nil, err } + return false, nil, nil + } + + for reportHash, reportedHash := range currentRetrievedReportsInfoMap{ + retrievedReportsInfoMap[reportHash] = reportedHash + } + } + + return true, retrievedReportsInfoMap, nil + } + + downloadSuccessful, retrievedReportsInfoMap, err := getDownloadedReportsInfoMap() + if (err != nil) { + errB := peerClient.CloseConnection(connectionIdentifier) + if (errB != nil) { return false, [21]byte{}, nil, [16]byte{}, [16]byte{}, nil, errB } + + return false, [21]byte{}, nil, [16]byte{}, [16]byte{}, nil, err + } + if (downloadSuccessful == false){ + + err := peerClient.CloseConnection(connectionIdentifier) + if (err != nil) { return false, [21]byte{}, nil, [16]byte{}, [16]byte{}, nil, err } + + return false, [21]byte{}, nil, [16]byte{}, [16]byte{}, nil, nil + } + + return true, connectionIdentifier, retrievedReportsInfoMap, minimumRange, maximumRange, identitiesInRequestRange, nil + } + + // We will retrieve based on identity range, not identities list + + anyIntersectionFound, intersectionRangeStart, intersectionRangeEnd, err := byteRange.GetIdentityIntersectionRangeFromTwoRanges(rangeToRetrieveStart, rangeToRetrieveEnd, theirHostedIdentitiesRangeStart, theirHostedIdentitiesRangeEnd) + if (err != nil) { return false, [21]byte{}, nil, [16]byte{}, [16]byte{}, nil, err } + if (anyIntersectionFound == false){ + // Host is not hosting any reports within the identites range that we are requesting + // Skip this host + return false, [21]byte{}, nil, [16]byte{}, [16]byte{}, nil, nil + } + + estimatedNumIdentitiesInIntersectionRange, err := byteRange.GetEstimatedIdentitySubrangeQuantity(theirHostedIdentitiesRangeStart, theirHostedIdentitiesRangeEnd, identityTypeIdentitiesQuantityInt64, intersectionRangeStart, intersectionRangeEnd) + if (err != nil) { return false, [21]byte{}, nil, [16]byte{}, [16]byte{}, nil, err } + + subrangesToQueryList, err := byteRange.SplitIdentityRangeIntoEqualSubranges(intersectionRangeStart, intersectionRangeEnd, estimatedNumIdentitiesInIntersectionRange, maximumIdentitiesPerRequest) + if (err != nil) { return false, [21]byte{}, nil, [16]byte{}, [16]byte{}, nil, err } + + hostProfileExists, connectionEstablished, connectionIdentifier, err := peerClient.EstablishNewConnectionToHost(allowClearnet, hostIdentityHash, networkType) + if (err != nil){ return false, [21]byte{}, nil, [16]byte{}, [16]byte{}, nil, err } + if (hostProfileExists == false){ + return false, [21]byte{}, nil, [16]byte{}, [16]byte{}, nil, nil + } + if (connectionEstablished == false){ + return false, [21]byte{}, nil, [16]byte{}, [16]byte{}, nil, nil + } + + getDownloadedReportsInfoMap := func()(bool, map[[30]byte][]byte, error){ + + retrievedReportsInfoMap := make(map[[30]byte][]byte) + + for _, subrangeObject := range subrangesToQueryList{ + + subrangeStart := subrangeObject.SubrangeStart + subrangeEnd := subrangeObject.SubrangeEnd + + emptyList := make([][16]byte, 0) + + downloadSuccessful, currentRetrievedReportsInfoMap, err := sendRequests.GetIdentityReportsInfoFromHost(connectionIdentifier, hostIdentityHash, networkType, identityTypeToRetrieve, subrangeStart, subrangeEnd, emptyList) + if (err != nil) { return false, nil, err } + if (downloadSuccessful == false){ + // Failed to download this sublist. + // Could be that host went offline permanently or just this connection failed. + // Either way, close connection to host. + err := logger.AddLogEntry("Network", "Failed to download identity reports info from host.") + if (err != nil) { return false, nil, err } + + return false, nil, nil + } + + for reportHash, reportedHash := range currentRetrievedReportsInfoMap{ + retrievedReportsInfoMap[reportHash] = reportedHash + } + } + + return true, retrievedReportsInfoMap, nil + } + + downloadSuccessful, retrievedReportsInfoMap, err := getDownloadedReportsInfoMap() + if (err != nil) { + errB := peerClient.CloseConnection(connectionIdentifier) + if (errB != nil) { return false, [21]byte{}, nil, [16]byte{}, [16]byte{}, nil, errB } + + return false, [21]byte{}, nil, [16]byte{}, [16]byte{}, nil, err + } + if (downloadSuccessful == false){ + + err := peerClient.CloseConnection(connectionIdentifier) + if (err != nil) { return false, [21]byte{}, nil, [16]byte{}, [16]byte{}, nil, err } + + return false, [21]byte{}, nil, [16]byte{}, [16]byte{}, nil, nil + } + + return true, connectionIdentifier, retrievedReportsInfoMap, intersectionRangeStart, intersectionRangeEnd, nil, nil + } + + hostDownloadSuccessful, connectionIdentifier, retrievedReportsInfoMap, expectedRangeStart, expectedRangeEnd, expectedReportedIdentityHashesList, err := getRetrievedReportsInfoMap() + if (err != nil) { return false, err } + if (hostDownloadSuccessful == false){ + // We did not download any reports from this host. Skip them. + return false, nil + } + + defer peerClient.CloseConnection(connectionIdentifier) + + // Now we see if we have any of these reports, and if they do not fulfill the requests parameters + // If they do not fulfill the request, we know host is malicious + // We still download the reports to prevent host from knowing that we have those reports downloaded + // We then mark the host as malicious + + reportHashesToDownloadList := make([][30]byte, 0) + + hostIsMalicious := false + + for receivedReportHash, receivedReportedHash := range retrievedReportsInfoMap{ + + //TODO: Ability to disable this step if only 1 of Host/Moderator mode is enabled + + storedReportExists, storedReportBytes, err := badgerDatabase.GetReport(receivedReportHash) + if (err != nil) { return false, err } + + checkIfHostIsOfferingInvalidReport := func()(bool, error){ + + if (storedReportExists == true){ + + ableToRead, storedReportHash, _, reportNetworkType, _, storedReportType, storedReportReportedHash, _, err := readReports.ReadReportAndHash(false, storedReportBytes) + if (err != nil) { return false, err } + if (ableToRead == false){ + return false, errors.New("Database corrupt: Contains invalid report") + } + if (storedReportHash != receivedReportHash){ + return false, errors.New("Database corrupt: Contains report with invalid entry key hash") + } + if (reportNetworkType != networkType){ + return true, nil + } + + areEqual := bytes.Equal(storedReportReportedHash, receivedReportedHash) + if (areEqual == false){ + return true, nil + } + + if (storedReportType != "Identity" && storedReportType != "Profile" && storedReportType != "Attribute"){ + return true, nil + } + } + + // Now we see if we have the reviewed profile metadata + + reportedType, err := helpers.GetReportedTypeFromReportedHash(receivedReportedHash) + if (err != nil){ return false, err } + + if (reportedType == "Profile"){ + + if (len(receivedReportedHash) != 28){ + reportedHashHex := encoding.EncodeBytesToHexString(receivedReportedHash) + return false, errors.New("GetReportedTypeFromReportedHash returning Profile for different length reportedHash: " + reportedHashHex) + } + + receivedReportedProfileHash := [28]byte(receivedReportedHash) + + metadataExists, _, profileNetworkType, profileAuthor, _, profileIsDisabled, _, _, err := contentMetadata.GetProfileMetadata(receivedReportedProfileHash) + if (err != nil) { return false, err } + if (metadataExists == false){ + return false, nil + } + if (profileNetworkType != networkType){ + // Reported profile belongs to a different networkType than the report + // The host should have checked for this before serving this report + // Host must be malicious + return true, nil + } + if (profileIsDisabled == true){ + // Disabled profiles cannot be reported + // Host must be malicious + return true, nil + } + + + if (expectedReportedIdentityHashesList != nil){ + + identityIsInList := slices.Contains(expectedReportedIdentityHashesList, profileAuthor) + if (identityIsInList == false){ + return true, nil + } + + } else { + + identityIsWithinRange, err := byteRange.CheckIfIdentityHashIsWithinRange(expectedRangeStart, expectedRangeEnd, profileAuthor) + if (err != nil) { return false, err } + if (identityIsWithinRange == false){ + return true, nil + } + } + } else if (reportedType == "Attribute"){ + + if (len(receivedReportedHash) != 27){ + reportedHashHex := encoding.EncodeBytesToHexString(receivedReportedHash) + return false, errors.New("GetReportedTypeFromReportedHash returning Attribute for different length reportedHash: " + reportedHashHex) + } + + receivedReportedAttributeHash := [27]byte(receivedReportedHash) + + metadataExists, _, attributeAuthorIdentityHash, attributeNetworkType, _, err := profileStorage.GetProfileAttributeMetadata(receivedReportedAttributeHash) + if (err != nil){ return false, err } + if (metadataExists == true){ + + if (expectedReportedIdentityHashesList != nil){ + + identityIsInList := slices.Contains(expectedReportedIdentityHashesList, attributeAuthorIdentityHash) + if (identityIsInList == false){ + return true, nil + } + } else { + + identityIsWithinRange, err := byteRange.CheckIfIdentityHashIsWithinRange(expectedRangeStart, expectedRangeEnd, attributeAuthorIdentityHash) + if (err != nil) { return false, err } + if (identityIsWithinRange == false){ + return true, nil + } + } + + if (attributeNetworkType != networkType){ + // Report is reporting attribute from different networkType + // Host should have checked this already. Host must be malicious. + return true, nil + } + + return false, nil + } + } + + // Host is not offering invalid report (as far as we know) + + return false, nil + } + + hostIsOfferingInvalidReport, err := checkIfHostIsOfferingInvalidReport() + if (err != nil) { return false, err } + + if (hostIsOfferingInvalidReport == true){ + // Host is malicious, we must still download the report + hostIsMalicious = true + + reportHashesToDownloadList = append(reportHashesToDownloadList, receivedReportHash) + continue + } + + if (storedReportExists == true){ + + // We already have the report, skip downloading it + continue + } + + // Report does not exist, we must check if we want to download it + // This works differently for Hosting and Moderation + + shouldDownload, err := checkIfReportShouldBeDownloaded(receivedReportHash, receivedReportedHash) + if (err != nil) { return false, err } + + if (shouldDownload == true){ + reportHashesToDownloadList = append(reportHashesToDownloadList, receivedReportHash) + continue + } + } + + if (len(reportHashesToDownloadList) == 0){ + // Nothing to download, done with this host. + + if (hostIsMalicious == true){ + err := maliciousHosts.AddHostToMaliciousHostsList(hostIdentityHash) + if (err != nil) { return false, err } + } + + return true, nil + } + + // Now we download reports + + maximumReportsInResponse := serverResponse.MaximumReportsInResponse_GetReports + + reportHashesSublistsList, err := helpers.SplitListIntoSublists(reportHashesToDownloadList, maximumReportsInResponse) + if (err != nil) { return false, err } + + numberOfDownloadedReports := 0 + + for _, reportHashesSublist := range reportHashesSublistsList{ + + downloadSuccessful, reportsList, err := sendRequests.GetReportsFromHost(connectionIdentifier, hostIdentityHash, networkType, reportHashesSublist, retrievedReportsInfoMap) + if (err != nil) { return false, err } + if (downloadSuccessful == false){ + // Request has failed + // We will stop downloading from this host + break + } + + for _, reportBytes := range reportsList{ + + reportIsWellFormed, err := reportStorage.AddReport(reportBytes) + if (err != nil) { return false, err } + + if (reportIsWellFormed == false){ + return false, errors.New("GetReportsFromHost not verifying reports are well formed.") + } + } + + numberOfDownloadedReports += len(reportsList) + } + + if (hostIsMalicious == true){ + err := maliciousHosts.AddHostToMaliciousHostsList(hostIdentityHash) + if (err != nil) { return false, err } + } + + if (numberOfDownloadedReports != 0){ + numberOfReportsDownloadedString := helpers.ConvertIntToString(numberOfDownloadedReports) + + err = logger.AddLogEntry("Network", "Successfully downloaded " + numberOfReportsDownloadedString + " identity reports from host.") + if (err != nil) { return false, err } + } + + return true, nil + } + + eligibleHostsList, err := eligibleHosts.GetEligibleHostsList(networkType) + if (err != nil) { return err } + + queriedHosts := 0 + + for _, hostIdentityHash := range eligibleHostsList{ + + successfulDownload, err := downloadReportsFromHost(hostIdentityHash) + if (err != nil) { return err } + if (successfulDownload == true){ + queriedHosts += 1 + } + if (queriedHosts >= numberOfHostsToQuery){ + return nil + } + } + + return nil +} + + +func DownloadMessageReportsFromHosts( + allowClearnet bool, + networkType byte, + rangeToRetrieveStart [10]byte, + rangeToRetrieveEnd [10]byte, + reportedMessageHashesToRetrieveList [][26]byte, + reportedMessagesInboxMap map[[26]byte][10]byte, // Message hash -> Message inbox + checkIfReportShouldBeDownloaded func([30]byte, [26]byte)(bool, error), + numberOfHostsToQuery int)error{ + + isValid := helpers.VerifyNetworkType(networkType) + if (isValid == false){ + networkTypeString := helpers.ConvertByteToString(networkType) + return errors.New("DownloadMessageReportsFromHosts called with invalid networkType: " + networkTypeString) + } + + //TODO: Verify inputs + + //Outputs: + // -bool: Download successful + // -error + downloadReportsFromHost := func(hostIdentityHash [16]byte)(bool, error){ + + exists, _, _, _, _, hostRawProfileMap, err := profileStorage.GetNewestUserProfile(hostIdentityHash, networkType) + if (err != nil) { return false, err } + if (exists == false){ + // Host profile was deleted, skip host + return false, nil + } + + // Outputs: + // -bool: Successfully downloaded from host + // -[21]byte: Connection identifier + // -map[[30]byte][26]byte: Map of report info: ReportHash-> Reported message Hash + // -[10]byte: Expected Inbox Start Range of retrieved reports + // -[10]byte: Expected Inbox End range of retrieved reports + // -error + getRetrievedReportsInfoMap := func()(bool, [21]byte, map[[30]byte][26]byte, [10]byte, [10]byte, error){ + + hostIsHostingMessages, theirInboxHostRangeStart, theirInboxHostRangeEnd, err := hostRanges.GetHostedMessageInboxesRangeFromHostRawProfileMap(hostRawProfileMap) + if (err != nil) { return false, [21]byte{}, nil, [10]byte{}, [10]byte{}, err } + if (hostIsHostingMessages == false){ + + // Host is not hosting messages. Nothing to download. + return false, [21]byte{}, nil, [10]byte{}, [10]byte{}, nil + } + + exists, hostedMessagesQuantity, err := readProfiles.GetFormattedProfileAttributeFromRawProfileMap(hostRawProfileMap, "MessagesQuantity") + if (err != nil) { return false, [21]byte{}, nil, [10]byte{}, [10]byte{}, err } + if (exists == false){ + return false, [21]byte{}, nil, [10]byte{}, [10]byte{}, errors.New("Database corrupt: Contains host profile missing MessagesQuantity") + } + + hostedMessagesQuantityInt64, err := helpers.ConvertStringToInt64(hostedMessagesQuantity) + if (err != nil) { + return false, [21]byte{}, nil, [10]byte{}, [10]byte{}, errors.New("Database corrupt: Contains host profile with invalid MessagesQuantity: " + hostedMessagesQuantity) + } + + exists, hostedMessageReportsQuantity, err := readProfiles.GetFormattedProfileAttributeFromRawProfileMap(hostRawProfileMap, "MessageReportsQuantity") + if (err != nil) { return false, [21]byte{}, nil, [10]byte{}, [10]byte{}, err } + if (exists == false){ + return false, [21]byte{}, nil, [10]byte{}, [10]byte{}, errors.New("Database corrupt: Contains host profile missing MessageReportsQuantity") + } + + hostedMessageReportsQuantityInt64, err := helpers.ConvertStringToInt64(hostedMessageReportsQuantity) + if (err != nil) { + return false, [21]byte{}, nil, [10]byte{}, [10]byte{}, errors.New("Databse corrupt: Contains host profile with invalid MessageReportsQuantity: " + hostedMessageReportsQuantity) + } + + if (hostedMessageReportsQuantityInt64 == 0){ + // Host is not hosting any message reports. Nothing to download. + return false, [21]byte{}, nil, [10]byte{}, [10]byte{}, nil + } + + maximumReportsInResponse := serverResponse.MaximumReportsInResponse_GetMessageReportsInfo + + estimatedReportsPerMessage := hostedMessageReportsQuantityInt64 / hostedMessagesQuantityInt64 + + maximumMessageHashesPerRequest := int64(maximumReportsInResponse) / estimatedReportsPerMessage + + // We will retrieve reports from either the reported hashes list, or request range + + if (len(reportedMessageHashesToRetrieveList) != 0){ + + // We will retrieve reported message hashes list + + messageHashesInTheirRangeList := make([][26]byte, 0) + + for _, messageHash := range reportedMessageHashesToRetrieveList{ + + messageInbox, exists := reportedMessagesInboxMap[messageHash] + if (exists == false) { + return false, [21]byte{}, nil, [10]byte{}, [10]byte{}, errors.New("ReportedMessagesInboxMap missing message hash.") + } + + inboxIsWithinTheirRange, err := byteRange.CheckIfInboxIsWithinRange(theirInboxHostRangeStart, theirInboxHostRangeEnd, messageInbox) + if (err != nil) { return false, [21]byte{}, nil, [10]byte{}, [10]byte{}, err } + + if (inboxIsWithinTheirRange == true){ + messageHashesInTheirRangeList = append(messageHashesInTheirRangeList, messageHash) + } + } + + sublistsToQueryList, err := helpers.SplitListIntoSublists(messageHashesInTheirRangeList, int(maximumMessageHashesPerRequest)) + if (err != nil) { return false, [21]byte{}, nil, [10]byte{}, [10]byte{}, err } + + hostProfileExists, connectionEstablished, connectionIdentifier, err := peerClient.EstablishNewConnectionToHost(allowClearnet, hostIdentityHash, networkType) + if (err != nil){ return false, [21]byte{}, nil, [10]byte{}, [10]byte{}, err } + if (hostProfileExists == false){ + return false, [21]byte{}, nil, [10]byte{}, [10]byte{}, nil + } + if (connectionEstablished == false){ + return false, [21]byte{}, nil, [10]byte{}, [10]byte{}, nil + } + + minimumRange, maximumRange := byteRange.GetMinimumMaximumInboxBounds() + + //Outputs: + // -bool: Download successful + // -map[[30]byte][26]byte: Downloaded reports info map (Report Hash -> Reported Message Hash) + // -error + getDownloadedReportsInfoMap := func()(bool, map[[30]byte][26]byte, error){ + + retrievedReportsInfoMap := make(map[[30]byte][26]byte) + + for _, messageHashesSublist := range sublistsToQueryList{ + + downloadSuccessful, currentRetrievedReportsInfoMap, err := sendRequests.GetMessageReportsInfoFromHost(connectionIdentifier, hostIdentityHash, networkType, minimumRange, maximumRange, messageHashesSublist) + if (err != nil) { return false, nil, err } + if (downloadSuccessful == false){ + // Failed to download this sublist. + // Could be that host went offline permanently or just this connection failed. + // Either way, end connection to host + err := logger.AddLogEntry("Network", "Failed to download message reports info from host.") + if (err != nil) { return false, nil, err } + + return false, nil, nil + } + + for reportHash, reportedHash := range currentRetrievedReportsInfoMap{ + retrievedReportsInfoMap[reportHash] = reportedHash + } + } + + return true, retrievedReportsInfoMap, nil + } + + downloadSuccessful, retrievedReportsInfoMap, err := getDownloadedReportsInfoMap() + if (err != nil) { + errB := peerClient.CloseConnection(connectionIdentifier) + if (errB != nil) { return false, [21]byte{}, nil, [10]byte{}, [10]byte{}, errB } + + return false, [21]byte{}, nil, [10]byte{}, [10]byte{}, err + } + if (downloadSuccessful == false){ + + err := peerClient.CloseConnection(connectionIdentifier) + if (err != nil) { return false, [21]byte{}, nil, [10]byte{}, [10]byte{}, err } + + return false, [21]byte{}, nil, [10]byte{}, [10]byte{}, nil + } + + return true, connectionIdentifier, retrievedReportsInfoMap, minimumRange, maximumRange, nil + } + + // We will retrieve reports based on message inbox range + + anyIntersectionFound, intersectionRangeStart, intersectionRangeEnd, err := byteRange.GetInboxIntersectionRangeFromTwoRanges(rangeToRetrieveStart, rangeToRetrieveEnd, theirInboxHostRangeStart, theirInboxHostRangeEnd) + if (err != nil) { return false, [21]byte{}, nil, [10]byte{}, [10]byte{}, err } + if (anyIntersectionFound == false){ + // Host is not hosting any reports within the inboxes range that we are requesting + // Skip this host + return false, [21]byte{}, nil, [10]byte{}, [10]byte{}, nil + } + + estimatedNumItemsInIntersectionRange, err := byteRange.GetEstimatedInboxSubrangeQuantity(theirInboxHostRangeStart, theirInboxHostRangeEnd, hostedMessagesQuantityInt64, intersectionRangeStart, intersectionRangeEnd) + if (err != nil) { return false, [21]byte{}, nil, [10]byte{}, [10]byte{}, err } + + subrangesToQueryList, err := byteRange.SplitInboxRangeIntoEqualSubranges(intersectionRangeStart, intersectionRangeEnd, estimatedNumItemsInIntersectionRange, maximumMessageHashesPerRequest) + if (err != nil) { return false, [21]byte{}, nil, [10]byte{}, [10]byte{}, err } + + hostProfileExists, connectionEstablished, connectionIdentifier, err := peerClient.EstablishNewConnectionToHost(allowClearnet, hostIdentityHash, networkType) + if (err != nil){ return false, [21]byte{}, nil, [10]byte{}, [10]byte{}, err } + if (hostProfileExists == false){ + return false, [21]byte{}, nil, [10]byte{}, [10]byte{}, nil + } + if (connectionEstablished == false){ + return false, [21]byte{}, nil, [10]byte{}, [10]byte{}, nil + } + + //Outputs: + // -bool: Download successful + // -map[[30]byte][26]byte: Downloaded reports info map (Report Hash -> Reported message hash) + // -error + getDownloadedReportsInfoMap := func()(bool, map[[30]byte][26]byte, error){ + + retrievedReportsInfoMap := make(map[[30]byte][26]byte) + + for _, subrangeObject := range subrangesToQueryList{ + + subrangeStart := subrangeObject.SubrangeStart + subrangeEnd := subrangeObject.SubrangeEnd + + emptyList := make([][26]byte, 0) + + downloadSuccessful, currentRetrievedReportsInfoMap, err := sendRequests.GetMessageReportsInfoFromHost(connectionIdentifier, hostIdentityHash, networkType, subrangeStart, subrangeEnd, emptyList) + if (err != nil) { return false, nil, err } + if (downloadSuccessful == false){ + // Failed to download this sublist. + // Could be that host went offline permanently or just this connection failed. + // Either way, stop downloading from host + err := logger.AddLogEntry("Network", "Failed to download message reports info from host.") + if (err != nil) { return false, nil, err } + + return false, nil, nil + } + + for reportHash, reportedHash := range currentRetrievedReportsInfoMap{ + retrievedReportsInfoMap[reportHash] = reportedHash + } + } + + return true, retrievedReportsInfoMap, nil + } + + downloadSuccessful, retrievedReportsInfoMap, err := getDownloadedReportsInfoMap() + if (err != nil) { + errB := peerClient.CloseConnection(connectionIdentifier) + if (errB != nil) { return false, [21]byte{}, nil, [10]byte{}, [10]byte{}, errB } + + return false, [21]byte{}, nil, [10]byte{}, [10]byte{}, err + } + if (downloadSuccessful == false){ + + err := peerClient.CloseConnection(connectionIdentifier) + if (err != nil) { return false, [21]byte{}, nil, [10]byte{}, [10]byte{}, err } + + return false, [21]byte{}, nil, [10]byte{}, [10]byte{}, nil + } + + return true, connectionIdentifier, retrievedReportsInfoMap, intersectionRangeStart, intersectionRangeEnd, nil + } + + hostDownloadSuccessful, connectionIdentifier, retrievedReportsInfoMap, expectedRangeStart, expectedRangeEnd, err := getRetrievedReportsInfoMap() + if (err != nil) { return false, err } + if (hostDownloadSuccessful == false){ + // We did not download any reports from this host. Skip them. + return false, nil + } + + defer peerClient.CloseConnection(connectionIdentifier) + + // Now we see if we have any of these reports, and if they do not fulfill the requests parameters + // If they do not fulfill the request, we know host is malicious + // We still download the reports to prevent host from knowing that we have those reports downloaded + // We then mark the host as malicious + + reportHashesToDownloadList := make([][30]byte, 0) + + hostIsMalicious := false + + for receivedReportHash, receivedReportedHash := range retrievedReportsInfoMap{ + + //TODO: Disable this step if only 1 of Host/Moderator mode is enabled + + storedReportExists, storedReportBytes, err := badgerDatabase.GetReport(receivedReportHash) + if (err != nil) { return false, err } + + checkIfHostIsOfferingInvalidReport := func()(bool, error){ + + if (storedReportExists == true){ + + ableToRead, storedReportHash, _, reportNetworkType, _, storedReportType, storedReportReportedHash, _, err := readReports.ReadReportAndHash(false, storedReportBytes) + if (err != nil) { return false, err } + if (ableToRead == false){ + return false, errors.New("Database corrupt: Contains invalid report") + } + if (storedReportHash != receivedReportHash){ + return false, errors.New("Database corrupt: Contains report with invalid entry key hash") + } + if (reportNetworkType != networkType){ + return true, nil + } + + areEqual := bytes.Equal(storedReportReportedHash, receivedReportedHash[:]) + if (areEqual == false){ + return true, nil + } + + if (storedReportType != "Message"){ + return true, nil + } + } + + // Now we see if we have the reviewed message metadata + + metadataExists, _, messageNetworkType, _, reportedMessageInbox, _, err := contentMetadata.GetMessageMetadata(receivedReportedHash) + if (err != nil) { return false, err } + if (metadataExists == false){ + return false, nil + } + if (messageNetworkType != networkType){ + // Report is reporting a message on a different networkType than the report + // The host should have checked for this already + // Host must be malicious + return true, nil + } + + inboxIsWithinRange, err := byteRange.CheckIfInboxIsWithinRange(expectedRangeStart, expectedRangeEnd, reportedMessageInbox) + if (err != nil) { return false, err } + if (inboxIsWithinRange == false){ + return true, nil + } + + return false, nil + } + + hostIsOfferingInvalidReport, err := checkIfHostIsOfferingInvalidReport() + if (err != nil) { return false, err } + + if (hostIsOfferingInvalidReport == true){ + // Host is malicious, we must still download the report + hostIsMalicious = true + + reportHashesToDownloadList = append(reportHashesToDownloadList, receivedReportHash) + continue + } + + if (storedReportExists == true){ + + // We already have the report, skip downloading it + continue + } + + // Report does not exist, we must check if we want to download it + // This works differently for Hosting and Moderation + // For instance, if the user is downloading reports for moderation, they will not redownload reports for messages that they have already reviewed + + shouldDownload, err := checkIfReportShouldBeDownloaded(receivedReportHash, receivedReportedHash) + if (err != nil) { return false, err } + if (shouldDownload == true){ + reportHashesToDownloadList = append(reportHashesToDownloadList, receivedReportHash) + continue + } + } + + if (len(reportHashesToDownloadList) == 0){ + // Nothing to download, done with this host. + + if (hostIsMalicious == true){ + err := maliciousHosts.AddHostToMaliciousHostsList(hostIdentityHash) + if (err != nil) { return false, err } + } + return true, nil + } + + // Now we download reports + + // We convert the [26]byte value to []byte to supply to GetReportsFromHost + reportsInfoMapForRequest := make(map[[30]byte][]byte) + + for reportHash, reportedMessageHash := range retrievedReportsInfoMap{ + + reportsInfoMapForRequest[reportHash] = reportedMessageHash[:] + } + + maximumReportsInResponse := serverResponse.MaximumReportsInResponse_GetReports + + reportHashesSublistsList, err := helpers.SplitListIntoSublists(reportHashesToDownloadList, maximumReportsInResponse) + if (err != nil) { return false, err } + + numberOfDownloadedReports := 0 + + for _, reportHashesSublist := range reportHashesSublistsList{ + + downloadSuccessful, reportsList, err := sendRequests.GetReportsFromHost(connectionIdentifier, hostIdentityHash, networkType, reportHashesSublist, reportsInfoMapForRequest) + if (err != nil) { return false, err } + if (downloadSuccessful == false){ + // Request has failed + // We will stop downloading from this host + break + } + + for _, reportBytes := range reportsList{ + + reportIsWellFormed, err := reportStorage.AddReport(reportBytes) + if (err != nil) { return false, err } + + if (reportIsWellFormed == false){ + return false, errors.New("GetReportsFromHost not verifying reports are well formed.") + } + } + + numberOfDownloadedReports += len(reportsList) + } + + if (hostIsMalicious == true){ + err := maliciousHosts.AddHostToMaliciousHostsList(hostIdentityHash) + if (err != nil) { return false, err } + } + + if (numberOfDownloadedReports != 0){ + numberOfReportsDownloadedString := helpers.ConvertIntToString(numberOfDownloadedReports) + + err = logger.AddLogEntry("Network", "Successfully downloaded " + numberOfReportsDownloadedString + " reports from host.") + if (err != nil) { return false, err } + } + + return true, nil + } + + eligibleHostsList, err := eligibleHosts.GetEligibleHostsList(networkType) + if (err != nil) { return err } + + queriedHosts := 0 + + for _, hostIdentityHash := range eligibleHostsList{ + + successfulDownload, err := downloadReportsFromHost(hostIdentityHash) + if (err != nil) { return err } + if (successfulDownload == true){ + queriedHosts += 1 + } + if (queriedHosts >= numberOfHostsToQuery){ + return nil + } + } + + return nil +} + +//Inputs: +// -bool: Allow clearnet +// -[]string: List of identity hashes to retrieve viewable consensus statuses for +// -map[[28]byte][16]byte: Map of Profile Hash -> Profile Identity Hash +// Outputs: +// -error +func DownloadViewableStatusesFromHosts( + allowClearnet bool, + networkType byte, + knownOrUnknown string, + identityHashesToRetrieveList [][16]byte, + profileHashesToRetrieveMap map[[28]byte][16]byte, + numberOfHostsToQuery int)error{ + + isValid := helpers.VerifyNetworkType(networkType) + if (isValid == false){ + networkTypeString := helpers.ConvertByteToString(networkType) + return errors.New("DownloadViewableStatusesFromHosts called with invalid networkType: " + networkTypeString) + } + + if (knownOrUnknown != "Known" && knownOrUnknown != "Unknown"){ + return errors.New("DownloadViewableStatusesFromHosts called with invalid knownOrUnknown: " + knownOrUnknown) + } + + //Outputs: + // -bool: Downloaded any statuses + // -error + downloadViewableStatusesFromHost := func(hostIdentityHash [16]byte)(bool, error){ + + exists, _, _, _, _, hostRawProfileMap, err := profileStorage.GetNewestUserProfile(hostIdentityHash, networkType) + if (err != nil) { return false, err } + if (exists == false){ + // Host profile was deleted, skip host + return false, nil + } + + hostIsHostingMateProfiles, hostMateRangeStart, hostMateRangeEnd, err := hostRanges.GetHostedIdentityHashRangeFromHostRawProfileMap(hostRawProfileMap, "Mate") + if (err != nil) { return false, err } + hostIsHostingHostProfiles, _, _, err := hostRanges.GetHostedIdentityHashRangeFromHostRawProfileMap(hostRawProfileMap, "Host") + if (err != nil) { return false, err } + hostIsHostingModeratorProfiles, _, _, err := hostRanges.GetHostedIdentityHashRangeFromHostRawProfileMap(hostRawProfileMap, "Moderator") + if (err != nil) { return false, err } + + checkIfHostIsHostingIdentityHash := func(identityHash [16]byte)(bool, error){ + + identityType, err := identity.GetIdentityTypeFromIdentityHash(identityHash) + if (err != nil) { return false, err } + if (identityType == "Mate"){ + + if (hostIsHostingMateProfiles == false){ + return false, nil + } + + isWithinTheirRange, err := byteRange.CheckIfIdentityHashIsWithinRange(hostMateRangeStart, hostMateRangeEnd, identityHash) + if (err != nil) { return false, err } + if (isWithinTheirRange == false){ + return false, nil + } + + return true, nil + } + if (identityType == "Host"){ + if (hostIsHostingHostProfiles == false){ + return false, nil + } + + return true, nil + } + // identityType == "Moderator" + if (hostIsHostingModeratorProfiles == false){ + return false, nil + } + + return true, nil + } + + relevantIdentityHashesToRetrieveList := make([][16]byte, 0) + + for _, identityHash := range identityHashesToRetrieveList{ + + isHostingIdentity, err := checkIfHostIsHostingIdentityHash(identityHash) + if (err != nil) { return false, err } + if (isHostingIdentity == false){ + continue + } + + statusIsKnown, _, queriedHostsList, err := trustedViewableStatus.GetTrustedIdentityIsViewableStatus(identityHash, networkType) + if (err != nil) { return false, err } + if (knownOrUnknown == "Known"){ + // We only want to retrieve known statuses. We dont need to avoid already queried hosts. + if (statusIsKnown == false){ + continue + } + } else { + // We only want to retrieve unknown statuses + if (statusIsKnown == true){ + continue + } + + // We must avoid hosts we have already queried + alreadyQueried := slices.Contains(queriedHostsList, hostIdentityHash) + if (alreadyQueried == true){ + continue + } + } + + relevantIdentityHashesToRetrieveList = append(relevantIdentityHashesToRetrieveList, identityHash) + } + + relevantProfileHashesToRetrieveList := make([][28]byte, 0) + + for profileHash, profileIdentityHash := range profileHashesToRetrieveMap{ + + isHostingIdentity, err := checkIfHostIsHostingIdentityHash(profileIdentityHash) + if (err != nil) { return false, err } + if (isHostingIdentity == false){ + continue + } + + statusIsKnown, _, queriedHostsList, err := trustedViewableStatus.GetTrustedProfileIsViewableStatus(profileHash) + if (err != nil) { return false, err } + if (knownOrUnknown == "Known"){ + // We only want to retrieve known statuses. We dont need to avoid already queried hosts. + if (statusIsKnown == false){ + continue + } + } else { + // We only want to retrieve unknown statuses + if (statusIsKnown == true){ + continue + } + + // We must avoid hosts we have already queried + alreadyQueried := slices.Contains(queriedHostsList, hostIdentityHash) + if (alreadyQueried == true){ + continue + } + } + + relevantProfileHashesToRetrieveList = append(relevantProfileHashesToRetrieveList, profileHash) + } + + if (len(relevantIdentityHashesToRetrieveList) == 0 && len(relevantProfileHashesToRetrieveList) == 0){ + // Nothing to retrieve from this host. Skip them. + return false, nil + } + + // Now we must split up our request to not exceed maximum size + // The requests/responses for identity hashes will always be smaller than profile hashes, for the same number of identities/profiles + // This is because profile hashes are larger than identity hashes + // We will split request so that we can fit the maximum amount of identity/profile hashes in all of the requests + + numberOfIdentityHashesToRetrieve := len(relevantIdentityHashesToRetrieveList) + numberOfProfileHashesToRetrieve := len(relevantProfileHashesToRetrieveList) + + maximumIdentityHashesInResponse := serverResponse.MaximumIdentitiesInResponse_GetIdentityViewableStatuses + maximumProfileHashesInResponse := serverResponse.MaximumProfilesInResponse_GetProfileViewableStatuses + + numberOfRequestsNeededForIdentityHashes := float64(numberOfIdentityHashesToRetrieve)/float64(maximumIdentityHashesInResponse) + numberOfRequestsNeededForProfileHashes := float64(numberOfProfileHashesToRetrieve)/float64(maximumProfileHashesInResponse) + + totalNumberOfRequestsNeeded, err := helpers.CeilFloat64ToInt64(numberOfRequestsNeededForIdentityHashes + numberOfRequestsNeededForProfileHashes) + if (err != nil) { return false, err } + + numberOfIdentityHashesPerRequest := int64(numberOfIdentityHashesToRetrieve)/totalNumberOfRequestsNeeded + numberOfProfileHashesPerRequest := int64(numberOfProfileHashesToRetrieve)/totalNumberOfRequestsNeeded + + identityHashesSublistsList, err := helpers.SplitListIntoSublists(relevantIdentityHashesToRetrieveList, int(numberOfIdentityHashesPerRequest)) + if (err != nil) { return false, err } + profileHashesSublistsList, err := helpers.SplitListIntoSublists(relevantProfileHashesToRetrieveList, int(numberOfProfileHashesPerRequest)) + if (err != nil) { return false, err } + + identityHashesSublistsListLength := len(identityHashesSublistsList) + profileHashesSublistsListLength := len(profileHashesSublistsList) + + getLongerListLength := func()int{ + if (identityHashesSublistsListLength > profileHashesSublistsListLength){ + return identityHashesSublistsListLength + } + return profileHashesSublistsListLength + } + + longerListLength := getLongerListLength() + + totalDownloadedStatuses := 0 + + hostProfileExists, connectionEstablished, connectionIdentifier, err := peerClient.EstablishNewConnectionToHost(allowClearnet, hostIdentityHash, networkType) + if (err != nil){ return false, err } + if (hostProfileExists == false){ + return false, nil + } + if (connectionEstablished == false){ + return false, nil + } + + defer peerClient.CloseConnection(connectionIdentifier) + + for i := 0; i < longerListLength; i++ { + + getIdentityHashesSublist := func()[][16]byte{ + if (i < identityHashesSublistsListLength){ + return identityHashesSublistsList[i] + } + emptyList := make([][16]byte, 0) + return emptyList + } + + getProfileHashesSublist := func()[][28]byte{ + if (i < profileHashesSublistsListLength){ + return profileHashesSublistsList[i] + } + emptyList := make([][28]byte, 0) + return emptyList + } + + identityHashesSublist := getIdentityHashesSublist() + profileHashesSublist := getProfileHashesSublist() + + downloadSuccessful, identityHashStatusesMap, profileHashStatusesMap, err := sendRequests.GetViewableStatusesFromHost(connectionIdentifier, hostIdentityHash, networkType, identityHashesSublist, profileHashesSublist) + if (err != nil) { return false, err } + if (downloadSuccessful == false){ + // This host has failed to respond. We will stop downloading from them. + return false, nil + } + + for identityHash, viewableStatus := range identityHashStatusesMap{ + + err := trustedViewableStatus.AddTrustedIdentityIsViewableStatus(identityHash, hostIdentityHash, networkType, viewableStatus) + if (err != nil) { return false, err } + + totalDownloadedStatuses += 1 + } + + for profileHash, viewableStatus := range profileHashStatusesMap{ + + err := trustedViewableStatus.AddTrustedProfileIsViewableStatus(profileHash, hostIdentityHash, viewableStatus) + if (err != nil) { return false, err } + + profileIdentityHash, exists := profileHashesToRetrieveMap[profileHash] + if (exists == false){ + return false, errors.New("profileHashesToRetrieveMap missing profileHash") + } + + if (viewableStatus == true){ + // The host will only indicate a viewable profile if they know that the identity is not banned + err := trustedViewableStatus.AddTrustedIdentityIsViewableStatus(profileIdentityHash, hostIdentityHash, networkType, true) + if (err != nil) { return false, err } + } + + totalDownloadedStatuses += 1 + } + } + + totalDownloadedStatusesString := helpers.ConvertIntToString(totalDownloadedStatuses) + + err = logger.AddLogEntry("Network", "Successfully downloaded " + totalDownloadedStatusesString + " viewable statuses from host.") + if (err != nil) { return false, err } + + return true, nil + } + + eligibleHostsList, err := eligibleHosts.GetEligibleHostsList(networkType) + if (err != nil) { return err } + + queriedHosts := 0 + + for _, hostIdentityHash := range eligibleHostsList{ + + successfulDownload, err := downloadViewableStatusesFromHost(hostIdentityHash) + if (err != nil) { return err } + if (successfulDownload == true){ + queriedHosts += 1 + } + if (queriedHosts >= numberOfHostsToQuery){ + return nil + } + } + + return nil +} + + +func DownloadModeratorAddressDepositsFromHosts(allowClearnet bool, networkType byte, cryptocurrencyName string, knownOrUnknown string, numberOfHostsToQuery int)error{ + + isValid := helpers.VerifyNetworkType(networkType) + if (isValid == false){ + networkTypeString := helpers.ConvertByteToString(networkType) + return errors.New("DownloadModeratorAddressDepositsFromHosts called with invalid networkType: " + networkTypeString) + } + + if (cryptocurrencyName != "Ethereum" && cryptocurrencyName != "Cardano"){ + return errors.New("DownloadModeratorAddressDepositsFromHosts called with invalid cryptocurrencyName: " + cryptocurrencyName) + } + + if (knownOrUnknown != "Unknown" && knownOrUnknown != "Known"){ + return errors.New("DownloadModeratorAddressDepositsFromHosts called with invalid knownOrUnknown: " + knownOrUnknown) + } + + //TODO: Check if local Ethereum/Cardano node is being used (either here or in function that calls this function) + //TODO: Check to see if less than 3 blockchain hosts exist, in which case, relax the required rule of 3 needed + + // We will first see which identity deposits are known/unknown + + //Outputs: + // -bool: Downloaded any deposits + // -error + downloadDepositsFromHost := func(hostIdentityHash [16]byte)(bool, error){ + + profileExists, _, _, _, _, hostRawProfileMap, err := profileStorage.GetNewestUserProfile(hostIdentityHash, networkType) + if (err != nil) { return false, err } + if (profileExists == false){ + return false, nil + } + + exists, isHostingBlockchain, err := readProfiles.GetFormattedProfileAttributeFromRawProfileMap(hostRawProfileMap, "Hosting" + cryptocurrencyName + "Blockchain") + if (err != nil) { return false, err } + if (exists == false || isHostingBlockchain != "Yes"){ + return false, nil + } + + localBlockchainNodeEnabled, addressesToUpdateList, err := moderatorScores.GetModeratorCryptoDepositAddressesList(cryptocurrencyName, knownOrUnknown) + if (err != nil){ return false, err } + if (localBlockchainNodeEnabled == true){ + // We must have enabled the node after our earlier check + // Skip all remaining hosts. + return false, nil + } + + if (len(addressesToUpdateList) == 0){ + // No addresses to update remain. + return false, nil + } + + //TODO: Add limit to size of below + + hostProfileExists, connectionEstablished, connectionIdentifier, err := peerClient.EstablishNewConnectionToHost(allowClearnet, hostIdentityHash, networkType) + if (err != nil){ return false, err } + if (hostProfileExists == false){ + return false, nil + } + if (connectionEstablished == false){ + return false, nil + } + + defer peerClient.CloseConnection(connectionIdentifier) + + successfulDownload, retrievedDepositObjectsList, addressesWithNoDepositsList, err := sendRequests.GetAddressDepositsFromHost(connectionIdentifier, hostIdentityHash, networkType, cryptocurrencyName, addressesToUpdateList) + if (err != nil) { return false, err } + if (successfulDownload == false){ + err := logger.AddLogEntry("Network", "Failed to retrieve address deposits from host.") + if (err != nil) { return false, err } + // Skip to next host + return false, nil + } + + err = trustedAddressDeposits.AddAddressDepositObjectsListToCache(hostIdentityHash, cryptocurrencyName, retrievedDepositObjectsList) + if (err != nil) { return false, err } + + err = trustedAddressDeposits.AddAddressesWithNoDepositsToCache(hostIdentityHash, cryptocurrencyName, addressesWithNoDepositsList) + if (err != nil) { return false, err } + + return true, nil + } + + // We query each host for the identity deposits we want + // After each host, we get the new known/unknown identities list to make + // sure we do not query deposits which were unknown but have become known + + hostsToQueryList, err := eligibleHosts.GetEligibleHostsList(networkType) + if (err != nil){ return err } + + hostsQueried := 0 + for _, hostIdentityHash := range hostsToQueryList{ + + downloadedAnyDeposits, err := downloadDepositsFromHost(hostIdentityHash) + if (err != nil) { return err } + if (downloadedAnyDeposits == true){ + hostsQueried += 1 + } + + if (hostsQueried >= numberOfHostsToQuery){ + break + } + } + + return nil +} + + + + diff --git a/internal/network/sendRequests/sendRequests.go b/internal/network/sendRequests/sendRequests.go new file mode 100644 index 0000000..d60b85e --- /dev/null +++ b/internal/network/sendRequests/sendRequests.go @@ -0,0 +1,1677 @@ + +// sendRequests provides functions for making requests to Seekia hosts +// Each function provides a way to send a server request to a host through an active connection +// This package is imported by queryHosts. +// queryHosts is a higher level package which provides functions to split requests up, to save retrieved data, and more + +package sendRequests + +//TODO: Add if request successful, remove host from unreachable list + +//TODO: Verify that input does not exceed maximum size for request of that type + +//TODO: Add ability to handle busy responses +// If host is busy, we will keep track of that in a new package and not try to recontact them for a while + +import "seekia/internal/byteRange" +import "seekia/internal/encoding" +import "seekia/internal/helpers" +import "seekia/internal/identity" +import "seekia/internal/messaging/readMessages" +import "seekia/internal/moderation/readReports" +import "seekia/internal/moderation/readReviews" +import "seekia/internal/network/maliciousHosts" +import "seekia/internal/network/mateCriteria" +import "seekia/internal/network/peerClient" +import "seekia/internal/network/serverRequest" +import "seekia/internal/network/serverResponse" +import "seekia/internal/parameters/readParameters" +import "seekia/internal/profiles/readProfiles" +import "seekia/internal/readContent" + +import "bytes" +import "errors" +import "slices" + +//Outputs: +// -bool: Download successsful +// -map[string]int64: Map of ParametersType -> Parameters broadcast time +// -error +func GetParametersInfoFromHost(connectionIdentifier [21]byte, hostIdentityHash [16]byte, networkType byte)(bool, map[string]int64, error){ + + connectionExists, connectionHostIdentityHash, connectionKey, connectionNetworkType, err := peerClient.GetConnectionMetadata(connectionIdentifier) + if (err != nil) { return false, nil, err } + if (connectionExists == false){ + return false, nil, nil + } + if (connectionHostIdentityHash != hostIdentityHash){ + return false, nil, errors.New("GetParametersInfoFromHost called with hostIdentityHash which does not match connection metadata.") + } + if (connectionNetworkType != networkType){ + return false, nil, errors.New("GetParametersInfoFromHost called with networkType which does not match connection metadata.") + } + + requestBytes, requestIdentifier, err := serverRequest.CreateServerRequest_GetParametersInfo(hostIdentityHash, connectionKey, networkType) + if (err != nil) { return false, nil, err } + + requestSuccessful, responseData, err := peerClient.SendRequestThroughConnection(connectionIdentifier, requestBytes) + if (err != nil) { return false, nil, err } + if (requestSuccessful == false){ + return false, nil, nil + } + + ableToRead, requestIdentifier_Received, hostIdentityHash_Received, parametersInfoMap, err := serverResponse.ReadServerResponse_GetParametersInfo(responseData, connectionKey) + if (err != nil) { return false, nil, err } + if (ableToRead == false){ + + // Host sent invalid response, must be malicious + + err := maliciousHosts.AddHostToMaliciousHostsList(hostIdentityHash) + if (err != nil){ return false, nil, err } + + return false, nil, nil + } + + // Now we check to make sure response is valid + + checkIfResponseIsValid := func()(bool, error){ + + if (requestIdentifier != requestIdentifier_Received){ + // Host sent invalid response, must be malicious + return false, nil + } + + if (hostIdentityHash != hostIdentityHash_Received){ + // Host sent invalid response, must be malicious + + return false, nil + } + + return true, nil + } + + responseIsValid, err := checkIfResponseIsValid() + if (err != nil) { return false, nil, err } + if (responseIsValid == false){ + + err := maliciousHosts.AddHostToMaliciousHostsList(hostIdentityHash) + if (err != nil){ return false, nil, err } + + return false, nil, nil + } + + return true, parametersInfoMap, nil +} + + +//Outputs: +// -bool: Download successful (could be unsuccessful because host keys not found, host is malicious, internet connection down, and more) +// -[][]byte: List of parameters +// -error +func GetParametersFromHost( + connectionIdentifier [21]byte, + hostIdentityHash [16]byte, + networkType byte, + parametersTypesToRetrieveList []string)(bool, [][]byte, error){ + + connectionExists, connectionHostIdentityHash, connectionKey, connectionNetworkType, err := peerClient.GetConnectionMetadata(connectionIdentifier) + if (err != nil) { return false, nil, err } + if (connectionExists == false){ + return false, nil, nil + } + if (connectionHostIdentityHash != hostIdentityHash){ + return false, nil, errors.New("GetParametersFromHost called with hostIdentityHash which does not match connection metadata.") + } + if (connectionNetworkType != networkType){ + return false, nil, errors.New("GetParametersFromHost called with networkType which does not match connection metadata.") + } + + requestBytes, requestIdentifier, err := serverRequest.CreateServerRequest_GetParameters(hostIdentityHash, connectionKey, networkType, parametersTypesToRetrieveList) + if (err != nil) { return false, nil, err } + + requestSuccessful, responseData, err := peerClient.SendRequestThroughConnection(connectionIdentifier, requestBytes) + if (err != nil) { return false, nil, err } + if (requestSuccessful == false){ + return false, nil, nil + } + + ableToRead, requestIdentifier_Received, hostIdentityHash_Received, parametersList, err := serverResponse.ReadServerResponse_GetParameters(responseData, connectionKey) + if (err != nil) { return false, nil, err } + if (ableToRead == false){ + + // Peer sent invalid response, must be malicious + + err := maliciousHosts.AddHostToMaliciousHostsList(hostIdentityHash) + if (err != nil) { return false, nil, err } + + return false, nil, nil + } + + // Now we check to make sure response is valid based on input request information + + checkIfResponseIsValid := func()(bool, error){ + + if (requestIdentifier != requestIdentifier_Received){ + // Host sent invalid response, must be malicious + return false, nil + } + + if (hostIdentityHash != hostIdentityHash_Received){ + + return false, nil + } + + for _, parametersBytes := range parametersList{ + + ableToRead, _, parametersNetworkType, _, parametersType, _, _, err := readParameters.ReadParameters(false, parametersBytes) + if (err != nil) { return false, err } + if (ableToRead == false){ + return false, errors.New("ReadServerResponse_GetParameters not verifying network parameters") + } + + if (parametersNetworkType != networkType){ + // We did not request parameters for this networkType. + return false, nil + } + + isExpected := slices.Contains(parametersTypesToRetrieveList, parametersType) + if (isExpected == false){ + // We did not request this parameters type. Response is malformed. + return false, nil + } + } + + return true, nil + } + + responseIsValid, err := checkIfResponseIsValid() + if (err != nil) { return false, nil, err } + if (responseIsValid == false){ + // Peer sent invalid response, must be malicious + + err := maliciousHosts.AddHostToMaliciousHostsList(hostIdentityHash) + if (err != nil) { return false, nil, err } + + return false, nil, nil + } + + return true, parametersList, nil +} + + +//Outputs: +// -bool: Download successful (could be unsuccessful because host keys not found, host is malicious, internet connection down, and more) +// -[]serverResponse.ProfileInfoStruct: Profiles info map list +// -error +func GetProfilesInfoFromHost( + connectionIdentifier [21]byte, + hostIdentityHash [16]byte, + networkType byte, + profileTypeToRetrieve string, + rangeStart [16]byte, + rangeEnd [16]byte, + identityHashesList [][16]byte, + criteria []byte, + getNewestProfilesOnly bool, + getViewableProfilesOnly bool)(bool, []serverResponse.ProfileInfoStruct, error){ + + connectionExists, connectionHostIdentityHash, connectionKey, connectionNetworkType, err := peerClient.GetConnectionMetadata(connectionIdentifier) + if (err != nil) { return false, nil, err } + if (connectionExists == false){ + return false, nil, nil + } + if (connectionHostIdentityHash != hostIdentityHash){ + return false, nil, errors.New("GetProfilesInfoFromHost called with hostIdentityHash which does not match connection metadata.") + } + if (connectionNetworkType != networkType){ + return false, nil, errors.New("GetProfilesInfoFromHost called with networkType which does not match connection metadata.") + } + + acceptableProfileVersionsList := []int{1} + + requestBytes, requestIdentifier, err := serverRequest.CreateServerRequest_GetProfilesInfo(hostIdentityHash, connectionKey, networkType, acceptableProfileVersionsList, profileTypeToRetrieve, rangeStart, rangeEnd, identityHashesList, criteria, getNewestProfilesOnly, getViewableProfilesOnly) + if (err != nil) { return false, nil, err } + + requestSuccessful, responseData, err := peerClient.SendRequestThroughConnection(connectionIdentifier, requestBytes) + if (err != nil) { return false, nil, err } + if (requestSuccessful == false){ + return false, nil, nil + } + + ableToRead, requestIdentifier_Received, hostIdentityHash_Received, profileInfoObjectsList, err := serverResponse.ReadServerResponse_GetProfilesInfo(responseData, connectionKey) + if (err != nil) { return false, nil, err } + if (ableToRead == false){ + + // Peer sent invalid response, must be malicious + + err := maliciousHosts.AddHostToMaliciousHostsList(hostIdentityHash) + if (err != nil){ return false, nil, err } + + return false, nil, nil + } + + // Now we check to make sure response is valid based on input request information + + checkIfResponseIsValid := func()(bool, error){ + + if (requestIdentifier != requestIdentifier_Received){ + // Host sent invalid response, must be malicious + return false, nil + } + + if (hostIdentityHash != hostIdentityHash_Received){ + // Peer sent invalid response, must be malicious + + return false, nil + } + + // Now we verify received profile identity hashes fulfill request filters + + // We use map to make sure that only 1 profile hash per identity is returned (if getNewestProfilesOnly == true) + // Map structure: Identity Hash -> Nothing + identitiesMap := make(map[[16]byte]struct{}) + + for _, profileInfoObject := range profileInfoObjectsList{ + + receivedProfileAuthor := profileInfoObject.ProfileAuthor + + identityType, err := identity.GetIdentityTypeFromIdentityHash(receivedProfileAuthor) + if (err != nil){ + receivedProfileAuthorHex := encoding.EncodeBytesToHexString(receivedProfileAuthor[:]) + return false, errors.New("ReadServerResponse_GetProfilesInfo not verifying profileInfoObject receivedProfileAuthor: " + receivedProfileAuthorHex) + } + + if (identityType != profileTypeToRetrieve){ + return false, nil + } + + if (len(identityHashesList) != 0){ + identityIsInRequestedList := slices.Contains(identityHashesList, receivedProfileAuthor) + if (identityIsInRequestedList == false){ + return false, nil + } + } + + isWithinRange, err := byteRange.CheckIfIdentityHashIsWithinRange(rangeStart, rangeEnd, receivedProfileAuthor) + if (err != nil) { return false, err } + if (isWithinRange == false){ + return false, nil + } + + if (getNewestProfilesOnly == true){ + + _, exists := identitiesMap[receivedProfileAuthor] + if (exists == true){ + // We received two profiles with the same author in a getNewestProfilesOnly response. + return false, nil + } + + identitiesMap[receivedProfileAuthor] = struct{}{} + } + } + + return true, nil + } + + responseIsValid, err := checkIfResponseIsValid() + if (err != nil) { return false, nil, err } + if (responseIsValid == false){ + + err := maliciousHosts.AddHostToMaliciousHostsList(hostIdentityHash) + if (err != nil){ return false, nil, err } + + return false, nil, nil + } + + return true, profileInfoObjectsList, nil +} + + +//Outputs: +// -bool: Download successful (could be unsuccessful because host keys not found, host is malicious, internet connection down, and more) +// -[][]byte: List of profiles +// -error +func GetProfilesFromHost( + connectionIdentifier [21]byte, + hostIdentityHash [16]byte, + networkType byte, + profileTypeToRetrieve string, + profileHashesList [][28]byte, + expectedProfileIdentityHashesMap map[[28]byte][16]byte, // Profile Hash -> Profile Identity hash + expectedProfileBroadcastTimesMap map[[28]byte]int64, // Profile Hash -> Profile Broadcast Time + expectedCriteria []byte)(bool, [][]byte, error){ + + connectionExists, connectionHostIdentityHash, connectionKey, connectionNetworkType, err := peerClient.GetConnectionMetadata(connectionIdentifier) + if (err != nil) { return false, nil, err } + if (connectionExists == false){ + return false, nil, nil + } + if (connectionHostIdentityHash != hostIdentityHash){ + return false, nil, errors.New("GetProfilesFromHost called with hostIdentityHash which does not match connection metadata.") + } + if (connectionNetworkType != networkType){ + return false, nil, errors.New("GetProfilesFromHost called with networkType which does not match connection metadata.") + } + + requestBytes, requestIdentifier, err := serverRequest.CreateServerRequest_GetProfiles(hostIdentityHash, connectionKey, networkType, profileTypeToRetrieve, profileHashesList) + if (err != nil) { return false, nil, err } + + requestSuccessful, responseData, err := peerClient.SendRequestThroughConnection(connectionIdentifier, requestBytes) + if (err != nil) { return false, nil, err } + if (requestSuccessful == false){ + return false, nil, nil + } + + ableToRead, requestIdentifier_Received, hostIdentityHash_Received, profilesList, err := serverResponse.ReadServerResponse_GetProfiles(responseData, connectionKey) + if (err != nil) { return false, nil, err } + if (ableToRead == false){ + + // Peer sent invalid response, must be malicious + + err := maliciousHosts.AddHostToMaliciousHostsList(hostIdentityHash) + if (err != nil) { return false, nil, err } + + return false, nil, nil + } + + // Now we check to make sure response is valid based on input request information + + checkIfResponseIsValid := func()(bool, error){ + + if (requestIdentifier != requestIdentifier_Received){ + // Host sent invalid response, must be malicious + return false, nil + } + + if (hostIdentityHash != hostIdentityHash_Received){ + + return false, nil + } + + if (len(profilesList) > len(profileHashesList)){ + return false, nil + } + + for _, profileBytes := range profilesList{ + + ableToRead, profileHash, profileVersion, profileNetworkType, profileIdentityHash, profileBroadcastTime, _, rawProfileMap, err := readProfiles.ReadProfileAndHash(false, profileBytes) + if (err != nil) { return false, err } + if (ableToRead == false){ + return false, errors.New("ReadServerResponse_GetProfiles not verifying received profile is valid.") + } + profileHashIsExpected := slices.Contains(profileHashesList, profileHash) + if (profileHashIsExpected == false){ + return false, nil + } + if (profileNetworkType != networkType){ + // The host must have sent us a different networkType profile when we requested profilesInfo + return false, nil + } + + expectedProfileIdentityHash, profileExists := expectedProfileIdentityHashesMap[profileHash] + if (profileExists == false){ + return false, errors.New("ExpectedProfileIdentityHashesMap missing entry for profile contained in ProfileHashesList") + } + if (profileIdentityHash != expectedProfileIdentityHash){ + return false, nil + } + + expectedProfileBroadcastTime, exists := expectedProfileBroadcastTimesMap[profileHash] + if (exists == false){ + return false, errors.New("expectedProfileBroadcastTimesMap missing entry for profile contained in ProfileHashesList") + } + if (profileBroadcastTime != expectedProfileBroadcastTime){ + // Profile is does not fulfill the expected BroadcastTime + return false, nil + } + + if (expectedCriteria != nil){ + + criteriaIsValid, fulfillsCriteria, err := mateCriteria.CheckIfMateProfileFulfillsCriteria(false, profileVersion, rawProfileMap, expectedCriteria) + if (err != nil) { return false, err } + if (criteriaIsValid == false){ + return false, errors.New("GetProfilesFromHost called with invalid expectedCriteria") + } + if (fulfillsCriteria == false){ + return false, nil + } + } + } + + return true, nil + } + + responseIsValid, err := checkIfResponseIsValid() + if (err != nil) { return false, nil, err } + if (responseIsValid == false){ + // Peer sent invalid response, must be malicious + + err := maliciousHosts.AddHostToMaliciousHostsList(hostIdentityHash) + if (err != nil) { return false, nil, err } + + return false, nil, nil + } + + return true, profilesList, nil +} + + +//Outputs: +// -bool: Download successful (could be unsuccessful because host keys not found, host is malicious, internet connection down, and more) +// -[][26]byte: Received message hashes list +// -error +func GetMessageHashesListFromHost( + connectionIdentifier [21]byte, + hostIdentityHash [16]byte, + networkType byte, + rangeStart [10]byte, + rangeEnd [10]byte, + inboxesToRetrieveList [][10]byte, + getViewableMessagesOnly bool, + getDecryptableMessagesOnly bool)(bool, [][26]byte, error){ + + connectionExists, connectionHostIdentityHash, connectionKey, connectionNetworkType, err := peerClient.GetConnectionMetadata(connectionIdentifier) + if (err != nil) { return false, nil, err } + if (connectionExists == false){ + return false, nil, nil + } + if (connectionHostIdentityHash != hostIdentityHash){ + return false, nil, errors.New("GetMessageHashesListFromHost called with hostIdentityHash which does not match connection metadata.") + } + if (connectionNetworkType != networkType){ + return false, nil, errors.New("GetMessageHashesListFromHost called with networkType which does not match connection metadata.") + } + + acceptableMessageVersionsList := []int{1} + + requestBytes, requestIdentifier, err := serverRequest.CreateServerRequest_GetMessageHashesList(hostIdentityHash, connectionKey, networkType, acceptableMessageVersionsList, rangeStart, rangeEnd, inboxesToRetrieveList, getViewableMessagesOnly, getDecryptableMessagesOnly) + if (err != nil) { return false, nil, err } + + requestSuccessful, responseData, err := peerClient.SendRequestThroughConnection(connectionIdentifier, requestBytes) + if (err != nil) { return false, nil, err } + if (requestSuccessful == false){ + return false, nil, nil + } + + ableToRead, requestIdentifier_Received, hostIdentityHash_Received, messageHashesList_Received, err := serverResponse.ReadServerResponse_GetMessageHashesList(responseData, connectionKey) + if (err != nil) { return false, nil, err } + if (ableToRead == false){ + + // Peer sent invalid response, must be malicious + + err := maliciousHosts.AddHostToMaliciousHostsList(hostIdentityHash) + if (err != nil){ return false, nil, err } + + return false, nil, nil + } + + // Now we check to make sure response is valid based on input request information + + checkIfResponseIsValid := func()(bool, error){ + + if (requestIdentifier != requestIdentifier_Received){ + // Host sent invalid response, must be malicious + return false, nil + } + + if (hostIdentityHash != hostIdentityHash_Received){ + // Peer sent invalid response, must be malicious + + return false, nil + } + + return true, nil + } + + responseIsValid, err := checkIfResponseIsValid() + if (err != nil) { return false, nil, err } + if (responseIsValid == false){ + + err := maliciousHosts.AddHostToMaliciousHostsList(hostIdentityHash) + if (err != nil){ return false, nil, err } + + return false, nil, nil + } + + return true, messageHashesList_Received, nil +} + + + +//Outputs: +// -bool: Download successful (could be unsuccessful because host keys not found, host is malicious, internet connection down, and more) +// -[][]byte: List of messages +// -error +func GetMessagesFromHost( + connectionIdentifier [21]byte, + hostIdentityHash [16]byte, + networkType byte, + messageHashesList [][26]byte, + expectedInboxRangeStart [10]byte, + expectedInboxRangeEnd [10]byte, + expectedInboxesList [][10]byte)(bool, [][]byte, error){ + + connectionExists, connectionHostIdentityHash, connectionKey, connectionNetworkType, err := peerClient.GetConnectionMetadata(connectionIdentifier) + if (err != nil) { return false, nil, err } + if (connectionExists == false){ + return false, nil, nil + } + if (connectionHostIdentityHash != hostIdentityHash){ + return false, nil, errors.New("GetMessagesFromHost called with hostIdentityHash which does not match connection metadata.") + } + if (connectionNetworkType != networkType){ + return false, nil, errors.New("GetMessagesFromHost called with networkType which does not match connection metadata.") + } + + requestBytes, requestIdentifier, err := serverRequest.CreateServerRequest_GetMessages(hostIdentityHash, connectionKey, networkType, messageHashesList) + if (err != nil) { return false, nil, err } + + requestSuccessful, responseData, err := peerClient.SendRequestThroughConnection(connectionIdentifier, requestBytes) + if (err != nil) { return false, nil, err } + if (requestSuccessful == false){ + return false, nil, nil + } + + ableToRead, requestIdentifier_Received, hostIdentityHash_Received, messagesList, err := serverResponse.ReadServerResponse_GetMessages(responseData, connectionKey) + if (err != nil) { return false, nil, err } + if (ableToRead == false){ + + // Peer sent invalid response, must be malicious + + err := maliciousHosts.AddHostToMaliciousHostsList(hostIdentityHash) + if (err != nil) { return false, nil, err } + + return false, nil, nil + } + + // Now we check to make sure response is valid based on input request information + + checkIfResponseIsValid := func()(bool, error){ + + if (requestIdentifier != requestIdentifier_Received){ + // Host sent invalid response, must be malicious + return false, nil + } + + if (hostIdentityHash != hostIdentityHash_Received){ + + return false, nil + } + + for _, messageBytes := range messagesList{ + + ableToRead, messageHash, _, messageNetworkType, messageInbox, _, _, _, _, _, _, err := readMessages.ReadChatMessagePublicDataAndHash(false, messageBytes) + if (err != nil) { return false, err } + if (ableToRead == false){ + return false, errors.New("ReadServerResponse_GetMessages not verifying each message.") + } + + isExpected := slices.Contains(messageHashesList, messageHash) + if (isExpected == false){ + return false, nil + } + if (messageNetworkType != networkType){ + // Host must have sent us a different networkType messageHash when we got messageHashesList response + return false, nil + } + + if (len(expectedInboxesList) != 0){ + + messageInboxIsExpected := slices.Contains(expectedInboxesList, messageInbox) + if (messageInboxIsExpected == false){ + return false, nil + } + } else { + + inboxIsWithinRange, err := byteRange.CheckIfInboxIsWithinRange(expectedInboxRangeStart, expectedInboxRangeEnd, messageInbox) + if (err != nil) { return false, err } + if (inboxIsWithinRange == false){ + return false, nil + } + } + } + + return true, nil + } + + responseIsValid, err := checkIfResponseIsValid() + if (err != nil) { return false, nil, err } + if (responseIsValid == false){ + // Peer sent invalid response, must be malicious + + err := maliciousHosts.AddHostToMaliciousHostsList(hostIdentityHash) + if (err != nil) { return false, nil, err } + + return false, nil, nil + } + + return true, messagesList, nil +} + + +//Outputs: +// -bool: Download successful (could be unsuccessful because host keys not found, host is malicious, internet connection down, and more) +// -map[[29]byte][]byte: Received reviews info map (Review Hash -> Reviewed Hash) +// -error +func GetIdentityReviewsInfoFromHost( + connectionIdentifier [21]byte, + hostIdentityHash [16]byte, + networkType byte, + identityTypeToRetrieve string, + rangeStart [16]byte, + rangeEnd [16]byte, + reviewedIdentityHashesList [][16]byte, + reviewersList [][16]byte)(bool, map[[29]byte][]byte, error){ + + connectionExists, connectionHostIdentityHash, connectionKey, connectionNetworkType, err := peerClient.GetConnectionMetadata(connectionIdentifier) + if (err != nil) { return false, nil, err } + if (connectionExists == false){ + return false, nil, nil + } + if (connectionHostIdentityHash != hostIdentityHash){ + return false, nil, errors.New("GetIdentityReviewsInfoFromHost called with hostIdentityHash which does not match connection metadata.") + } + if (connectionNetworkType != networkType){ + return false, nil, errors.New("GetIdentityReviewsInfoFromHost called with networkType which does not match connection metadata.") + } + + acceptableReviewVersionList := []int{1} + + requestBytes, requestIdentifier, err := serverRequest.CreateServerRequest_GetIdentityReviewsInfo(hostIdentityHash, connectionKey, networkType, acceptableReviewVersionList, identityTypeToRetrieve, rangeStart, rangeEnd, reviewedIdentityHashesList, reviewersList) + if (err != nil) { return false, nil, err } + + requestSuccessful, responseBytes, err := peerClient.SendRequestThroughConnection(connectionIdentifier, requestBytes) + if (err != nil) { return false, nil, err } + if (requestSuccessful == false){ + return false, nil, nil + } + + ableToRead, requestIdentifier_Received, hostIdentityHash_Received, reviewsInfoMap, err := serverResponse.ReadServerResponse_GetIdentityReviewsInfo(responseBytes, connectionKey) + if (err != nil) { return false, nil, err } + if (ableToRead == false){ + + // Peer sent invalid response, must be malicious + + err := maliciousHosts.AddHostToMaliciousHostsList(hostIdentityHash) + if (err != nil){ return false, nil, err } + + return false, nil, nil + } + + // Now we check to make sure response is valid based on input request information + + checkIfResponseIsValid := func()(bool, error){ + + if (requestIdentifier != requestIdentifier_Received){ + // Host sent invalid response, must be malicious + return false, nil + } + + if (hostIdentityHash != hostIdentityHash_Received){ + // Peer sent invalid response, must be malicious + + return false, nil + } + + // Now we verify received reviews info map fulfills request filters + + for _, reviewedHash := range reviewsInfoMap{ + + // reviewedHash is either Profile hash or identity hash or attribute hash + + reviewedHashType, err := helpers.GetReviewedTypeFromReviewedHash(reviewedHash) + if (err != nil) { + reviewedHashHex := encoding.EncodeBytesToHexString(reviewedHash) + return false, errors.New("ReadServerResponse_GetReviewsInfo not verifying reviewsInfoMap reviewedHash: " + reviewedHashHex) + } + + if (reviewedHashType != "Identity" && reviewedHashType != "Profile" && reviewedHashType != "Attribute"){ + // Must be a message hash. Host is malicious. + reviewedHashHex := encoding.EncodeBytesToHexString(reviewedHash) + return false, errors.New("ReadServerResponse_GetReviewsInfo not verifying reviewsInfoMap reviewedHash: " + reviewedHashHex) + } + + if (reviewedHashType == "Identity"){ + + if (len(reviewedHash) != 16){ + reviewedHashHex := encoding.EncodeBytesToHexString(reviewedHash) + return false, errors.New("GetReviewedTypeFromReviewedHash returning Identity for different length reviewedHash: " + reviewedHashHex) + } + + reviewedIdentityHash := [16]byte(reviewedHash) + + if (len(reviewedIdentityHashesList) == 0){ + + // We only need to verify identityType if a reviewedHashesList was not requested + + identityType, err := identity.GetIdentityTypeFromIdentityHash(reviewedIdentityHash) + if (err != nil){ + reviewedIdentityHashHex := encoding.EncodeBytesToHexString(reviewedIdentityHash[:]) + return false, errors.New("GetReviewedTypeFromReviewedHash fails to verify identity hash: " + reviewedIdentityHashHex) + } + + if (identityType != identityTypeToRetrieve){ + return false, nil + } + } + + isWithinRange, err := byteRange.CheckIfIdentityHashIsWithinRange(rangeStart, rangeEnd, reviewedIdentityHash) + if (err != nil) { return false, err } + if (isWithinRange == false){ + return false, nil + } + + if (len(reviewedIdentityHashesList) != 0){ + + identityHashIsInRequestedList := slices.Contains(reviewedIdentityHashesList, reviewedIdentityHash) + if (identityHashIsInRequestedList == false){ + return false, nil + } + } + continue + + } else if (reviewedHashType == "Profile"){ + + if (len(reviewedHash) != 28){ + reviewedHashHex := encoding.EncodeBytesToHexString(reviewedHash) + return false, errors.New("GetReviewedTypeFromReviewedHash returning Profile for different length reviewedHash: " + reviewedHashHex) + } + + reviewedProfileHash := [28]byte(reviewedHash) + + if (len(reviewedIdentityHashesList) == 0){ + + // We only need to verify profileType if a reviewedIdentityHashesList was not requested + + profileType, isDisabled, err := readProfiles.ReadProfileHashMetadata(reviewedProfileHash) + if (err != nil) { + reviewedProfileHashHex := encoding.EncodeBytesToHexString(reviewedProfileHash[:]) + return false, errors.New("ReadServerResponse_GetReviewsInfo not verifying reviewsInfoMap reviewedHash: " + reviewedProfileHashHex) + } + if (isDisabled == true){ + return false, errors.New("ReadServerResponse_GetReviewsInfo not verifying reviewsInfoMap reviewedHash: Profile hash is disabled.") + } + if (profileType != identityTypeToRetrieve){ + return false, nil + } + } + } + } + + return true, nil + } + + responseIsValid, err := checkIfResponseIsValid() + if (err != nil) { return false, nil, err } + if (responseIsValid == false){ + + err := maliciousHosts.AddHostToMaliciousHostsList(hostIdentityHash) + if (err != nil){ return false, nil, err } + + return false, nil, nil + } + + return true, reviewsInfoMap, nil +} + + +//Outputs: +// -bool: Download successful (could be unsuccessful because host keys not found, host is malicious, internet connection down, and more) +// -map[[29]byte][26]byte: Received reviews info map (Review Hash -> Reviewed Message Hash) +// -error +func GetMessageReviewsInfoFromHost( + connectionIdentifier [21]byte, + hostIdentityHash [16]byte, + networkType byte, + rangeStart [10]byte, + rangeEnd [10]byte, + reviewedMessageHashesList [][26]byte, + reviewersList [][16]byte)(bool, map[[29]byte][26]byte, error){ + + connectionExists, connectionHostIdentityHash, connectionKey, connectionNetworkType, err := peerClient.GetConnectionMetadata(connectionIdentifier) + if (err != nil) { return false, nil, err } + if (connectionExists == false){ + return false, nil, nil + } + if (connectionHostIdentityHash != hostIdentityHash){ + return false, nil, errors.New("GetMessageReviewsInfoFromHost called with hostIdentityHash which does not match connection metadata.") + } + if (connectionNetworkType != networkType){ + return false, nil, errors.New("GetMessageReviewsInfoFromHost called with networkType which does not match connection metadata.") + } + + acceptableReviewVersionList := []int{1} + + requestBytes, requestIdentifier, err := serverRequest.CreateServerRequest_GetMessageReviewsInfo(hostIdentityHash, connectionKey, networkType, acceptableReviewVersionList, rangeStart, rangeEnd, reviewedMessageHashesList, reviewersList) + if (err != nil) { return false, nil, err } + + requestSuccessful, responseBytes, err := peerClient.SendRequestThroughConnection(connectionIdentifier, requestBytes) + if (err != nil) { return false, nil, err } + if (requestSuccessful == false){ + return false, nil, nil + } + + ableToRead, requestIdentifier_Received, hostIdentityHash_Received, reviewsInfoMap, err := serverResponse.ReadServerResponse_GetMessageReviewsInfo(responseBytes, connectionKey) + if (err != nil) { return false, nil, err } + if (ableToRead == false){ + + // Peer sent invalid response, must be malicious + + err := maliciousHosts.AddHostToMaliciousHostsList(hostIdentityHash) + if (err != nil){ return false, nil, err } + + return false, nil, nil + } + + // Now we check to make sure response is valid based on input request information + + checkIfResponseIsValid := func()(bool, error){ + + if (requestIdentifier != requestIdentifier_Received){ + // Host sent invalid response, must be malicious + return false, nil + } + + if (hostIdentityHash != hostIdentityHash_Received){ + // Peer sent invalid response, must be malicious + + return false, nil + } + + // Now we verify received reviews info map fulfills request filters + + for _, reviewedHash := range reviewsInfoMap{ + + // reviewedHash is a message hash + + if (len(reviewedMessageHashesList) != 0){ + + messageHashIsInRequestedList := slices.Contains(reviewedMessageHashesList, reviewedHash) + if (messageHashIsInRequestedList == false){ + return false, nil + } + } + } + + return true, nil + } + + responseIsValid, err := checkIfResponseIsValid() + if (err != nil) { return false, nil, err } + if (responseIsValid == false){ + + err := maliciousHosts.AddHostToMaliciousHostsList(hostIdentityHash) + if (err != nil){ return false, nil, err } + + return false, nil, nil + } + + return true, reviewsInfoMap, nil +} + + +//Outputs: +// -bool: Download successful (could be unsuccessful because host keys not found, host is malicious, internet connection down, and more) +// -[][]byte: Retrieved list of reviews +// -error +func GetReviewsFromHost( + connectionIdentifier [21]byte, + hostIdentityHash [16]byte, + networkType byte, + reviewHashesList [][29]byte, + expectedReviewsInfoMap map[[29]byte][]byte, // Review Hash -> Reviewed Hash + expectedReviewersList [][16]byte)(bool, [][]byte, error){ + + connectionExists, connectionHostIdentityHash, connectionKey, connectionNetworkType, err := peerClient.GetConnectionMetadata(connectionIdentifier) + if (err != nil) { return false, nil, err } + if (connectionExists == false){ + return false, nil, nil + } + if (connectionHostIdentityHash != hostIdentityHash){ + return false, nil, errors.New("GetReviewsFromHost called with hostIdentityHash which does not match connection metadata.") + } + if (connectionNetworkType != networkType){ + return false, nil, errors.New("GetReviewsFromHost called with networkType which does not match connection metadata.") + } + + requestBytes, requestIdentifier, err := serverRequest.CreateServerRequest_GetReviews(hostIdentityHash, connectionKey, networkType, reviewHashesList) + if (err != nil) { return false, nil, err } + + requestSuccessful, responseData, err := peerClient.SendRequestThroughConnection(connectionIdentifier, requestBytes) + if (err != nil) { return false, nil, err } + if (requestSuccessful == false){ + return false, nil, nil + } + + ableToRead, requestIdentifier_Received, hostIdentityHash_Received, reviewsList, err := serverResponse.ReadServerResponse_GetReviews(responseData, connectionKey) + if (err != nil) { return false, nil, err } + if (ableToRead == false){ + + // Peer sent invalid response, must be malicious + + err := maliciousHosts.AddHostToMaliciousHostsList(hostIdentityHash) + if (err != nil) { return false, nil, err } + + return false, nil, nil + } + + // Now we check to make sure response is valid based on input request information + + checkIfResponseIsValid := func()(bool, error){ + + if (requestIdentifier != requestIdentifier_Received){ + // Host sent invalid response, must be malicious + return false, nil + } + + if (hostIdentityHash != hostIdentityHash_Received){ + + return false, nil + } + + for _, reviewBytes := range reviewsList{ + + ableToRead, reviewHash, _, reviewNetworkType, authorIdentityHash, _, _, reviewReviewedHash, _, _, err := readReviews.ReadReviewAndHash(false, reviewBytes) + if (err != nil) { return false, err } + if (ableToRead == false){ + return false, errors.New("ReadServerResponse_GetReviews not verifying each review") + } + if (reviewNetworkType != networkType){ + return false, nil + } + + if (len(expectedReviewersList) != 0){ + + isExpectedReviewer := slices.Contains(expectedReviewersList, authorIdentityHash) + if (isExpectedReviewer == false){ + return false, nil + } + } + + expectedReviewedHash, exists := expectedReviewsInfoMap[reviewHash] + if (exists == false) { + // We received an unexpected reviewHash + return false, nil + } + + areEqual := bytes.Equal(reviewReviewedHash, expectedReviewedHash) + if (areEqual == false){ + // We received an unexpected reviewedHash for expected reviewHash + return false, nil + } + } + + return true, nil + } + + responseIsValid, err := checkIfResponseIsValid() + if (err != nil) { return false, nil, err } + if (responseIsValid == false){ + // Peer sent invalid response, must be malicious + + err := maliciousHosts.AddHostToMaliciousHostsList(hostIdentityHash) + if (err != nil) { return false, nil, err } + + return false, nil, nil + } + + return true, reviewsList, nil +} + + +//Outputs: +// -bool: Download successful (could be unsuccessful because host keys not found, host is malicious, internet connection down, and more) +// -map[[30]byte][]byte: Reports info map (Report Hash -> Reported Hash) +// -error +func GetIdentityReportsInfoFromHost( + connectionIdentifier [21]byte, + hostIdentityHash [16]byte, + networkType byte, + identityTypeToRetrieve string, + rangeStart [16]byte, + rangeEnd [16]byte, + reportedIdentityHashesList [][16]byte)(bool, map[[30]byte][]byte, error){ + + connectionExists, connectionHostIdentityHash, connectionKey, connectionNetworkType, err := peerClient.GetConnectionMetadata(connectionIdentifier) + if (err != nil) { return false, nil, err } + if (connectionExists == false){ + return false, nil, nil + } + if (connectionHostIdentityHash != hostIdentityHash){ + return false, nil, errors.New("GetIdentityReportsInfoFromHost called with hostIdentityHash which does not match connection metadata.") + } + if (connectionNetworkType != networkType){ + return false, nil, errors.New("GetIdentityReportsInfoFromHost called with networkType which does not match connection metadata.") + } + + acceptableReportVersionsList := []int{1} + + requestBytes, requestIdentifier, err := serverRequest.CreateServerRequest_GetIdentityReportsInfo(hostIdentityHash, connectionKey, networkType, acceptableReportVersionsList, identityTypeToRetrieve, rangeStart, rangeEnd, reportedIdentityHashesList) + if (err != nil) { return false, nil, err } + + requestSuccessful, responseBytes, err := peerClient.SendRequestThroughConnection(connectionIdentifier, requestBytes) + if (err != nil) { return false, nil, err } + if (requestSuccessful == false){ + return false, nil, nil + } + + ableToRead, requestIdentifier_Received, hostIdentityHash_Received, reportsInfoMap, err := serverResponse.ReadServerResponse_GetIdentityReportsInfo(responseBytes, connectionKey) + if (err != nil) { return false, nil, err } + if (ableToRead == false){ + + // Peer sent invalid response, must be malicious + + err := maliciousHosts.AddHostToMaliciousHostsList(hostIdentityHash) + if (err != nil){ return false, nil, err } + + return false, nil, nil + } + + // Now we check to make sure response is valid based on input request information + + checkIfResponseIsValid := func()(bool, error){ + + if (requestIdentifier != requestIdentifier_Received){ + // Host sent invalid response, must be malicious + return false, nil + } + + if (hostIdentityHash != hostIdentityHash_Received){ + // Peer sent invalid response, must be malicious + + return false, nil + } + + // Now we verify received reports info map fulfills request filters + + for _, reportedHash := range reportsInfoMap{ + + // reportedHash is either Profile hash or identity hash or Attribute hash + + reportedType, err := helpers.GetReportedTypeFromReportedHash(reportedHash) + if (err != nil){ + reportedHashHex := encoding.EncodeBytesToHexString(reportedHash) + return false, errors.New("ReadServerResponse_GetReportsInfo not verifying reportedHash: " + reportedHashHex) + } + + if (reportedType != "Identity" && reportedType != "Profile" && reportedType != "Attribute"){ + reportedHashHex := encoding.EncodeBytesToHexString(reportedHash) + return false, errors.New("ReadServerResponse_GetReportsInfo not verifying reportedHash: " + reportedHashHex) + } + + if (reportedType == "Identity"){ + + if (len(reportedHash) != 16){ + reportedHashHex := encoding.EncodeBytesToHexString(reportedHash) + return false, errors.New("GetReportedTypeFromReportedHash returning Identity for different length reportedHash: " + reportedHashHex) + } + + reportedIdentityHash := [16]byte(reportedHash) + + if (len(reportedIdentityHashesList) == 0){ + + identityType, err := identity.GetIdentityTypeFromIdentityHash(reportedIdentityHash) + if (err != nil){ + reportedIdentityHashHex := encoding.EncodeBytesToHexString(reportedIdentityHash[:]) + return false, errors.New("GetReportedTypeFromReportedHash not verifying identityHash: " + reportedIdentityHashHex) + } + + if (identityType != identityTypeToRetrieve){ + // We only need to verify identityType if a reportedHashesList was not requested + return false, nil + } + } + + isWithinRange, err := byteRange.CheckIfIdentityHashIsWithinRange(rangeStart, rangeEnd, reportedIdentityHash) + if (err != nil) { return false, err } + if (isWithinRange == false){ + return false, nil + } + + if (len(reportedIdentityHashesList) != 0){ + identityHashIsInRequestedList := slices.Contains(reportedIdentityHashesList, reportedIdentityHash) + if (identityHashIsInRequestedList == false){ + return false, nil + } + } + + continue + } + if (reportedType == "Profile"){ + + if (len(reportedHash) != 28){ + reportedHashHex := encoding.EncodeBytesToHexString(reportedHash) + return false, errors.New("GetReportedTypeFromReportedHash returning Profile for different length reportedHash: " + reportedHashHex) + } + + reportedProfileHash := [28]byte(reportedHash) + + if (len(reportedIdentityHashesList) == 0){ + + // We only need to verify profileType if a reportedHashesList was not requested + + profileType, isDisabled, err := readProfiles.ReadProfileHashMetadata(reportedProfileHash) + if (err != nil) { + return false, errors.New("GetReportedTypeFromReportedHash not verifying profileHash") + } + if (isDisabled == true){ + return false, errors.New("GetReportedTypeFromReportedHash not verifying profileHash is not disabled.") + } + + if (profileType != identityTypeToRetrieve){ + return false, nil + } + } + } + } + + return true, nil + } + + responseIsValid, err := checkIfResponseIsValid() + if (err != nil) { return false, nil, err } + if (responseIsValid == false){ + + err := maliciousHosts.AddHostToMaliciousHostsList(hostIdentityHash) + if (err != nil){ return false, nil, err } + + return false, nil, nil + } + + return true, reportsInfoMap, nil +} + + +//Outputs: +// -bool: Download successful (could be unsuccessful because host keys not found, host is malicious, internet connection down, and more) +// -map[[30]byte][26]byte: Reports info map (Report Hash -> Reported Message Hash) +// -error +func GetMessageReportsInfoFromHost( + connectionIdentifier [21]byte, + hostIdentityHash [16]byte, + networkType byte, + rangeStart [10]byte, + rangeEnd [10]byte, + reportedMessageHashesList [][26]byte)(bool, map[[30]byte][26]byte, error){ + + connectionExists, connectionHostIdentityHash, connectionKey, connectionNetworkType, err := peerClient.GetConnectionMetadata(connectionIdentifier) + if (err != nil) { return false, nil, err } + if (connectionExists == false){ + return false, nil, nil + } + if (connectionHostIdentityHash != hostIdentityHash){ + return false, nil, errors.New("GetMessageReportsInfoFromHost called with hostIdentityHash which does not match connection metadata.") + } + if (connectionNetworkType != networkType){ + return false, nil, errors.New("GetMessageReportsInfoFromHost called with networkType which does not match connection metadata.") + } + + acceptableReportVersionsList := []int{1} + + requestBytes, requestIdentifier, err := serverRequest.CreateServerRequest_GetMessageReportsInfo(hostIdentityHash, connectionKey, networkType, acceptableReportVersionsList, rangeStart, rangeEnd, reportedMessageHashesList) + if (err != nil) { return false, nil, err } + + requestSuccessful, responseBytes, err := peerClient.SendRequestThroughConnection(connectionIdentifier, requestBytes) + if (err != nil) { return false, nil, err } + if (requestSuccessful == false){ + return false, nil, nil + } + + ableToRead, requestIdentifier_Received, hostIdentityHash_Received, reportsInfoMap, err := serverResponse.ReadServerResponse_GetMessageReportsInfo(responseBytes, connectionKey) + if (err != nil) { return false, nil, err } + if (ableToRead == false){ + + // Peer sent invalid response, must be malicious + + err := maliciousHosts.AddHostToMaliciousHostsList(hostIdentityHash) + if (err != nil){ return false, nil, err } + + return false, nil, nil + } + + // Now we check to make sure response is valid based on input request information + + checkIfResponseIsValid := func()(bool, error){ + + if (requestIdentifier != requestIdentifier_Received){ + // Host sent invalid response, must be malicious + return false, nil + } + + if (hostIdentityHash != hostIdentityHash_Received){ + // Peer sent invalid response, must be malicious + + return false, nil + } + + // Now we verify received reports info map fulfills request filters + + for _, reportedHash := range reportsInfoMap{ + + // reportedHash is a message hash + + if (len(reportedMessageHashesList) != 0){ + + messageHashIsInRequestedList := slices.Contains(reportedMessageHashesList, reportedHash) + if (messageHashIsInRequestedList == false){ + return false, nil + } + } + } + + return true, nil + } + + responseIsValid, err := checkIfResponseIsValid() + if (err != nil) { return false, nil, err } + if (responseIsValid == false){ + + err := maliciousHosts.AddHostToMaliciousHostsList(hostIdentityHash) + if (err != nil){ return false, nil, err } + + return false, nil, nil + } + + return true, reportsInfoMap, nil +} + + + +//Outputs: +// -bool: Download successful (could be unsuccessful because host keys not found, host is malicious, internet connection down, and more) +// -[][]byte: Retrieved list of reports +// -error +func GetReportsFromHost( + connectionIdentifier [21]byte, + hostIdentityHash [16]byte, + networkType byte, + reportHashesList [][30]byte, + expectedReportsInfoMap map[[30]byte][]byte)(bool, [][]byte, error){ + + connectionExists, connectionHostIdentityHash, connectionKey, connectionNetworkType, err := peerClient.GetConnectionMetadata(connectionIdentifier) + if (err != nil) { return false, nil, err } + if (connectionExists == false){ + return false, nil, nil + } + if (connectionHostIdentityHash != hostIdentityHash){ + return false, nil, errors.New("GetReportsFromHost called with hostIdentityHash which does not match connection metadata.") + } + if (connectionNetworkType != networkType){ + return false, nil, errors.New("GetReportsFromHost called with networkType which does not match connection metadata.") + } + + requestBytes, requestIdentifier, err := serverRequest.CreateServerRequest_GetReports(hostIdentityHash, connectionKey, networkType, reportHashesList) + if (err != nil) { return false, nil, err } + + requestSuccessful, responseData, err := peerClient.SendRequestThroughConnection(connectionIdentifier, requestBytes) + if (err != nil) { return false, nil, err } + if (requestSuccessful == false){ + return false, nil, nil + } + + ableToRead, requestIdentifier_Received, hostIdentityHash_Received, reportsList, err := serverResponse.ReadServerResponse_GetReports(responseData, connectionKey) + if (err != nil) { return false, nil, err } + if (ableToRead == false){ + + // Peer sent invalid response, must be malicious + + err := maliciousHosts.AddHostToMaliciousHostsList(hostIdentityHash) + if (err != nil) { return false, nil, err } + + return false, nil, nil + } + + // Now we check to make sure response is valid based on input request information + + checkIfResponseIsValid := func()(bool, error){ + + if (requestIdentifier != requestIdentifier_Received){ + // Host sent invalid response, must be malicious + return false, nil + } + + if (hostIdentityHash != hostIdentityHash_Received){ + + return false, nil + } + + for _, reportBytes := range reportsList{ + + ableToRead, reportHash, _, reportNetworkType, _, _, reportReportedHash, _, err := readReports.ReadReportAndHash(false, reportBytes) + if (err != nil) { return false, err } + if (ableToRead == false){ + // Report is invalid, peer must be malicious. + return false, nil + } + if (reportNetworkType != networkType){ + return false, nil + } + + expectedReportedHash, exists := expectedReportsInfoMap[reportHash] + if (exists == false) { + // We received an unexpected reportHash + return false, nil + } + + areEqual := bytes.Equal(reportReportedHash, expectedReportedHash) + if (areEqual == false){ + // We received an unexpected reportedHash for expected reportHash + return false, nil + } + } + + return true, nil + } + + responseIsValid, err := checkIfResponseIsValid() + if (err != nil) { return false, nil, err } + if (responseIsValid == false){ + // Peer sent invalid response, must be malicious + + err := maliciousHosts.AddHostToMaliciousHostsList(hostIdentityHash) + if (err != nil) { return false, nil, err } + + return false, nil, nil + } + + return true, reportsList, nil +} + + +//Outputs: +// -bool: Successful download +// -[]serverResponse.DepositStruct +// -[]string: List of addresses with no deposits +// -error +func GetAddressDepositsFromHost( + connectionIdentifier [21]byte, + hostIdentityHash [16]byte, + networkType byte, + cryptocurrency string, + cryptoAddressesList []string)(bool, []serverResponse.DepositStruct, []string, error){ + + connectionExists, connectionHostIdentityHash, connectionKey, connectionNetworkType, err := peerClient.GetConnectionMetadata(connectionIdentifier) + if (err != nil) { return false, nil, nil, err } + if (connectionExists == false){ + return false, nil, nil, nil + } + if (connectionHostIdentityHash != hostIdentityHash){ + return false, nil, nil, errors.New("GetAddressDepositsFromHost called with hostIdentityHash which does not match connection metadata.") + } + if (connectionNetworkType != networkType){ + return false, nil, nil, errors.New("GetAddressDepositsFromHost called with networkType which does not match connection metadata.") + } + + requestBytes, requestIdentifier, err := serverRequest.CreateServerRequest_GetAddressDeposits(hostIdentityHash, connectionKey, networkType, cryptocurrency, cryptoAddressesList) + if (err != nil) { return false, nil, nil, err } + + requestSuccessful, responseData, err := peerClient.SendRequestThroughConnection(connectionIdentifier, requestBytes) + if (err != nil) { return false, nil, nil, err } + if (requestSuccessful == false){ + return false, nil, nil, nil + } + + ableToRead, requestIdentifier_Received, hostIdentityHash_Received, addressDepositObjectsList, err := serverResponse.ReadServerResponse_GetAddressDeposits(responseData, connectionKey) + if (err != nil) { return false, nil, nil, err } + if (ableToRead == false){ + // Peer sent invalid response, must be malicious + + err := maliciousHosts.AddHostToMaliciousHostsList(hostIdentityHash) + if (err != nil) { return false, nil, nil, err } + + return false, nil, nil, nil + } + + // Now we check to make sure response is valid based on input request information + + // This map will store all addresses which have deposits + addressesWithDepositsMap := make(map[string]struct{}) + + checkIfResponseIsValid := func()(bool, error){ + + if (requestIdentifier != requestIdentifier_Received){ + // Host sent invalid response, must be malicious + return false, nil + } + + if (hostIdentityHash != hostIdentityHash_Received){ + + return false, nil + } + + for _, depositObject := range addressDepositObjectsList{ + + depositAddress := depositObject.Address + + addressExists := slices.Contains(cryptoAddressesList, depositAddress) + if (addressExists == false){ + return false, nil + } + + addressesWithDepositsMap[depositAddress] = struct{}{} + } + + return true, nil + } + + responseIsValid, err := checkIfResponseIsValid() + if (err != nil) { return false, nil, nil, err } + if (responseIsValid == false){ + // Host sent invalid response, they must be malicious + + err := maliciousHosts.AddHostToMaliciousHostsList(hostIdentityHash) + if (err != nil) { return false, nil, nil, err } + + return false, nil, nil, nil + } + + addressesWithoutDepositsList := make([]string, 0) + + for _, cryptoAddress := range cryptoAddressesList{ + + _, exists := addressesWithDepositsMap[cryptoAddress] + if (exists == false){ + addressesWithoutDepositsList = append(addressesWithoutDepositsList, cryptoAddress) + } + } + + return true, addressDepositObjectsList, addressesWithoutDepositsList, nil +} + + +//Inputs: +// -[21]byte: Connection Identifier +// -[16]byte: Host Identity Hash +// -byte: Network Type +// -[][16]byte: Identity hashes to retrieve viewable statuses for +// -[][28]byte: Profiles hashes to retrieve viewable statuses for +//Outputs: +// -bool: Successful download +// -map[[16]byte]bool: Identity Hash Statuses Map (Identity Hash -> true/false) (true = viewable, false = unviewable) +// -map[[28]byte]bool: Profile Hash Statuses Map (Profile Hash -> true/false) (true = viewable, false = unviewable) +// -error +func GetViewableStatusesFromHost( + connectionIdentifier [21]byte, + hostIdentityHash [16]byte, + networkType byte, + identityHashesList [][16]byte, + profileHashesList [][28]byte)(bool, map[[16]byte]bool, map[[28]byte]bool, error){ + + connectionExists, connectionHostIdentityHash, connectionKey, connectionNetworkType, err := peerClient.GetConnectionMetadata(connectionIdentifier) + if (err != nil) { return false, nil, nil, err } + if (connectionExists == false){ + return false, nil, nil, nil + } + if (connectionHostIdentityHash != hostIdentityHash){ + return false, nil, nil, errors.New("GetViewableStatusesFromHost called with hostIdentityHash which does not match connection metadata.") + } + if (connectionNetworkType != networkType){ + return false, nil, nil, errors.New("GetViewableStatusesFromHost called with networkType which does not match connection metadata.") + } + + requestBytes, requestIdentifier, err := serverRequest.CreateServerRequest_GetViewableStatuses(hostIdentityHash, connectionKey, networkType, identityHashesList, profileHashesList) + if (err != nil) { return false, nil, nil, err } + + requestSuccessful, responseData, err := peerClient.SendRequestThroughConnection(connectionIdentifier, requestBytes) + if (err != nil) { return false, nil, nil, err } + if (requestSuccessful == false){ + return false, nil, nil, nil + } + + ableToRead, requestIdentifier_Received, hostIdentityHash_Received, identityHashStatusesMap, profileHashStatusesMap, err := serverResponse.ReadServerResponse_GetViewableStatuses(responseData, connectionKey) + if (err != nil) { return false, nil, nil, err } + if (ableToRead == false){ + // Peer sent invalid response, must be malicious + + err := maliciousHosts.AddHostToMaliciousHostsList(hostIdentityHash) + if (err != nil) { return false, nil, nil, err } + + return false, nil, nil, nil + } + + // Now we check to make sure response is valid based on input request information + + checkIfResponseIsValid := func()(bool, error){ + + if (requestIdentifier != requestIdentifier_Received){ + // Host sent invalid response, must be malicious + return false, nil + } + + if (hostIdentityHash != hostIdentityHash_Received){ + + return false, nil + } + + for identityHash, _ := range identityHashStatusesMap{ + + isRequestedIdentityHash := slices.Contains(identityHashesList, identityHash) + if (isRequestedIdentityHash == false){ + return false, nil + } + } + + for profileHash, _ := range profileHashStatusesMap{ + + isRequestedProfileHash := slices.Contains(profileHashesList, profileHash) + if (isRequestedProfileHash == false){ + return false, nil + } + } + + return true, nil + } + + responseIsValid, err := checkIfResponseIsValid() + if (err != nil) { return false, nil, nil, err } + if (responseIsValid == false){ + // Peer sent invalid response, must be malicious + + err := maliciousHosts.AddHostToMaliciousHostsList(hostIdentityHash) + if (err != nil) { return false, nil, nil, err } + + return false, nil, nil, nil + } + + return true, identityHashStatusesMap, profileHashStatusesMap, nil +} + + +//Inputs: +// -[21]byte: Connection Identifier +// -[16]byte: Host Identity Hash +// -byte: Network Type +// -string: Content Type ("Profile"/"Message"/"Review"/"Report"/"Parameters") +// -[][]byte: Contents list +//Outputs: +// -bool: Successful download +// -map[string]bool: Received Content Accepted Info Map (Content Hash -> true/false) +// -error +func BroadcastContentToHost( + connectionIdentifier [21]byte, + hostIdentityHash [16]byte, + networkType byte, + contentType string, + contentList [][]byte)(bool, map[string]bool, error){ + + connectionExists, connectionHostIdentityHash, connectionKey, connectionNetworkType, err := peerClient.GetConnectionMetadata(connectionIdentifier) + if (err != nil) { return false, nil, err } + if (connectionExists == false){ + return false, nil, nil + } + if (connectionHostIdentityHash != hostIdentityHash){ + return false, nil, errors.New("BroadcastContentToHost called with hostIdentityHash which does not match connection metadata.") + } + if (connectionNetworkType != networkType){ + return false, nil, errors.New("BroadcastContentToHost called with networkType which does not match connection metadata.") + } + + requestBytes, requestIdentifier, err := serverRequest.CreateServerRequest_BroadcastContent(hostIdentityHash, connectionKey, networkType, contentType, contentList) + if (err != nil) { return false, nil, err } + + requestSuccessful, responseData, err := peerClient.SendRequestThroughConnection(connectionIdentifier, requestBytes) + if (err != nil) { return false, nil, err } + if (requestSuccessful == false){ + return false, nil, nil + } + + ableToRead, requestIdentifier_Received, hostIdentityHash_Received, contentAcceptedInfoMap, err := serverResponse.ReadServerResponse_BroadcastContent(responseData, connectionKey) + if (err != nil) { return false, nil, err } + if (ableToRead == false){ + // Peer sent invalid response, must be malicious + + err := maliciousHosts.AddHostToMaliciousHostsList(hostIdentityHash) + if (err != nil) { return false, nil, err } + + return false, nil, nil + } + + // Now we check to make sure response is valid based on input request information + + checkIfResponseIsValid := func()(bool, error){ + + if (requestIdentifier != requestIdentifier_Received){ + // Host sent invalid response, must be malicious + return false, nil + } + + if (hostIdentityHash != hostIdentityHash_Received){ + + return false, nil + } + + for _, contentBytes := range contentList{ + + ableToRead, contentHash, err := readContent.GetContentHashFromContentBytes(false, contentType, contentBytes) + if (err != nil) { return false, err } + if (ableToRead == false){ + return false, errors.New("CreateServerRequest_BroadcastContent not verifying content is valid.") + } + + _, exists := contentAcceptedInfoMap[string(contentHash)] + if (exists == false){ + // Host did not tell us if they accepted the content + // Host is malicious + return false, nil + } + } + + return true, nil + } + + responseIsValid, err := checkIfResponseIsValid() + if (err != nil) { return false, nil, err } + if (responseIsValid == false){ + // Peer sent invalid response, must be malicious + + err := maliciousHosts.AddHostToMaliciousHostsList(hostIdentityHash) + if (err != nil) { return false, nil, err } + + return false, nil, nil + } + + return true, contentAcceptedInfoMap, nil +} + + + diff --git a/internal/network/serverRequest/serverRequest.go b/internal/network/serverRequest/serverRequest.go new file mode 100644 index 0000000..17946cd --- /dev/null +++ b/internal/network/serverRequest/serverRequest.go @@ -0,0 +1,1783 @@ + + +// serverRequest provides functions to create and read server requests +// These are MessagePack encoded requests sent to Seekia hosts + +package serverRequest + +// Each encrypted request has a RequestTime and a RequestIdentifier +// RequestTime ensures an attacker cannot replay a request and compare the response size to gain information +// The host will reject any requests with a requestTime that is too old + +// RequestIdentifier is random for each request and ensures that the response is intended for the specific request +// Otherwise, a man-in-the-middle attacker could replay old requests created during the same connection, to trick the requestor +// For example, the requestor could make a GetProfilesInfo request, get a response A, and later make a different GetProfilesInfo request +// The attacker could replay response A, and the requestor would wrongly designate the host as malicious (for providing invalid information) +// Request Identifiers prevent this attack + +// Acceptable versions is a []int that describes the acceptable versions of profiles/messages/reviews/reports that the requestor can read +// As new versions are created, different hosts and clients will only be able to read certain versions of content +// This list allows hosts and clients who have not upgraded to newer versions to continue serving/downloading content on the network + +// See a description of each request and its purpose in Specification.md + +//TODO: Add GetFundedStatuses +// Will be used to get the identity/profile IsFunded statuses from hosts +// This will lessen the load on the credit account servers +// We don't need this for messages, because we will always retrieve their fundedStatuses directly from the credits servers +// Retrieving them from hosts is a privacy risk, and will take too long to retrieve individually + +import "seekia/internal/cryptography/chaPolyShrink" +import "seekia/internal/encoding" +import "seekia/internal/helpers" +import "seekia/internal/readContent" + +import messagepack "github.com/vmihailenco/msgpack/v5" + +import "crypto/rand" +import "time" +import "errors" + +func getNewRequestIdentifier()([16]byte, error){ + + var identifierArray [16]byte + + _, err := rand.Read(identifierArray[:]) + if (err != nil) { return [16]byte{}, err } + + return identifierArray, nil +} + +func verifyAcceptableVersionsList(inputList []int)error{ + + // We use this map to detect duplicates + versionsMap := make(map[int]struct{}) + + for _, versionInt := range inputList{ + + if (versionInt < 1 || versionInt > 500){ + return errors.New("Acceptable versions list is invalid: Version int is not valid.") + } + + _, exists := versionsMap[versionInt] + if (exists == true){ + return errors.New("Acceptable versions list is invalid: Duplicate exists.") + } + versionsMap[versionInt] = struct{}{} + } + + return nil +} + +//Outputs: +// -[]byte: Request Bytes +// -[16]byte: Request identifier +// -error +func CreateServerRequest_EstablishConnectionKey(hostIdentityHash [16]byte, networkType byte, requestorNaclPublicKey [32]byte, requestorKyberPublicKey [1568]byte)([]byte, [16]byte, error){ + + requestIdentifier, err := getNewRequestIdentifier() + if (err != nil) { return nil, [16]byte{}, err } + + type requestContentStruct struct{ + RequestVersion int + RequestIdentifier [16]byte + RequestType string + RecipientHost [16]byte + NetworkType byte + RequestorNaclKey [32]byte + RequestorKyberKey [1568]byte + } + + requestContentObject := requestContentStruct{ + RequestVersion: 1, + RequestIdentifier: requestIdentifier, + RequestType: "EstablishConnectionKey", + RecipientHost: hostIdentityHash, + NetworkType: networkType, + RequestorNaclKey: requestorNaclPublicKey, + RequestorKyberKey: requestorKyberPublicKey, + } + + finalRequest, err := encoding.EncodeMessagePackBytes(requestContentObject) + if (err != nil) { return nil, [16]byte{}, err } + + return finalRequest, requestIdentifier, nil +} + +//Outputs: +// -[16]byte: Host identity being requested from +// -[16]byte: Request identifier +// -byte: Network type +// -[32]byte: Requestor Nacl Key +// -[1568]byte: Requestor Kyber Key +// -error +func ReadServerRequest_EstablishConnectionKey(inputRequestBytes []byte)([16]byte, [16]byte, byte, [32]byte, [1568]byte, error){ + + //TODO: Verify everything + + type requestContentStruct struct{ + RequestIdentifier [16]byte + RequestType string + RecipientHost [16]byte + NetworkType byte + RequestorNaclKey [32]byte + RequestorKyberKey [1568]byte + } + + var requestContentObject requestContentStruct + + err := encoding.DecodeMessagePackBytes(false, inputRequestBytes, &requestContentObject) + if (err != nil) { return [16]byte{}, [16]byte{}, 0, [32]byte{}, [1568]byte{}, err } + + requestIdentifier := requestContentObject.RequestIdentifier + requestType := requestContentObject.RequestType + recipientHostIdentityHash := requestContentObject.RecipientHost + networkType := requestContentObject.NetworkType + requestorNaclKey := requestContentObject.RequestorNaclKey + requestorKyberKey := requestContentObject.RequestorKyberKey + + if (requestType != "EstablishConnectionKey"){ + return [16]byte{}, [16]byte{}, 0, [32]byte{}, [1568]byte{}, errors.New("Request is not EstablishConnectionKey") + } + + return recipientHostIdentityHash, requestIdentifier, networkType, requestorNaclKey, requestorKyberKey, nil +} + +//TODO: Deal with parameters versions somehow +//Outputs: +// -[]byte: Request Bytes +// -[16]byte: Request identifier +// -error +func CreateServerRequest_GetParametersInfo(hostIdentityHash [16]byte, connectionKey [32]byte, networkType byte)([]byte, [16]byte, error){ + + //TODO: Verify inputs + + requestIdentifier, err := getNewRequestIdentifier() + if (err != nil) { return nil, [16]byte{}, err } + + currentTime := time.Now().Unix() + + type innerRequestStruct struct{ + RequestVersion int + RequestType string + RequestTime int64 + RequestIdentifier [16]byte + RecipientHost [16]byte + NetworkType byte + } + + innerRequestObject := innerRequestStruct{ + RequestVersion: 1, + RequestType: "GetParametersInfo", + RequestTime: currentTime, + RequestIdentifier: requestIdentifier, + RecipientHost: hostIdentityHash, + NetworkType: networkType, + } + + innerRequestBytes, err := encoding.EncodeMessagePackBytes(innerRequestObject) + if (err != nil) { return nil, [16]byte{}, err } + + finalEncryptedRequest, err := createEncryptedRequest(connectionKey, innerRequestBytes) + if (err != nil) { return nil, [16]byte{}, err } + + return finalEncryptedRequest, requestIdentifier, nil +} + +//Outputs: +// -[]byte: Request Bytes +// -[16]byte: Request identifier +// -error +func CreateServerRequest_GetParameters(hostIdentityHash [16]byte, connectionKey [32]byte, networkType byte, parametersTypesList []string)([]byte, [16]byte, error){ + + //TODO: Verify inputs + + if (len(parametersTypesList) == 0){ + return nil, [16]byte{}, errors.New("CreateServerRequest_GetParameters called with invalid parametersTypesList: Empty list") + } + + requestIdentifier, err := getNewRequestIdentifier() + if (err != nil) { return nil, [16]byte{}, err } + + currentTime := time.Now().Unix() + + type innerRequestStruct struct{ + RequestVersion int + RequestType string + RequestTime int64 + RequestIdentifier [16]byte + RecipientHost [16]byte + NetworkType byte + ParametersTypesList []string + } + + innerRequestObject := innerRequestStruct{ + RequestVersion: 1, + RequestType: "GetParameters", + RequestTime: currentTime, + RequestIdentifier: requestIdentifier, + RecipientHost: hostIdentityHash, + NetworkType: networkType, + ParametersTypesList: parametersTypesList, + } + + innerRequestBytes, err := encoding.EncodeMessagePackBytes(innerRequestObject) + if (err != nil) { return nil, [16]byte{}, err } + + finalEncryptedRequest, err := createEncryptedRequest(connectionKey, innerRequestBytes) + if (err != nil) { return nil, [16]byte{}, err } + + return finalEncryptedRequest, requestIdentifier, nil +} + + +//Outputs: +// -[]string: Parameters types list +// -error +func ReadDecryptedServerRequest_GetParameters(decryptedRequestBytes []byte)([]string, error){ + + type decryptedRequestStruct struct{ + RequestVersion int + RequestType string + ParametersTypesList []string + } + + var decryptedRequestObject decryptedRequestStruct + + err := encoding.DecodeMessagePackBytes(false, decryptedRequestBytes, &decryptedRequestObject) + if (err != nil) { return nil, err } + + requestVersion := decryptedRequestObject.RequestVersion + requestType := decryptedRequestObject.RequestType + parametersTypesList := decryptedRequestObject.ParametersTypesList + + if (requestVersion != 1){ + return nil, errors.New("Invalid request: Unknown requestVersion.") + } + + if (requestType != "GetParameters"){ + return nil, errors.New("Invalid request: requestType not GetParameters") + } + + //TODO: Verify everything + + return parametersTypesList, nil +} + +//Outputs: +// -[]byte: Request Bytes +// -[16]byte: Request identifier +// -error +func CreateServerRequest_GetProfilesInfo(hostIdentityHash [16]byte, + connectionKey [32]byte, + networkType byte, + acceptableVersionsList []int, + profileTypeToRetrieve string, + rangeStart [16]byte, + rangeEnd [16]byte, + identityHashesList [][16]byte, + criteria []byte, + getNewestOnly bool, + getViewableOnly bool)([]byte, [16]byte, error){ + + //TODO: Verify inputs + + if (profileTypeToRetrieve != "Mate" && profileTypeToRetrieve != "Host" && profileTypeToRetrieve != "Moderator"){ + return nil, [16]byte{}, errors.New("CreateServerRequest_GetProfilesInfo called with invalid profileTypeToRetrieve: " + profileTypeToRetrieve) + } + + err := verifyAcceptableVersionsList(acceptableVersionsList) + if (err != nil) { return nil, [16]byte{}, err } + + requestIdentifier, err := getNewRequestIdentifier() + if (err != nil) { return nil, [16]byte{}, err } + + currentTime := time.Now().Unix() + + type innerRequestStruct struct{ + RequestVersion int + RequestType string + RequestTime int64 + RequestIdentifier [16]byte + RecipientHost [16]byte + NetworkType byte + AcceptableVersions []int + ProfileType string + GetNewestOnly bool + GetViewableOnly bool + IdentityHashesList [][16]byte + RangeStart [16]byte + RangeEnd [16]byte + Criteria []byte + } + + innerRequestObject := innerRequestStruct{ + RequestVersion: 1, + RequestType: "GetProfilesInfo", + RequestTime: currentTime, + RequestIdentifier: requestIdentifier, + RecipientHost: hostIdentityHash, + NetworkType: networkType, + AcceptableVersions: acceptableVersionsList, + ProfileType: profileTypeToRetrieve, + GetNewestOnly: getNewestOnly, + GetViewableOnly: getViewableOnly, + IdentityHashesList: identityHashesList, + RangeStart: rangeStart, + RangeEnd: rangeEnd, + } + + if (profileTypeToRetrieve == "Mate"){ + innerRequestObject.Criteria = criteria + } + + innerRequestBytes, err := encoding.EncodeMessagePackBytes(innerRequestObject) + if (err != nil) { return nil, [16]byte{}, err } + + finalEncryptedRequest, err := createEncryptedRequest(connectionKey, innerRequestBytes) + if (err != nil) { return nil, [16]byte{}, err } + + return finalEncryptedRequest, requestIdentifier, nil +} + + + +//Outputs: +// -[]int: Acceptable versions list +// -string: Profile Type To Retrieve +// -[16]byte: Range Start +// -[16]byte: Range End +// -[][16]byte: Identity hashes list +// -[]byte: Criteria +// -bool: Get Newest profiles only +// -bool: Get Viewable profiles only +// -error +func ReadDecryptedServerRequest_GetProfilesInfo(decryptedRequestBytes []byte)([]int, string, [16]byte, [16]byte, [][16]byte, []byte, bool, bool, error){ + + type decryptedRequestStruct struct{ + RequestVersion int + RequestType string + AcceptableVersions []int + ProfileType string + IdentityHashesList [][16]byte + RangeStart [16]byte + RangeEnd [16]byte + Criteria []byte + GetNewestOnly bool + GetViewableOnly bool + } + + var decryptedRequestObject decryptedRequestStruct + + err := encoding.DecodeMessagePackBytes(false, decryptedRequestBytes, &decryptedRequestObject) + if (err != nil) { return nil, "", [16]byte{}, [16]byte{}, nil, nil, false, false, err } + + requestVersion := decryptedRequestObject.RequestVersion + requestType := decryptedRequestObject.RequestType + acceptableVersionsList := decryptedRequestObject.AcceptableVersions + profileTypeToRetrieve := decryptedRequestObject.ProfileType + identityHashesList := decryptedRequestObject.IdentityHashesList + rangeStart := decryptedRequestObject.RangeStart + rangeEnd := decryptedRequestObject.RangeEnd + getNewestOnly := decryptedRequestObject.GetNewestOnly + getViewableOnly := decryptedRequestObject.GetViewableOnly + + if (requestVersion != 1){ + return nil, "", [16]byte{}, [16]byte{}, nil, nil, false, false, errors.New("Invalid request: Unknown requestVersion") + } + + if (requestType != "GetProfilesInfo"){ + return nil, "", [16]byte{}, [16]byte{}, nil, nil, false, false, errors.New("Invalid request: requestType not GetProfilesInfo") + } + + err = verifyAcceptableVersionsList(acceptableVersionsList) + if (err != nil){ + return nil, "", [16]byte{}, [16]byte{}, nil, nil, false, false, err + } + + //TODO: Verify everything + + getProfileCriteria := func()([]byte, error){ + + if (profileTypeToRetrieve != "Mate"){ + return nil, nil + } + + criteria := decryptedRequestObject.Criteria + + return criteria, nil + } + + profileCriteria, err := getProfileCriteria() + if (err != nil) { return nil, "", [16]byte{}, [16]byte{}, nil, nil, false, false, err } + + return acceptableVersionsList, profileTypeToRetrieve, rangeStart, rangeEnd, identityHashesList, profileCriteria, getNewestOnly, getViewableOnly, nil +} + + + +//Outputs: +// -[]byte: Request Bytes +// -[16]byte: Request identifier +// -error +func CreateServerRequest_GetProfiles(hostIdentityHash [16]byte, + connectionKey [32]byte, + networkType byte, + profileTypeToRetrieve string, + profileHashesList [][28]byte)([]byte, [16]byte, error){ + + //TODO: Verify inputs + + if (profileTypeToRetrieve != "Mate" && profileTypeToRetrieve != "Host" && profileTypeToRetrieve != "Moderator"){ + return nil, [16]byte{}, errors.New("CreateServerRequest_GetProfiles called with invalid profileTypeToRetrieve: " + profileTypeToRetrieve) + } + + if (len(profileHashesList) == 0){ + return nil, [16]byte{}, errors.New("CreateServerRequest_GetProfiles called with empty profileHashesList.") + } + + containsDuplicates, _ := helpers.CheckIfListContainsDuplicates(profileHashesList) + if (containsDuplicates == true){ + return nil, [16]byte{}, errors.New("CreateServerRequest_GetProfiles called with profileHashesList containing duplicates.") + } + + requestIdentifier, err := getNewRequestIdentifier() + if (err != nil) { return nil, [16]byte{}, err } + + currentTime := time.Now().Unix() + + type innerRequestStruct struct{ + RequestVersion int + RequestType string + RequestTime int64 + RequestIdentifier [16]byte + RecipientHost [16]byte + NetworkType byte + ProfileType string + ProfileHashesList [][28]byte + } + + innerRequestObject := innerRequestStruct{ + RequestVersion: 1, + RequestType: "GetProfiles", + RequestTime: currentTime, + RequestIdentifier: requestIdentifier, + RecipientHost: hostIdentityHash, + NetworkType: networkType, + ProfileType: profileTypeToRetrieve, + ProfileHashesList: profileHashesList, + } + + innerRequestBytes, err := encoding.EncodeMessagePackBytes(innerRequestObject) + if (err != nil) { return nil, [16]byte{}, err } + + finalEncryptedRequest, err := createEncryptedRequest(connectionKey, innerRequestBytes) + if (err != nil) { return nil, [16]byte{}, err } + + return finalEncryptedRequest, requestIdentifier, nil +} + + + +//Outputs: +// -string: Profile Type To Retrieve +// -[][28]byte: Profile hashes list +// -error +func ReadDecryptedServerRequest_GetProfiles(decryptedRequestBytes []byte)(string, [][28]byte, error){ + + type decryptedRequestStruct struct{ + RequestVersion int + RequestType string + ProfileType string + ProfileHashesList [][28]byte + } + + var decryptedRequestObject decryptedRequestStruct + + err := encoding.DecodeMessagePackBytes(false, decryptedRequestBytes, &decryptedRequestObject) + if (err != nil) { return "", nil, err } + + requestVersion := decryptedRequestObject.RequestVersion + requestType := decryptedRequestObject.RequestType + profileTypeToRetrieve := decryptedRequestObject.ProfileType + profileHashesList := decryptedRequestObject.ProfileHashesList + + if (requestVersion != 1){ + return "", nil, errors.New("Invalid request: Unknown requestVersion.") + } + + if (requestType != "GetProfiles"){ + return "", nil, errors.New("Invalid request: requestType not GetProfiles") + } + + //TODO: Verify everything + + return profileTypeToRetrieve, profileHashesList, nil +} + + +//Outputs: +// -[]byte: Request Bytes +// -[16]byte: Request identifier +// -error +func CreateServerRequest_GetMessageHashesList(hostIdentityHash [16]byte, + connectionKey [32]byte, + networkType byte, + acceptableVersionsList []int, + rangeStart [10]byte, + rangeEnd [10]byte, + inboxesList [][10]byte, + getViewableOnly bool, + getDecryptableOnly bool)([]byte, [16]byte, error){ + + //TODO: Verify inputs + + err := verifyAcceptableVersionsList(acceptableVersionsList) + if (err != nil) { return nil, [16]byte{}, err } + + requestIdentifier, err := getNewRequestIdentifier() + if (err != nil) { return nil, [16]byte{}, err } + + currentTime := time.Now().Unix() + + type innerRequestStruct struct{ + RequestVersion int + RequestType string + RequestTime int64 + RequestIdentifier [16]byte + RecipientHost [16]byte + NetworkType byte + AcceptableVersions []int + GetViewableOnly bool + GetDecryptableOnly bool + RangeStart [10]byte + RangeEnd [10]byte + InboxesList [][10]byte + } + + innerRequestObject := innerRequestStruct{ + RequestVersion: 1, + RequestType: "GetMessageHashesList", + RequestTime: currentTime, + RequestIdentifier: requestIdentifier, + RecipientHost: hostIdentityHash, + NetworkType: networkType, + AcceptableVersions: acceptableVersionsList, + GetViewableOnly: getViewableOnly, + GetDecryptableOnly: getDecryptableOnly, + RangeStart: rangeStart, + RangeEnd: rangeEnd, + InboxesList: inboxesList, + } + + innerRequestBytes, err := encoding.EncodeMessagePackBytes(innerRequestObject) + if (err != nil) { return nil, [16]byte{}, err } + + finalEncryptedRequest, err := createEncryptedRequest(connectionKey, innerRequestBytes) + if (err != nil) { return nil, [16]byte{}, err } + + return finalEncryptedRequest, requestIdentifier, nil +} + + +//Outputs: +// -[]int: Acceptable versions list +// -[10]byte: Inbox Range Start +// -[10]byte: Inbox Range End +// -[][10]byte: Inboxes list +// -bool: Get Viewable messages only +// -bool: Get Decryptable messages only +// -error +func ReadDecryptedServerRequest_GetMessageHashesList(decryptedRequestBytes []byte)([]int, [10]byte, [10]byte, [][10]byte, bool, bool, error){ + + type decryptedRequestStruct struct{ + RequestVersion int + RequestType string + AcceptableVersions []int + InboxesList [][10]byte + RangeStart [10]byte + RangeEnd [10]byte + GetViewableOnly bool + GetDecryptableOnly bool + } + + var decryptedRequestObject decryptedRequestStruct + + err := encoding.DecodeMessagePackBytes(false, decryptedRequestBytes, &decryptedRequestObject) + if (err != nil) { return nil, [10]byte{}, [10]byte{}, nil, false, false, err } + + requestVersion := decryptedRequestObject.RequestVersion + requestType := decryptedRequestObject.RequestType + acceptableVersionsList := decryptedRequestObject.AcceptableVersions + inboxesList := decryptedRequestObject.InboxesList + rangeStart := decryptedRequestObject.RangeStart + rangeEnd := decryptedRequestObject.RangeEnd + getViewableOnly := decryptedRequestObject.GetViewableOnly + getDecryptableOnly := decryptedRequestObject.GetDecryptableOnly + + if (requestVersion != 1){ + return nil, [10]byte{}, [10]byte{}, nil, false, false, errors.New("Invalid request: Unknown requestVersion.") + } + + if (requestType != "GetMessageHashesList"){ + return nil, [10]byte{}, [10]byte{}, nil, false, false, errors.New("Invalid request: requestType not GetMessageHashesList") + } + + err = verifyAcceptableVersionsList(acceptableVersionsList) + if (err != nil){ return nil, [10]byte{}, [10]byte{}, nil, false, false, err } + + //TODO: Verify everything + + return acceptableVersionsList, rangeStart, rangeEnd, inboxesList, getViewableOnly, getDecryptableOnly, nil +} + + + +//Outputs: +// -[]byte: Request Bytes +// -[16]byte: Request identifier +// -error +func CreateServerRequest_GetMessages(hostIdentityHash [16]byte, + connectionKey [32]byte, + networkType byte, + messageHashesList [][26]byte)([]byte, [16]byte, error){ + + //TODO: Verify inputs + + containsDuplicate, _ := helpers.CheckIfListContainsDuplicates(messageHashesList) + if (containsDuplicate == true){ + return nil, [16]byte{}, errors.New("CreateServerRequest_GetMessages called with messageHashesList containing duplicates.") + } + + if (len(messageHashesList) == 0){ + return nil, [16]byte{}, errors.New("Invalid messageHashesList: Empty list") + } + + requestIdentifier, err := getNewRequestIdentifier() + if (err != nil) { return nil, [16]byte{}, err } + + currentTime := time.Now().Unix() + + type innerRequestStruct struct{ + RequestVersion int + RequestType string + RequestTime int64 + RequestIdentifier [16]byte + RecipientHost [16]byte + NetworkType byte + MessageHashesList [][26]byte + } + + innerRequestObject := innerRequestStruct{ + RequestVersion: 1, + RequestType: "GetMessages", + RequestTime: currentTime, + RequestIdentifier: requestIdentifier, + RecipientHost: hostIdentityHash, + NetworkType: networkType, + MessageHashesList: messageHashesList, + } + + innerRequestBytes, err := encoding.EncodeMessagePackBytes(innerRequestObject) + if (err != nil) { return nil, [16]byte{}, err } + + finalEncryptedRequest, err := createEncryptedRequest(connectionKey, innerRequestBytes) + if (err != nil) { return nil, [16]byte{}, err } + + return finalEncryptedRequest, requestIdentifier, nil +} + + +//Outputs: +// -[][26]byte: Message Hashes List +// -error +func ReadDecryptedServerRequest_GetMessages(decryptedRequestBytes []byte)([][26]byte, error){ + + type decryptedRequestStruct struct{ + RequestVersion int + RequestType string + MessageHashesList [][26]byte + } + + var decryptedRequestObject decryptedRequestStruct + + err := encoding.DecodeMessagePackBytes(true, decryptedRequestBytes, &decryptedRequestObject) + if (err != nil) { return nil, err } + + requestVersion := decryptedRequestObject.RequestVersion + requestType := decryptedRequestObject.RequestType + messageHashesList := decryptedRequestObject.MessageHashesList + + if (requestVersion != 1){ + return nil, errors.New("Invalid request: Unknown requestVersion.") + } + + if (requestType != "GetMessages"){ + return nil, errors.New("Invalid request: requestType not GetMessages") + } + + //TODO: Verify everything + + return messageHashesList, nil +} + + +//Outputs: +// -[]byte: Request Bytes +// -[16]byte: Request identifier +// -error +func CreateServerRequest_GetIdentityReviewsInfo(hostIdentityHash [16]byte, + connectionKey [32]byte, + networkType byte, + acceptableVersionsList []int, + identityTypeToRetrieve string, + rangeStart [16]byte, + rangeEnd [16]byte, + reviewedIdentityHashesList [][16]byte, // A list of identity hashes + reviewersList [][16]byte)([]byte, [16]byte, error){ + + //TODO: Verify inputs + + if (identityTypeToRetrieve != "Mate" && identityTypeToRetrieve != "Host" && identityTypeToRetrieve != "Moderator"){ + return nil, [16]byte{}, errors.New("CreateServerRequest_GetIdentityReviewsInfo called with invalid identityTypeToRetrieve: " + identityTypeToRetrieve) + } + + err := verifyAcceptableVersionsList(acceptableVersionsList) + if (err != nil) { return nil, [16]byte{}, err } + + requestIdentifier, err := getNewRequestIdentifier() + if (err != nil) { return nil, [16]byte{}, err } + + currentTime := time.Now().Unix() + + type innerRequestStruct struct{ + RequestVersion int + RequestType string + RequestTime int64 + RequestIdentifier [16]byte + RecipientHost [16]byte + NetworkType byte + AcceptableVersions []int + IdentityType string + ReviewedIdentitiesList [][16]byte + ReviewersList [][16]byte + RangeStart [16]byte + RangeEnd [16]byte + } + + innerRequestObject := innerRequestStruct{ + RequestVersion: 1, + RequestType: "GetIdentityReviewsInfo", + RequestTime: currentTime, + RequestIdentifier: requestIdentifier, + RecipientHost: hostIdentityHash, + NetworkType: networkType, + AcceptableVersions: acceptableVersionsList, + IdentityType: identityTypeToRetrieve, + ReviewedIdentitiesList: reviewedIdentityHashesList, + ReviewersList: reviewersList, + RangeStart: rangeStart, + RangeEnd: rangeEnd, + } + + innerRequestBytes, err := encoding.EncodeMessagePackBytes(innerRequestObject) + if (err != nil) { return nil, [16]byte{}, err } + + finalEncryptedRequest, err := createEncryptedRequest(connectionKey, innerRequestBytes) + if (err != nil) { return nil, [16]byte{}, err } + + return finalEncryptedRequest, requestIdentifier, nil +} + +//Outputs: +// -[]int: Acceptable review versions +// -string: IdentityType +// -[16]byte: Range Start +// -[16]byte: Range End +// -[][16]byte: Reviewed identity hashes list (If empty, ignore) +// -[][16]byte: Reviewers list (identity hashes of reviewers) +// -error +func ReadDecryptedServerRequest_GetIdentityReviewsInfo(decryptedRequestBytes []byte)([]int, string, [16]byte, [16]byte, [][16]byte, [][16]byte, error){ + + type decryptedRequestStruct struct{ + RequestVersion int + RequestType string + AcceptableVersions []int + ReviewedIdentitiesList [][16]byte + ReviewersList [][16]byte + IdentityType string + RangeStart [16]byte + RangeEnd [16]byte + } + + var decryptedRequestObject decryptedRequestStruct + + err := encoding.DecodeMessagePackBytes(true, decryptedRequestBytes, &decryptedRequestObject) + if (err != nil) { return nil, "", [16]byte{}, [16]byte{}, nil, nil, err } + + requestVersion := decryptedRequestObject.RequestVersion + requestType := decryptedRequestObject.RequestType + acceptableVersionsList := decryptedRequestObject.AcceptableVersions + reviewedIdentityHashesList := decryptedRequestObject.ReviewedIdentitiesList + reviewersList := decryptedRequestObject.ReviewersList + identityType := decryptedRequestObject.IdentityType + rangeStart := decryptedRequestObject.RangeStart + rangeEnd := decryptedRequestObject.RangeEnd + + if (requestVersion != 1){ + return nil, "", [16]byte{}, [16]byte{}, nil, nil, errors.New("Invalid request: Unknown requestVersion.") + } + + if (requestType != "GetIdentityReviewsInfo"){ + return nil, "", [16]byte{}, [16]byte{}, nil, nil, errors.New("Invalid request: requestType not GetIdentityReviewsInfo") + } + + err = verifyAcceptableVersionsList(acceptableVersionsList) + if (err != nil){ return nil, "", [16]byte{}, [16]byte{}, nil, nil, err } + + //TODO: Verify everything + + return acceptableVersionsList, identityType, rangeStart, rangeEnd, reviewedIdentityHashesList, reviewersList, nil +} + + +//Outputs: +// -[]byte: Request Bytes +// -[16]byte: Request identifier +// -error +func CreateServerRequest_GetMessageReviewsInfo(hostIdentityHash [16]byte, + connectionKey [32]byte, + networkType byte, + acceptableVersionsList []int, + rangeStart [10]byte, + rangeEnd [10]byte, + reviewedMessageHashesList [][26]byte, + reviewersList [][16]byte)([]byte, [16]byte, error){ + + //TODO: Verify inputs + + err := verifyAcceptableVersionsList(acceptableVersionsList) + if (err != nil) { return nil, [16]byte{}, err } + + requestIdentifier, err := getNewRequestIdentifier() + if (err != nil) { return nil, [16]byte{}, err } + + currentTime := time.Now().Unix() + + type innerRequestStruct struct{ + RequestVersion int + RequestType string + RequestTime int64 + RequestIdentifier [16]byte + RecipientHost [16]byte + NetworkType byte + AcceptableVersions []int + ReviewedMessagesList [][26]byte + ReviewersList [][16]byte + RangeStart [10]byte + RangeEnd [10]byte + } + + innerRequestObject := innerRequestStruct{ + RequestVersion: 1, + RequestType: "GetMessageReviewsInfo", + RequestTime: currentTime, + RequestIdentifier: requestIdentifier, + RecipientHost: hostIdentityHash, + NetworkType: networkType, + AcceptableVersions: acceptableVersionsList, + ReviewedMessagesList: reviewedMessageHashesList, + ReviewersList: reviewersList, + RangeStart: rangeStart, + RangeEnd: rangeEnd, + } + + innerRequestBytes, err := encoding.EncodeMessagePackBytes(innerRequestObject) + if (err != nil) { return nil, [16]byte{}, err } + + finalEncryptedRequest, err := createEncryptedRequest(connectionKey, innerRequestBytes) + if (err != nil) { return nil, [16]byte{}, err } + + return finalEncryptedRequest, requestIdentifier, nil +} + + +//Outputs: +// -[]int: Acceptable review versions +// -[10]byte: Range Start +// -[10]byte: Range End +// -[][26]byte: Reviewed hashes list (A list of message hashes) +// -[][16]byte: Reviewers list (identity hashes of reviewers) +// -error +func ReadDecryptedServerRequest_GetMessageReviewsInfo(decryptedRequestBytes []byte)([]int, [10]byte, [10]byte, [][26]byte, [][16]byte, error){ + + type decryptedRequestStruct struct{ + RequestVersion int + RequestType string + AcceptableVersions []int + ReviewedMessagesList [][26]byte + ReviewersList [][16]byte + RangeStart [10]byte + RangeEnd [10]byte + } + + var decryptedRequestObject decryptedRequestStruct + + err := encoding.DecodeMessagePackBytes(true, decryptedRequestBytes, &decryptedRequestObject) + if (err != nil) { return nil, [10]byte{}, [10]byte{}, nil, nil, err } + + requestVersion := decryptedRequestObject.RequestVersion + requestType := decryptedRequestObject.RequestType + acceptableVersionsList := decryptedRequestObject.AcceptableVersions + reviewedMessageHashesList := decryptedRequestObject.ReviewedMessagesList + reviewersList := decryptedRequestObject.ReviewersList + rangeStart := decryptedRequestObject.RangeStart + rangeEnd := decryptedRequestObject.RangeEnd + + if (requestVersion != 1){ + return nil, [10]byte{}, [10]byte{}, nil, nil, errors.New("Invalid request: Unknown requestVersion.") + } + + if (requestType != "GetMessageReviewsInfo"){ + return nil, [10]byte{}, [10]byte{}, nil, nil, errors.New("Invalid request: requestType not GetMessageReviewsInfo") + } + + err = verifyAcceptableVersionsList(acceptableVersionsList) + if (err != nil){ return nil, [10]byte{}, [10]byte{}, nil, nil, err } + + //TODO: Verify everything + + return acceptableVersionsList, rangeStart, rangeEnd, reviewedMessageHashesList, reviewersList, nil +} + + +//Outputs: +// -[]byte: Request Bytes +// -[16]byte: Request identifier +// -error +func CreateServerRequest_GetReviews(hostIdentityHash [16]byte, + connectionKey [32]byte, + networkType byte, + reviewHashesList [][29]byte)([]byte, [16]byte, error){ + + //TODO: Verify inputs + + if (len(reviewHashesList) == 0){ + return nil, [16]byte{}, errors.New("CreateServerRequest_GetReviews called with empty reviewHashesList") + } + + containsDuplicate, _ := helpers.CheckIfListContainsDuplicates(reviewHashesList) + if (containsDuplicate == true){ + return nil, [16]byte{}, errors.New("CreateServerRequest_GetReviews called with reviewHashesList containing duplicates.") + } + + requestIdentifier, err := getNewRequestIdentifier() + if (err != nil) { return nil, [16]byte{}, err } + + currentTime := time.Now().Unix() + + type innerRequestStruct struct{ + RequestVersion int + RequestType string + RequestTime int64 + RequestIdentifier [16]byte + RecipientHost [16]byte + NetworkType byte + ReviewHashesList [][29]byte + } + + innerRequestObject := innerRequestStruct{ + RequestVersion: 1, + RequestType: "GetReviews", + RequestTime: currentTime, + RequestIdentifier: requestIdentifier, + RecipientHost: hostIdentityHash, + NetworkType: networkType, + ReviewHashesList: reviewHashesList, + } + + innerRequestBytes, err := encoding.EncodeMessagePackBytes(innerRequestObject) + if (err != nil) { return nil, [16]byte{}, err } + + finalEncryptedRequest, err := createEncryptedRequest(connectionKey, innerRequestBytes) + if (err != nil) { return nil, [16]byte{}, err } + + return finalEncryptedRequest, requestIdentifier, nil +} + + +//Outputs: +// -[][29]byte: Review Hashes List +// -error +func ReadDecryptedServerRequest_GetReviews(decryptedRequestBytes []byte)([][29]byte, error){ + + type decryptedRequestStruct struct{ + RequestVersion int + RequestType string + ReviewHashesList [][29]byte + } + + var decryptedRequestObject decryptedRequestStruct + + err := encoding.DecodeMessagePackBytes(true, decryptedRequestBytes, &decryptedRequestObject) + if (err != nil) { return nil, err } + + requestVersion := decryptedRequestObject.RequestVersion + requestType := decryptedRequestObject.RequestType + reviewHashesList := decryptedRequestObject.ReviewHashesList + + if (requestVersion != 1){ + return nil, errors.New("Invalid request: Unknown requestVersion.") + } + + if (requestType != "GetReviews"){ + return nil, errors.New("Invalid request: requestType not GetReviews") + } + + //TODO: Verify review hashes list + + return reviewHashesList, nil +} + + +//Outputs: +// -[]byte: Request bytes +// -[16]byte: Request identifier +// -error +func CreateServerRequest_GetIdentityReportsInfo(hostIdentityHash [16]byte, + connectionKey [32]byte, + networkType byte, + acceptableVersionsList []int, + identityTypeToRetrieve string, + rangeStart [16]byte, + rangeEnd [16]byte, + reportedIdentityHashesList [][16]byte)([]byte, [16]byte, error){ + + //TODO: Verify inputs + + if (identityTypeToRetrieve != "Mate" && identityTypeToRetrieve != "Host" && identityTypeToRetrieve != "Moderator"){ + return nil, [16]byte{}, errors.New("CreateServerRequest_GetIdentityReportsInfo called with invalid identityTypeToRetrieve: " + identityTypeToRetrieve) + } + + err := verifyAcceptableVersionsList(acceptableVersionsList) + if (err != nil) { return nil, [16]byte{}, err } + + requestIdentifier, err := getNewRequestIdentifier() + if (err != nil) { return nil, [16]byte{}, err } + + currentTime := time.Now().Unix() + + type innerRequestStruct struct{ + RequestVersion int + RequestType string + RequestTime int64 + RequestIdentifier [16]byte + RecipientHost [16]byte + NetworkType byte + AcceptableVersions []int + IdentityType string + ReportedIdentitiesList [][16]byte + RangeStart [16]byte + RangeEnd [16]byte + } + + innerRequestObject := innerRequestStruct{ + RequestVersion: 1, + RequestType: "GetIdentityReportsInfo", + RequestTime: currentTime, + RequestIdentifier: requestIdentifier, + RecipientHost: hostIdentityHash, + NetworkType: networkType, + AcceptableVersions: acceptableVersionsList, + ReportedIdentitiesList: reportedIdentityHashesList, + RangeStart: rangeStart, + RangeEnd: rangeEnd, + IdentityType: identityTypeToRetrieve, + } + + innerRequestBytes, err := encoding.EncodeMessagePackBytes(innerRequestObject) + if (err != nil) { return nil, [16]byte{}, err } + + finalEncryptedRequest, err := createEncryptedRequest(connectionKey, innerRequestBytes) + if (err != nil) { return nil, [16]byte{}, err } + + return finalEncryptedRequest, requestIdentifier, nil +} + + +//Outputs: +// -[]int: Acceptable versions list +// -string: IdentityType +// -[16]byte: Range Start +// -[16]byte: Range End +// -[][16]byte: Reported identity hashes list +// -error +func ReadDecryptedServerRequest_GetIdentityReportsInfo(decryptedRequestBytes []byte)([]int, string, [16]byte, [16]byte, [][16]byte, error){ + + type decryptedRequestStruct struct{ + RequestVersion int + RequestType string + AcceptableVersions []int + IdentityType string + ReportedIdentitiesList [][16]byte + RangeStart [16]byte + RangeEnd [16]byte + } + + var decryptedRequestObject decryptedRequestStruct + + err := encoding.DecodeMessagePackBytes(true, decryptedRequestBytes, &decryptedRequestObject) + if (err != nil) { return nil, "", [16]byte{}, [16]byte{}, nil, err } + + requestVersion := decryptedRequestObject.RequestVersion + requestType := decryptedRequestObject.RequestType + acceptableVersionsList := decryptedRequestObject.AcceptableVersions + identityTypeToRetrieve := decryptedRequestObject.IdentityType + reportedIdentitiesList := decryptedRequestObject.ReportedIdentitiesList + rangeStart := decryptedRequestObject.RangeStart + rangeEnd := decryptedRequestObject.RangeEnd + + if (requestVersion != 1){ + return nil, "", [16]byte{}, [16]byte{}, nil, errors.New("Invalid request: Unknown requestVersion.") + } + + if (requestType != "GetIdentityReportsInfo"){ + return nil, "", [16]byte{}, [16]byte{}, nil, errors.New("Invalid request: requestType not GetIdentityReportsInfo: " + requestType) + } + + //TODO: Verify everything + + err = verifyAcceptableVersionsList(acceptableVersionsList) + if (err != nil){ return nil, "", [16]byte{}, [16]byte{}, nil, err } + + return acceptableVersionsList, identityTypeToRetrieve, rangeStart, rangeEnd, reportedIdentitiesList, nil +} + + +//Outputs: +// -[]byte: Request bytes +// -[16]byte: Request identifier +// -error +func CreateServerRequest_GetMessageReportsInfo(hostIdentityHash [16]byte, + connectionKey [32]byte, + networkType byte, + acceptableVersionsList []int, + rangeStart [10]byte, + rangeEnd [10]byte, + reportedMessageHashesList [][26]byte)([]byte, [16]byte, error){ + + //TODO: Verify inputs + + err := verifyAcceptableVersionsList(acceptableVersionsList) + if (err != nil) { return nil, [16]byte{}, err } + + requestIdentifier, err := getNewRequestIdentifier() + if (err != nil) { return nil, [16]byte{}, err } + + currentTime := time.Now().Unix() + + type innerRequestStruct struct{ + RequestVersion int + RequestType string + RequestTime int64 + RequestIdentifier [16]byte + RecipientHost [16]byte + NetworkType byte + AcceptableVersions []int + ReportedMessagesList [][26]byte + RangeStart [10]byte + RangeEnd [10]byte + } + + innerRequestObject := innerRequestStruct{ + RequestVersion: 1, + RequestType: "GetMessageReportsInfo", + RequestTime: currentTime, + RequestIdentifier: requestIdentifier, + RecipientHost: hostIdentityHash, + NetworkType: networkType, + AcceptableVersions: acceptableVersionsList, + ReportedMessagesList: reportedMessageHashesList, + RangeStart: rangeStart, + RangeEnd: rangeEnd, + } + + innerRequestBytes, err := encoding.EncodeMessagePackBytes(innerRequestObject) + if (err != nil) { return nil, [16]byte{}, err } + + finalEncryptedRequest, err := createEncryptedRequest(connectionKey, innerRequestBytes) + if (err != nil) { return nil, [16]byte{}, err } + + return finalEncryptedRequest, requestIdentifier, nil +} + + +//Outputs: +// -[]int: Acceptable versions list +// -[10]byte: Range Start +// -[10]byte: Range End +// -[][26]byte: Reported message hashes list +// -error +func ReadDecryptedServerRequest_GetMessageReportsInfo(decryptedRequestBytes []byte)([]int, [10]byte, [10]byte, [][26]byte, error){ + + type decryptedRequestStruct struct{ + RequestVersion int + RequestType string + AcceptableVersions []int + ReportedMessagesList [][26]byte + RangeStart [10]byte + RangeEnd [10]byte + } + + var decryptedRequestObject decryptedRequestStruct + + err := encoding.DecodeMessagePackBytes(true, decryptedRequestBytes, &decryptedRequestObject) + if (err != nil) { return nil, [10]byte{}, [10]byte{}, nil, err } + + requestVersion := decryptedRequestObject.RequestVersion + requestType := decryptedRequestObject.RequestType + acceptableVersionsList := decryptedRequestObject.AcceptableVersions + reportedMessagesList := decryptedRequestObject.ReportedMessagesList + rangeStart := decryptedRequestObject.RangeStart + rangeEnd := decryptedRequestObject.RangeEnd + + if (requestVersion != 1){ + return nil, [10]byte{}, [10]byte{}, nil, errors.New("Invalid request: Unknown requestVersion.") + } + + if (requestType != "GetMessageReportsInfo"){ + return nil, [10]byte{}, [10]byte{}, nil, errors.New("Invalid request: requestType not GetMessageReportsInfo: " + requestType) + } + + //TODO: Verify everything + + err = verifyAcceptableVersionsList(acceptableVersionsList) + if (err != nil){ return nil, [10]byte{}, [10]byte{}, nil, err } + + return acceptableVersionsList, rangeStart, rangeEnd, reportedMessagesList, nil +} + + +//Outputs: +// -[]byte: Request bytes +// -[16]byte: Request identifier +// -error +func CreateServerRequest_GetReports(hostIdentityHash [16]byte, + connectionKey [32]byte, + networkType byte, + reportHashesList [][30]byte)([]byte, [16]byte, error){ + + //TODO: Verify inputs + + if (reportHashesList == nil){ + return nil, [16]byte{}, errors.New("CreateServerRequest_GetReports called with nil reportHashesList") + } + + if (len(reportHashesList) == 0){ + return nil, [16]byte{}, errors.New("CreateServerRequest_GetReports called with empty reportHashesList") + } + + containsDuplicate, _ := helpers.CheckIfListContainsDuplicates(reportHashesList) + if (containsDuplicate == true){ + return nil, [16]byte{}, errors.New("CreateServerRequest_GetReports called with reportHashesList containing duplicates.") + } + + requestIdentifier, err := getNewRequestIdentifier() + if (err != nil) { return nil, [16]byte{}, err } + + currentTime := time.Now().Unix() + + type innerRequestStruct struct{ + RequestVersion int + RequestType string + RequestTime int64 + RequestIdentifier [16]byte + RecipientHost [16]byte + NetworkType byte + ReportHashesList [][30]byte + } + + innerRequestObject := innerRequestStruct{ + RequestVersion: 1, + RequestType: "GetReports", + RequestTime: currentTime, + RequestIdentifier: requestIdentifier, + RecipientHost: hostIdentityHash, + NetworkType: networkType, + ReportHashesList: reportHashesList, + } + + innerRequestBytes, err := encoding.EncodeMessagePackBytes(innerRequestObject) + if (err != nil) { return nil, [16]byte{}, err } + + finalEncryptedRequest, err := createEncryptedRequest(connectionKey, innerRequestBytes) + if (err != nil) { return nil, [16]byte{}, err } + + return finalEncryptedRequest, requestIdentifier, nil +} + + +//Outputs: +// -[][30]byte: Report Hashes List +// -error +func ReadDecryptedServerRequest_GetReports(decryptedRequestBytes []byte)([][30]byte, error){ + + type innerRequestStruct struct{ + RequestVersion int + RequestType string + ReportHashesList [][30]byte + } + + var innerRequestObject innerRequestStruct + + err := encoding.DecodeMessagePackBytes(true, decryptedRequestBytes, &innerRequestObject) + if (err != nil) { return nil, err } + + requestVersion := innerRequestObject.RequestVersion + requestType := innerRequestObject.RequestType + reportHashesList := innerRequestObject.ReportHashesList + + if (requestVersion != 1){ + return nil, errors.New("Invalid request: Unknown requestVersion.") + } + + if (requestType != "GetReports"){ + return nil, errors.New("Invalid request: requestType not GetReports: " + requestType) + } + + //TODO: Verify reportHashesList + + return reportHashesList, nil +} + + +//Outputs: +// -[]byte: Request bytes +// -[16]byte: Request identifier +// -error +func CreateServerRequest_GetAddressDeposits(hostIdentityHash [16]byte, + connectionKey [32]byte, + networkType byte, + cryptocurrencyName string, + addressesList []string)([]byte, [16]byte, error){ + + //TODO: Verify inputs + + if (cryptocurrencyName != "Ethereum" && cryptocurrencyName != "Cardano"){ + return nil, [16]byte{}, errors.New("CreateServerRequest_GetAddressDeposits called with invalid cryptocurrencyName: " + cryptocurrencyName) + } + + if (len(addressesList) == 0){ + return nil, [16]byte{}, errors.New("CreateServerRequest_GetAddressDeposits called with empty addressesList.") + } + + containsDuplicates, _ := helpers.CheckIfListContainsDuplicates(addressesList) + if (containsDuplicates == true){ + return nil, [16]byte{}, errors.New("CreateServerRequest_GetAddressDeposits called with addressesList containing duplicates.") + } + + requestIdentifier, err := getNewRequestIdentifier() + if (err != nil) { return nil, [16]byte{}, err } + + currentTime := time.Now().Unix() + + type innerRequestStruct struct{ + RequestVersion int + RequestType string + RequestTime int64 + RequestIdentifier [16]byte + RecipientHost [16]byte + NetworkType byte + Cryptocurrency string + AddressesList []string + } + + innerRequestObject := innerRequestStruct{ + RequestVersion: 1, + RequestType: "GetAddressDeposits", + RequestTime: currentTime, + RequestIdentifier: requestIdentifier, + RecipientHost: hostIdentityHash, + NetworkType: networkType, + Cryptocurrency: cryptocurrencyName, + AddressesList: addressesList, + } + + innerRequestBytes, err := encoding.EncodeMessagePackBytes(innerRequestObject) + if (err != nil) { return nil, [16]byte{}, err } + + finalEncryptedRequest, err := createEncryptedRequest(connectionKey, innerRequestBytes) + if (err != nil) { return nil, [16]byte{}, err } + + return finalEncryptedRequest, requestIdentifier, nil +} + + +//Outputs: +// -string: Cryptocurrency +// -[]string: Addresses list +// -error +func ReadDecryptedServerRequest_GetAddressDeposits(decryptedRequestBytes []byte)(string, []string, error){ + + type decryptedRequestStruct struct{ + RequestVersion int + RequestType string + Cryptocurrency string + AddressesList []string + } + + var decryptedRequestObject decryptedRequestStruct + + err := encoding.DecodeMessagePackBytes(true, decryptedRequestBytes, &decryptedRequestObject) + if (err != nil) { return "", nil, err } + + requestVersion := decryptedRequestObject.RequestVersion + requestType := decryptedRequestObject.RequestType + cryptocurrency := decryptedRequestObject.Cryptocurrency + addressesList := decryptedRequestObject.AddressesList + + if (requestVersion != 1){ + return "", nil, errors.New("Invalid request: Unknown requestVersion.") + } + + if (requestType != "GetAddressDeposits"){ + return "", nil, errors.New("Invalid request: requestType not GetAddressDeposits: " + requestType) + } + + //TODO: Verify everything + + return cryptocurrency, addressesList, nil +} + + +// The reason we don't need a Messages option is because hosts will always use verifiedStickyStatuses when hosting messages +// A user would never need to know the banned status of a message they have received +// The message could not be banned unless the sender or recipient reported the message +// The recipient will only care if the identity is banned, not the message +//Outputs: +// -[]byte: Request bytes +// -[16]byte: Request identifier +// -error +func CreateServerRequest_GetViewableStatuses(hostIdentityHash [16]byte, + connectionKey [32]byte, + networkType byte, + identityHashesList [][16]byte, + profileHashesList [][28]byte)([]byte, [16]byte, error){ + + //TODO: Verify inputs + + if (len(identityHashesList) == 0 && len(profileHashesList) == 0){ + return nil, [16]byte{}, errors.New("CreateServerRequest_GetViewableStatuses called when identity hashes list and profile hashes list are both empty.") + } + + requestIdentifier, err := getNewRequestIdentifier() + if (err != nil) { return nil, [16]byte{}, err } + + currentTime := time.Now().Unix() + + type innerRequestStruct struct{ + RequestVersion int + RequestType string + RequestTime int64 + RequestIdentifier [16]byte + RecipientHost [16]byte + NetworkType byte + IdentityHashesList [][16]byte + ProfileHashesList [][28]byte + } + + innerRequestObject := innerRequestStruct{ + RequestVersion: 1, + RequestType: "GetViewableStatuses", + RequestTime: currentTime, + RequestIdentifier: requestIdentifier, + RecipientHost: hostIdentityHash, + NetworkType: networkType, + IdentityHashesList: identityHashesList, + ProfileHashesList: profileHashesList, + } + + innerRequestBytes, err := encoding.EncodeMessagePackBytes(innerRequestObject) + if (err != nil) { return nil, [16]byte{}, err } + + finalEncryptedRequest, err := createEncryptedRequest(connectionKey, innerRequestBytes) + if (err != nil) { return nil, [16]byte{}, err } + + return finalEncryptedRequest, requestIdentifier, nil +} + + +//Outputs: +// -[][16]byte: Identity Hashes List +// -[][28]byte: Profile hashes list +// -error +func ReadDecryptedServerRequest_GetViewableStatuses(decryptedRequestBytes []byte)([][16]byte, [][28]byte, error){ + + type requestStruct struct{ + RequestVersion int + RequestType string + IdentityHashesList [][16]byte + ProfileHashesList [][28]byte + } + + var decryptedRequestObject requestStruct + + err := encoding.DecodeMessagePackBytes(true, decryptedRequestBytes, &decryptedRequestObject) + if (err != nil) { return nil, nil, err } + + requestVersion := decryptedRequestObject.RequestVersion + requestType := decryptedRequestObject.RequestType + identityHashesList := decryptedRequestObject.IdentityHashesList + profileHashesList := decryptedRequestObject.ProfileHashesList + + if (requestVersion != 1){ + return nil, nil, errors.New("Invalid request: Unknown request version.") + } + + if (requestType != "GetViewableStatuses"){ + return nil, nil, errors.New("Invalid request: requestType not GetViewableStatuses: " + requestType) + } + + //TODO: Verify everything + + if (len(identityHashesList) == 0 && len(profileHashesList) == 0){ + return nil, nil, errors.New("GetViewableStatuses request contains empty identityHashesList and profileHashesList") + } + + return identityHashesList, profileHashesList, nil +} + + +//Outputs: +// -[]byte: Final Request +// -[16]byte: Request identifier +// -error +func CreateServerRequest_BroadcastContent(hostIdentityHash [16]byte, + connectionKey [32]byte, + networkType byte, + contentType string, + contentToBroadcastList [][]byte)([]byte, [16]byte, error){ + + //TODO: Verify inputs + + if (contentType != "Profile" && contentType != "Message" && contentType != "Review" && contentType != "Report" && contentType != "Parameters"){ + return nil, [16]byte{}, errors.New("CreateServerRequest_BroadcastContent called with invalid contentType: " + contentType) + } + + if (len(contentToBroadcastList) == 0){ + return nil, [16]byte{}, errors.New("Empty contentToBroadcast list") + } + + messagepackContentList := make([]messagepack.RawMessage, 0, len(contentToBroadcastList)) + + for _, contentBytes := range contentToBroadcastList{ + + messagepackContentList = append(messagepackContentList, contentBytes) + } + + requestIdentifier, err := getNewRequestIdentifier() + if (err != nil) { return nil, [16]byte{}, err } + + currentTime := time.Now().Unix() + + type requestContentStruct struct{ + RequestVersion int + RequestType string + RequestTime int64 + RequestIdentifier [16]byte + RecipientHost [16]byte + NetworkType byte + ContentType string + ContentList []messagepack.RawMessage + } + + requestContentObject := requestContentStruct{ + RequestVersion: 1, + RequestType: "BroadcastContent", + RequestTime: currentTime, + RequestIdentifier: requestIdentifier, + RecipientHost: hostIdentityHash, + NetworkType: networkType, + ContentType: contentType, + ContentList: messagepackContentList, + } + + innerRequestBytes, err := encoding.EncodeMessagePackBytes(requestContentObject) + if (err != nil) { return nil, [16]byte{}, err } + + finalEncryptedRequest, err := createEncryptedRequest(connectionKey, innerRequestBytes) + if (err != nil) { return nil, [16]byte{}, err } + + return finalEncryptedRequest, requestIdentifier, nil +} + + +//Outputs: +// -string: Content Type +// -[][]byte: Content list +// -error +func ReadDecryptedServerRequest_BroadcastContent(decryptedMessagePackBytes []byte)(string, [][]byte, error){ + + type requestContentStruct struct{ + RequestVersion int + RequestType string + ContentType string + ContentList []messagepack.RawMessage + } + + var requestContentObject requestContentStruct + + err := encoding.DecodeMessagePackBytes(true, decryptedMessagePackBytes, &requestContentObject) + if (err != nil) { return "", nil, err } + + requestVersion := requestContentObject.RequestVersion + requestType := requestContentObject.RequestType + contentType := requestContentObject.ContentType + messagepackContentList := requestContentObject.ContentList + + if (requestVersion != 1){ + return "", nil, errors.New("Invalid request: Unknown request version.") + } + + if (requestType != "BroadcastContent"){ + return "", nil, errors.New("Invalid request: requestType not BroadcastContent: " + requestType) + } + + if (contentType != "Profile" && contentType != "Message" && contentType != "Review" && contentType != "Report" && contentType != "Parameters"){ + return "", nil, errors.New("Invalid BroadcastContent request: Invalid contentType: " + contentType) + } + + //TODO: Verify everything + + // We use the map below to ensure we only received each piece of content once + receivedContentHashesMap := make(map[string]struct{}) + + contentList := make([][]byte, 0, len(messagepackContentList)) + + for _, contentBytes := range messagepackContentList{ + + ableToRead, contentHash, err := readContent.GetContentHashFromContentBytes(true, contentType, contentBytes) + if (err != nil) { return "", nil, err } + if (ableToRead == false){ + // Requestor sent invalid content. Requestor is malicious. + return "", nil, errors.New("Invalid BroadcastContent request: Invalid " + contentType) + } + + _, exists := receivedContentHashesMap[string(contentHash)] + if (exists == true){ + // We received a duplicate content hash. Requestor is malicious. + return "", nil, errors.New("Invalid BroadcastContent request: received duplicate content hash") + } + receivedContentHashesMap[string(contentHash)] = struct{}{} + + contentList = append(contentList, contentBytes) + } + + if (len(contentList) == 0){ + return "", nil, errors.New("Invalid BroadcastContent request: Empty content list") + } + + return contentType, contentList, nil +} + + +// This function will read the standard fields that are contained within all encrypted requests +//Outputs: +// -[16]byte: Request recipient host +// -string: Request Type +// -[16]byte: Request Identifier +// -byte: Network Type +// -error +func ReadDecryptedServerRequest_StandardData(decryptedMessagePackBytes []byte)([16]byte, string, [16]byte, byte, error){ + + type standardRequestContentStruct struct{ + RequestType string + RequestTime int64 + RequestIdentifier [16]byte + RecipientHost [16]byte + NetworkType byte + } + + var standardRequestContentObject standardRequestContentStruct + + err := encoding.DecodeMessagePackBytes(true, decryptedMessagePackBytes, &standardRequestContentObject) + if (err != nil) { return [16]byte{}, "", [16]byte{}, 0, err } + + //TODO: Verify everything + + requestType := standardRequestContentObject.RequestType + recipientHost := standardRequestContentObject.RecipientHost + requestIdentifier := standardRequestContentObject.RequestIdentifier + requestTime := standardRequestContentObject.RequestTime + networkType := standardRequestContentObject.NetworkType + + currentTime := time.Now().Unix() + timePassed := currentTime - requestTime + if (timePassed > 3600){ + //Request has timed out, probably sent by malicious party + return [16]byte{}, "", [16]byte{}, 0, errors.New(requestType + " Request has timed out.") + } + + return recipientHost, requestType, requestIdentifier, networkType, nil +} + + +func createEncryptedRequest(connectionKey [32]byte, contentToEncrypt []byte)([]byte, error){ + + chaPolyNonce, err := helpers.GetNewRandom24ByteArray() + if (err != nil) { return nil, err } + + cipheredContent, err := chaPolyShrink.EncryptChaPolyShrink(contentToEncrypt, connectionKey, chaPolyNonce, true, 1000, false, [32]byte{}) + if (err != nil) { return nil, err } + + type encryptedRequestStruct struct{ + ChaPolyNonce [24]byte + CipheredContent []byte + } + + encryptedRequestObject := encryptedRequestStruct{ + ChaPolyNonce: chaPolyNonce, + CipheredContent: cipheredContent, + } + + encryptedRequestBytes, err := encoding.EncodeMessagePackBytes(encryptedRequestObject) + if (err != nil) { return nil, err } + + return encryptedRequestBytes, nil +} + +//Outputs: +// -bool: Request is well formed and able to decrypt +// -[]byte: Decrypted request messagePack +// -error (decryption key inputs are malformed) +func ReadEncryptedRequest(inputRequest []byte, connectionKey [32]byte)(bool, []byte, error){ + + //TODO: Verify everything + + type encryptedRequestStruct struct{ + ChaPolyNonce [24]byte + CipheredContent []byte + } + + var encryptedRequestObject encryptedRequestStruct + + err := encoding.DecodeMessagePackBytes(false, inputRequest, &encryptedRequestObject) + if (err != nil) { + return false, nil, nil + } + + chaPolyNonce := encryptedRequestObject.ChaPolyNonce + cipheredContent := encryptedRequestObject.CipheredContent + + ableToDecrypt, decryptedBytes, err := chaPolyShrink.DecryptChaPolyShrink(cipheredContent, connectionKey, chaPolyNonce, false, [32]byte{}) + if (err != nil) { + return false, nil, nil + } + if (ableToDecrypt == false){ + return false, nil, nil + } + + return true, decryptedBytes, nil +} + + diff --git a/internal/network/serverRequest/serverRequest_test.go b/internal/network/serverRequest/serverRequest_test.go new file mode 100644 index 0000000..2f04a30 --- /dev/null +++ b/internal/network/serverRequest/serverRequest_test.go @@ -0,0 +1,1541 @@ +package serverRequest_test + +import "seekia/resources/geneticReferences/traits" +import "seekia/resources/geneticReferences/monogenicDiseases" +import "seekia/resources/geneticReferences/polygenicDiseases" + +import "seekia/internal/network/serverRequest" + +import "seekia/internal/byteRange" +import "seekia/internal/cryptocurrency/ethereumAddress" +import "seekia/internal/cryptocurrency/cardanoAddress" +import "seekia/internal/cryptography/kyber" +import "seekia/internal/cryptography/nacl" +import "seekia/internal/generate" +import "seekia/internal/helpers" +import "seekia/internal/identity" +import "seekia/internal/messaging/inbox" +import "seekia/internal/parameters/readParameters" +import "seekia/internal/profiles/profileFormat" + +import "testing" +import "reflect" +import "crypto/rand" +import "errors" +import "slices" + + +//TODO: Add None Case +//TODO: Replace the Random retrieval of certain parameters with a for loop that will cycle through each option +// For example, instead of choosing a profileType to retrieve by random, try all profileTypes +// This way, the tests will not have to be run multiple times to test all profileTypes + +func getNewRandomConnectionKey()([32]byte, error){ + + var newKeyArray [32]byte + + _, err := rand.Read(newKeyArray[:]) + if (err != nil) { return [32]byte{}, err } + + return newKeyArray, nil +} + + +func TestCreateAndReadRequest_EstablishConnectionKey(t *testing.T){ + + randomHostIdentityHash, err := identity.GetNewRandomIdentityHash(true, "Host") + if (err != nil) { + t.Fatalf("Failed to create random host identity hash: " + err.Error()) + } + + networkType, err := helpers.GetRandomByteWithinRange(1, 2) + if (err != nil){ + t.Fatalf("GetRandomByteWithinRange failed: " + err.Error()) + } + + requestorNaclPublicKey, err := nacl.GetNewRandomPublicNaclKey() + if (err != nil) { + t.Fatalf("GetNewRandomPublicNaclKey failed: " + err.Error()) + } + + requestorKyberPublicKey, err := kyber.GetNewRandomPublicKyberKey() + if (err != nil) { + t.Fatalf("GetNewRandomPublicKyberKey failed: " + err.Error()) + } + + serverRequestBytes, requestIdentifier, err := serverRequest.CreateServerRequest_EstablishConnectionKey(randomHostIdentityHash, networkType, requestorNaclPublicKey, requestorKyberPublicKey) + if (err != nil) { + t.Fatalf("Failed to create EstablishConnectionKey request: " + err.Error()) + } + + receivedHostIdentityHash, requestIdentifier_Received, requestNetworkType_Received, requestorNaclKey_Received, requestorKyberKey_Received, err := serverRequest.ReadServerRequest_EstablishConnectionKey(serverRequestBytes) + if (err != nil) { + t.Fatalf("Failed to read EstablishConnectionKey request: " + err.Error()) + } + + if (randomHostIdentityHash != receivedHostIdentityHash){ + t.Fatalf("EstablishConnectionKey receivedHostIdentityHash does not match.") + } + if (requestIdentifier_Received != requestIdentifier){ + t.Fatalf("EstablishConnectionKey requestIdentifier does not match.") + } + if (networkType != requestNetworkType_Received){ + t.Fatalf("EstablishConnectionKey networkType does not match.") + } + + if (requestorNaclPublicKey != requestorNaclKey_Received){ + t.Fatalf("EstablishConnectionKey receivedRequestorNaclKey does not match.") + } + + if (requestorKyberPublicKey != requestorKyberKey_Received){ + t.Fatalf("EstablishConnectionKey receivedRequestorKyberKey does not match.") + } +} + + +func TestCreateAndReadRequest_GetParametersInfo(t *testing.T){ + + recipientHostIdentityHash, err := identity.GetNewRandomIdentityHash(true, "Host") + if (err != nil) { + t.Fatalf("Failed to create random host identity hash: " + err.Error()) + } + + connectionKey, err := getNewRandomConnectionKey() + if (err != nil) { + t.Fatalf("getNewRandomConnectionKey failed: " + err.Error()) + } + + networkType, err := helpers.GetRandomByteWithinRange(1, 2) + if (err != nil){ + t.Fatalf("GetRandomByteWithinRange failed: " + err.Error()) + } + + requestBytes, requestIdentifier, err := serverRequest.CreateServerRequest_GetParametersInfo(recipientHostIdentityHash, connectionKey, networkType) + if (err != nil) { + t.Fatalf("Failed to create GetParametersInfo server request: " + err.Error()) + } + + ableToDecrypt, decryptedRequest, err := serverRequest.ReadEncryptedRequest(requestBytes, connectionKey) + if (err != nil) { + t.Fatalf("Failed to decrypt GetParametersInfo server request: invalid inputs: " + err.Error()) + } + if (ableToDecrypt == false) { + t.Fatalf("Failed to decrypt GetParametersInfo request.") + } + + recipientHost_Received, requestType_Received, requestIdentifier_Received, networkType_Received, err := serverRequest.ReadDecryptedServerRequest_StandardData(decryptedRequest) + if (err != nil) { + t.Fatalf("Failed to read decrypted GetParametersInfo request: " + err.Error()) + } + + if (recipientHost_Received != recipientHostIdentityHash){ + t.Fatalf("Failed to read decrypted GetParametersInfo request: Mismatched recipientHost") + } + if (requestType_Received != "GetParametersInfo"){ + t.Fatalf("Failed to read decrypted GetParametersInfo request: Invalid requestType") + } + if (requestIdentifier_Received != requestIdentifier){ + t.Fatalf("Failed to read decrypted GetParametersInfo request: Mismatched requestIdentifier") + } + if (networkType_Received != networkType){ + t.Fatalf("Failed to read decrypted GetParametersInfo request: Mismatched networkType") + } +} + + + +func TestCreateAndReadRequest_GetParameters(t *testing.T){ + + recipientHostIdentityHash, err := identity.GetNewRandomIdentityHash(true, "Host") + if (err != nil) { + t.Fatalf("Failed to create random host identity hash: " + err.Error()) + } + + connectionKey, err := getNewRandomConnectionKey() + if (err != nil) { + t.Fatalf("getNewRandomConnectionKey failed: " + err.Error()) + } + + networkType, err := helpers.GetRandomByteWithinRange(1, 2) + if (err != nil) { + t.Fatalf("GetRandomByteWithinRange failed: " + err.Error()) + } + + allParametersTypesList := readParameters.GetAllParametersTypesList() + + parametersTypesList := make([]string, 0) + + for index, element := range allParametersTypesList{ + + randomBool := helpers.GetRandomBool() + + if (index == 0 || randomBool == true){ + // We always include the first item, so we always include at least 1 parametersType + parametersTypesList = append(parametersTypesList, element) + } + } + + requestBytes, requestIdentifier, err := serverRequest.CreateServerRequest_GetParameters(recipientHostIdentityHash, connectionKey, networkType, parametersTypesList) + if (err != nil) { + t.Fatalf("Failed to create GetParameters server request: " + err.Error()) + } + + ableToDecrypt, decryptedRequest, err := serverRequest.ReadEncryptedRequest(requestBytes, connectionKey) + if (err != nil) { + t.Fatalf("Failed to decrypt GetParameters server request: invalid inputs: " + err.Error()) + } + if (ableToDecrypt == false) { + t.Fatalf("Failed to decrypt GetParameters request.") + } + + recipientHost_Received, requestType_Received, requestIdentifier_Received, networkType_Received, err := serverRequest.ReadDecryptedServerRequest_StandardData(decryptedRequest) + if (err != nil) { + t.Fatalf("Failed to read decrypted GetParameters request: " + err.Error()) + } + + if (recipientHost_Received != recipientHostIdentityHash){ + t.Fatalf("Failed to read decrypted GetParameters request: Mismatched recipientHost") + } + if (requestType_Received != "GetParameters"){ + t.Fatalf("Failed to read decrypted GetParameters request: Invalid requestType") + } + if (requestIdentifier_Received != requestIdentifier){ + t.Fatalf("Failed to read decrypted GetParameters request: Mismatched requestIdentifier") + } + if (networkType_Received != networkType){ + t.Fatalf("Failed to read decrypted GetParameters request: Mismatched networkType") + } + + parametersTypesList_Received, err := serverRequest.ReadDecryptedServerRequest_GetParameters(decryptedRequest) + if (err != nil){ + t.Fatalf("Failed to read GetParameters non-standard decrypted: " + err.Error()) + } + + areEqual := slices.Equal(parametersTypesList, parametersTypesList_Received) + if (areEqual == false){ + t.Fatalf("Failed to read decrypted GetParameters request: Mismatched parametersTypesList") + } + +} + +func TestCreateAndReadRequest_GetProfilesInfo(t *testing.T){ + + recipientHostIdentityHash, err := identity.GetNewRandomIdentityHash(true, "Host") + if (err != nil) { + t.Fatalf("Failed to create random host identity hash: " + err.Error()) + } + + connectionKey, err := getNewRandomConnectionKey() + if (err != nil) { + t.Fatalf("getNewRandomConnectionKey failed: " + err.Error()) + } + + networkType, err := helpers.GetRandomByteWithinRange(1, 2) + if (err != nil) { + t.Fatalf("GetRandomByteWithinRange failed: " + err.Error()) + } + + acceptableVersionsList := []int{1, 2} + + profileTypeToRetrieve, err := helpers.GetRandomItemFromList([]string{"Mate", "Host", "Moderator"}) + if (err != nil) { + t.Fatalf("Failed to get random profileType: " + err.Error()) + } + + rangeStart, err := identity.GetNewRandomIdentityHash(false, "") + if (err != nil) { + t.Fatalf("Failed to create random identity hash: " + err.Error()) + } + rangeEnd, err := identity.GetNewRandomIdentityHash(false, "") + if (err != nil) { + t.Fatalf("Failed to create random identity hash: " + err.Error()) + } + + identityHashesList := make([][16]byte, 0) + + for i:=0; i < 100; i++{ + + newIdentityHash, err := identity.GetNewRandomIdentityHash(true, profileTypeToRetrieve) + if (err != nil) { + t.Fatalf("Failed to create random identity hash: " + err.Error()) + } + + isWithinRange, err := byteRange.CheckIfIdentityHashIsWithinRange(rangeStart, rangeEnd, newIdentityHash) + if (err != nil){ + t.Fatalf("Failed to check if identity hash is within range: " + err.Error()) + } + if (isWithinRange == true){ + identityHashesList = append(identityHashesList, newIdentityHash) + } + } + + //TODO: Add random criteria + + requestBytes, requestIdentifier, err := serverRequest.CreateServerRequest_GetProfilesInfo(recipientHostIdentityHash, connectionKey, networkType, acceptableVersionsList, profileTypeToRetrieve, rangeStart, rangeEnd, identityHashesList, nil, false, true) + if (err != nil) { + t.Fatalf("Failed to create GetProfilesInfo server request: " + err.Error()) + } + + ableToDecrypt, decryptedRequestBytes, err := serverRequest.ReadEncryptedRequest(requestBytes, connectionKey) + if (err != nil) { + t.Fatalf("Failed to decrypt GetProfilesInfo server request: invalid inputs: " + err.Error()) + } + if (ableToDecrypt == false) { + t.Fatalf("Failed to decrypt GetProfiles request.") + } + + + recipientHost_Received, requestType_Received, requestIdentifier_Received, networkType_Received, err := serverRequest.ReadDecryptedServerRequest_StandardData(decryptedRequestBytes) + if (err != nil) { + t.Fatalf("Failed to read decrypted GetProfilesInfo request: " + err.Error()) + } + + if (recipientHost_Received != recipientHostIdentityHash){ + t.Fatalf("Failed to read decrypted GetProfilesInfo request: Mismatched recipientHost") + } + if (requestType_Received != "GetProfilesInfo"){ + t.Fatalf("Failed to read decrypted GetProfilesInfo request: Invalid requestType") + } + if (requestIdentifier_Received != requestIdentifier){ + t.Fatalf("Failed to read decrypted GetProfilesInfo request: Mismatched requestIdentifier") + } + if (networkType_Received != networkType){ + t.Fatalf("Failed to read decrypted GetProfilesInfo request: Mismatched networkType") + } + + acceptableVersionsList_Received, profileTypeToRetrieve_Received, rangeStart_Received, rangeEnd_Received, identityHashesList_Received, criteria_Received, getNewestProfilesOnly_Received, getViewableProfilesOnly_Received, err := serverRequest.ReadDecryptedServerRequest_GetProfilesInfo(decryptedRequestBytes) + if (err != nil){ + t.Fatalf("Failed to read GetProfilesInfo non-standard decrypted: " + err.Error()) + } + + if (profileTypeToRetrieve_Received != profileTypeToRetrieve){ + t.Fatalf("Failed to read decrypted GetProfilesInfo request: Mismatched profileTypeToRetrieve") + } + if (rangeStart != rangeStart_Received){ + t.Fatalf("Failed to read decrypted GetProfilesInfo request: Mismatched rangeStart") + } + if (rangeEnd != rangeEnd_Received){ + t.Fatalf("Failed to read decrypted GetProfilesInfo request: Mismatched rangeEnd") + } + if (getNewestProfilesOnly_Received != false){ + t.Fatalf("Failed to read decrypted GetProfilesInfo request: Mismatched getNewestProfilesOnly") + } + if (getViewableProfilesOnly_Received != true){ + t.Fatalf("Failed to read decrypted GetProfilesInfo request: Mismatched getViewableProfilesOnly") + } + if (criteria_Received != nil){ + t.Fatalf("Failed to read decrypted GetProfilesInfo request: Mismatched criteria") + } + + areEqual := slices.Equal(identityHashesList, identityHashesList_Received) + if (areEqual == false){ + t.Fatalf("Failed to read decrypted GetProfilesInfo request: Mismatched identityHashesList") + } + + areEqual = slices.Equal(acceptableVersionsList, acceptableVersionsList_Received) + if (areEqual == false){ + t.Fatalf("Failed to read decrypted GetProfilesInfo request: Mismatched acceptableVersionsList") + } +} + + +func TestCreateAndReadRequest_GetProfiles(t *testing.T){ + + recipientHostIdentityHash, err := identity.GetNewRandomIdentityHash(true, "Host") + if (err != nil) { + t.Fatalf("Failed to create random host identity hash: " + err.Error()) + } + + connectionKey, err := getNewRandomConnectionKey() + if (err != nil) { + t.Fatalf("getNewRandomConnectionKey failed: " + err.Error()) + } + + networkType, err := helpers.GetRandomByteWithinRange(1, 2) + if (err != nil) { + t.Fatalf("GetRandomByteWithinRange failed: " + err.Error()) + } + + profileTypeToRetrieve, err := helpers.GetRandomItemFromList([]string{"Mate", "Moderator", "Host"}) + if (err != nil) { + t.Fatalf("Failed to get random profileType: " + err.Error()) + } + + profileHashesList := make([][28]byte, 0, 100) + + for i:=0; i < 100; i++{ + + newProfileHash, err := helpers.GetNewRandomProfileHash(true, profileTypeToRetrieve, false, false) + if (err != nil) { + t.Fatalf("Failed to get random profile hash: " + err.Error()) + } + profileHashesList = append(profileHashesList, newProfileHash) + } + + requestBytes, requestIdentifier, err := serverRequest.CreateServerRequest_GetProfiles(recipientHostIdentityHash, connectionKey, networkType, profileTypeToRetrieve, profileHashesList) + if (err != nil) { + t.Fatalf("Failed to create GetProfiles server request: " + err.Error()) + } + + ableToDecrypt, decryptedRequestBytes, err := serverRequest.ReadEncryptedRequest(requestBytes, connectionKey) + if (err != nil) { + t.Fatalf("Failed to decrypt GetProfiles server request: invalid inputs: " + err.Error()) + } + if (ableToDecrypt == false) { + t.Fatalf("Failed to decrypt GetProfiles request.") + } + + recipientHost_Received, requestType_Received, requestIdentifier_Received, networkType_Received, err := serverRequest.ReadDecryptedServerRequest_StandardData(decryptedRequestBytes) + if (err != nil) { + t.Fatalf("Failed to read decrypted GetProfiles request: " + err.Error()) + } + + if (recipientHost_Received != recipientHostIdentityHash){ + t.Fatalf("Failed to read decrypted GetProfiles request: Mismatched recipientHost") + } + if (requestType_Received != "GetProfiles"){ + t.Fatalf("Failed to read decrypted GetProfiles request: Invalid requestType") + } + if (requestIdentifier_Received != requestIdentifier){ + t.Fatalf("Failed to read decrypted GetProfiles request: Mismatched requestIdentifier") + } + if (networkType_Received != networkType){ + t.Fatalf("Failed to read decrypted GetProfiles request: Mismatched networkType") + } + + profileTypeToRetrieve_Received, profileHashesList_Received, err := serverRequest.ReadDecryptedServerRequest_GetProfiles(decryptedRequestBytes) + if (err != nil){ + t.Fatalf("Failed to read GetProfiles non-standard decrypted: " + err.Error()) + } + + if (profileTypeToRetrieve_Received != profileTypeToRetrieve){ + t.Fatalf("Failed to read decrypted GetProfiles request: Mismatched profileTypeToRetrieve") + } + + areEqual := slices.Equal(profileHashesList, profileHashesList_Received) + if (areEqual == false){ + t.Fatalf("Failed to read decrypted GetProfiles request: Mismatched profileHashesList") + } +} + + +func TestCreateAndReadRequest_GetMessageHashesList(t *testing.T){ + + recipientHostIdentityHash, err := identity.GetNewRandomIdentityHash(true, "Host") + if (err != nil) { + t.Fatalf("Failed to create random host identity hash: " + err.Error()) + } + + connectionKey, err := getNewRandomConnectionKey() + if (err != nil) { + t.Fatalf("getNewRandomConnectionKey failed: " + err.Error()) + } + + networkType, err := helpers.GetRandomByteWithinRange(1, 2) + if (err != nil) { + t.Fatalf("GetRandomByteWithinRange failed: " + err.Error()) + } + + acceptableVersionsList := []int{1, 2, 3} + + rangeStart, err := inbox.GetNewRandomInbox() + if (err != nil) { + t.Fatalf("GetNewRandomInbox failed: " + err.Error()) + } + + rangeEnd, err := inbox.GetNewRandomInbox() + if (err != nil) { + t.Fatalf("GetNewRandomInbox failed: " + err.Error()) + } + + inboxesList := make([][10]byte, 0) + + for i:=0; i < 100; i++{ + + newInbox, err := inbox.GetNewRandomInbox() + if (err != nil){ + t.Fatalf("GetNewRandomInbox failed: " + err.Error()) + } + + isWithinRange, err := byteRange.CheckIfInboxIsWithinRange(rangeStart, rangeEnd, newInbox) + if (err != nil){ + t.Fatalf("Failed to CheckIfInboxIsWithinRange: " + err.Error()) + } + + if (isWithinRange == true){ + + inboxesList = append(inboxesList, newInbox) + } + } + + getViewableMessagesOnly := helpers.GetRandomBool() + getDecryptableMessagesOnly := helpers.GetRandomBool() + + requestBytes, requestIdentifier, err := serverRequest.CreateServerRequest_GetMessageHashesList(recipientHostIdentityHash, connectionKey, networkType, acceptableVersionsList, rangeStart, rangeEnd, inboxesList, getViewableMessagesOnly, getDecryptableMessagesOnly) + if (err != nil) { + t.Fatalf("Failed to create GetMessageHashesList server request: " + err.Error()) + } + + ableToDecrypt, decryptedRequestBytes, err := serverRequest.ReadEncryptedRequest(requestBytes, connectionKey) + if (err != nil) { + t.Fatalf("Failed to decrypt GetMessageHashesList server request: invalid inputs: " + err.Error()) + } + if (ableToDecrypt == false) { + t.Fatalf("Failed to decrypt GetMessageHashesList request.") + } + + recipientHost_Received, requestType_Received, requestIdentifier_Received, networkType_Received, err := serverRequest.ReadDecryptedServerRequest_StandardData(decryptedRequestBytes) + if (err != nil) { + t.Fatalf("Failed to read decrypted GetMessageHashesList request: " + err.Error()) + } + + if (recipientHost_Received != recipientHostIdentityHash){ + t.Fatalf("Failed to read decrypted GetMessageHashesList request: Mismatched recipientHost") + } + if (requestType_Received != "GetMessageHashesList"){ + t.Fatalf("Failed to read decrypted GetMessageHashesList request: Invalid requestType") + } + if (requestIdentifier_Received != requestIdentifier){ + t.Fatalf("Failed to read decrypted GetMessageHashesList request: Mismatched requestIdentifier") + } + if (networkType_Received != networkType){ + t.Fatalf("Failed to read decrypted GetMessageHashesList request: Mismatched networkType") + } + + acceptableVersionsList_Received, rangeStart_Received, rangeEnd_Received, inboxesList_Received, getViewableMessagesOnly_Received, getDecryptableMessagesOnly_Received, err := serverRequest.ReadDecryptedServerRequest_GetMessageHashesList(decryptedRequestBytes) + if (err != nil){ + t.Fatalf("Failed to read GetMessageHashesList non-standard decrypted: " + err.Error()) + } + + if (rangeStart != rangeStart_Received){ + t.Fatalf("Failed to read decrypted GetMessageHashesList request: Mismatched rangeStart") + } + if (rangeEnd != rangeEnd_Received){ + t.Fatalf("Failed to read decrypted GetMessageHashesList request: Mismatched rangeEnd") + } + if (getViewableMessagesOnly_Received != getViewableMessagesOnly){ + t.Fatalf("Failed to read decrypted GetMessageHashesList request: Mismatched getViewableMessagesOnly") + } + if (getDecryptableMessagesOnly_Received != getDecryptableMessagesOnly){ + t.Fatalf("Failed to read decrypted GetMessageHashesList request: Mismatched getDecryptableMessagesOnly") + } + + areEqual := slices.Equal(acceptableVersionsList, acceptableVersionsList_Received) + if (areEqual == false){ + t.Fatalf("Failed to read decrypted GetMessageHashesList request: Mismatched acceptableVersionsList") + } + + areEqual = slices.Equal(inboxesList, inboxesList_Received) + if (areEqual == false){ + t.Fatalf("Failed to read decrypted GetMessageHashesList request: Mismatched inboxesList") + } +} + + +func TestCreateAndReadRequest_GetMessages(t *testing.T){ + + recipientHostIdentityHash, err := identity.GetNewRandomIdentityHash(true, "Host") + if (err != nil) { + t.Fatalf("Failed to create random host identity hash: " + err.Error()) + } + + connectionKey, err := getNewRandomConnectionKey() + if (err != nil) { + t.Fatalf("getNewRandomConnectionKey failed: " + err.Error()) + } + + networkType, err := helpers.GetRandomByteWithinRange(1, 2) + if (err != nil) { + t.Fatalf("GetRandomByteWithinRange failed: " + err.Error()) + } + + messageHashesList := make([][26]byte, 0, 100) + + for i:=0; i < 100; i++{ + + newMessageHash, err := helpers.GetNewRandomMessageHash() + if (err != nil){ + t.Fatalf("GetNewRandomMessageHash failed: " + err.Error()) + } + messageHashesList = append(messageHashesList, newMessageHash) + } + + requestBytes, requestIdentifier, err := serverRequest.CreateServerRequest_GetMessages(recipientHostIdentityHash, connectionKey, networkType, messageHashesList) + if (err != nil) { + t.Fatalf("Failed to create GetMessages server request: " + err.Error()) + } + + ableToDecrypt, decryptedRequestBytes, err := serverRequest.ReadEncryptedRequest(requestBytes, connectionKey) + if (err != nil) { + t.Fatalf("Failed to decrypt GetMessages server request: invalid inputs: " + err.Error()) + } + if (ableToDecrypt == false) { + t.Fatalf("Failed to decrypt GetMessages request.") + } + + recipientHost_Received, requestType_Received, requestIdentifier_Received, networkType_Received, err := serverRequest.ReadDecryptedServerRequest_StandardData(decryptedRequestBytes) + if (err != nil) { + t.Fatalf("Failed to read decrypted GetMessages request: " + err.Error()) + } + + if (recipientHost_Received != recipientHostIdentityHash){ + t.Fatalf("Failed to read decrypted GetMessages request: Mismatched recipientHost") + } + if (requestType_Received != "GetMessages"){ + t.Fatalf("Failed to read decrypted GetMessages request: Invalid requestType") + } + if (requestIdentifier_Received != requestIdentifier){ + t.Fatalf("Failed to read decrypted GetMessages request: Mismatched requestIdentifier") + } + if (networkType_Received != networkType){ + t.Fatalf("Failed to read decrypted GetMessages request: Mismatched networkType") + } + + messageHashesList_Received, err := serverRequest.ReadDecryptedServerRequest_GetMessages(decryptedRequestBytes) + if (err != nil){ + t.Fatalf("Failed to read GetMessages non-standard decrypted: " + err.Error()) + } + + areEqual := slices.Equal(messageHashesList, messageHashesList_Received) + if (areEqual == false){ + t.Fatalf("Failed to read decrypted GetMessages request: Mismatched messageHashesList") + } +} + + + +func TestCreateAndReadRequest_GetIdentityReviewsInfo(t *testing.T){ + + recipientHostIdentityHash, err := identity.GetNewRandomIdentityHash(true, "Host") + if (err != nil) { + t.Fatalf("Failed to create random host identity hash: " + err.Error()) + } + + connectionKey, err := getNewRandomConnectionKey() + if (err != nil) { + t.Fatalf("getNewRandomConnectionKey failed: " + err.Error()) + } + + networkType, err := helpers.GetRandomByteWithinRange(1, 2) + if (err != nil) { + t.Fatalf("GetRandomByteWithinRange failed: " + err.Error()) + } + + acceptableVersionsList := []int{1, 2, 3, 4, 5} + + identityTypeToRetrieve, err := helpers.GetRandomItemFromList([]string{"Mate", "Host", "Moderator"}) + if (err != nil) { + t.Fatalf("Failed to get random identityType: " + err.Error()) + } + + rangeStart, err := identity.GetNewRandomIdentityHash(false, "") + if (err != nil){ + t.Fatalf("Failed to create random identity hash: " + err.Error()) + } + + rangeEnd, err := identity.GetNewRandomIdentityHash(false, "") + if (err != nil){ + t.Fatalf("Failed to create random identity hash: " + err.Error()) + } + + reviewedIdentityHashesList := make([][16]byte, 0) + + for i:=0; i < 100; i++{ + + newIdentityHash, err := identity.GetNewRandomIdentityHash(true, identityTypeToRetrieve) + if (err != nil) { + t.Fatalf("Failed to create random identity hash: " + err.Error()) + } + + isWithinRange, err := byteRange.CheckIfIdentityHashIsWithinRange(rangeStart, rangeEnd, newIdentityHash) + if (err != nil){ + t.Fatalf("CheckIfIdentityHashIsWithinRange failed: " + err.Error()) + } + + if (isWithinRange == true){ + reviewedIdentityHashesList = append(reviewedIdentityHashesList, newIdentityHash) + } + } + + reviewersList := make([][16]byte, 0, 100) + + for i:=0; i < 100; i++{ + + newIdentityHash, err := identity.GetNewRandomIdentityHash(true, "Moderator") + if (err != nil) { + t.Fatalf("Failed to create random identity hash: " + err.Error()) + } + + reviewersList = append(reviewersList, newIdentityHash) + } + + requestBytes, requestIdentifier, err := serverRequest.CreateServerRequest_GetIdentityReviewsInfo(recipientHostIdentityHash, connectionKey, networkType, acceptableVersionsList, identityTypeToRetrieve, rangeStart, rangeEnd, reviewedIdentityHashesList, reviewersList) + if (err != nil) { + t.Fatalf("Failed to create GetIdentityReviewsInfo server request: " + err.Error()) + } + + ableToDecrypt, decryptedRequestBytes, err := serverRequest.ReadEncryptedRequest(requestBytes, connectionKey) + if (err != nil) { + t.Fatalf("Failed to decrypt GetIdentityReviewsInfo server request: invalid inputs: " + err.Error()) + } + if (ableToDecrypt == false) { + t.Fatalf("Failed to decrypt GetIdentityReviewsInfo request.") + } + + recipientHost_Received, requestType_Received, requestIdentifier_Received, networkType_Received, err := serverRequest.ReadDecryptedServerRequest_StandardData(decryptedRequestBytes) + if (err != nil) { + t.Fatalf("Failed to read decrypted GetIdentityReviewsInfo request: " + err.Error()) + } + + if (recipientHost_Received != recipientHostIdentityHash){ + t.Fatalf("Failed to read decrypted GetIdentityReviewsInfo request: Mismatched recipientHost") + } + if (requestType_Received != "GetIdentityReviewsInfo"){ + t.Fatalf("Failed to read decrypted GetIdentityReviewsInfo request: Invalid requestType: " + requestType_Received) + } + if (requestIdentifier_Received != requestIdentifier){ + t.Fatalf("Failed to read decrypted GetIdentityReviewsInfo request: Mismatched requestIdentifier") + } + if (networkType_Received != networkType){ + t.Fatalf("Failed to read decrypted GetIdentityReviewsInfo request: Mismatched networkType") + } + + acceptableVersionsList_Received, identityTypeToRetrieve_Received, rangeStart_Received, rangeEnd_Received, reviewedIdentityHashesList_Received, reviewersList_Received, err := serverRequest.ReadDecryptedServerRequest_GetIdentityReviewsInfo(decryptedRequestBytes) + if (err != nil){ + t.Fatalf("Failed to read GetIdentityReviewsInfo non-standard decrypted: " + err.Error()) + } + if (identityTypeToRetrieve_Received != identityTypeToRetrieve){ + t.Fatalf("Failed to read decrypted GetIdentityReviewsInfo request: Mismatched identityType" + identityTypeToRetrieve_Received) + } + if (rangeStart != rangeStart_Received){ + t.Fatalf("Failed to read decrypted GetIdentityReviewsInfo request: Mismatched rangeStart") + } + if (rangeEnd != rangeEnd_Received){ + t.Fatalf("Failed to read decrypted GetIdentityReviewsInfo request: Mismatched rangeEnd") + } + + areEqual := slices.Equal(acceptableVersionsList, acceptableVersionsList_Received) + if (areEqual == false){ + t.Fatalf("Failed to read decrypted GetIdentityReviewsInfo request: Mismatched acceptableVersionsList") + } + + areEqual = slices.Equal(reviewedIdentityHashesList, reviewedIdentityHashesList_Received) + if (areEqual == false){ + t.Fatalf("Failed to read decrypted GetIdentityReviewsInfo request: Mismatched reviewedIdentityHashesList") + } + + areEqual = slices.Equal(reviewersList, reviewersList_Received) + if (areEqual == false){ + t.Fatalf("Failed to read decrypted GetIdentityReviewsInfo request: Mismatched reviewersList") + } +} + + + +func TestCreateAndReadRequest_GetMessageReviewsInfo(t *testing.T){ + + recipientHostIdentityHash, err := identity.GetNewRandomIdentityHash(true, "Host") + if (err != nil) { + t.Fatalf("Failed to create random host identity hash: " + err.Error()) + } + + connectionKey, err := getNewRandomConnectionKey() + if (err != nil) { + t.Fatalf("getNewRandomConnectionKey failed: " + err.Error()) + } + + networkType, err := helpers.GetRandomByteWithinRange(1, 2) + if (err != nil) { + t.Fatalf("GetRandomByteWithinRange failed: " + err.Error()) + } + + acceptableVersionsList := []int{1, 2, 3, 4, 5} + + rangeStart, err := inbox.GetNewRandomInbox() + if (err != nil) { + t.Fatalf("GetNewRandomInbox failed: " + err.Error()) + } + + rangeEnd, err := inbox.GetNewRandomInbox() + if (err != nil) { + t.Fatalf("GetNewRandomInbox failed: " + err.Error()) + } + + reviewedMessageHashesList := make([][26]byte, 0, 100) + + for i:=0; i < 100; i++{ + + newMessageHash, err := helpers.GetNewRandomMessageHash() + if (err != nil) { + t.Fatalf("GetNewRandomMessageHash failed: " + err.Error()) + } + + reviewedMessageHashesList = append(reviewedMessageHashesList, newMessageHash) + } + + reviewersList := make([][16]byte, 0, 100) + + for i:=0; i < 100; i++{ + + newIdentityHash, err := identity.GetNewRandomIdentityHash(true, "Moderator") + if (err != nil) { + t.Fatalf("Failed to create random identity hash: " + err.Error()) + } + + reviewersList = append(reviewersList, newIdentityHash) + } + + requestBytes, requestIdentifier, err := serverRequest.CreateServerRequest_GetMessageReviewsInfo(recipientHostIdentityHash, connectionKey, networkType, acceptableVersionsList, rangeStart, rangeEnd, reviewedMessageHashesList, reviewersList) + if (err != nil) { + t.Fatalf("Failed to create GetMessageReviewsInfo server request: " + err.Error()) + } + + ableToDecrypt, decryptedRequestBytes, err := serverRequest.ReadEncryptedRequest(requestBytes, connectionKey) + if (err != nil) { + t.Fatalf("Failed to decrypt GetMessageReviewsInfo server request: invalid inputs: " + err.Error()) + } + if (ableToDecrypt == false) { + t.Fatalf("Failed to decrypt GetMessageReviewsInfo request.") + } + + recipientHost_Received, requestType_Received, requestIdentifier_Received, networkType_Received, err := serverRequest.ReadDecryptedServerRequest_StandardData(decryptedRequestBytes) + if (err != nil) { + t.Fatalf("Failed to read decrypted GetMessageReviewsInfo request: " + err.Error()) + } + + if (recipientHost_Received != recipientHostIdentityHash){ + t.Fatalf("Failed to read decrypted GetMessageReviewsInfo request: Mismatched recipientHost") + } + if (requestType_Received != "GetMessageReviewsInfo"){ + t.Fatalf("Failed to read decrypted GetMessageReviewsInfo request: Invalid requestType") + } + if (requestIdentifier_Received != requestIdentifier){ + t.Fatalf("Failed to read decrypted GetMessageReviewsInfo request: Mismatched requestIdentifier") + } + if (networkType_Received != networkType){ + t.Fatalf("Failed to read decrypted GetMessageReviewsInfo request: Mismatched networkType") + } + + acceptableVersionsList_Received, rangeStart_Received, rangeEnd_Received, reviewedMessageHashesList_Received, reviewersList_Received, err := serverRequest.ReadDecryptedServerRequest_GetMessageReviewsInfo(decryptedRequestBytes) + if (err != nil){ + t.Fatalf("Failed to read GetMessageReviewsInfo non-standard decrypted: " + err.Error()) + } + + if (rangeStart != rangeStart_Received){ + t.Fatalf("Failed to read decrypted GetMessageReviewsInfo request: Mismatched rangeStart") + } + if (rangeEnd != rangeEnd_Received){ + t.Fatalf("Failed to read decrypted GetMessageReviewsInfo request: Mismatched rangeEnd") + } + + areEqual := slices.Equal(acceptableVersionsList, acceptableVersionsList_Received) + if (areEqual == false){ + t.Fatalf("Failed to read decrypted GetMessageReviewsInfo request: Mismatched acceptableVersionsList") + } + + areEqual = slices.Equal(reviewedMessageHashesList, reviewedMessageHashesList_Received) + if (areEqual == false){ + t.Fatalf("Failed to read decrypted GetMessageReviewsInfo request: Mismatched reviewedHashesList") + } + + areEqual = slices.Equal(reviewersList, reviewersList_Received) + if (areEqual == false){ + t.Fatalf("Failed to read decrypted GetMessageReviewsInfo request: Mismatched reviewersList") + } +} + + + +func TestCreateAndReadRequest_GetReviews(t *testing.T){ + + recipientHostIdentityHash, err := identity.GetNewRandomIdentityHash(true, "Host") + if (err != nil) { + t.Fatalf("Failed to create random host identity hash: " + err.Error()) + } + + connectionKey, err := getNewRandomConnectionKey() + if (err != nil) { + t.Fatalf("getNewRandomConnectionKey failed: " + err.Error()) + } + + networkType, err := helpers.GetRandomByteWithinRange(1, 2) + if (err != nil) { + t.Fatalf("GetRandomByteWithinRange failed: " + err.Error()) + } + + reviewHashesList := make([][29]byte, 0, 100) + + for i:=0; i < 100; i++{ + + newReviewHash, err := helpers.GetNewRandomReviewHash(false, "") + if (err != nil){ + t.Fatalf("GetNewRandomReviewHash failed: " + err.Error()) + } + + reviewHashesList = append(reviewHashesList, newReviewHash) + } + + requestBytes, requestIdentifier, err := serverRequest.CreateServerRequest_GetReviews(recipientHostIdentityHash, connectionKey, networkType, reviewHashesList) + if (err != nil) { + t.Fatalf("Failed to create GetReviews server request: " + err.Error()) + } + + ableToDecrypt, decryptedRequestBytes, err := serverRequest.ReadEncryptedRequest(requestBytes, connectionKey) + if (err != nil) { + t.Fatalf("Failed to decrypt GetReviews server request: invalid inputs: " + err.Error()) + } + if (ableToDecrypt == false) { + t.Fatalf("Failed to decrypt GetReviews request.") + } + + recipientHost_Received, requestType_Received, requestIdentifier_Received, networkType_Received, err := serverRequest.ReadDecryptedServerRequest_StandardData(decryptedRequestBytes) + if (err != nil) { + t.Fatalf("Failed to read decrypted GetReviews request: " + err.Error()) + } + + if (recipientHost_Received != recipientHostIdentityHash){ + t.Fatalf("Failed to read decrypted GetReviews request: Mismatched recipientHost") + } + if (requestType_Received != "GetReviews"){ + t.Fatalf("Failed to read decrypted GetReviews request: Invalid requestType") + } + if (requestIdentifier_Received != requestIdentifier){ + t.Fatalf("Failed to read decrypted GetReviews request: Mismatched requestIdentifier") + } + if (networkType_Received != networkType){ + t.Fatalf("Failed to read decrypted GetReviews request: Mismatched networkType") + } + + reviewHashesList_Received, err := serverRequest.ReadDecryptedServerRequest_GetReviews(decryptedRequestBytes) + if (err != nil){ + t.Fatalf("Failed to read GetReviews non-standard decrypted: " + err.Error()) + } + + areEqual := slices.Equal(reviewHashesList, reviewHashesList_Received) + if (areEqual == false){ + t.Fatalf("Failed to read decrypted GetReviews request: Mismatched messageHashesList") + } +} + +func TestCreateAndReadRequest_GetIdentityReportsInfo(t *testing.T){ + + recipientHostIdentityHash, err := identity.GetNewRandomIdentityHash(true, "Host") + if (err != nil) { + t.Fatalf("Failed to create random host identity hash: " + err.Error()) + } + + connectionKey, err := getNewRandomConnectionKey() + if (err != nil) { + t.Fatalf("getNewRandomConnectionKey failed: " + err.Error()) + } + + networkType, err := helpers.GetRandomByteWithinRange(1, 2) + if (err != nil) { + t.Fatalf("GetRandomByteWithinRange failed: " + err.Error()) + } + + acceptableVersionsList := []int{2} + + identityTypeToRetrieve, err := helpers.GetRandomItemFromList([]string{"Mate", "Host", "Moderator"}) + if (err != nil) { + t.Fatalf("Failed to get random identityType: " + err.Error()) + } + + rangeStart, err := identity.GetNewRandomIdentityHash(false, "") + if (err != nil) { + t.Fatalf("Failed to create random identity hash: " + err.Error()) + } + + rangeEnd, err := identity.GetNewRandomIdentityHash(false, "") + if (err != nil) { + t.Fatalf("Failed to create random identity hash: " + err.Error()) + } + + reportedIdentityHashesList := make([][16]byte, 0) + + for i:=0; i < 100; i++{ + + newIdentityHash, err := identity.GetNewRandomIdentityHash(true, identityTypeToRetrieve) + if (err != nil) { + t.Fatalf("Failed to create random identity hash") + } + + isWithinRange, err := byteRange.CheckIfIdentityHashIsWithinRange(rangeStart, rangeEnd, newIdentityHash) + if (err != nil){ + t.Fatalf("GetIdentityHashWithinRange failed: " + err.Error()) + } + + if (isWithinRange == true){ + reportedIdentityHashesList = append(reportedIdentityHashesList, newIdentityHash) + } + } + + requestBytes, requestIdentifier, err := serverRequest.CreateServerRequest_GetIdentityReportsInfo(recipientHostIdentityHash, connectionKey, networkType, acceptableVersionsList, identityTypeToRetrieve, rangeStart, rangeEnd, reportedIdentityHashesList) + if (err != nil) { + t.Fatalf("Failed to create GetIdentityReportsInfo server request: " + err.Error()) + } + + ableToDecrypt, decryptedRequestBytes, err := serverRequest.ReadEncryptedRequest(requestBytes, connectionKey) + if (err != nil) { + t.Fatalf("Failed to decrypt GetIdentityReportsInfo server request: invalid inputs: " + err.Error()) + } + if (ableToDecrypt == false) { + t.Fatalf("Failed to decrypt GetIdentityReportsInfo request.") + } + + recipientHost_Received, requestType_Received, requestIdentifier_Received, networkType_Received, err := serverRequest.ReadDecryptedServerRequest_StandardData(decryptedRequestBytes) + if (err != nil) { + t.Fatalf("Failed to read decrypted GetIdentityReportsInfo request: " + err.Error()) + } + + if (recipientHost_Received != recipientHostIdentityHash){ + t.Fatalf("Failed to read decrypted GetIdentityReportsInfo request: Mismatched recipientHost") + } + if (requestType_Received != "GetIdentityReportsInfo"){ + t.Fatalf("Failed to read decrypted GetIdentityReportsInfo request: Invalid requestType: " + requestType_Received) + } + if (requestIdentifier_Received != requestIdentifier){ + t.Fatalf("Failed to read decrypted GetIdentityReportsInfo request: Mismatched requestIdentifier") + } + if (networkType_Received != networkType){ + t.Fatalf("Failed to read decrypted GetIdentityReportsInfo request: Mismatched networkType") + } + + acceptableVersionsList_Received, identityTypeToRetrieve_Received, rangeStart_Received, rangeEnd_Received, reportedIdentityHashesList_Received, err := serverRequest.ReadDecryptedServerRequest_GetIdentityReportsInfo(decryptedRequestBytes) + if (err != nil){ + t.Fatalf("Failed to read GetIdentityReportsInfo non-standard decrypted: " + err.Error()) + } + + if (identityTypeToRetrieve_Received != identityTypeToRetrieve){ + t.Fatalf("Failed to read decrypted GetIdentityReportsInfo request: Mismatched identityType: " + identityTypeToRetrieve_Received) + } + + if (rangeStart != rangeStart_Received){ + t.Fatalf("Failed to read decrypted GetIdentityReportsInfo request: Mismatched rangeStart") + } + if (rangeEnd != rangeEnd_Received){ + t.Fatalf("Failed to read decrypted GetIdentityReportsInfo request: Mismatched rangeEnd") + } + + areEqual := slices.Equal(acceptableVersionsList, acceptableVersionsList_Received) + if (areEqual == false){ + t.Fatalf("Failed to read decrypted GetIdentityReportsInfo request: Mismatched acceptableVersionsList") + } + + areEqual = slices.Equal(reportedIdentityHashesList, reportedIdentityHashesList_Received) + if (areEqual == false){ + t.Fatalf("Failed to read decrypted GetIdentityReportsInfo request: Mismatched reportedIdentityHashesList") + } +} + + +func TestCreateAndReadRequest_GetMessageReportsInfo(t *testing.T){ + + recipientHostIdentityHash, err := identity.GetNewRandomIdentityHash(true, "Host") + if (err != nil) { + t.Fatalf("Failed to create random host identity hash: " + err.Error()) + } + + connectionKey, err := getNewRandomConnectionKey() + if (err != nil) { + t.Fatalf("getNewRandomConnectionKey failed: " + err.Error()) + } + + networkType, err := helpers.GetRandomByteWithinRange(1, 2) + if (err != nil) { + t.Fatalf("GetRandomByteWithinRange failed: " + err.Error()) + } + + acceptableVersionsList := []int{2} + + rangeStart, err := inbox.GetNewRandomInbox() + if (err != nil) { + t.Fatalf("GetNewRandomInbox failed: " + err.Error()) + } + rangeEnd, err := inbox.GetNewRandomInbox() + if (err != nil) { + t.Fatalf("GetNewRandomInbox failed: " + err.Error()) + } + + reportedMessageHashesList := make([][26]byte, 0, 100) + + for i:=0; i < 100; i++{ + + newMessageHash, err := helpers.GetNewRandomMessageHash() + if (err != nil) { + t.Fatalf("GetNewRandomMessageHash failed: " + err.Error()) + } + + reportedMessageHashesList = append(reportedMessageHashesList, newMessageHash) + } + + requestBytes, requestIdentifier, err := serverRequest.CreateServerRequest_GetMessageReportsInfo(recipientHostIdentityHash, connectionKey, networkType, acceptableVersionsList, rangeStart, rangeEnd, reportedMessageHashesList) + if (err != nil) { + t.Fatalf("Failed to create GetMessageReportsInfo server request: " + err.Error()) + } + + ableToDecrypt, decryptedRequestBytes, err := serverRequest.ReadEncryptedRequest(requestBytes, connectionKey) + if (err != nil) { + t.Fatalf("Failed to decrypt GetMessageReportsInfo server request: invalid inputs: " + err.Error()) + } + if (ableToDecrypt == false) { + t.Fatalf("Failed to decrypt GetMessageReportsInfo request.") + } + + recipientHost_Received, requestType_Received, requestIdentifier_Received, networkType_Received, err := serverRequest.ReadDecryptedServerRequest_StandardData(decryptedRequestBytes) + if (err != nil) { + t.Fatalf("Failed to read decrypted GetMessageReportsInfo request: " + err.Error()) + } + + if (recipientHost_Received != recipientHostIdentityHash){ + t.Fatalf("Failed to read decrypted GetMessageReportsInfo request: Mismatched recipientHost") + } + if (requestType_Received != "GetMessageReportsInfo"){ + t.Fatalf("Failed to read decrypted GetMessageReportsInfo request: Invalid requestType: " + requestType_Received) + } + if (requestIdentifier_Received != requestIdentifier){ + t.Fatalf("Failed to read decrypted GetMessageReportsInfo request: Mismatched requestIdentifier") + } + if (networkType_Received != networkType){ + t.Fatalf("Failed to read decrypted GetMessageReportsInfo request: Mismatched networkType") + } + + acceptableVersionsList_Received, rangeStart_Received, rangeEnd_Received, reportedMessageHashesList_Received, err := serverRequest.ReadDecryptedServerRequest_GetMessageReportsInfo(decryptedRequestBytes) + if (err != nil){ + t.Fatalf("Failed to read GetMessageReportsInfo non-standard decrypted: " + err.Error()) + } + + if (rangeStart != rangeStart_Received){ + t.Fatalf("Failed to read decrypted GetMessageReportsInfo request: Mismatched rangeStart") + } + if (rangeEnd != rangeEnd_Received){ + t.Fatalf("Failed to read decrypted GetMessageReportsInfo request: Mismatched rangeEnd") + } + + areEqual := slices.Equal(acceptableVersionsList, acceptableVersionsList_Received) + if (areEqual == false){ + t.Fatalf("Failed to read decrypted GetMessageReportsInfo request: Mismatched acceptableVersionsList") + } + + areEqual = slices.Equal(reportedMessageHashesList, reportedMessageHashesList_Received) + if (areEqual == false){ + t.Fatalf("Failed to read decrypted GetMessageReportsInfo request: Mismatched reportedHashesList") + } + +} + + +func TestCreateAndReadRequest_GetReports(t *testing.T){ + + recipientHostIdentityHash, err := identity.GetNewRandomIdentityHash(true, "Host") + if (err != nil) { + t.Fatalf("Failed to create random host identity hash: " + err.Error()) + } + + connectionKey, err := getNewRandomConnectionKey() + if (err != nil) { + t.Fatalf("getNewRandomConnectionKey failed: " + err.Error()) + } + + networkType, err := helpers.GetRandomByteWithinRange(1, 2) + if (err != nil) { + t.Fatalf("GetRandomByteWithinRange failed: " + err.Error()) + } + + reportHashesList := make([][30]byte, 0, 100) + + for i:=0; i < 100; i++{ + + newReportHash, err := helpers.GetNewRandomReportHash(false, "") + if (err != nil){ + t.Fatalf("GetNewRandomReportHash failed: " + err.Error()) + } + + reportHashesList = append(reportHashesList, newReportHash) + } + + requestBytes, requestIdentifier, err := serverRequest.CreateServerRequest_GetReports(recipientHostIdentityHash, connectionKey, networkType, reportHashesList) + if (err != nil) { + t.Fatalf("Failed to create GetReports server request: " + err.Error()) + } + + ableToDecrypt, decryptedRequestBytes, err := serverRequest.ReadEncryptedRequest(requestBytes, connectionKey) + if (err != nil) { + t.Fatalf("Failed to decrypt GetReports server request: invalid inputs: " + err.Error()) + } + if (ableToDecrypt == false) { + t.Fatalf("Failed to decrypt GetReports request.") + } + + recipientHost_Received, requestType_Received, requestIdentifier_Received, networkType_Received, err := serverRequest.ReadDecryptedServerRequest_StandardData(decryptedRequestBytes) + if (err != nil) { + t.Fatalf("Failed to read decrypted GetReports request: " + err.Error()) + } + + if (recipientHost_Received != recipientHostIdentityHash){ + t.Fatalf("Failed to read decrypted GetReports request: Mismatched recipientHost") + } + if (requestType_Received != "GetReports"){ + t.Fatalf("Failed to read decrypted GetReports request: Invalid requestType: " + requestType_Received) + } + if (requestIdentifier_Received != requestIdentifier){ + t.Fatalf("Failed to read decrypted GetReports request: Mismatched requestIdentifier") + } + if (networkType_Received != networkType){ + t.Fatalf("Failed to read decrypted GetReports request: Mismatched networkType") + } + + reportHashesList_Received, err := serverRequest.ReadDecryptedServerRequest_GetReports(decryptedRequestBytes) + if (err != nil){ + t.Fatalf("Failed to read GetReports non-standard decrypted: " + err.Error()) + } + + areEqual := slices.Equal(reportHashesList, reportHashesList_Received) + if (areEqual == false){ + t.Fatalf("Failed to read decrypted GetReports request: Mismatched messageHashesList") + } +} + +func TestCreateAndReadRequest_GetAddressDeposits(t *testing.T){ + + cryptocurrencyNamesList := []string{"Ethereum", "Cardano"} + + for _, cryptocurrencyName := range cryptocurrencyNamesList{ + + recipientHostIdentityHash, err := identity.GetNewRandomIdentityHash(true, "Host") + if (err != nil) { + t.Fatalf("Failed to create random host identity hash: " + err.Error()) + } + + connectionKey, err := getNewRandomConnectionKey() + if (err != nil) { + t.Fatalf("getNewRandomConnectionKey failed: " + err.Error()) + } + + networkType, err := helpers.GetRandomByteWithinRange(1, 2) + if (err != nil) { + t.Fatalf("GetRandomByteWithinRange failed: " + err.Error()) + } + + cryptocurrencyAddressesList := make([]string, 0, 100) + + for i:=0; i < 100; i++{ + + if (cryptocurrencyName == "Ethereum"){ + + newAddress, err := ethereumAddress.GetNewRandomEthereumAddress() + if (err != nil) { + t.Fatalf("Failed to create random Ethereum address: " + err.Error()) + } + + cryptocurrencyAddressesList = append(cryptocurrencyAddressesList, newAddress) + + } else if (cryptocurrencyName == "Cardano"){ + + newAddress, err := cardanoAddress.GetNewRandomCardanoAddress() + if (err != nil) { + t.Fatalf("Failed to create random Cardano address: " + err.Error()) + } + + cryptocurrencyAddressesList = append(cryptocurrencyAddressesList, newAddress) + } + } + + requestBytes, requestIdentifier, err := serverRequest.CreateServerRequest_GetAddressDeposits(recipientHostIdentityHash, connectionKey, networkType, cryptocurrencyName, cryptocurrencyAddressesList) + if (err != nil) { + t.Fatalf("Failed to create GetAddressDeposits server request: " + err.Error()) + } + + ableToDecrypt, decryptedRequestBytes, err := serverRequest.ReadEncryptedRequest(requestBytes, connectionKey) + if (err != nil) { + t.Fatalf("Failed to decrypt GetAddressDeposits server request: invalid inputs: " + err.Error()) + } + if (ableToDecrypt == false) { + t.Fatalf("Failed to decrypt GetProfileHashesList request.") + } + + recipientHost_Received, requestType_Received, requestIdentifier_Received, networkType_Received, err := serverRequest.ReadDecryptedServerRequest_StandardData(decryptedRequestBytes) + if (err != nil) { + t.Fatalf("Failed to read decrypted GetAddressDeposits request: " + err.Error()) + } + + if (recipientHost_Received != recipientHostIdentityHash){ + t.Fatalf("Failed to read decrypted GetAddressDeposits request: Mismatched recipientHost") + } + if (requestType_Received != "GetAddressDeposits"){ + t.Fatalf("Failed to read decrypted GetAddressDeposits request: Invalid requestType: " + requestType_Received) + } + if (requestIdentifier_Received != requestIdentifier){ + t.Fatalf("Failed to read decrypted GetAddressDeposits request: Mismatched requestIdentifier") + } + if (networkType_Received != networkType){ + t.Fatalf("Failed to read decrypted GetAddressDeposits request: Mismatched networkType") + } + + cryptocurrency_Received, cryptocurrencyAddressesList_Received, err := serverRequest.ReadDecryptedServerRequest_GetAddressDeposits(decryptedRequestBytes) + if (err != nil){ + t.Fatalf("Failed to read GetAddressDeposits non-standard decrypted: " + err.Error()) + } + + if (cryptocurrency_Received != cryptocurrencyName){ + t.Fatalf("Failed to read decrypted GetAddressDeposits request: Mismatched cryptocurrency") + } + + areEqual := slices.Equal(cryptocurrencyAddressesList, cryptocurrencyAddressesList_Received) + if (areEqual == false){ + t.Fatalf("Failed to read decrypted GetAddressDeposits request: Mismatched identityHashesList") + } + } +} + + +func TestCreateAndReadRequest_GetViewableStatuses(t *testing.T){ + + recipientHostIdentityHash, err := identity.GetNewRandomIdentityHash(true, "Host") + if (err != nil) { + t.Fatalf("Failed to create random host identity hash: " + err.Error()) + } + + connectionKey, err := getNewRandomConnectionKey() + if (err != nil) { + t.Fatalf("getNewRandomConnectionKey failed: " + err.Error()) + } + + networkType, err := helpers.GetRandomByteWithinRange(1, 2) + if (err != nil) { + t.Fatalf("GetRandomByteWithinRange failed: " + err.Error()) + } + + identityHashesList := make([][16]byte, 0, 100) + + for i:=0; i < 100; i++{ + + newIdentityHash, err := identity.GetNewRandomIdentityHash(false, "") + if (err != nil) { + t.Fatalf("Failed to create random identity hash: " + err.Error()) + } + + identityHashesList = append(identityHashesList, newIdentityHash) + } + + profileHashesList := make([][28]byte, 0, 50) + + for k:=0; k < 50; k++{ + + newProfileHash, err := helpers.GetNewRandomProfileHash(false, "", true, false) + if (err != nil) { + t.Fatalf("Failed to get random profile hash: " + err.Error()) + } + + profileHashesList = append(profileHashesList, newProfileHash) + } + + requestBytes, requestIdentifier, err := serverRequest.CreateServerRequest_GetViewableStatuses(recipientHostIdentityHash, connectionKey, networkType, identityHashesList, profileHashesList) + if (err != nil) { + t.Fatalf("Failed to create GetViewableStatuses server request: " + err.Error()) + } + + ableToDecrypt, decryptedRequestBytes, err := serverRequest.ReadEncryptedRequest(requestBytes, connectionKey) + if (err != nil) { + t.Fatalf("Failed to decrypt GetViewableStatuses server request: invalid inputs: " + err.Error()) + } + if (ableToDecrypt == false) { + t.Fatalf("Failed to decrypt GetViewableStatuses request.") + } + + recipientHost_Received, requestType_Received, requestIdentifier_Received, networkType_Received, err := serverRequest.ReadDecryptedServerRequest_StandardData(decryptedRequestBytes) + if (err != nil) { + t.Fatalf("Failed to read decrypted GetViewableStatuses request: " + err.Error()) + } + + if (recipientHost_Received != recipientHostIdentityHash){ + t.Fatalf("Failed to read decrypted GetViewableStatuses request: Mismatched recipientHost") + } + if (requestType_Received != "GetViewableStatuses"){ + t.Fatalf("Failed to read decrypted GetViewableStatuses request: Invalid requestType: " + requestType_Received) + } + if (requestIdentifier_Received != requestIdentifier){ + t.Fatalf("Failed to read decrypted GetViewableStatuses request: Mismatched requestIdentifier") + } + if (networkType_Received != networkType){ + t.Fatalf("Failed to read decrypted GetViewableStatuses request: Mismatched networkType") + } + + identityHashesList_Received, profileHashesList_Received, err := serverRequest.ReadDecryptedServerRequest_GetViewableStatuses(decryptedRequestBytes) + if (err != nil){ + t.Fatalf("Failed to read GetViewableStatuses non-standard decrypted: " + err.Error()) + } + + areEqual := slices.Equal(identityHashesList, identityHashesList_Received) + if (areEqual == false){ + t.Fatalf("Failed to read decrypted GetViewableStatuses request: Mismatched identityHashesList") + } + + areEqual = slices.Equal(profileHashesList, profileHashesList_Received) + if (areEqual == false){ + t.Fatalf("Failed to read decrypted GetViewableStatuses request: Mismatched profileHashesList") + } +} + + +func TestCreateAndReadRequest_BroadcastContent(t *testing.T){ + + // We initialize these variables so we can create fake profiles + + traits.InitializeTraitVariables() + monogenicDiseases.InitializeMonogenicDiseaseVariables() + polygenicDiseases.InitializePolygenicDiseaseVariables() + + err := profileFormat.InitializeProfileFormatVariables() + if (err != nil) { + t.Fatalf("Failed to initialize profile format variables: " + err.Error()) + } + + contentTypesToBroadcastList := []string{"Profile", "Message", "Review", "Report", "Parameters"} + + for _, contentTypeToBroadcast := range contentTypesToBroadcastList{ + + recipientHostIdentityHash, err := identity.GetNewRandomIdentityHash(true, "Host") + if (err != nil) { + t.Fatalf("Failed to create random host identity hash: " + err.Error()) + } + + connectionKey, err := getNewRandomConnectionKey() + if (err != nil) { + t.Fatalf("getNewRandomConnectionKey failed: " + err.Error()) + } + + networkType, err := helpers.GetRandomByteWithinRange(1, 2) + if (err != nil){ + t.Fatalf("GetRandomByteWithinRange failed: " + err.Error()) + } + + getRandomContentBytes := func()([]byte, error){ + + if (contentTypeToBroadcast == "Profile"){ + + //TODO: Add Host once we can create Host profiles + profileType, err := helpers.GetRandomItemFromList([]string{"Mate", "Moderator"}) + if (err != nil) { return nil, err } + + profilePublicIdentityKey, profilePrivateIdentityKey, err := identity.GetNewRandomPublicPrivateIdentityKeys() + if (err != nil) { return nil, err } + + fakeProfileBytes, err := generate.GetFakeProfile(profileType, profilePublicIdentityKey, profilePrivateIdentityKey, networkType) + if (err != nil) { + return nil, errors.New("Failed to create fake profile: " + err.Error()) + } + + return fakeProfileBytes, nil + } + if (contentTypeToBroadcast == "Review"){ + + reviewType, err := helpers.GetRandomItemFromList([]string{"Profile", "Identity", "Message"}) + if (err != nil) { return nil, err } + + fakeReviewBytes, err := generate.GetFakeReview(reviewType, networkType) + if (err != nil) { + return nil, errors.New("Failed to create fake review: " + err.Error()) + } + + return fakeReviewBytes, nil + } + if (contentTypeToBroadcast == "Message"){ + + textOrImage, err := helpers.GetRandomItemFromList([]string{"Text", "Image"}) + if (err != nil) { return nil, err } + + fakeMessageBytes, _, _, err := generate.GetFakeMessage(networkType, textOrImage) + if (err != nil) { + return nil, errors.New("Failed to create fake message: " + err.Error()) + } + + return fakeMessageBytes, nil + } + if (contentTypeToBroadcast == "Report"){ + + reportType, err := helpers.GetRandomItemFromList([]string{"Profile", "Identity", "Message"}) + if (err != nil) { return nil, err } + + fakeReportBytes, err := generate.GetFakeReport(reportType, networkType) + if (err != nil) { + return nil, errors.New("Failed to create fake report: " + err.Error()) + } + + return fakeReportBytes, nil + } + // contentTypeToBroadcast = "Parameters" + + parametersTypesList := readParameters.GetAllParametersTypesList() + + randomParametersType, err := helpers.GetRandomItemFromList(parametersTypesList) + if (err != nil) { return nil, err } + + fakeParametersBytes, err := generate.GetFakeParameters(randomParametersType, networkType) + if (err != nil) { + return nil, errors.New("Failed to get fake parameters: " + err.Error()) + } + + return fakeParametersBytes, nil + } + + contentList := make([][]byte, 0, 20) + + for i:=0; i < 20; i++{ + + randomContentBytes, err := getRandomContentBytes() + if (err != nil) { + t.Fatalf("Failed to get random content: " + err.Error()) + } + + contentList = append(contentList, randomContentBytes) + } + + requestBytes, requestIdentifier, err := serverRequest.CreateServerRequest_BroadcastContent(recipientHostIdentityHash, connectionKey, networkType, contentTypeToBroadcast, contentList) + if (err != nil) { + t.Fatalf("Failed to create BroadcastContent server request: " + err.Error()) + } + + ableToDecrypt, decryptedRequestBytes, err := serverRequest.ReadEncryptedRequest(requestBytes, connectionKey) + if (err != nil) { + t.Fatalf("Failed to decrypt BroadcastContent server request: invalid inputs: " + err.Error()) + } + if (ableToDecrypt == false) { + t.Fatalf("Failed to decrypt BroadcastContent request.") + } + + recipientHost_Received, requestType_Received, requestIdentifier_Received, networkType_Received, err := serverRequest.ReadDecryptedServerRequest_StandardData(decryptedRequestBytes) + if (err != nil) { + t.Fatalf("Failed to read decrypted BroadcastContent request: " + err.Error()) + } + + if (recipientHost_Received != recipientHostIdentityHash){ + t.Fatalf("Failed to read decrypted BroadcastContent request: Mismatched recipientHost") + } + if (requestType_Received != "BroadcastContent"){ + t.Fatalf("Failed to read decrypted BroadcastContent request: Invalid requestType") + } + if (requestIdentifier_Received != requestIdentifier){ + t.Fatalf("Failed to read decrypted BroadcastContent request: Mismatched requestIdentifier") + } + if (networkType_Received != networkType){ + t.Fatalf("Failed to read decrypted BroadcastContent request: Mismatched networkType") + } + + contentTypeToBroadcast_Received, contentList_Received, err := serverRequest.ReadDecryptedServerRequest_BroadcastContent(decryptedRequestBytes) + if (err != nil){ + t.Fatalf("Failed to read BroadcastContent non-standard decrypted: " + err.Error()) + } + + if (contentTypeToBroadcast_Received != contentTypeToBroadcast){ + t.Fatalf("Failed to read decrypted BroadcastContent request: Mismatched contentTypeToBroadcast") + } + + areEqual := reflect.DeepEqual(contentList, contentList_Received) + if (areEqual == false){ + t.Fatalf("Failed to read decrypted BroadcastContent request: Mismatched contentList") + } + } +} + + diff --git a/internal/network/serverResponse/serverResponse.go b/internal/network/serverResponse/serverResponse.go new file mode 100644 index 0000000..0f72d25 --- /dev/null +++ b/internal/network/serverResponse/serverResponse.go @@ -0,0 +1,1719 @@ + +// serverResponse provides functions to create and read server responses + +package serverResponse + +// See a description of each response and its purpose in Specification.md + +//TODO: Verify all received values and function inputs + +import "seekia/internal/cryptography/blake3" +import "seekia/internal/cryptography/chaPolyShrink" +import "seekia/internal/cryptography/edwardsKeys" +import "seekia/internal/cryptography/kyber" +import "seekia/internal/cryptography/nacl" +import "seekia/internal/encoding" +import "seekia/internal/helpers" +import "seekia/internal/identity" +import "seekia/internal/messaging/readMessages" +import "seekia/internal/moderation/readReports" +import "seekia/internal/moderation/readReviews" +import "seekia/internal/parameters/readParameters" +import "seekia/internal/profiles/readProfiles" + +import messagepack "github.com/vmihailenco/msgpack/v5" + +import "slices" +import "crypto/rand" +import "errors" + +// The variables below specify the maximum number of items that can fit into a response +// They are used when making requests to know when to break up the request so it does not exceed the maximum size +//TODO: Make these numbers accurate + +const MaximumProfilesInResponse_GetProfilesInfo int = 100000 +const MaximumMessageHashesInResponse_GetMessageHashesList int = 100000 +const MaximumMessagesInResponse_GetMessages int = 100000 +const MaximumReviewHashesInResponse_GetReviewHashesList int = 100000 +const MaximumReviewsInResponse_GetIdentityReviewsInfo int = 100000 +const MaximumReviewsInResponse_GetMessageReviewsInfo int = 100000 +const MaximumReviewsInResponse_GetReviews int = 100000 +const MaximumReportsInResponse_GetIdentityReportsInfo int = 100000 +const MaximumReportsInResponse_GetMessageReportsInfo int = 100000 +const MaximumReportsInResponse_GetReports int = 100000 +const MaximumBalancesInResponse_GetAddressDeposits int = 100000 +const MaximumProfilesInResponse_GetProfileViewableStatuses int = 10000 +const MaximumIdentitiesInResponse_GetIdentityViewableStatuses int = 1000 + +func GetMaximumProfilesInResponse_GetProfiles(profileType string)(int, error){ + + if (profileType == "Mate"){ + return 1000, nil + } + if (profileType == "Host"){ + return 10000, nil + } + if (profileType == "Moderator"){ + return 10000, nil + } + + return 0, errors.New("GetMaximumProfilesInResponse_GetProfiles called with invalid profile type: " + profileType) +} + +//Outputs: +// -[]byte: Response bytes +// -[32]byte: Connection Key +// -error +func CreateServerResponse_EstablishConnectionKey(hostPublicIdentityKey [32]byte, hostPrivateIdentityKey [64]byte, requestIdentifier [16]byte, requestorNaclPublicKey [32]byte, requestorKyberPublicKey [1568]byte)([]byte, [32]byte, error){ + + var connectionKey [32]byte + _, err := rand.Read(connectionKey[:]) + if (err != nil) { return nil, [32]byte{}, err } + + type responseStruct struct{ + HostIdentityKey [32]byte + RequestIdentifier [16]byte + ResponseType string + ConnectionKey [32]byte + } + + responseObject := responseStruct{ + HostIdentityKey: hostPublicIdentityKey, + RequestIdentifier: requestIdentifier, + ResponseType: "EstablishConnectionKey", + ConnectionKey: connectionKey, + } + + responseContentBytes, err := encoding.EncodeMessagePackBytes(responseObject) + if (err != nil) { return nil, [32]byte{}, err } + + responseSignedBytes, err := createResponseSignedContent(hostPrivateIdentityKey, responseContentBytes) + if (err != nil) { return nil, [32]byte{}, err } + + chaPolyKey, err := helpers.GetNewRandom32ByteArray() + if (err != nil) { return nil, [32]byte{}, err } + + chaPolyNonce, err := helpers.GetNewRandom24ByteArray() + if (err != nil) { return nil, [32]byte{}, err } + + cipheredContent, err := chaPolyShrink.EncryptChaPolyShrink(responseSignedBytes, chaPolyKey, chaPolyNonce, true, 50, false, [32]byte{}) + if (err != nil) { return nil, [32]byte{}, err } + + keyPieceA, err := helpers.GetNewRandom32ByteArray() + if (err != nil) { return nil, [32]byte{}, err } + + keyPieceB := helpers.XORTwo32ByteArrays(keyPieceA, chaPolyKey) + + naclEncryptedKeyPieceA, err := nacl.EncryptKeyWithNacl(requestorNaclPublicKey, keyPieceA) + if (err != nil) { return nil, [32]byte{}, err } + + kyberEncryptedKeyPieceB, err := kyber.EncryptKeyWithKyber(requestorKyberPublicKey, keyPieceB) + if (err != nil) { return nil, [32]byte{}, err } + + keyPiecesList := slices.Concat(naclEncryptedKeyPieceA[:], kyberEncryptedKeyPieceB[:]) + + type encryptedResponseStruct struct{ + KeyPieces []byte + ChaPolyNonce [24]byte + CipheredContent []byte + } + + encryptedResponseObject := encryptedResponseStruct{ + KeyPieces: keyPiecesList, + ChaPolyNonce: chaPolyNonce, + CipheredContent: cipheredContent, + } + + encryptedResponseBytes, err := encoding.EncodeMessagePackBytes(encryptedResponseObject) + if (err != nil) { return nil, [32]byte{}, err } + + return encryptedResponseBytes, connectionKey, nil +} + +//Outputs: +// -bool: Able to read +// -[16]byte: Request identifier +// -[16]byte: Host Identity Hash +// -[32]byte: Connection key +// -error (if inputs to function are invalid) +func ReadServerResponse_EstablishConnectionKey(responseBytes []byte, requestorNaclPublicKey [32]byte, requestorNaclPrivateKey [32]byte, requestorKyberPrivateKey [1536]byte)(bool, [16]byte, [16]byte, [32]byte, error){ + + type encryptedResponseStruct struct{ + KeyPieces [1648]byte + ChaPolyNonce [24]byte + CipheredContent []byte + } + + var encryptedResponseObject encryptedResponseStruct + + err := encoding.DecodeMessagePackBytes(false, responseBytes, &encryptedResponseObject) + if (err != nil) { + return false, [16]byte{}, [16]byte{}, [32]byte{}, nil + } + + keyPiecesListArray := encryptedResponseObject.KeyPieces + chaPolyNonce := encryptedResponseObject.ChaPolyNonce + cipheredContent := encryptedResponseObject.CipheredContent + + naclEncryptedKeyPieceA := [80]byte(keyPiecesListArray[:80]) + kyberEncryptedKeyPieceB := [1568]byte(keyPiecesListArray[80:]) + + ableToDecrypt, keyPieceA, err := nacl.DecryptNaclEncryptedKey(naclEncryptedKeyPieceA, requestorNaclPublicKey, requestorNaclPrivateKey) + if (err != nil) { return false, [16]byte{}, [16]byte{}, [32]byte{}, err } + if (ableToDecrypt == false){ + return false, [16]byte{}, [16]byte{}, [32]byte{}, nil + } + + keyPieceB, err := kyber.DecryptKyberEncryptedKey(kyberEncryptedKeyPieceB, requestorKyberPrivateKey) + if (err != nil) { + return false, [16]byte{}, [16]byte{}, [32]byte{}, nil + } + + contentChaPolyKey := helpers.XORTwo32ByteArrays(keyPieceA, keyPieceB) + + ableToDecrypt, decryptedResponseBytes, err := chaPolyShrink.DecryptChaPolyShrink(cipheredContent, contentChaPolyKey, chaPolyNonce, false, [32]byte{}) + if (err != nil) { + return false, [16]byte{}, [16]byte{}, [32]byte{}, nil + } + if (ableToDecrypt == false){ + return false, [16]byte{}, [16]byte{}, [32]byte{}, nil + } + + ableToRead, requestIdentifier, hostIdentityHash, contentBytes, err := readResponseSignedContent(decryptedResponseBytes, "EstablishConnectionKey") + if (err != nil) { return false, [16]byte{}, [16]byte{}, [32]byte{}, err } + if (ableToRead == false){ + return false, [16]byte{}, [16]byte{}, [32]byte{}, nil + } + + type responseContentStruct struct{ + ConnectionKey [32]byte + } + + var responseContentObject responseContentStruct + + err = encoding.DecodeMessagePackBytes(true, contentBytes, &responseContentObject) + if (err != nil) { + return false, [16]byte{}, [16]byte{}, [32]byte{}, nil + } + + connectionKey := responseContentObject.ConnectionKey + + return true, requestIdentifier, hostIdentityHash, connectionKey, nil +} + + +//Outputs: +// -[]byte: Response bytes +// -error +func CreateServerResponse_GetParametersInfo(hostPublicIdentityKey [32]byte, hostPrivateIdentityKey [64]byte, connectionKey [32]byte, requestIdentifier [16]byte, parametersInfoMap map[string]int64)([]byte, error){ + + if (len(parametersInfoMap) == 0){ + return nil, errors.New("CreateServerResponse_GetParametersInfo called with empty ParametersInfoMap") + } + + type responseContentStruct struct{ + HostIdentityKey [32]byte + RequestIdentifier [16]byte + ResponseType string + ParametersInfo map[string]int64 // Parameters Type -> Parameters broadcast time + } + + responseContentObject := responseContentStruct{ + HostIdentityKey: hostPublicIdentityKey, + RequestIdentifier: requestIdentifier, + ResponseType: "GetParametersInfo", + ParametersInfo: parametersInfoMap, + } + + innerResponseBytes, err := encoding.EncodeMessagePackBytes(responseContentObject) + if (err != nil) { return nil, err } + + encryptedResponse, err := createEncryptedAndSignedResponse(connectionKey, hostPrivateIdentityKey, innerResponseBytes) + if (err != nil) { return nil, err } + + return encryptedResponse, nil +} + +//Outputs: +// -bool: Able to read server response +// -[16]byte: Request Identifier +// -[16]byte: Host Identity Hash +// -map[string]int64: Parameters Info map (Parameters Type -> Parameters broadcast time) +// -error (returns err if input keys are invalid) +func ReadServerResponse_GetParametersInfo(responseBytes []byte, connectionKey [32]byte)(bool, [16]byte, [16]byte, map[string]int64, error){ + + ableToRead, requestIdentifier, hostIdentityHash, decryptedContentBytes, err := readEncryptedAndSignedResponse(responseBytes, connectionKey, "GetParametersInfo") + if (err != nil) { return false, [16]byte{}, [16]byte{}, nil, err } + if (ableToRead == false){ + //Response is malformed + return false, [16]byte{}, [16]byte{}, nil, nil + } + + type responseContentStruct struct{ + ParametersInfo map[string]int64 + } + + var responseContentObject responseContentStruct + + err = encoding.DecodeMessagePackBytes(true, decryptedContentBytes, &responseContentObject) + if (err != nil) { + // Response malformed + return false, [16]byte{}, [16]byte{}, nil, nil + } + + parametersInfoMap := responseContentObject.ParametersInfo + + //TODO: Verify parameters + + return true, requestIdentifier, hostIdentityHash, parametersInfoMap, nil +} + + +//Outputs: +// -[]byte: Response bytes +// -error +func CreateServerResponse_GetParameters(hostPublicIdentityKey [32]byte, hostPrivateIdentityKey [64]byte, connectionKey [32]byte, requestIdentifier [16]byte, parametersList [][]byte)([]byte, error){ + + //TODO: Verify inputs + + messagepackParametersList := make([]messagepack.RawMessage, 0, len(parametersList)) + + for _, parametersBytes := range parametersList{ + messagepackParametersList = append(messagepackParametersList, parametersBytes) + } + + type responseContentStruct struct{ + HostIdentityKey [32]byte + RequestIdentifier [16]byte + ResponseType string + ParametersList []messagepack.RawMessage + } + + responseContentObject := responseContentStruct{ + HostIdentityKey: hostPublicIdentityKey, + RequestIdentifier: requestIdentifier, + ResponseType: "GetParameters", + ParametersList: messagepackParametersList, + } + + innerResponseBytes, err := encoding.EncodeMessagePackBytes(responseContentObject) + if (err != nil) { return nil, err } + + encryptedResponse, err := createEncryptedAndSignedResponse(connectionKey, hostPrivateIdentityKey, innerResponseBytes) + if (err != nil) { return nil, err } + + return encryptedResponse, nil +} + + +//Outputs: +// -bool: Able to read server response +// -[16]byte: Request Identifier +// -[16]byte: Host Identity Hash +// -[][]byte: Parameters list +// -error (returns err if input keys are invalid) +func ReadServerResponse_GetParameters(responseBytes []byte, connectionKey [32]byte)(bool, [16]byte, [16]byte, [][]byte, error){ + + //TODO: Verify inputs + + ableToRead, requestIdentifier, hostIdentityHash, decryptedContentBytes, err := readEncryptedAndSignedResponse(responseBytes, connectionKey, "GetParameters") + if (err != nil) { return false, [16]byte{}, [16]byte{}, nil, err } + if (ableToRead == false){ + //Response is malformed + return false, [16]byte{}, [16]byte{}, nil, nil + } + + type responseContentStruct struct{ + ParametersList []messagepack.RawMessage + } + + var responseContentObject responseContentStruct + + err = encoding.DecodeMessagePackBytes(true, decryptedContentBytes, &responseContentObject) + if (err != nil) { + // Response is malformed + return false, [16]byte{}, [16]byte{}, nil, nil + } + + parametersListRawMessagepack := responseContentObject.ParametersList + + // We use the map below to ensure we only received each parameter type once + receivedParametersTypesMap := make(map[string]struct{}) + + // We use this to ensure that all parameters network types are the same + parametersNetworkType := byte(0) + + parametersList := make([][]byte, 0, len(parametersListRawMessagepack)) + + for index, parametersBytes := range parametersListRawMessagepack{ + + ableToRead, _, currentNetworkType, _, parametersType, _, _, err := readParameters.ReadParameters(true, parametersBytes) + if (err != nil) { return false, [16]byte{}, [16]byte{}, nil, err } + if (ableToRead == false){ + // Response is malformed + return false, [16]byte{}, [16]byte{}, nil, nil + } + + if (index == 0){ + parametersNetworkType = currentNetworkType + + } else if (parametersNetworkType != currentNetworkType){ + + // We received two parameters with different network types. + // Response is malformed. + return false, [16]byte{}, [16]byte{}, nil, nil + } + + _, exists := receivedParametersTypesMap[parametersType] + if (exists == true){ + // We received a duplicate parameters type. Response is malformed. + return false, [16]byte{}, [16]byte{}, nil, nil + } + receivedParametersTypesMap[parametersType] = struct{}{} + + parametersList = append(parametersList, parametersBytes) + } + + return true, requestIdentifier, hostIdentityHash, parametersList, nil +} + +// This is used for the GetProfilesInfo request +type ProfileInfoStruct struct{ + ProfileHash [28]byte + ProfileAuthor [16]byte + ProfileBroadcastTime int64 +} + +//Outputs: +// -[]byte: Response bytes +// -error +func CreateServerResponse_GetProfilesInfo(hostPublicIdentityKey [32]byte, hostPrivateIdentityKey [64]byte, connectionKey [32]byte, requestIdentifier [16]byte, profileInfoObjectsList []ProfileInfoStruct)([]byte, error){ + + //TODO: Verify inputs + + for _, profileInfoObject := range profileInfoObjectsList{ + + profileHash := profileInfoObject.ProfileHash + profileAuthor := profileInfoObject.ProfileAuthor + profileBroadcastTime := profileInfoObject.ProfileBroadcastTime + + profileType, _, err := readProfiles.ReadProfileHashMetadata(profileHash) + if (err != nil){ + profileHashHex := encoding.EncodeBytesToHexString(profileHash[:]) + return nil, errors.New("Cannot create response: Invalid profileInfoObjectsList: Invalid ProfileHash: " + profileHashHex + ". Reason: " + err.Error()) + } + + authorIdentityType, err := identity.GetIdentityTypeFromIdentityHash(profileAuthor) + if (err != nil){ + profileAuthorHex := encoding.EncodeBytesToHexString(profileAuthor[:]) + return nil, errors.New("Cannot create response: invalid profileInfoObjectsList: Invalid profileAuthor: " + profileAuthorHex) + } + + if (authorIdentityType != profileType){ + return nil, errors.New("Cannot create response: Invalid profileInfoObjectsList: ProfileHash profile type does not match profileAuthor identity type.") + } + + isValid := helpers.VerifyBroadcastTime(profileBroadcastTime) + if (isValid == false){ + broadcastTimeString := helpers.ConvertInt64ToString(profileBroadcastTime) + return nil, errors.New("Cannot create response: Invalid profileInfoObjectsList: Invalid broadcastTime: " + broadcastTimeString) + } + } + + type responseContentStruct struct{ + HostIdentityKey [32]byte + RequestIdentifier [16]byte + ResponseType string + ProfilesInfo []ProfileInfoStruct + } + + responseContentObject := responseContentStruct{ + HostIdentityKey: hostPublicIdentityKey, + RequestIdentifier: requestIdentifier, + ResponseType: "GetProfilesInfo", + ProfilesInfo: profileInfoObjectsList, + } + + innerResponseBytes, err := encoding.EncodeMessagePackBytes(responseContentObject) + if (err != nil) { return nil, err } + + encryptedResponse, err := createEncryptedAndSignedResponse(connectionKey, hostPrivateIdentityKey, innerResponseBytes) + if (err != nil) { return nil, err } + + return encryptedResponse, nil +} + + +//Outputs: +// -bool: Able to read server response +// -[16]byte: Request Identifier +// -[16]byte: Host Identity Hash +// -[]ProfileInfoStruct: Profiles Info map list +// -error (returns err if input keys are invalid) +func ReadServerResponse_GetProfilesInfo(responseBytes []byte, connectionKey [32]byte)(bool, [16]byte, [16]byte, []ProfileInfoStruct, error){ + + //TODO: Verify inputs + + ableToRead, requestIdentifier, hostIdentityHash, decryptedContentBytes, err := readEncryptedAndSignedResponse(responseBytes, connectionKey, "GetProfilesInfo") + if (err != nil) { return false, [16]byte{}, [16]byte{}, nil, err } + if (ableToRead == false){ + //Response is malformed + return false, [16]byte{}, [16]byte{}, nil, nil + } + + type responseContentStruct struct{ + ProfilesInfo []ProfileInfoStruct + } + + var responseContentObject responseContentStruct + + err = encoding.DecodeMessagePackBytes(true, decryptedContentBytes, &responseContentObject) + if (err != nil) { + // Response malformed + return false, [16]byte{}, [16]byte{}, nil, nil + } + + //TODO: Validate info + profileInfoObjectsList := responseContentObject.ProfilesInfo + + return true, requestIdentifier, hostIdentityHash, profileInfoObjectsList, nil +} + + +//Outputs: +// -[]byte: Response bytes +// -error +func CreateServerResponse_GetProfiles(hostPublicIdentityKey [32]byte, hostPrivateIdentityKey [64]byte, connectionKey [32]byte, requestIdentifier [16]byte, profilesList [][]byte)([]byte, error){ + + //TODO: Verify inputs + + messagepackProfilesList := make([]messagepack.RawMessage, 0, len(profilesList)) + + for _, profileBytes := range profilesList{ + + messagepackProfilesList = append(messagepackProfilesList, profileBytes) + } + + type responseContentStruct struct{ + HostIdentityKey [32]byte + RequestIdentifier [16]byte + ResponseType string + ProfilesList []messagepack.RawMessage + } + + responseContentObject := responseContentStruct{ + HostIdentityKey: hostPublicIdentityKey, + RequestIdentifier: requestIdentifier, + ResponseType: "GetProfiles", + ProfilesList: messagepackProfilesList, + } + + innerResponseBytes, err := encoding.EncodeMessagePackBytes(responseContentObject) + if (err != nil) { return nil, err } + + encryptedResponse, err := createEncryptedAndSignedResponse(connectionKey, hostPrivateIdentityKey, innerResponseBytes) + if (err != nil) { return nil, err } + + return encryptedResponse, nil +} + + +//Outputs: +// -bool: Able to read server response +// -[16]byte: Request Identifier +// -[16]byte: Host Identity Hash +// -[][]byte: Profiles list +// -error (returns err if input keys are invalid) +func ReadServerResponse_GetProfiles(responseBytes []byte, connectionKey [32]byte)(bool, [16]byte, [16]byte, [][]byte, error){ + + //TODO: Verify inputs + + ableToRead, requestIdentifier, hostIdentityHash, decryptedContentBytes, err := readEncryptedAndSignedResponse(responseBytes, connectionKey, "GetProfiles") + if (err != nil) { return false, [16]byte{}, [16]byte{}, nil, err } + if (ableToRead == false){ + //Response is malformed + return false, [16]byte{}, [16]byte{}, nil, nil + } + + type responseContentStruct struct{ + ProfilesList []messagepack.RawMessage + } + + var responseContentObject responseContentStruct + + err = encoding.DecodeMessagePackBytes(true, decryptedContentBytes, &responseContentObject) + if (err != nil) { + // Response is malformed + return false, [16]byte{}, [16]byte{}, nil, nil + } + + messagepackProfilesList := responseContentObject.ProfilesList + + // We use the map below to ensure we only received each profile once + receivedProfileHashesMap := make(map[[28]byte]struct{}) + + profilesList := make([][]byte, 0, len(messagepackProfilesList)) + + // We use this variable to make sure the network type of each profile is the same + profilesNetworkType := byte(0) + + for index, profileBytes := range messagepackProfilesList{ + + ableToRead, profileHash, _, profileNetworkType, _, _, _, _, err := readProfiles.ReadProfileAndHash(true, profileBytes) + if (err != nil) { return false, [16]byte{}, [16]byte{}, nil, err } + if (ableToRead == false){ + // Profile is malformed. Host is malicious. + return false, [16]byte{}, [16]byte{}, nil, nil + } + + _, exists := receivedProfileHashesMap[profileHash] + if (exists == true){ + // We received a duplicate profile. Host is malicious. + return false, [16]byte{}, [16]byte{}, nil, nil + } + receivedProfileHashesMap[profileHash] = struct{}{} + + if (index == 0){ + profilesNetworkType = profileNetworkType + + } else if (profilesNetworkType != profileNetworkType){ + // Profiles of differing network type cannot be sent in the same response + // Response is malformed + return false, [16]byte{}, [16]byte{}, nil, nil + } + + profilesList = append(profilesList, profileBytes) + } + + return true, requestIdentifier, hostIdentityHash, profilesList, nil +} + + +//Outputs: +// -[]byte: Response bytes +// -error +func CreateServerResponse_GetMessageHashesList(hostPublicIdentityKey [32]byte, hostPrivateIdentityKey [64]byte, connectionKey [32]byte, requestIdentifier [16]byte, messageHashesList [][26]byte)([]byte, error){ + + type responseContentStruct struct{ + HostIdentityKey [32]byte + RequestIdentifier [16]byte + ResponseType string + MessageHashesList [][26]byte + } + + responseContentObject := responseContentStruct{ + HostIdentityKey: hostPublicIdentityKey, + RequestIdentifier: requestIdentifier, + ResponseType: "GetMessageHashesList", + MessageHashesList: messageHashesList, + } + + innerResponseBytes, err := encoding.EncodeMessagePackBytes(responseContentObject) + if (err != nil) { return nil, err } + + encryptedResponse, err := createEncryptedAndSignedResponse(connectionKey, hostPrivateIdentityKey, innerResponseBytes) + if (err != nil) { return nil, err } + + return encryptedResponse, nil +} + + +//Outputs: +// -bool: Able to read server response +// -[16]byte: Request Identifier +// -[16]byte: Host Identity Hash +// -[][26]byte: Message Hashes List +// -error (returns err if input keys are invalid) +func ReadServerResponse_GetMessageHashesList(responseBytes []byte, connectionKey [32]byte)(bool, [16]byte, [16]byte, [][26]byte, error){ + + //TODO: Verify inputs + + ableToRead, requestIdentifier, hostIdentityHash, decryptedContentBytes, err := readEncryptedAndSignedResponse(responseBytes, connectionKey, "GetMessageHashesList") + if (err != nil) { return false, [16]byte{}, [16]byte{}, nil, err } + if (ableToRead == false){ + //Response is malformed + return false, [16]byte{}, [16]byte{}, nil, nil + } + + type responseContentStruct struct{ + MessageHashesList [][26]byte + } + + var responseContentObject responseContentStruct + + err = encoding.DecodeMessagePackBytes(true, decryptedContentBytes, &responseContentObject) + if (err != nil) { + // Response malformed + return false, [16]byte{}, [16]byte{}, nil, nil + } + + messageHashesList := responseContentObject.MessageHashesList + + return true, requestIdentifier, hostIdentityHash, messageHashesList, nil +} + + +//Outputs: +// -[]byte: Response bytes +// -error +func CreateServerResponse_GetMessages(hostPublicIdentityKey [32]byte, hostPrivateIdentityKey [64]byte, connectionKey [32]byte, requestIdentifier [16]byte, messagesList [][]byte)([]byte, error){ + + //TODO: Verify inputs + + messagepackMessagesList := make([]messagepack.RawMessage, 0, len(messagesList)) + + for _, messageBytes := range messagesList{ + messagepackMessagesList = append(messagepackMessagesList, messageBytes) + } + + type responseContentStruct struct{ + HostIdentityKey [32]byte + RequestIdentifier [16]byte + ResponseType string + MessagesList []messagepack.RawMessage + } + + responseContentObject := responseContentStruct{ + HostIdentityKey: hostPublicIdentityKey, + RequestIdentifier: requestIdentifier, + ResponseType: "GetMessages", + MessagesList: messagepackMessagesList, + } + + innerResponseBytes, err := encoding.EncodeMessagePackBytes(responseContentObject) + if (err != nil) { return nil, err } + + encryptedResponse, err := createEncryptedAndSignedResponse(connectionKey, hostPrivateIdentityKey, innerResponseBytes) + if (err != nil) { return nil, err } + + return encryptedResponse, nil +} + + +//Outputs: +// -bool: Able to read server response +// -[16]byte: Request Identifier +// -[16]byte: Host Identity Hash +// -[][]byte: Messages list +// -error (returns err if input keys are invalid) +func ReadServerResponse_GetMessages(responseBytes []byte, connectionKey [32]byte)(bool, [16]byte, [16]byte, [][]byte, error){ + + //TODO: Verify inputs + + ableToRead, requestIdentifier, hostIdentityHash, decryptedContentBytes, err := readEncryptedAndSignedResponse(responseBytes, connectionKey, "GetMessages") + if (err != nil) { return false, [16]byte{}, [16]byte{}, nil, err } + if (ableToRead == false){ + //Response is malformed + return false, [16]byte{}, [16]byte{}, nil, nil + } + + type responseContentStruct struct{ + MessagesList []messagepack.RawMessage + } + + var responseContentObject responseContentStruct + + err = encoding.DecodeMessagePackBytes(true, decryptedContentBytes, &responseContentObject) + if (err != nil) { + // Response is malformed + return false, [16]byte{}, [16]byte{}, nil, nil + } + + messagepackMessagesList := responseContentObject.MessagesList + + // We use this map to detect duplicates + messageHashesMap := make(map[[26]byte]struct{}) + + messagesList := make([][]byte, 0, len(messagepackMessagesList)) + + // We use this variable to make sure the network type of all messages in the response is the same + messagesNetworkType := byte(0) + + for index, messageBytes := range messagepackMessagesList{ + + ableToRead, messageHash, _, messageNetworkType, _, _, _, _, _, _, _, err := readMessages.ReadChatMessagePublicDataAndHash(true, messageBytes) + if (err != nil) { return false, [16]byte{}, [16]byte{}, nil, err } + if (ableToRead == false){ + // Message is invalid, host must be malicious. + return false, [16]byte{}, [16]byte{}, nil, nil + } + + _, exists := messageHashesMap[messageHash] + if (exists == true){ + // Host sent duplicate message. + return false, [16]byte{}, [16]byte{}, nil, nil + } + + messageHashesMap[messageHash] = struct{}{} + + if (index == 0){ + messagesNetworkType = messageNetworkType + + } else if (messagesNetworkType != messageNetworkType){ + // All messages in the response must contain the same networkType + return false, [16]byte{}, [16]byte{}, nil, nil + } + + messagesList = append(messagesList, messageBytes) + } + + return true, requestIdentifier, hostIdentityHash, messagesList, nil +} + + +//Outputs: +// -[]byte: Response bytes +// -error +func CreateServerResponse_GetIdentityReviewsInfo(hostPublicIdentityKey [32]byte, hostPrivateIdentityKey [64]byte, connectionKey [32]byte, requestIdentifier [16]byte, reviewsInfoMap map[[29]byte][]byte)([]byte, error){ + + //TODO: Verify inputs + + type responseContentStruct struct{ + HostIdentityKey [32]byte + RequestIdentifier [16]byte + ResponseType string + ReviewsInfo map[[29]byte][]byte // ReviewHash -> ReviewedHash + } + + responseContentObject := responseContentStruct{ + HostIdentityKey: hostPublicIdentityKey, + RequestIdentifier: requestIdentifier, + ResponseType: "GetIdentityReviewsInfo", + ReviewsInfo: reviewsInfoMap, + } + + innerResponseBytes, err := encoding.EncodeMessagePackBytes(responseContentObject) + if (err != nil) { return nil, err } + + encryptedResponse, err := createEncryptedAndSignedResponse(connectionKey, hostPrivateIdentityKey, innerResponseBytes) + if (err != nil) { return nil, err } + + return encryptedResponse, nil +} + + +//Outputs: +// -bool: Able to read server response +// -[16]byte: Request Identifier +// -[16]byte: Host Identity Hash +// -map[[29]byte][]byte: Reviews Info map (Review Hash -> Reviewed Hash) +// -error (returns err if input keys are invalid) +func ReadServerResponse_GetIdentityReviewsInfo(responseBytes []byte, connectionKey [32]byte)(bool, [16]byte, [16]byte, map[[29]byte][]byte, error){ + + //TODO: Verify inputs + + ableToRead, requestIdentifier, hostIdentityHash, decryptedContentBytes, err := readEncryptedAndSignedResponse(responseBytes, connectionKey, "GetIdentityReviewsInfo") + if (err != nil) { return false, [16]byte{}, [16]byte{}, nil, err } + if (ableToRead == false){ + //Response is malformed + return false, [16]byte{}, [16]byte{}, nil, nil + } + + type responseContentStruct struct{ + ReviewsInfo map[[29]byte][]byte + } + + var responseContentObject responseContentStruct + + err = encoding.DecodeMessagePackBytes(true, decryptedContentBytes, &responseContentObject) + if (err != nil) { + // Response malformed + return false, [16]byte{}, [16]byte{}, nil, nil + } + + reviewsInfoMap := responseContentObject.ReviewsInfo + + for reviewHash, reviewedHash := range reviewsInfoMap{ + + reviewedType, err := helpers.GetReviewedTypeFromReviewedHash(reviewedHash) + if (err != nil) { + // Response is malformed + return false, [16]byte{}, [16]byte{}, nil, nil + } + if (reviewedType != "Identity" && reviewedType != "Profile" && reviewedType != "Attribute"){ + // Response is malformed + return false, [16]byte{}, [16]byte{}, nil, nil + } + + isValid, err := readReviews.VerifyReviewHash(reviewHash, true, reviewedType) + if (err != nil) { return false, [16]byte{}, [16]byte{}, nil, err } + if (isValid == false){ + // Received reviewsInfoMap contains invalid reviewHash + // Response is malformed + return false, [16]byte{}, [16]byte{}, nil, nil + } + } + + return true, requestIdentifier, hostIdentityHash, reviewsInfoMap, nil +} + + +//Outputs: +// -[]byte: Response bytes +// -error +func CreateServerResponse_GetMessageReviewsInfo(hostPublicIdentityKey [32]byte, hostPrivateIdentityKey [64]byte, connectionKey [32]byte, requestIdentifier [16]byte, reviewsInfoMap map[[29]byte][26]byte)([]byte, error){ + + //TODO: Verify inputs + + type responseContentStruct struct{ + HostIdentityKey [32]byte + RequestIdentifier [16]byte + ResponseType string + ReviewsInfo map[[29]byte][26]byte // Review Hash -> Reviewed Message Hash + } + + responseContentObject := responseContentStruct{ + HostIdentityKey: hostPublicIdentityKey, + RequestIdentifier: requestIdentifier, + ResponseType: "GetMessageReviewsInfo", + ReviewsInfo: reviewsInfoMap, + } + + innerResponseBytes, err := encoding.EncodeMessagePackBytes(responseContentObject) + if (err != nil) { return nil, err } + + encryptedResponse, err := createEncryptedAndSignedResponse(connectionKey, hostPrivateIdentityKey, innerResponseBytes) + if (err != nil) { return nil, err } + + return encryptedResponse, nil +} + + +//Outputs: +// -bool: Able to read server response +// -[16]byte: Request Identifier +// -[16]byte: Host Identity Hash +// -map[[29]byte][26]byte: Reviews Info map (Review Hash -> Reviewed Message Hash) +// -error (returns err if input keys are invalid) +func ReadServerResponse_GetMessageReviewsInfo(responseBytes []byte, connectionKey [32]byte)(bool, [16]byte, [16]byte, map[[29]byte][26]byte, error){ + + //TODO: Verify inputs + + ableToRead, requestIdentifier, hostIdentityHash, decryptedContentBytes, err := readEncryptedAndSignedResponse(responseBytes, connectionKey, "GetMessageReviewsInfo") + if (err != nil) { return false, [16]byte{}, [16]byte{}, nil, err } + if (ableToRead == false){ + //Response is malformed + return false, [16]byte{}, [16]byte{}, nil, nil + } + + type responseContentStruct struct{ + ReviewsInfo map[[29]byte][26]byte + } + + var responseContentObject responseContentStruct + + err = encoding.DecodeMessagePackBytes(true, decryptedContentBytes, &responseContentObject) + if (err != nil) { + // Response malformed + return false, [16]byte{}, [16]byte{}, nil, nil + } + + reviewsInfoMap := responseContentObject.ReviewsInfo + + for reviewHash, _ := range reviewsInfoMap{ + + isValid, err := readReviews.VerifyReviewHash(reviewHash, true, "Message") + if (err != nil) { return false, [16]byte{}, [16]byte{}, nil, err } + if (isValid == false){ + // Received reviewsInfoMap contains invalid reviewHash + // Response is malformed + return false, [16]byte{}, [16]byte{}, nil, nil + } + } + + return true, requestIdentifier, hostIdentityHash, reviewsInfoMap, nil +} + + +//Outputs: +// -[]byte: Response bytes +// -error +func CreateServerResponse_GetReviews(hostPublicIdentityKey [32]byte, hostPrivateIdentityKey [64]byte, connectionKey [32]byte, requestIdentifier [16]byte, reviewsList [][]byte)([]byte, error){ + + //TODO: Verify inputs + + messagepackReviewsList := make([]messagepack.RawMessage, 0, len(reviewsList)) + + for _, reviewBytes := range reviewsList{ + messagepackReviewsList = append(messagepackReviewsList, reviewBytes) + } + + type responseContentStruct struct{ + HostIdentityKey [32]byte + RequestIdentifier [16]byte + ResponseType string + ReviewsList []messagepack.RawMessage + } + + responseContentObject := responseContentStruct{ + HostIdentityKey: hostPublicIdentityKey, + RequestIdentifier: requestIdentifier, + ResponseType: "GetReviews", + ReviewsList: messagepackReviewsList, + } + + innerResponseBytes, err := encoding.EncodeMessagePackBytes(responseContentObject) + if (err != nil) { return nil, err } + + encryptedResponse, err := createEncryptedAndSignedResponse(connectionKey, hostPrivateIdentityKey, innerResponseBytes) + if (err != nil) { return nil, err } + + return encryptedResponse, nil +} + + +//Outputs: +// -bool: Able to read server response +// -[16]byte: Request Identifier +// -[16]byte: Host Identity Hash +// -[][]byte: Reviews list +// -error (returns err if input keys are invalid) +func ReadServerResponse_GetReviews(responseBytes []byte, connectionKey [32]byte)(bool, [16]byte, [16]byte, [][]byte, error){ + + //TODO: Verify inputs + + ableToRead, requestIdentifier, hostIdentityHash, decryptedContentBytes, err := readEncryptedAndSignedResponse(responseBytes, connectionKey, "GetReviews") + if (err != nil) { return false, [16]byte{}, [16]byte{}, nil, err } + if (ableToRead == false){ + //Response is malformed + return false, [16]byte{}, [16]byte{}, nil, nil + } + + type responseContentStruct struct{ + ReviewsList []messagepack.RawMessage + } + + var responseContentObject responseContentStruct + + err = encoding.DecodeMessagePackBytes(true, decryptedContentBytes, &responseContentObject) + if (err != nil) { + // Response is malformed + return false, [16]byte{}, [16]byte{}, nil, nil + } + + messagepackReviewsList := responseContentObject.ReviewsList + + // We use a map to detect duplicates + reviewHashesMap := make(map[[29]byte]struct{}) + + reviewsList := make([][]byte, 0, len(messagepackReviewsList)) + + // We use this variable to ensure that all reviews in the response belong to the same networkType + reviewsNetworkType := byte(0) + + for index, reviewBytes := range messagepackReviewsList{ + + ableToRead, reviewHash, _, reviewNetworkType, _, _, _, _, _, _, err := readReviews.ReadReviewAndHash(true, reviewBytes) + if (err != nil) { return false, [16]byte{}, [16]byte{}, nil, err } + if (ableToRead == false){ + // Review is invalid, host must be malicious. + return false, [16]byte{}, [16]byte{}, nil, nil + } + + _, exists := reviewHashesMap[reviewHash] + if (exists == true){ + // Duplicate review exists + // Response is malformed + return false, [16]byte{}, [16]byte{}, nil, nil + } + + reviewHashesMap[reviewHash] = struct{}{} + + if (index == 0){ + reviewsNetworkType = reviewNetworkType + + } else if (reviewsNetworkType != reviewNetworkType){ + + // Response contains two reviews with differing networkType + // Response is malformed + return false, [16]byte{}, [16]byte{}, nil, nil + } + + reviewsList = append(reviewsList, reviewBytes) + } + + return true, requestIdentifier, hostIdentityHash, reviewsList, nil +} + + +//Outputs: +// -[]byte: Response bytes +// -error +func CreateServerResponse_GetIdentityReportsInfo(hostPublicIdentityKey [32]byte, hostPrivateIdentityKey [64]byte, connectionKey [32]byte, requestIdentifier [16]byte, reportsInfoMap map[[30]byte][]byte)([]byte, error){ + + //TODO: Verify inputs + + type responseContentStruct struct{ + HostIdentityKey [32]byte + RequestIdentifier [16]byte + ResponseType string + ReportsInfo map[[30]byte][]byte // Report hash -> Reported hash + } + + responseContentObject := responseContentStruct{ + HostIdentityKey: hostPublicIdentityKey, + RequestIdentifier: requestIdentifier, + ResponseType: "GetIdentityReportsInfo", + ReportsInfo: reportsInfoMap, + } + + innerResponseBytes, err := encoding.EncodeMessagePackBytes(responseContentObject) + if (err != nil) { return nil, err } + + encryptedResponse, err := createEncryptedAndSignedResponse(connectionKey, hostPrivateIdentityKey, innerResponseBytes) + if (err != nil) { return nil, err } + + return encryptedResponse, nil +} + + +//Outputs: +// -bool: Able to read server response +// -[16]byte: Request Identifier +// -[16]byte: Host Identity Hash +// -map[[30]byte][]byte: Reports Info map (Report Hash -> Reported hash) +// -error (returns err if input keys are invalid) +func ReadServerResponse_GetIdentityReportsInfo(responseBytes []byte, connectionKey [32]byte)(bool, [16]byte, [16]byte, map[[30]byte][]byte, error){ + + //TODO: Verify inputs + + ableToRead, requestIdentifier, hostIdentityHash, decryptedContentBytes, err := readEncryptedAndSignedResponse(responseBytes, connectionKey, "GetIdentityReportsInfo") + if (err != nil) { return false, [16]byte{}, [16]byte{}, nil, err } + if (ableToRead == false){ + //Response is malformed + return false, [16]byte{}, [16]byte{}, nil, nil + } + + type responseContentStruct struct{ + ReportsInfo map[[30]byte][]byte + } + + var responseContentObject responseContentStruct + + err = encoding.DecodeMessagePackBytes(true, decryptedContentBytes, &responseContentObject) + if (err != nil) { + // Response malformed + return false, [16]byte{}, [16]byte{}, nil, nil + } + + reportsInfoMap := responseContentObject.ReportsInfo + + for reportHash, reportedHash := range reportsInfoMap{ + + reportedType, err := helpers.GetReportedTypeFromReportedHash(reportedHash) + if (err != nil) { + // Response is malformed + return false, [16]byte{}, [16]byte{}, nil, nil + } + if (reportedType != "Identity" && reportedType != "Profile" && reportedType != "Attribute"){ + // Response is malformed + return false, [16]byte{}, [16]byte{}, nil, nil + } + + isValid, err := readReports.VerifyReportHash(reportHash, true, reportedType) + if (err != nil){ return false, [16]byte{}, [16]byte{}, nil, err } + if (isValid == false){ + //Response contains invalid reportHash + // Response is malformed + return false, [16]byte{}, [16]byte{}, nil, nil + } + } + + return true, requestIdentifier, hostIdentityHash, reportsInfoMap, nil +} + + + +//Outputs: +// -[]byte: Response bytes +// -error +func CreateServerResponse_GetMessageReportsInfo(hostPublicIdentityKey [32]byte, hostPrivateIdentityKey [64]byte, connectionKey [32]byte, requestIdentifier [16]byte, reportsInfoMap map[[30]byte][26]byte)([]byte, error){ + + //TODO: Verify inputs + + type responseContentStruct struct{ + HostIdentityKey [32]byte + RequestIdentifier [16]byte + ResponseType string + ReportsInfo map[[30]byte][26]byte // Report Hash -> Reported message hash + } + + responseContentObject := responseContentStruct{ + HostIdentityKey: hostPublicIdentityKey, + RequestIdentifier: requestIdentifier, + ResponseType: "GetMessageReportsInfo", + ReportsInfo: reportsInfoMap, + } + + innerResponseBytes, err := encoding.EncodeMessagePackBytes(responseContentObject) + if (err != nil) { return nil, err } + + encryptedResponse, err := createEncryptedAndSignedResponse(connectionKey, hostPrivateIdentityKey, innerResponseBytes) + if (err != nil) { return nil, err } + + return encryptedResponse, nil +} + + +//Outputs: +// -bool: Able to read server response +// -[16]byte: Request Identifier +// -[16]byte: Host Identity Hash +// -map[[30]byte][26]byte: Reports Info map (Report hash -> Reported message hash) +// -error (returns err if input keys are invalid) +func ReadServerResponse_GetMessageReportsInfo(responseBytes []byte, connectionKey [32]byte)(bool, [16]byte, [16]byte, map[[30]byte][26]byte, error){ + + //TODO: Verify inputs + + ableToRead, requestIdentifier, hostIdentityHash, decryptedContentBytes, err := readEncryptedAndSignedResponse(responseBytes, connectionKey, "GetMessageReportsInfo") + if (err != nil) { return false, [16]byte{}, [16]byte{}, nil, err } + if (ableToRead == false){ + //Response is malformed + return false, [16]byte{}, [16]byte{}, nil, nil + } + + type responseContentStruct struct{ + ReportsInfo map[[30]byte][26]byte + } + + var responseContentObject responseContentStruct + + err = encoding.DecodeMessagePackBytes(true, decryptedContentBytes, &responseContentObject) + if (err != nil) { + // Response malformed + return false, [16]byte{}, [16]byte{}, nil, nil + } + + reportsInfoMap := responseContentObject.ReportsInfo + + for reportHash, _ := range reportsInfoMap{ + + isValid, err := readReports.VerifyReportHash(reportHash, true, "Message") + if (err != nil) { return false, [16]byte{}, [16]byte{}, nil, err } + if (isValid == false){ + // Response contains invalid reportHash + // Response is malformed + return false, [16]byte{}, [16]byte{}, nil, nil + } + } + + return true, requestIdentifier, hostIdentityHash, reportsInfoMap, nil +} + + +//Outputs: +// -[]byte: Response bytes +// -error +func CreateServerResponse_GetReports(hostPublicIdentityKey [32]byte, hostPrivateIdentityKey [64]byte, connectionKey [32]byte, requestIdentifier [16]byte, reportsList [][]byte)([]byte, error){ + + //TODO: Verify inputs + + messagepackReportsList := make([]messagepack.RawMessage, 0, len(reportsList)) + + for _, reportBytes := range reportsList{ + + messagepackReportsList = append(messagepackReportsList, reportBytes) + } + + type responseContentStruct struct{ + HostIdentityKey [32]byte + RequestIdentifier [16]byte + ResponseType string + ReportsList []messagepack.RawMessage + } + + responseContentObject := responseContentStruct{ + HostIdentityKey: hostPublicIdentityKey, + RequestIdentifier: requestIdentifier, + ResponseType: "GetReports", + ReportsList: messagepackReportsList, + } + + innerResponseBytes, err := encoding.EncodeMessagePackBytes(responseContentObject) + if (err != nil) { return nil, err } + + encryptedResponse, err := createEncryptedAndSignedResponse(connectionKey, hostPrivateIdentityKey, innerResponseBytes) + if (err != nil) { return nil, err } + + return encryptedResponse, nil +} + + +//Outputs: +// -bool: Able to read server response +// -[16]byte: Request Identifier +// -[16]byte: Host Identity Hash +// -[][]byte: Reports list +// -error (returns err if input keys are invalid) +func ReadServerResponse_GetReports(responseBytes []byte, connectionKey [32]byte)(bool, [16]byte, [16]byte, [][]byte, error){ + + //TODO: Verify inputs + + ableToRead, requestIdentifier, hostIdentityHash, decryptedContentBytes, err := readEncryptedAndSignedResponse(responseBytes, connectionKey, "GetReports") + if (err != nil) { return false, [16]byte{}, [16]byte{}, nil, err } + if (ableToRead == false){ + //Response is malformed + return false, [16]byte{}, [16]byte{}, nil, nil + } + + type responseContentStruct struct{ + ReportsList []messagepack.RawMessage + } + + var responseContentObject responseContentStruct + + err = encoding.DecodeMessagePackBytes(true, decryptedContentBytes, &responseContentObject) + if (err != nil) { + // Response is malformed + return false, [16]byte{}, [16]byte{}, nil, nil + } + + messagepackReportsList := responseContentObject.ReportsList + + // We use this map to detect duplicates + reportHashesMap := make(map[[30]byte]struct{}) + + reportsList := make([][]byte, 0, len(messagepackReportsList)) + + // We use this variable to ensure all reports in response belong to the same network type + reportsNetworkType := byte(0) + + for index, reportBytes := range messagepackReportsList{ + + ableToRead, reportHash, _, reportNetworkType, _, _, _, _, err := readReports.ReadReportAndHash(true, reportBytes) + if (err != nil) { return false, [16]byte{}, [16]byte{}, nil, err } + if (ableToRead == false){ + // Response is malformed + return false, [16]byte{}, [16]byte{}, nil, nil + } + + _, exists := reportHashesMap[reportHash] + if (exists == true){ + // A duplicate report exists + // Response is malformed + return false, [16]byte{}, [16]byte{}, nil, nil + } + reportHashesMap[reportHash] = struct{}{} + + if (index == 0){ + reportsNetworkType = reportNetworkType + + } else if (reportsNetworkType != reportNetworkType){ + // All reports in response must belong to the same network type + // Response is malformed + return false, [16]byte{}, [16]byte{}, nil, nil + } + + reportsList = append(reportsList, reportBytes) + } + + return true, requestIdentifier, hostIdentityHash, reportsList, nil +} + +// This object type is used to represent a deposit to an address +type DepositStruct struct{ + + // The cryptocurrency address where funds were deposited + Address string + + // The unix time of the block when deposit(s) were made + DepositTime int64 + + // The sum of all deposit amounts in the block to the specified address, in crypto atomic units (example: wei) + //TODO: Change this to big.Int + DepositAmount int64 +} + +//Outputs: +// -[]byte: Response bytes +// -error +func CreateServerResponse_GetAddressDeposits(hostPublicIdentityKey [32]byte, hostPrivateIdentityKey [64]byte, connectionKey [32]byte, requestIdentifier [16]byte, depositObjectsList []DepositStruct)([]byte, error){ + + //TODO: Verify inputs + + type responseContentStruct struct{ + HostIdentityKey [32]byte + RequestIdentifier [16]byte + ResponseType string + AddressDepositsList []DepositStruct + } + + responseContentObject := responseContentStruct{ + HostIdentityKey: hostPublicIdentityKey, + RequestIdentifier: requestIdentifier, + ResponseType: "GetAddressDeposits", + AddressDepositsList: depositObjectsList, + } + + innerResponseBytes, err := encoding.EncodeMessagePackBytes(responseContentObject) + if (err != nil) { return nil, err } + + encryptedResponse, err := createEncryptedAndSignedResponse(connectionKey, hostPrivateIdentityKey, innerResponseBytes) + if (err != nil) { return nil, err } + + return encryptedResponse, nil +} + + +//Outputs: +// -bool: Able to read server response +// -[16]byte: Request Identifier +// -[16]byte: Host Identity Hash +// -[]DepositStruct: Deposit objects list +// -error (returns err if input keys are invalid) +func ReadServerResponse_GetAddressDeposits(responseBytes []byte, connectionKey [32]byte)(bool, [16]byte, [16]byte, []DepositStruct, error){ + + //TODO: Verify inputs + + ableToRead, requestIdentifier, hostIdentityHash, decryptedContentBytes, err := readEncryptedAndSignedResponse(responseBytes, connectionKey, "GetAddressDeposits") + if (err != nil) { return false, [16]byte{}, [16]byte{}, nil, err } + if (ableToRead == false){ + //Response is malformed + return false, [16]byte{}, [16]byte{}, nil, nil + } + + type responseContentStruct struct{ + AddressDepositsList []DepositStruct + } + + var responseContentObject responseContentStruct + + err = encoding.DecodeMessagePackBytes(true, decryptedContentBytes, &responseContentObject) + if (err != nil) { + // Response malformed + return false, [16]byte{}, [16]byte{}, nil, nil + } + + addressDepositsList := responseContentObject.AddressDepositsList + + //TODO: validate info + + return true, requestIdentifier, hostIdentityHash, addressDepositsList, nil +} + + +//Outputs: +// -[]byte: Response bytes +// -error +func CreateServerResponse_GetViewableStatuses(hostPublicIdentityKey [32]byte, hostPrivateIdentityKey [64]byte, connectionKey [32]byte, requestIdentifier [16]byte, identityHashStatusesMap map[[16]byte]bool, profileHashStatusesMap map[[28]byte]bool)([]byte, error){ + + //TODO: Verify inputs + + type responseContentStruct struct{ + HostIdentityKey [32]byte + RequestIdentifier [16]byte + ResponseType string + IdentityHashStatuses map[[16]byte]bool + ProfileHashStatuses map[[28]byte]bool + } + + responseContentObject := responseContentStruct{ + HostIdentityKey: hostPublicIdentityKey, + RequestIdentifier: requestIdentifier, + ResponseType: "GetViewableStatuses", + IdentityHashStatuses: identityHashStatusesMap, + ProfileHashStatuses: profileHashStatusesMap, + } + + innerResponseBytes, err := encoding.EncodeMessagePackBytes(responseContentObject) + if (err != nil) { return nil, err } + + encryptedResponse, err := createEncryptedAndSignedResponse(connectionKey, hostPrivateIdentityKey, innerResponseBytes) + if (err != nil) { return nil, err } + + return encryptedResponse, nil +} + + +//Outputs: +// -bool: Able to read server response +// -[16]byte: Request Identifier +// -[16]byte: Host Identity Hash +// -map[[16]byte]bool: Identity Hash Statuses Map (Identity Hash -> true/false)( true = viewable, false = unviewable) +// -map[[28]byte]bool: Profile hash statuses map (Profile Hash -> true/false)( true = viewable, false = unviewable) +// -error (returns err if input keys are invalid) +func ReadServerResponse_GetViewableStatuses(responseBytes []byte, connectionKey [32]byte)(bool, [16]byte, [16]byte, map[[16]byte]bool, map[[28]byte]bool, error){ + + ableToRead, requestIdentifier, hostIdentityHash, decryptedContentBytes, err := readEncryptedAndSignedResponse(responseBytes, connectionKey, "GetViewableStatuses") + if (err != nil) { return false, [16]byte{}, [16]byte{}, nil, nil, err } + if (ableToRead == false){ + //Response is malformed + return false, [16]byte{}, [16]byte{}, nil, nil, nil + } + + type responseContentStruct struct{ + IdentityHashStatuses map[[16]byte]bool + ProfileHashStatuses map[[28]byte]bool + } + + var responseContentObject responseContentStruct + + err = encoding.DecodeMessagePackBytes(true, decryptedContentBytes, &responseContentObject) + if (err != nil) { + // Response malformed + return false, [16]byte{}, [16]byte{}, nil, nil, nil + } + + identityHashStatusesMap := responseContentObject.IdentityHashStatuses + profileHashStatusesMap := responseContentObject.ProfileHashStatuses + + //TODO: Verify maps + + return true, requestIdentifier, hostIdentityHash, identityHashStatusesMap, profileHashStatusesMap, nil +} + + +//Outputs: +// -[]byte: Response bytes +// -error +func CreateServerResponse_BroadcastContent(hostPublicIdentityKey [32]byte, hostPrivateIdentityKey [64]byte, connectionKey [32]byte, requestIdentifier [16]byte, contentAcceptedInfoMap map[string]bool)([]byte, error){ + + //TODO: Verify inputs + + type responseContentStruct struct{ + HostIdentityKey [32]byte + RequestIdentifier [16]byte + ResponseType string + ContentAcceptedInfo map[string]bool //Content Hash -> true/false + } + + responseContentObject := responseContentStruct{ + HostIdentityKey: hostPublicIdentityKey, + RequestIdentifier: requestIdentifier, + ResponseType: "BroadcastContent", + ContentAcceptedInfo: contentAcceptedInfoMap, + } + + innerResponseBytes, err := encoding.EncodeMessagePackBytes(responseContentObject) + if (err != nil) { return nil, err } + + encryptedResponse, err := createEncryptedAndSignedResponse(connectionKey, hostPrivateIdentityKey, innerResponseBytes) + if (err != nil) { return nil, err } + + return encryptedResponse, nil +} + + +//Outputs: +// -bool: Able to read server response +// -[16]byte: Request Identifier +// -[16]byte: Host Identity Hash +// -map[string]bool: Content Accepted Info Map (Content Hash -> true/false) +// -error (returns err if input keys are invalid) +func ReadServerResponse_BroadcastContent(responseBytes []byte, connectionKey [32]byte)(bool, [16]byte, [16]byte, map[string]bool, error){ + + //TODO: Verify stuff + + ableToRead, requestIdentifier, hostIdentityHash, decryptedContentBytes, err := readEncryptedAndSignedResponse(responseBytes, connectionKey, "BroadcastContent") + if (err != nil) { return false, [16]byte{}, [16]byte{}, nil, err } + if (ableToRead == false){ + //Response is malformed + return false, [16]byte{}, [16]byte{}, nil, nil + } + + type responseContentStruct struct{ + ContentAcceptedInfo map[string]bool + } + + var responseContentObject responseContentStruct + + err = encoding.DecodeMessagePackBytes(true, decryptedContentBytes, &responseContentObject) + if (err != nil) { + // Response is malformed + return false, [16]byte{}, [16]byte{}, nil, nil + } + + contentAcceptedInfoMap := responseContentObject.ContentAcceptedInfo + + for contentHash, _ := range contentAcceptedInfoMap{ + + _, err := helpers.GetContentTypeFromContentHash([]byte(contentHash)) + if (err != nil){ + // Invalid content hash + return false, [16]byte{}, [16]byte{}, nil, nil + } + } + + return true, requestIdentifier, hostIdentityHash, contentAcceptedInfoMap, nil +} + +func createEncryptedAndSignedResponse(connectionKey [32]byte, hostPrivateIdentityKey [64]byte, innerResponseBytes []byte)([]byte, error){ + + responseSignedBytes, err := createResponseSignedContent(hostPrivateIdentityKey, innerResponseBytes) + if (err != nil) { return nil, err } + + encryptedResponse, err := createEncryptedResponse(responseSignedBytes, connectionKey) + if (err != nil) { return nil, err } + + return encryptedResponse, nil +} + +//Outputs: +// -bool: Able to read (response is valid) +// -[16]byte: Request identifier +// -[16]byte: Host identity hash +// -[]byte: Content Bytes +// -error (return err if there is a bug in the function) +func readEncryptedAndSignedResponse(inputResponse []byte, connectionKey [32]byte, expectedResponseType string)(bool, [16]byte, [16]byte, []byte, error){ + + ableToDecrypt, decryptedResponseBytes, err := readEncryptedResponse(inputResponse, connectionKey) + if (err != nil) { return false, [16]byte{}, [16]byte{}, nil, err } + if (ableToDecrypt == false){ + return false, [16]byte{}, [16]byte{}, nil, nil + } + + ableToRead, requestIdentifier, hostIdentityHash, contentBytes, err := readResponseSignedContent(decryptedResponseBytes, expectedResponseType) + if (err != nil) { return false, [16]byte{}, [16]byte{}, nil, err } + if (ableToRead == false){ + //Response is malformed + return false, [16]byte{}, [16]byte{}, nil, nil + } + + return true, requestIdentifier, hostIdentityHash, contentBytes, nil +} + + +func createEncryptedResponse(contentToEncrypt []byte, connectionKey [32]byte)([]byte, error){ + + chaPolyNonce, err := helpers.GetNewRandom24ByteArray() + if (err != nil) { return nil, err } + + cipheredContent, err := chaPolyShrink.EncryptChaPolyShrink(contentToEncrypt, connectionKey, chaPolyNonce, true, 100, false, [32]byte{}) + if (err != nil) { return nil, err } + + type encryptedResponseStruct struct{ + ChaPolyNonce [24]byte + CipheredContent []byte + } + + encryptedResponseObject := encryptedResponseStruct{ + ChaPolyNonce: chaPolyNonce, + CipheredContent: cipheredContent, + } + + encryptedResponseBytes, err := encoding.EncodeMessagePackBytes(encryptedResponseObject) + if (err != nil) { return nil, err } + + return encryptedResponseBytes, nil +} + +//Outputs: +// -bool: Response well formed and able to decrypt +// -[]byte: Decrypted response +// -error (decryption key inputs are malformed) +func readEncryptedResponse(inputResponse []byte, connectionKey [32]byte)(bool, []byte, error){ + + type encryptedResponseStruct struct{ + ChaPolyNonce [24]byte + CipheredContent []byte + } + + var encryptedResponseObject encryptedResponseStruct + + err := encoding.DecodeMessagePackBytes(true, inputResponse, &encryptedResponseObject) + if (err != nil) { + return false, nil, nil + } + + chaPolyNonce := encryptedResponseObject.ChaPolyNonce + cipheredContent := encryptedResponseObject.CipheredContent + + ableToDecrypt, decryptedBytes, err := chaPolyShrink.DecryptChaPolyShrink(cipheredContent, connectionKey, chaPolyNonce, false, [32]byte{}) + if (err != nil) { + return false, nil, nil + } + if (ableToDecrypt == false){ + return false, nil, nil + } + + return true, decryptedBytes, nil +} + + +func createResponseSignedContent(hostIdentityPrivateKey [64]byte, responseContent messagepack.RawMessage)([]byte, error){ + + contentHash, err := blake3.Get32ByteBlake3Hash(responseContent) + if (err != nil) { return nil, err } + + responseSignature := edwardsKeys.CreateSignature(hostIdentityPrivateKey, contentHash) + + type finalResponseStruct struct { + Signature [64]byte + Content messagepack.RawMessage + } + + finalResponseObject := finalResponseStruct{ + Signature: responseSignature, + Content: responseContent, + } + + finalResponseBytes, err := encoding.EncodeMessagePackBytes(finalResponseObject) + if (err != nil) { return nil, err } + + return finalResponseBytes, nil +} + +//Outputs: +// -bool: Able to read (response is valid) +// -[16]byte: Request identifier +// -[16]byte: Host identity hash +// -[]byte: Content bytes +// -error (return err if there is a bug in the function) +func readResponseSignedContent(inputBytes []byte, expectedResponseType string)(bool, [16]byte, [16]byte, []byte, error){ + + type outerResponseStruct struct{ + Signature [64]byte + Content messagepack.RawMessage + } + + var outerResponseObject outerResponseStruct + + err := encoding.DecodeMessagePackBytes(true, inputBytes, &outerResponseObject) + if (err != nil) { + return false, [16]byte{}, [16]byte{}, nil, nil + } + + responseSignature := outerResponseObject.Signature + responseContentBytes := outerResponseObject.Content + + if (len(responseContentBytes) == 0){ + return false, [16]byte{}, [16]byte{}, nil, nil + } + + responseContentHashed, err := blake3.Get32ByteBlake3Hash(responseContentBytes) + if (err != nil) { return false, [16]byte{}, [16]byte{}, nil, err } + + type partialResponseStruct struct { + HostIdentityKey [32]byte + RequestIdentifier [16]byte + ResponseType string + } + + var partialResponseObject partialResponseStruct + + err = encoding.DecodeMessagePackBytes(true, responseContentBytes, &partialResponseObject) + if (err != nil) { + return false, [16]byte{}, [16]byte{}, nil, nil + } + + requestIdentifier := partialResponseObject.RequestIdentifier + hostIdentityKey := partialResponseObject.HostIdentityKey + responseType := partialResponseObject.ResponseType + + isValid := edwardsKeys.VerifySignature(hostIdentityKey, responseSignature, responseContentHashed) + if (isValid == false){ + return false, [16]byte{}, [16]byte{}, nil, nil + } + + hostIdentityHash, err := identity.ConvertIdentityKeyToIdentityHash(hostIdentityKey, "Host") + if (err != nil) { return false, [16]byte{}, [16]byte{}, nil, err } + + if (responseType != expectedResponseType){ + return false, [16]byte{}, [16]byte{}, nil, nil + } + + return true, requestIdentifier, hostIdentityHash, responseContentBytes, nil +} + + + diff --git a/internal/network/serverResponse/serverResponse_test.go b/internal/network/serverResponse/serverResponse_test.go new file mode 100644 index 0000000..0f3ca15 --- /dev/null +++ b/internal/network/serverResponse/serverResponse_test.go @@ -0,0 +1,1432 @@ +package serverResponse_test + +import "seekia/resources/geneticReferences/traits" +import "seekia/resources/geneticReferences/monogenicDiseases" +import "seekia/resources/geneticReferences/polygenicDiseases" + +import "seekia/internal/network/serverResponse" + +import "seekia/internal/cryptocurrency/cardanoAddress" +import "seekia/internal/cryptocurrency/ethereumAddress" +import "seekia/internal/cryptography/kyber" +import "seekia/internal/cryptography/nacl" +import "seekia/internal/generate" +import "seekia/internal/helpers" +import "seekia/internal/identity" +import "seekia/internal/parameters/readParameters" +import "seekia/internal/profiles/profileFormat" + +import "crypto/rand" +import "testing" +import "time" +import "reflect" +import "maps" +import "slices" +import "errors" + +//TODO: Test the None case for all responses where it is permissible + +//TODO: Replace the Random retrieval of certain parameters with a for loop that will cycle through each option +// For example, instead of choosing a profileType to retrieve by random, try all profileTypes +// This way, the tests will not have to be run multiple times to test all profileTypes + +func getNewRandomRequestIdentifier()([16]byte, error){ + + var requestIdentifier [16]byte + + _, err := rand.Read(requestIdentifier[:]) + if (err != nil) { return [16]byte{}, err } + + return requestIdentifier, nil +} + +func getNewRandomConnectionKey()([32]byte, error){ + + var newKeyArray [32]byte + + _, err := rand.Read(newKeyArray[:]) + if (err != nil) { return [32]byte{}, err } + + return newKeyArray, nil +} + + +func TestCreateAndReadResponse_EstablishConnectionKey(t *testing.T){ + + hostPublicIdentityKey, hostPrivateIdentityKey, err := identity.GetNewRandomPublicPrivateIdentityKeys() + if (err != nil) { + t.Fatalf("Failed to create random identity keys: " + err.Error()) + } + + hostIdentityHash, err := identity.ConvertIdentityKeyToIdentityHash(hostPublicIdentityKey, "Host") + if (err != nil) { + t.Fatalf("Failed to get host identity hash: " + err.Error()) + } + + requestorNaclPublicKey, requestorNaclPrivateKey, err := nacl.GetNewRandomPublicPrivateNaclKeys() + if (err != nil) { + t.Fatalf("Failed to create random nacl keys: " + err.Error()) + } + + requestorKyberPublicKey, requestorKyberPrivateKey, err := kyber.GetNewRandomPublicPrivateKyberKeys() + if (err != nil) { + t.Fatalf("Failed to create random kyber keys: " + err.Error()) + } + + requestIdentifier, err := getNewRandomRequestIdentifier() + if (err != nil) { + t.Fatalf("getNewRandomRequestIdentifier failed: " + err.Error()) + } + + responseBytes, connectionKey, err := serverResponse.CreateServerResponse_EstablishConnectionKey(hostPublicIdentityKey, hostPrivateIdentityKey, requestIdentifier, requestorNaclPublicKey, requestorKyberPublicKey) + if (err != nil) { + t.Fatalf("Failed to create EstablishConnectionKey response: " + err.Error()) + } + + ableToRead, requestIdentifier_Received, hostIdentityHash_Received, connectionKey_Received, err := serverResponse.ReadServerResponse_EstablishConnectionKey(responseBytes, requestorNaclPublicKey, requestorNaclPrivateKey, requestorKyberPrivateKey) + if (err != nil) { + t.Fatalf("Failed to read EstablishConnectionKey response: " + err.Error()) + } + if (ableToRead == false) { + t.Fatalf("Failed to read EstablishConnectionKey response.") + } + + if (requestIdentifier_Received != requestIdentifier){ + t.Fatalf("Failed to read EstablishConnectionKey response: mismatched requestIdentifier.") + } + if (hostIdentityHash_Received != hostIdentityHash){ + t.Fatalf("Failed to read EstablishConnectionKey response: Mismatched hostIdentityHash.") + } + + if (connectionKey != connectionKey_Received){ + t.Fatalf("Failed to read EstablishConnectionKey response: Mismatched connectionKey.") + } +} + + +func TestCreateAndReadResponse_GetParametersInfo(t *testing.T){ + + hostPublicIdentityKey, hostPrivateIdentityKey, err := identity.GetNewRandomPublicPrivateIdentityKeys() + if (err != nil) { + t.Fatalf("Failed to create random identity keys: " + err.Error()) + } + + hostIdentityHash, err := identity.ConvertIdentityKeyToIdentityHash(hostPublicIdentityKey, "Host") + if (err != nil) { + t.Fatalf("Failed to get host identity hash: " + err.Error()) + } + + connectionKey, err := getNewRandomConnectionKey() + if (err != nil) { + t.Fatalf("getNewRandomConnectionKey failed: " + err.Error()) + } + + requestIdentifier, err := getNewRandomRequestIdentifier() + if (err != nil) { + t.Fatalf("getNewRandomRequestIdentifier failed: " + err.Error()) + } + + parametersTypesList := readParameters.GetAllParametersTypesList() + + helpers.RandomizeListOrder(parametersTypesList) + + parametersInfoMap := make(map[string]int64) + + for index, element := range parametersTypesList{ + + randomBool := helpers.GetRandomBool() + + if (index == 0 || randomBool == true){ + + parametersInfoMap[element] = time.Now().Unix() + } + } + + responseBytes, err := serverResponse.CreateServerResponse_GetParametersInfo(hostPublicIdentityKey, hostPrivateIdentityKey, connectionKey, requestIdentifier, parametersInfoMap) + if (err != nil) { + t.Fatalf("Failed to create GetParametersInfo response.") + } + + ableToRead, requestIdentifier_Received, hostIdentityHash_Received, parametersInfoMap_Received, err := serverResponse.ReadServerResponse_GetParametersInfo(responseBytes, connectionKey) + if (err != nil) { + t.Fatalf("Failed to read GetParametersInfo response: " + err.Error()) + } + if (ableToRead == false) { + t.Fatalf("Failed to read GetParametersInfo response.") + } + + if (requestIdentifier_Received != requestIdentifier){ + t.Fatalf("Failed to read GetParametersInfo response: mismatched requestIdentifier.") + } + if (hostIdentityHash_Received != hostIdentityHash){ + t.Fatalf("Failed to read GetParametersInfo response: Mismatched hostIdentityHash.") + } + + areEqual := maps.Equal(parametersInfoMap, parametersInfoMap_Received) + if (areEqual == false){ + t.Fatalf("Failed to read GetParametersInfo response: mismatched parametersInfoMap") + } +} + +func TestCreateAndReadResponse_GetParameters(t *testing.T){ + + hostPublicIdentityKey, hostPrivateIdentityKey, err := identity.GetNewRandomPublicPrivateIdentityKeys() + if (err != nil) { + t.Fatalf("Failed to create random identity keys.") + } + + hostIdentityHash, err := identity.ConvertIdentityKeyToIdentityHash(hostPublicIdentityKey, "Host") + if (err != nil) { + t.Fatalf("Failed to get host identity hash.") + } + + connectionKey, err := getNewRandomConnectionKey() + if (err != nil) { + t.Fatalf("getNewRandomConnectionKey failed: " + err.Error()) + } + + requestIdentifier, err := getNewRandomRequestIdentifier() + if (err != nil) { + t.Fatalf("getNewRandomRequestIdentifier failed: " + err.Error()) + } + + networkType, err := helpers.GetRandomByteWithinRange(1, 2) + if (err != nil){ + t.Fatalf("GetRandomByteWithinRange failed: " + err.Error()) + } + + parametersTypesList := readParameters.GetAllParametersTypesList() + + helpers.RandomizeListOrder(parametersTypesList) + + parametersList := make([][]byte, 0) + + for index, parametersType := range parametersTypesList{ + + randomBool := helpers.GetRandomBool() + + if (index == 0 || randomBool == true){ + + fakeParametersBytes, err := generate.GetFakeParameters(parametersType, networkType) + if (err != nil) { + t.Fatalf("Failed to get fake parameters: " + err.Error()) + } + + parametersList = append(parametersList, fakeParametersBytes) + } + } + + responseBytes, err := serverResponse.CreateServerResponse_GetParameters(hostPublicIdentityKey, hostPrivateIdentityKey, connectionKey, requestIdentifier, parametersList) + if (err != nil) { + t.Fatalf("Failed to create GetParameters response: " + err.Error()) + } + + ableToRead, requestIdentifier_Received, hostIdentityHash_Received, parametersList_Received, err := serverResponse.ReadServerResponse_GetParameters(responseBytes, connectionKey) + if (err != nil) { + t.Fatalf("Failed to read GetParameters response: " + err.Error()) + } + if (ableToRead == false) { + t.Fatalf("Failed to read GetParameters response.") + } + + if (requestIdentifier_Received != requestIdentifier){ + t.Fatalf("Failed to read GetParameters response: mismatched requestIdentifier.") + } + if (hostIdentityHash_Received != hostIdentityHash){ + t.Fatalf("Failed to read GetParameters response: Mismatched hostIdentityHash.") + } + + areEqual := reflect.DeepEqual(parametersList, parametersList_Received) + if (areEqual == false){ + t.Fatalf("Failed to read GetParameters response: mismatched parametersList.") + } +} + + +func TestCreateAndReadResponse_GetProfilesInfo(t *testing.T){ + + hostPublicIdentityKey, hostPrivateIdentityKey, err := identity.GetNewRandomPublicPrivateIdentityKeys() + if (err != nil) { + t.Fatalf("Failed to create random identity keys: " + err.Error()) + } + + hostIdentityHash, err := identity.ConvertIdentityKeyToIdentityHash(hostPublicIdentityKey, "Host") + if (err != nil) { + t.Fatalf("Failed to get host identity hash: " + err.Error()) + } + + connectionKey, err := getNewRandomConnectionKey() + if (err != nil) { + t.Fatalf("getNewRandomConnectionKey failed: " + err.Error()) + } + + requestIdentifier, err := getNewRandomRequestIdentifier() + if (err != nil) { + t.Fatalf("getNewRandomRequestIdentifier failed: " + err.Error()) + } + + randomProfileType, err := helpers.GetRandomItemFromList([]string{"Mate", "Host", "Moderator"}) + if (err != nil) { + t.Fatalf("Failed to get random profileType: " + err.Error()) + } + + profileInfoObjectsList := make([]serverResponse.ProfileInfoStruct, 0, 1000) + + for i:=0; i<1000; i++{ + + profileHash, err := helpers.GetNewRandomProfileHash(true, randomProfileType, false, false) + if (err != nil) { + t.Fatalf("Failed to create random profile hash: " + err.Error()) + } + + profileAuthor, err := identity.GetNewRandomIdentityHash(true, randomProfileType) + if (err != nil) { + t.Fatalf("Failed to create random identity hash: " + err.Error()) + } + + profileBroadcastTime := time.Now().Unix() + + profileInfoObject := serverResponse.ProfileInfoStruct{ + ProfileHash: profileHash, + ProfileAuthor: profileAuthor, + ProfileBroadcastTime: profileBroadcastTime, + } + + profileInfoObjectsList = append(profileInfoObjectsList, profileInfoObject) + } + + responseBytes, err := serverResponse.CreateServerResponse_GetProfilesInfo(hostPublicIdentityKey, hostPrivateIdentityKey, connectionKey, requestIdentifier, profileInfoObjectsList) + if (err != nil) { + t.Fatalf("Failed to create GetProfilesInfo response: " + err.Error()) + } + + ableToRead, requestIdentifier_Received, hostIdentityHash_Received, profilesInfoObjectsList_Received, err := serverResponse.ReadServerResponse_GetProfilesInfo(responseBytes, connectionKey) + if (err != nil) { + t.Fatalf("Failed to read GetProfilesInfo response: " + err.Error()) + } + if (ableToRead == false) { + t.Fatalf("Failed to read GetProfilesInfo response.") + } + + if (requestIdentifier_Received != requestIdentifier){ + t.Fatalf("Failed to read GetProfilesInfo response: mismatched requestIdentifier.") + } + if (hostIdentityHash_Received != hostIdentityHash){ + t.Fatalf("Failed to read GetProfilesInfo response: Mismatched hostIdentityHash.") + } + + areEqual := slices.Equal(profileInfoObjectsList, profilesInfoObjectsList_Received) + if (areEqual == false){ + t.Fatalf("Received profilesInfoObjectsList does not match original.") + } + + //TODO: Add None case +} + + +func TestCreateAndReadResponse_GetProfiles(t *testing.T){ + + err := profileFormat.InitializeProfileFormatVariables() + if (err != nil) { + t.Fatalf("Failed to initialize profile format variables: " + err.Error()) + } + + traits.InitializeTraitVariables() + monogenicDiseases.InitializeMonogenicDiseaseVariables() + polygenicDiseases.InitializePolygenicDiseaseVariables() + + hostPublicIdentityKey, hostPrivateIdentityKey, err := identity.GetNewRandomPublicPrivateIdentityKeys() + if (err != nil) { + t.Fatalf("Failed to create random identity keys: " + err.Error()) + } + + hostIdentityHash, err := identity.ConvertIdentityKeyToIdentityHash(hostPublicIdentityKey, "Host") + if (err != nil) { + t.Fatalf("Failed to get host identity hash: " + err.Error()) + } + + connectionKey, err := getNewRandomConnectionKey() + if (err != nil) { + t.Fatalf("getNewRandomConnectionKey failed: " + err.Error()) + } + + requestIdentifier, err := getNewRandomRequestIdentifier() + if (err != nil) { + t.Fatalf("getNewRandomRequestIdentifier failed: " + err.Error()) + } + + networkType, err := helpers.GetRandomByteWithinRange(1, 2) + if (err != nil){ + t.Fatalf("GetRandomByteWithinRange failed: " + err.Error()) + } + + profilesList := make([][]byte, 0, 10) + + for i:=0; i<10; i++{ + + fakeProfileType, err := helpers.GetRandomItemFromList([]string{"Mate", "Host", "Moderator"}) + if (err != nil) { + t.Fatalf("Failed to get random profileType: " + err.Error()) + } + + profilePublicIdentityKey, profilePrivateIdentityKey, err := identity.GetNewRandomPublicPrivateIdentityKeys() + if (err != nil) { + t.Fatalf("Failed to create random identity keys: " + err.Error()) + } + + fakeProfileBytes, err := generate.GetFakeProfile(fakeProfileType, profilePublicIdentityKey, profilePrivateIdentityKey, networkType) + if (err != nil) { + t.Fatalf("Failed to create fake profile: " + err.Error()) + } + + profilesList = append(profilesList, fakeProfileBytes) + } + + responseBytes, err := serverResponse.CreateServerResponse_GetProfiles(hostPublicIdentityKey, hostPrivateIdentityKey, connectionKey, requestIdentifier, profilesList) + if (err != nil) { + t.Fatalf("Failed to create GetProfiles response: " + err.Error()) + } + + ableToRead, requestIdentifier_Received, hostIdentityHash_Received, profilesList_Received, err := serverResponse.ReadServerResponse_GetProfiles(responseBytes, connectionKey) + if (err != nil) { + t.Fatalf("Failed to read GetProfiles response: " + err.Error()) + } + if (ableToRead == false) { + t.Fatalf("Failed to read GetProfiles response.") + } + + if (requestIdentifier_Received != requestIdentifier){ + t.Fatalf("Failed to read GetProfiles response: mismatched requestIdentifier.") + } + if (hostIdentityHash_Received != hostIdentityHash){ + t.Fatalf("Failed to read GetProfiles response: Mismatched hostIdentityHash.") + } + + areEqual := reflect.DeepEqual(profilesList, profilesList_Received) + if (areEqual == false){ + t.Fatalf("Failed to read GetProfiles response: mismatched profilesList.") + } + + // Now we test empty list case + + emptyProfilesList := make([][]byte, 0) + + responseBytes, err = serverResponse.CreateServerResponse_GetProfiles(hostPublicIdentityKey, hostPrivateIdentityKey, connectionKey, requestIdentifier, emptyProfilesList) + if (err != nil) { + t.Fatalf("Failed to create GetProfiles response: " + err.Error()) + } + + ableToRead, requestIdentifier_Received, hostIdentityHash_Received, profilesList_Received, err = serverResponse.ReadServerResponse_GetProfiles(responseBytes, connectionKey) + if (err != nil) { + t.Fatalf("Failed to read GetProfiles response: " + err.Error()) + } + if (ableToRead == false) { + t.Fatalf("Failed to read GetProfiles response.") + } + + if (requestIdentifier_Received != requestIdentifier){ + t.Fatalf("Failed to read GetProfiles response: mismatched requestIdentifier.") + } + if (hostIdentityHash_Received != hostIdentityHash){ + t.Fatalf("Failed to read GetProfiles response: Mismatched hostIdentityHash.") + } + + if (len(profilesList_Received) != 0){ + t.Fatalf("Failed to read GetProfiles response: profiles list not empty.") + } +} + + + +func TestCreateAndReadResponse_GetMessageHashesList(t *testing.T){ + + hostPublicIdentityKey, hostPrivateIdentityKey, err := identity.GetNewRandomPublicPrivateIdentityKeys() + if (err != nil) { + t.Fatalf("Failed to create random identity keys: " + err.Error()) + } + + hostIdentityHash, err := identity.ConvertIdentityKeyToIdentityHash(hostPublicIdentityKey, "Host") + if (err != nil) { + t.Fatalf("Failed to get host identity hash: " + err.Error()) + } + + connectionKey, err := getNewRandomConnectionKey() + if (err != nil) { + t.Fatalf("getNewRandomConnectionKey failed: " + err.Error()) + } + + requestIdentifier, err := getNewRandomRequestIdentifier() + if (err != nil) { + t.Fatalf("getNewRandomRequestIdentifier failed: " + err.Error()) + } + + messageHashesList := make([][26]byte, 0, 1000) + + for i:=0; i<1000; i++{ + + messageHash, err := helpers.GetNewRandomMessageHash() + if (err != nil) { + t.Fatalf("GetNewRandomMessageHash failed: " + err.Error()) + } + + messageHashesList = append(messageHashesList, messageHash) + } + + responseBytes, err := serverResponse.CreateServerResponse_GetMessageHashesList(hostPublicIdentityKey, hostPrivateIdentityKey, connectionKey, requestIdentifier, messageHashesList) + if (err != nil) { + t.Fatalf("Failed to create GetMessageHashesList response: " + err.Error()) + } + + ableToRead, requestIdentifier_Received, hostIdentityHash_Received, messageHashesList_Received, err := serverResponse.ReadServerResponse_GetMessageHashesList(responseBytes, connectionKey) + if (err != nil) { + t.Fatalf("Failed to read GetMessageHashesList response: " + err.Error()) + } + if (ableToRead == false) { + t.Fatalf("Failed to read GetMessageHashesList response.") + } + + if (requestIdentifier_Received != requestIdentifier){ + t.Fatalf("Failed to read GetMessageHashesList response: mismatched requestIdentifier.") + } + if (hostIdentityHash_Received != hostIdentityHash){ + t.Fatalf("Failed to read GetMessageHashesList response: Mismatched hostIdentityHash.") + } + + areEqual := slices.Equal(messageHashesList, messageHashesList_Received) + if (areEqual == false){ + t.Fatalf("Failed to read GetMessageHashesList response: Received messageHashesList does not match.") + } + + //TODO: Add None case + +} + + + +func TestCreateAndReadResponse_GetMessages(t *testing.T){ + + hostPublicIdentityKey, hostPrivateIdentityKey, err := identity.GetNewRandomPublicPrivateIdentityKeys() + if (err != nil) { + t.Fatalf("Failed to create random identity keys: " + err.Error()) + } + + hostIdentityHash, err := identity.ConvertIdentityKeyToIdentityHash(hostPublicIdentityKey, "Host") + if (err != nil) { + t.Fatalf("Failed to get host identity hash: " + err.Error()) + } + + connectionKey, err := getNewRandomConnectionKey() + if (err != nil) { + t.Fatalf("getNewRandomConnectionKey failed: " + err.Error()) + } + + requestIdentifier, err := getNewRandomRequestIdentifier() + if (err != nil) { + t.Fatalf("getNewRandomRequestIdentifier failed: " + err.Error()) + } + + networkType, err := helpers.GetRandomByteWithinRange(1, 2) + if (err != nil){ + t.Fatalf("GetRandomByteWithinRange failed: " + err.Error()) + } + + messagesList := make([][]byte, 0, 10) + + for i:=0; i<10; i++{ + + fakeMessageBytes, _, _, err := generate.GetFakeMessage(networkType, "Text") + if (err != nil) { + t.Fatalf("Failed to create fake message: " + err.Error()) + } + + messagesList = append(messagesList, fakeMessageBytes) + } + + responseBytes, err := serverResponse.CreateServerResponse_GetMessages(hostPublicIdentityKey, hostPrivateIdentityKey, connectionKey, requestIdentifier, messagesList) + if (err != nil) { + t.Fatalf("Failed to create GetMessages response: " + err.Error()) + } + + ableToRead, requestIdentifier_Received, hostIdentityHash_Received, messagesList_Received, err := serverResponse.ReadServerResponse_GetMessages(responseBytes, connectionKey) + if (err != nil) { + t.Fatalf("Failed to read GetMessages response: " + err.Error()) + } + if (ableToRead == false) { + t.Fatalf("Failed to read GetMessages response.") + } + + if (requestIdentifier_Received != requestIdentifier){ + t.Fatalf("Failed to read GetMessages response: mismatched requestIdentifier.") + } + if (hostIdentityHash_Received != hostIdentityHash){ + t.Fatalf("Failed to read GetMessages response: Mismatched hostIdentityHash.") + } + + areEqual := reflect.DeepEqual(messagesList, messagesList_Received) + if (areEqual == false){ + t.Fatalf("Failed to read GetMessages response: mismatched messagesList.") + } + + // Now we test empty list case + + emptyMessagesList := make([][]byte, 0) + + responseBytes, err = serverResponse.CreateServerResponse_GetMessages(hostPublicIdentityKey, hostPrivateIdentityKey, connectionKey, requestIdentifier, emptyMessagesList) + if (err != nil) { + t.Fatalf("Failed to create GetMessages response: " + err.Error()) + } + + ableToRead, requestIdentifier_Received, hostIdentityHash_Received, messagesList_Received, err = serverResponse.ReadServerResponse_GetMessages(responseBytes, connectionKey) + if (err != nil) { + t.Fatalf("Failed to read GetMessages response: " + err.Error()) + } + if (ableToRead == false) { + t.Fatalf("Failed to read GetMessages response.") + } + + if (requestIdentifier_Received != requestIdentifier){ + t.Fatalf("Failed to read GetMessages response: mismatched requestIdentifier.") + } + if (hostIdentityHash_Received != hostIdentityHash){ + t.Fatalf("Failed to read GetMessages response: Mismatched hostIdentityHash.") + } + + if (len(messagesList_Received) != 0){ + t.Fatalf("Failed to read GetMessages response: messages list not empty.") + } +} + + + +func TestCreateAndReadResponse_GetIdentityReviewsInfo(t *testing.T){ + + hostPublicIdentityKey, hostPrivateIdentityKey, err := identity.GetNewRandomPublicPrivateIdentityKeys() + if (err != nil) { + t.Fatalf("Failed to create random identity keys: " + err.Error()) + } + + hostIdentityHash, err := identity.ConvertIdentityKeyToIdentityHash(hostPublicIdentityKey, "Host") + if (err != nil) { + t.Fatalf("Failed to get host identity hash: " + err.Error()) + } + + connectionKey, err := getNewRandomConnectionKey() + if (err != nil) { + t.Fatalf("getNewRandomConnectionKey failed: " + err.Error()) + } + + requestIdentifier, err := getNewRandomRequestIdentifier() + if (err != nil) { + t.Fatalf("getNewRandomRequestIdentifier failed: " + err.Error()) + } + + // Map structure: Review Hash -> Reviewed hash + reviewsInfoMap := make(map[[29]byte][]byte) + + for i:=0; i<1000; i++{ + + // We randomly choose either a profile hash, identity hash, or attribute hash + + reviewedType, err := helpers.GetRandomItemFromList([]string{"Profile", "Identity", "Attribute"}) + if (err != nil) { + + t.Fatalf("Failed to get random reviewedType: " + err.Error()) + + } + + reviewHash, err := helpers.GetNewRandomReviewHash(true, reviewedType) + if (err != nil){ + + t.Fatalf("Failed to get random review hash: " + err.Error()) + } + + if (reviewedType == "Profile"){ + + randomProfileHash, err := helpers.GetNewRandomProfileHash(false, "", true, false) + if (err != nil) { + t.Fatalf("Failed to get random profile Hash: " + err.Error()) + } + + reviewsInfoMap[reviewHash] = randomProfileHash[:] + + } else if (reviewedType == "Identity"){ + + randomIdentityHash, err := identity.GetNewRandomIdentityHash(false, "") + if (err != nil) { + t.Fatalf("Failed to create random identity hash: " + err.Error()) + } + + reviewsInfoMap[reviewHash] = randomIdentityHash[:] + + } else { + + //reviewedType == "Attribute" + + randomAttributeHash, err := helpers.GetNewRandomAttributeHash(false, "", false, false) + if (err != nil){ + t.Fatalf("Failed to get random attribute hash: " + err.Error()) + } + + reviewsInfoMap[reviewHash] = randomAttributeHash[:] + } + } + + responseBytes, err := serverResponse.CreateServerResponse_GetIdentityReviewsInfo(hostPublicIdentityKey, hostPrivateIdentityKey, connectionKey, requestIdentifier, reviewsInfoMap) + if (err != nil) { + t.Fatalf("Failed to create GetIdentityReviewsInfo response: " + err.Error()) + } + + ableToRead, requestIdentifier_Received, hostIdentityHash_Received, reviewsInfoMap_Received, err := serverResponse.ReadServerResponse_GetIdentityReviewsInfo(responseBytes, connectionKey) + if (err != nil) { + t.Fatalf("Failed to read GetIdentityReviewsInfo response: " + err.Error()) + } + if (ableToRead == false) { + t.Fatalf("Failed to read GetIdentityReviewsInfo response.") + } + + if (requestIdentifier_Received != requestIdentifier){ + t.Fatalf("Failed to read GetIdentityReviewsInfo response: mismatched requestIdentifier.") + } + if (hostIdentityHash_Received != hostIdentityHash){ + t.Fatalf("Failed to read GetIdentityReviewsInfo response: Mismatched hostIdentityHash.") + } + + areEqual := reflect.DeepEqual(reviewsInfoMap, reviewsInfoMap_Received) + if (areEqual == false){ + t.Fatalf("Failed to read GetIdentityReviewsInfo response: mismatched reviewsInfoMap") + } + + //TODO: Add None case + +} + + +func TestCreateAndReadResponse_GetMessageReviewsInfo(t *testing.T){ + + hostPublicIdentityKey, hostPrivateIdentityKey, err := identity.GetNewRandomPublicPrivateIdentityKeys() + if (err != nil) { + t.Fatalf("Failed to create random identity keys: " + err.Error()) + } + + hostIdentityHash, err := identity.ConvertIdentityKeyToIdentityHash(hostPublicIdentityKey, "Host") + if (err != nil) { + t.Fatalf("Failed to get host identity hash: " + err.Error()) + } + + connectionKey, err := getNewRandomConnectionKey() + if (err != nil) { + t.Fatalf("getNewRandomConnectionKey failed: " + err.Error()) + } + + requestIdentifier, err := getNewRandomRequestIdentifier() + if (err != nil) { + t.Fatalf("getNewRandomRequestIdentifier failed: " + err.Error()) + } + + // Map structure: Review Hash -> Reviewed message hash + reviewsInfoMap := make(map[[29]byte][26]byte) + + for i:=0; i<1000; i++{ + + reviewHash, err := helpers.GetNewRandomReviewHash(true, "Message") + if (err != nil){ + t.Fatalf("Failed to get new random review hash: " + err.Error()) + } + + messageHash, err := helpers.GetNewRandomMessageHash() + if (err != nil) { + t.Fatalf("GetNewRandomMessageHash failed: " + err.Error()) + } + + reviewsInfoMap[reviewHash] = messageHash + } + + responseBytes, err := serverResponse.CreateServerResponse_GetMessageReviewsInfo(hostPublicIdentityKey, hostPrivateIdentityKey, connectionKey, requestIdentifier, reviewsInfoMap) + if (err != nil) { + t.Fatalf("Failed to create GetMessageReviewsInfo response: " + err.Error()) + } + + ableToRead, requestIdentifier_Received, hostIdentityHash_Received, reviewsInfoMap_Received, err := serverResponse.ReadServerResponse_GetMessageReviewsInfo(responseBytes, connectionKey) + if (err != nil) { + t.Fatalf("Failed to read GetMessageReviewsInfo response: " + err.Error()) + } + if (ableToRead == false) { + t.Fatalf("Failed to read GetMessageReviewsInfo response.") + } + + if (requestIdentifier_Received != requestIdentifier){ + t.Fatalf("Failed to read GetMessageReviewsInfo response: mismatched requestIdentifier.") + } + if (hostIdentityHash_Received != hostIdentityHash){ + t.Fatalf("Failed to read GetMessageReviewsInfo response: Mismatched hostIdentityHash.") + } + + areEqual := maps.Equal(reviewsInfoMap, reviewsInfoMap_Received) + if (areEqual == false){ + t.Fatalf("Failed to read GetMessageReviewsInfo response: mismatched reviewsInfoMap") + } + + //TODO: Add None case + +} + +func TestCreateAndReadResponse_GetReviews(t *testing.T){ + + hostPublicIdentityKey, hostPrivateIdentityKey, err := identity.GetNewRandomPublicPrivateIdentityKeys() + if (err != nil) { + t.Fatalf("Failed to create random identity keys: " + err.Error()) + } + + hostIdentityHash, err := identity.ConvertIdentityKeyToIdentityHash(hostPublicIdentityKey, "Host") + if (err != nil) { + t.Fatalf("Failed to get host identity hash: " + err.Error()) + } + + connectionKey, err := getNewRandomConnectionKey() + if (err != nil) { + t.Fatalf("getNewRandomConnectionKey failed: " + err.Error()) + } + + requestIdentifier, err := getNewRandomRequestIdentifier() + if (err != nil) { + t.Fatalf("getNewRandomRequestIdentifier failed: " + err.Error()) + } + + networkType, err := helpers.GetRandomByteWithinRange(1, 2) + if (err != nil){ + t.Fatalf("GetRandomByteWithinRange failed: " + err.Error()) + } + + reviewsList := make([][]byte, 0, 10) + + for i:=0; i<10; i++{ + + reviewType, err := helpers.GetRandomItemFromList([]string{"Profile", "Identity", "Message", "Attribute"}) + if (err != nil) { + t.Fatalf("Cannot get random reviewType: " + err.Error()) + } + + fakeReviewBytes, err := generate.GetFakeReview(reviewType, networkType) + if (err != nil) { + t.Fatalf("Failed to create fake review: " + err.Error()) + } + + reviewsList = append(reviewsList, fakeReviewBytes) + } + + responseBytes, err := serverResponse.CreateServerResponse_GetReviews(hostPublicIdentityKey, hostPrivateIdentityKey, connectionKey, requestIdentifier, reviewsList) + if (err != nil) { + t.Fatalf("Failed to create GetReviews response: " + err.Error()) + } + + ableToRead, requestIdentifier_Received, hostIdentityHash_Received, reviewsList_Received, err := serverResponse.ReadServerResponse_GetReviews(responseBytes, connectionKey) + if (err != nil) { + t.Fatalf("Failed to read GetReviews response: " + err.Error()) + } + if (ableToRead == false) { + t.Fatalf("Failed to read GetReviews response.") + } + + if (requestIdentifier_Received != requestIdentifier){ + t.Fatalf("Failed to read GetReviews response: mismatched requestIdentifier.") + } + if (hostIdentityHash_Received != hostIdentityHash){ + t.Fatalf("Failed to read GetReviews response: Mismatched hostIdentityHash.") + } + + areEqual := reflect.DeepEqual(reviewsList, reviewsList_Received) + if (areEqual == false){ + t.Fatalf("Failed to read GetReviews response: mismatched reviewsList.") + } + + // Now we test empty list case + + emptyReviewsList := make([][]byte, 0) + + responseBytes, err = serverResponse.CreateServerResponse_GetReviews(hostPublicIdentityKey, hostPrivateIdentityKey, connectionKey, requestIdentifier, emptyReviewsList) + if (err != nil) { + t.Fatalf("Failed to create GetReviews response: " + err.Error()) + } + + ableToRead, requestIdentifier_Received, hostIdentityHash_Received, reviewsList_Received, err = serverResponse.ReadServerResponse_GetReviews(responseBytes, connectionKey) + if (err != nil) { + t.Fatalf("Failed to read GetReviews response: " + err.Error()) + } + if (ableToRead == false) { + t.Fatalf("Failed to read GetReviews response.") + } + + if (requestIdentifier_Received != requestIdentifier){ + t.Fatalf("Failed to read GetReviews response: mismatched requestIdentifier.") + } + if (hostIdentityHash_Received != hostIdentityHash){ + t.Fatalf("Failed to read GetReviews response: Mismatched hostIdentityHash.") + } + + if (len(reviewsList_Received) != 0){ + t.Fatalf("Failed to read GetReviews response: reviews list not empty.") + } +} + +func TestCreateAndReadResponse_GetIdentityReportsInfo(t *testing.T){ + + hostPublicIdentityKey, hostPrivateIdentityKey, err := identity.GetNewRandomPublicPrivateIdentityKeys() + if (err != nil) { + t.Fatalf("Failed to create random identity keys: " + err.Error()) + } + + hostIdentityHash, err := identity.ConvertIdentityKeyToIdentityHash(hostPublicIdentityKey, "Host") + if (err != nil) { + t.Fatalf("Failed to get host identity hash: " + err.Error()) + } + + connectionKey, err := getNewRandomConnectionKey() + if (err != nil) { + t.Fatalf("getNewRandomConnectionKey failed: " + err.Error()) + } + + requestIdentifier, err := getNewRandomRequestIdentifier() + if (err != nil) { + t.Fatalf("getNewRandomRequestIdentifier failed: " + err.Error()) + } + + // Map Structure: Report Hash -> Reported hash + reportsInfoMap := make(map[[30]byte][]byte) + + for i:=0; i<1000; i++{ + + // We randomly choose either a profile hash, identity hash or attribute hash + reportedType, err := helpers.GetRandomItemFromList([]string{"Profile", "Identity", "Attribute"}) + if (err != nil) { + t.Fatalf("Failed to get random reviewedType: " + err.Error()) + } + + reportHash, err := helpers.GetNewRandomReportHash(true, reportedType) + if (err != nil){ + t.Fatalf("Failed to get random report hash: " + err.Error()) + } + + if (reportedType == "Profile"){ + + profileHash, err := helpers.GetNewRandomProfileHash(false, "", true, false) + if (err != nil) { + t.Fatalf("Failed to create random profile hash: " + err.Error()) + } + + reportsInfoMap[reportHash] = profileHash[:] + + } else if (reportedType == "Identity"){ + + randomIdentityHash, err := identity.GetNewRandomIdentityHash(false, "") + if (err != nil) { + t.Fatalf("Failed to create random identity hash: " + err.Error()) + } + + reportsInfoMap[reportHash] = randomIdentityHash[:] + + } else { + + // reportedType == "Attribute" + + attributeHash, err := helpers.GetNewRandomAttributeHash(false, "", false, false) + if (err != nil){ + t.Fatalf("Failed to get random attribute hash: " + err.Error()) + } + + reportsInfoMap[reportHash] = attributeHash[:] + } + } + + responseBytes, err := serverResponse.CreateServerResponse_GetIdentityReportsInfo(hostPublicIdentityKey, hostPrivateIdentityKey, connectionKey, requestIdentifier, reportsInfoMap) + if (err != nil) { + t.Fatalf("Failed to create GetIdentityReportsInfo response: " + err.Error()) + } + + ableToRead, requestIdentifier_Received, hostIdentityHash_Received, reportsInfoMap_Received, err := serverResponse.ReadServerResponse_GetIdentityReportsInfo(responseBytes, connectionKey) + if (err != nil) { + t.Fatalf("Failed to read GetIdentityReportsInfo response: " + err.Error()) + } + if (ableToRead == false) { + t.Fatalf("Failed to read GetIdentityReportsInfo response") + } + + if (requestIdentifier_Received != requestIdentifier){ + t.Fatalf("Failed to read GetIdentityReportsInfo response: mismatched requestIdentifier.") + } + if (hostIdentityHash_Received != hostIdentityHash){ + t.Fatalf("Failed to read GetIdentityReportsInfo response: Mismatched hostIdentityHash.") + } + + areEqual := reflect.DeepEqual(reportsInfoMap, reportsInfoMap_Received) + if (areEqual == false){ + t.Fatalf("Failed to read GetIdentityReportsInfo response: mismatched reportsInfoMap") + } + + //TODO: Add None case + +} + + +func TestCreateAndReadResponse_GetMessageReportsInfo(t *testing.T){ + + hostPublicIdentityKey, hostPrivateIdentityKey, err := identity.GetNewRandomPublicPrivateIdentityKeys() + if (err != nil) { + t.Fatalf("Failed to create random identity keys: " + err.Error()) + } + + hostIdentityHash, err := identity.ConvertIdentityKeyToIdentityHash(hostPublicIdentityKey, "Host") + if (err != nil) { + t.Fatalf("Failed to get host identity hash: " + err.Error()) + } + + connectionKey, err := getNewRandomConnectionKey() + if (err != nil) { + t.Fatalf("getNewRandomConnectionKey failed: " + err.Error()) + } + + requestIdentifier, err := getNewRandomRequestIdentifier() + if (err != nil) { + t.Fatalf("getNewRandomRequestIdentifier failed: " + err.Error()) + } + + // Map Structure: Report Hash -> Reported message hash + reportsInfoMap := make(map[[30]byte][26]byte) + + for i:=0; i<1000; i++{ + + reportHash, err := helpers.GetNewRandomReportHash(true, "Message") + if (err != nil){ + t.Fatalf("GetNewRandomReportHash failed: " + err.Error()) + } + + messageHash, err := helpers.GetNewRandomMessageHash() + if (err != nil){ + t.Fatalf("GetNewRandomMessageHash failed: " + err.Error()) + } + + reportsInfoMap[reportHash] = messageHash + } + + responseBytes, err := serverResponse.CreateServerResponse_GetMessageReportsInfo(hostPublicIdentityKey, hostPrivateIdentityKey, connectionKey, requestIdentifier, reportsInfoMap) + if (err != nil) { + t.Fatalf("Failed to create GetMessageReportsInfo response: " + err.Error()) + } + + ableToRead, requestIdentifier_Received, hostIdentityHash_Received, reportsInfoMap_Received, err := serverResponse.ReadServerResponse_GetMessageReportsInfo(responseBytes, connectionKey) + if (err != nil) { + t.Fatalf("Failed to read GetMessageReportsInfo response: " + err.Error()) + } + if (ableToRead == false) { + t.Fatalf("Failed to read GetMessageReportsInfo response") + } + + if (requestIdentifier_Received != requestIdentifier){ + t.Fatalf("Failed to read GetMessageReportsInfo response: mismatched requestIdentifier.") + } + if (hostIdentityHash_Received != hostIdentityHash){ + t.Fatalf("Failed to read GetMessageReportsInfo response: Mismatched hostIdentityHash.") + } + + areEqual := maps.Equal(reportsInfoMap, reportsInfoMap_Received) + if (areEqual == false){ + t.Fatalf("Failed to read GetMessageReportsInfo response: mismatched reportsInfoMap") + } + + //TODO: Add None case + +} + + +func TestCreateAndReadResponse_GetReports(t *testing.T){ + + hostPublicIdentityKey, hostPrivateIdentityKey, err := identity.GetNewRandomPublicPrivateIdentityKeys() + if (err != nil) { + t.Fatalf("Failed to create random identity keys: " + err.Error()) + } + + hostIdentityHash, err := identity.ConvertIdentityKeyToIdentityHash(hostPublicIdentityKey, "Host") + if (err != nil) { + t.Fatalf("Failed to get host identity hash: " + err.Error()) + } + + connectionKey, err := getNewRandomConnectionKey() + if (err != nil) { + t.Fatalf("getNewRandomConnectionKey failed: " + err.Error()) + } + + requestIdentifier, err := getNewRandomRequestIdentifier() + if (err != nil) { + t.Fatalf("getNewRandomRequestIdentifier failed: " + err.Error()) + } + + networkType, err := helpers.GetRandomByteWithinRange(1, 2) + if (err != nil){ + t.Fatalf("GetRandomByteWithinRange failed: " + err.Error()) + } + + reportsList := make([][]byte, 0, 10) + + for i:=0; i<10; i++{ + + reportType, err := helpers.GetRandomItemFromList([]string{"Profile", "Identity", "Attribute", "Message"}) + if (err != nil) { + t.Fatalf("Cannot get random reportType: " + err.Error()) + } + + fakeReportBytes, err := generate.GetFakeReport(reportType, networkType) + if (err != nil) { + t.Fatalf("Failed to create fake report: " + err.Error()) + } + + reportsList = append(reportsList, fakeReportBytes) + } + + responseBytes, err := serverResponse.CreateServerResponse_GetReports(hostPublicIdentityKey, hostPrivateIdentityKey, connectionKey, requestIdentifier, reportsList) + if (err != nil) { + t.Fatalf("Failed to create GetReports response: " + err.Error()) + } + + ableToRead, requestIdentifier_Received, hostIdentityHash_Received, reportsList_Received, err := serverResponse.ReadServerResponse_GetReports(responseBytes, connectionKey) + if (err != nil) { + t.Fatalf("Failed to read GetReports response: " + err.Error()) + } + if (ableToRead == false) { + t.Fatalf("Failed to read GetReports response.") + } + + if (requestIdentifier_Received != requestIdentifier){ + t.Fatalf("Failed to read GetReports response: mismatched requestIdentifier.") + } + if (hostIdentityHash_Received != hostIdentityHash){ + t.Fatalf("Failed to read GetReports response: Mismatched hostIdentityHash.") + } + + areEqual := reflect.DeepEqual(reportsList, reportsList_Received) + if (areEqual == false){ + t.Fatalf("Failed to read GetReports response: mismatched reportsList.") + } + + // Now we test empty list case + + emptyReportsList := make([][]byte, 0) + + responseBytes, err = serverResponse.CreateServerResponse_GetReports(hostPublicIdentityKey, hostPrivateIdentityKey, connectionKey, requestIdentifier, emptyReportsList) + if (err != nil) { + t.Fatalf("Failed to create GetReports response: " + err.Error()) + } + + ableToRead, requestIdentifier_Received, hostIdentityHash_Received, reportsList_Received, err = serverResponse.ReadServerResponse_GetReports(responseBytes, connectionKey) + if (err != nil) { + t.Fatalf("Failed to read GetReports response: " + err.Error()) + } + if (ableToRead == false) { + t.Fatalf("Failed to read GetReports response.") + } + + if (requestIdentifier_Received != requestIdentifier){ + t.Fatalf("Failed to read GetReports response: mismatched requestIdentifier.") + } + if (hostIdentityHash_Received != hostIdentityHash){ + t.Fatalf("Failed to read GetReports response: Mismatched hostIdentityHash.") + } + + if (len(reportsList_Received) != 0){ + t.Fatalf("Failed to read GetReports response: reports list not empty.") + } +} + + +func TestCreateAndReadResponse_GetAddressDeposits(t *testing.T){ + + cryptocurrencyNamesList := []string{"Ethereum", "Cardano"} + + for _, cryptocurrencyName := range cryptocurrencyNamesList{ + + hostPublicIdentityKey, hostPrivateIdentityKey, err := identity.GetNewRandomPublicPrivateIdentityKeys() + if (err != nil) { + t.Fatalf("Failed to create random identity keys: " + err.Error()) + } + + hostIdentityHash, err := identity.ConvertIdentityKeyToIdentityHash(hostPublicIdentityKey, "Host") + if (err != nil) { + t.Fatalf("Failed to get host identity hash: " + err.Error()) + } + + connectionKey, err := getNewRandomConnectionKey() + if (err != nil) { + t.Fatalf("getNewRandomConnectionKey failed: " + err.Error()) + } + + requestIdentifier, err := getNewRandomRequestIdentifier() + if (err != nil) { + t.Fatalf("getNewRandomRequestIdentifier failed: " + err.Error()) + } + + addressDepositObjectsList := make([]serverResponse.DepositStruct, 0, 10) + + for i:=0; i<10; i++{ + + getNewAddress := func()(string, error){ + + if (cryptocurrencyName == "Ethereum"){ + + newAddress, err := ethereumAddress.GetNewRandomEthereumAddress() + if (err != nil) { + return "", errors.New("Failed to create random ethereum address: " + err.Error()) + } + + return newAddress, nil + } + + // cryptocurrencyName == "Cardano" + + newAddress, err := cardanoAddress.GetNewRandomCardanoAddress() + if (err != nil) { + return "", errors.New("Failed to create random Cardano address: " + err.Error()) + } + + return newAddress, nil + } + + newAddress, err := getNewAddress() + if (err != nil){ + t.Fatalf(err.Error()) + } + + depositTime := time.Now().Unix() + + depositAmount := helpers.GetRandomIntWithinRange(10, 100000) + + depositObject := serverResponse.DepositStruct{ + Address: newAddress, + DepositTime: depositTime, + DepositAmount: int64(depositAmount), + } + + addressDepositObjectsList = append(addressDepositObjectsList, depositObject) + } + + responseBytes, err := serverResponse.CreateServerResponse_GetAddressDeposits(hostPublicIdentityKey, hostPrivateIdentityKey, connectionKey, requestIdentifier, addressDepositObjectsList) + if (err != nil) { + t.Fatalf("Failed to create GetAddressDeposits response: " + err.Error()) + } + + ableToRead, requestIdentifier_Received, hostIdentityHash_Received, addressDepositObjectsList_Received, err := serverResponse.ReadServerResponse_GetAddressDeposits(responseBytes, connectionKey) + if (err != nil) { + t.Fatalf("Failed to read GetAddressDeposits response: " + err.Error()) + } + if (ableToRead == false) { + t.Fatalf("Failed to read GetAddressDeposits response.") + } + + if (requestIdentifier_Received != requestIdentifier){ + t.Fatalf("Failed to read GetAddressDeposits response: mismatched requestIdentifier.") + } + if (hostIdentityHash_Received != hostIdentityHash){ + t.Fatalf("Failed to read GetAddressDeposits response: Mismatched hostIdentityHash.") + } + + areEqual := slices.Equal(addressDepositObjectsList, addressDepositObjectsList_Received) + if (areEqual == false){ + t.Fatalf("Failed to read GetAddressDeposits response: Received addressDepositsObjectsList does not match original.") + } + } + + //TODO: Add None case +} + + +func TestCreateAndReadResponse_GetViewableStatuses(t *testing.T){ + + hostPublicIdentityKey, hostPrivateIdentityKey, err := identity.GetNewRandomPublicPrivateIdentityKeys() + if (err != nil) { + t.Fatalf("Failed to create random identity keys: " + err.Error()) + } + + hostIdentityHash, err := identity.ConvertIdentityKeyToIdentityHash(hostPublicIdentityKey, "Host") + if (err != nil) { + t.Fatalf("Failed to get host identity hash: " + err.Error()) + } + + connectionKey, err := getNewRandomConnectionKey() + if (err != nil) { + t.Fatalf("getNewRandomConnectionKey failed: " + err.Error()) + } + + requestIdentifier, err := getNewRandomRequestIdentifier() + if (err != nil) { + t.Fatalf("getNewRandomRequestIdentifier failed: " + err.Error()) + } + + identityHashStatusesMap := make(map[[16]byte]bool) + profileHashStatusesMap := make(map[[28]byte]bool) + + for i:=0; i<100; i++{ + + contentIsViewableBool := helpers.GetRandomBool() + + randomBool := helpers.GetRandomBool() + if (randomBool == true){ + + userIdentityHash, err := identity.GetNewRandomIdentityHash(false, "") + if (err != nil) { + t.Fatalf("Failed to create random identity hash: " + err.Error()) + } + + identityHashStatusesMap[userIdentityHash] = contentIsViewableBool + + } else { + + newProfileHash, err := helpers.GetNewRandomProfileHash(false, "", true, false) + if (err != nil) { + t.Fatalf("Failed to create random profile hash: " + err.Error()) + } + + profileHashStatusesMap[newProfileHash] = contentIsViewableBool + } + } + + responseBytes, err := serverResponse.CreateServerResponse_GetViewableStatuses(hostPublicIdentityKey, hostPrivateIdentityKey, connectionKey, requestIdentifier, identityHashStatusesMap, profileHashStatusesMap) + if (err != nil) { + t.Fatalf("Failed to create GetViewableStatuses response: " + err.Error()) + } + + ableToRead, requestIdentifier_Received, hostIdentityHash_Received, identityHashStatusesMap_Received, profileHashStatusesMap_Received, err := serverResponse.ReadServerResponse_GetViewableStatuses(responseBytes, connectionKey) + if (err != nil) { + t.Fatalf("Failed to read GetViewableStatuses response: " + err.Error()) + } + if (ableToRead == false) { + t.Fatalf("Failed to read GetViewableStatuses response.") + } + + if (requestIdentifier_Received != requestIdentifier){ + t.Fatalf("Failed to read GetViewableStatuses response: mismatched requestIdentifier.") + } + if (hostIdentityHash_Received != hostIdentityHash){ + t.Fatalf("Failed to read GetViewableStatuses response: Mismatched hostIdentityHash.") + } + + areEqual := maps.Equal(identityHashStatusesMap, identityHashStatusesMap_Received) + if (areEqual == false){ + t.Fatalf("Failed to read GetViewableStatuses response: mismatched identityHashStatusesMap") + } + + areEqual = maps.Equal(profileHashStatusesMap, profileHashStatusesMap_Received) + if (areEqual == false){ + t.Fatalf("Failed to read GetViewableStatuses response: mismatched profileHashStatusesMap") + } + + //TODO: Add None case + +} + + + +func TestCreateAndReadResponse_BroadcastContent(t *testing.T){ + + contentTypesList := []string{"Profile", "Message", "Review", "Report", "Parameters"} + + for _, contentType := range contentTypesList{ + + hostPublicIdentityKey, hostPrivateIdentityKey, err := identity.GetNewRandomPublicPrivateIdentityKeys() + if (err != nil) { + t.Fatalf("Failed to create random identity keys: " + err.Error()) + } + + hostIdentityHash, err := identity.ConvertIdentityKeyToIdentityHash(hostPublicIdentityKey, "Host") + if (err != nil) { + t.Fatalf("Failed to get host identity hash: " + err.Error()) + } + + connectionKey, err := getNewRandomConnectionKey() + if (err != nil) { + t.Fatalf("getNewRandomConnectionKey failed: " + err.Error()) + } + + requestIdentifier, err := getNewRandomRequestIdentifier() + if (err != nil) { + t.Fatalf("getNewRandomRequestIdentifier failed: " + err.Error()) + } + + contentAcceptedInfoMap := make(map[string]bool) + + for i:=0; i<100; i++{ + + getRandomContentHash := func()([]byte, error){ + + if (contentType == "Profile"){ + randomProfileHash, err := helpers.GetNewRandomProfileHash(false, "", false, false) + if (err != nil) { return nil, err } + + return randomProfileHash[:], nil + } + if (contentType == "Message"){ + + randomMessageHash, err := helpers.GetNewRandomMessageHash() + if (err != nil){ return nil, err } + + return randomMessageHash[:], nil + } + if (contentType == "Review"){ + + randomReviewHash, err := helpers.GetNewRandomReviewHash(false, "") + if (err != nil) { return nil, err } + + return randomReviewHash[:], nil + } + if (contentType == "Report"){ + + randomReportHash, err := helpers.GetNewRandomReportHash(false, "") + if (err != nil) { return nil, err } + + return randomReportHash[:], nil + } + // contentType == "Parameters" + + randomParametersHash, err := helpers.GetNewRandomParametersHash() + if (err != nil) { return nil, err } + + return randomParametersHash[:], nil + } + + randomContentHash, err := getRandomContentHash() + if (err != nil) { + t.Fatalf("Failed to get random content hash: " + err.Error()) + } + + randomBool := helpers.GetRandomBool() + + contentAcceptedInfoMap[string(randomContentHash)] = randomBool + } + + responseBytes, err := serverResponse.CreateServerResponse_BroadcastContent(hostPublicIdentityKey, hostPrivateIdentityKey, connectionKey, requestIdentifier, contentAcceptedInfoMap) + if (err != nil) { + t.Fatalf("Failed to create BroadcastContent response: " + err.Error()) + } + + ableToRead, requestIdentifier_Received, hostIdentityHash_Received, contentAcceptedInfoMap_Received, err := serverResponse.ReadServerResponse_BroadcastContent(responseBytes, connectionKey) + if (err != nil) { + t.Fatalf("Failed to read BroadcastContent response: " + err.Error()) + } + if (ableToRead == false) { + t.Fatalf("Failed to read BroadcastContent response.") + } + + if (requestIdentifier_Received != requestIdentifier){ + t.Fatalf("Failed to read BroadcastContent response: mismatched requestIdentifier.") + } + if (hostIdentityHash_Received != hostIdentityHash){ + t.Fatalf("Failed to read BroadcastContent response: Mismatched hostIdentityHash.") + } + + areEqual := maps.Equal(contentAcceptedInfoMap, contentAcceptedInfoMap_Received) + if (areEqual == false){ + t.Fatalf("Failed to read BroadcastContent response: mismatched contentAcceptedInfoMap") + } + + //TODO: Add None case + + } + +} + diff --git a/internal/network/spendCredit/spendCredit.go b/internal/network/spendCredit/spendCredit.go new file mode 100644 index 0000000..c68ca8c --- /dev/null +++ b/internal/network/spendCredit/spendCredit.go @@ -0,0 +1,34 @@ + +// spendCredit provides functions to communicate with the account credit servers to fund messages, profiles, and reports + +package spendCredit + + +//TODO: Build this package + +import "seekia/internal/network/myFundedStatus" + +import "time" + +//Outputs: +// -bool: Funds are sufficient +// -bool: Message fund is successful +// -error +func FundMyMessageWithServer(messageIdentifier [20]byte, messageHash [26]byte, messageNetworkType byte, messageSizeInBytes int, messageDuration int)(bool, bool, error){ + + //TODO + // The function should contact account credits server and fund message + // This function should unfreeze the funds associated with the message + // If the funds are not sufficient, it should refreeze them + + time.Sleep(time.Second) + + currentTime := time.Now().Unix() + + messageExpiration := currentTime + int64(messageDuration) + + err := myFundedStatus.SetMyMessageIsFundedStatus(messageHash, true, messageExpiration) + if (err != nil) { return false, false, err } + + return true, true, nil +} diff --git a/internal/network/temporaryDownloads/temporaryDownloads.go b/internal/network/temporaryDownloads/temporaryDownloads.go new file mode 100644 index 0000000..8a9dee6 --- /dev/null +++ b/internal/network/temporaryDownloads/temporaryDownloads.go @@ -0,0 +1,44 @@ + +// temporaryDownloads provides functions to keep track of a user's temporary downloads +// These are reviews, reports, profiles and messages that a user is downloading temporarily to look at + +package temporaryDownloads + +// These downloads will be initiated through the GUI as the user browses content +// Thus, the content will be outside of a user's downloads range (host/moderator range) +// Temporary downloads will primarily be used by moderators, but can be used by anyone who wants to view content outside of their downloads range + +// The Seekia client needs a way to keep track of temporarily downloaded content +// It will then avoid deleting this content until the download KeepDuration expires +// This KeepDuration can be changed by the user. It will default to 10 minutes. + +// An example of a temporary download would be: +// -A moderator who wants to download all profiles created by a user who is outside of their range +// -A moderator who wants to download all reviews for a particular identity outside of their range + +//TODO: Build this package +// We have to keep track of what kind of data is being downloaded +// Examples: +// -Reviews authored by a moderator +// -Profiles authored by user +// -Reviews/Reports of an identity +// -Reviews/Reports of a message + +//TODO: Clear temporary downloads when network type is changed + +func CheckIfIdentityReviewsAreWithinMyTemporaryDownloads(identityHash [16]byte)(bool, error){ + + //TODO + return false, nil +} + + +func CheckIfMessageReviewsAreWithinMyTemporaryDownloads(messageHash [26]byte)(bool, error){ + + //TODO + + return false, nil +} + + + diff --git a/internal/network/trustedFundedStatus/trustedFundedStatus.go b/internal/network/trustedFundedStatus/trustedFundedStatus.go new file mode 100644 index 0000000..73c54c4 --- /dev/null +++ b/internal/network/trustedFundedStatus/trustedFundedStatus.go @@ -0,0 +1,67 @@ + +// trustedFundedStatus provides functions to store and retrieve the trusted funded status of identities, messages, profiles, and reports +// "trusted" means we are retrieving funded statuses from hosts +// This is different from verifiedFundedStatus, which retrieves the statuses from the account credit servers + +package trustedFundedStatus + +// For our own funded statuses, we use different packages: +// myIdentityBalance for our identities, and myFundedStatus for content +// This is because we want to use the account credit servers for our own statuses, +// and we want to store the statuses in myDatastores instead of the badgerDatabase + +//TODO: Build this package +// We need to make fundedStatus requests to hosts +// We should store the statuses in badgerDatabase +// If a status has not been checked in 1 month, it should be considered unknown + +//Outputs: +// -bool: Funded status is known +// -bool: Identity is funded +// -int64: Last time we checked +// -error +func GetTrustedIdentityIsFundedStatus(userIdentityHash [16]byte, networkType byte)(bool, bool, int64, error){ + + //TODO + + return true, true, 0, nil +} + +//Outputs: +// -bool: Funded status is known +// -bool: Profile is funded +// -error +func GetTrustedMateProfileIsFundedStatus(profileHash [28]byte)(bool, bool, error){ + + //TODO + + return true, true, nil +} + +//Outputs: +// -bool: Funded status is known +// -bool: Message is funded +// -error +func GetTrustedMessageIsFundedStatus(messageHash [26]byte)(bool, bool, error){ + + //TODO + + return true, true, nil +} + + +//Outputs: +// -bool: Funded status is known +// -bool: Report is funded +// -error +func GetTrustedReportIsFundedStatus(reportHash [30]byte)(bool, bool, error){ + + //TODO + + return true, true, nil +} + + + + + diff --git a/internal/network/unreachableHosts/unreachableHosts.go b/internal/network/unreachableHosts/unreachableHosts.go new file mode 100644 index 0000000..2c27d68 --- /dev/null +++ b/internal/network/unreachableHosts/unreachableHosts.go @@ -0,0 +1,47 @@ + +// unreachableHosts provides functions to keep track of unreachable hosts + +package unreachableHosts + +// A host may be unreachable temporarily, or due to a misconfiguration of the user's client, not the host. +// Thus, the unreachable hosts list must be pruned periodically. +// The list should be reset whenever the client changes their configuration (assuming no hosts were reachable before). +// +// Each host has an unreachable status for each network type +// This is because a host can be hosting on two network types at the same time +// For example, a host's server could crash on one network type but not the other +// +// A host may be unreachable from their Tor address but reachable from their clearnet address. +// An example of why could be that their Tor address is being DDOSed. +// Thus, requestors keep track of an unreachable status for the Tor and clearnet address of each host. +// If a host updates their Tor/Clearnet address, their unreachable status will be unknown for the new address. + +// A host must be unreachable two times via each address type for them to be considered unreachable. +// This is to give lenience for instances when a connection cuts out unexpectedly, but the host is still reachable. +// This feature also prevents malicious hosts from sharing unreachable addresses to waste requestor time. +// The user will try each address type for the host, and then stop trying until the unreachable status is reset for that host. +// Even if the host changes their addresses, the requestor will still consider them unreachable. + +// We also have to deal with the scenario when a user is unable to access the entire internet/only the Tor network +// All unreachable status updates during that time are invalid +// Thus, before updating the status of hosts using this package, we should verify +// using check.torproject.org, and a clearnet internet site (example.com), to make sure they can connect. + + +//TODO: Build package + +func CheckIfHostIsUnreachable(hostIdentityHash [16]byte, networkType byte, addressType string)(bool, error){ + + return false, nil +} + + +func AddHostIsUnreachableEvent(hostIdentityHash [16]byte, networkType byte, addressType string)error{ + + //TODO: Add time of event? + + return nil +} + + + diff --git a/internal/network/verifiedFundedStatus/verifiedFundedStatus.go b/internal/network/verifiedFundedStatus/verifiedFundedStatus.go new file mode 100644 index 0000000..1b5c849 --- /dev/null +++ b/internal/network/verifiedFundedStatus/verifiedFundedStatus.go @@ -0,0 +1,107 @@ + +// verifiedFundedStatus provides functions to store and retrieve the verified funded status of identities, messages, profiles, and reports +// "verified" means we are retrieving statuses from the account credit servers +// This is different from trustedFundedStatus, which retrieves the information from hosts + +package verifiedFundedStatus + +// We only have to check the status once for mate profiles, reports, and messages +// This is because for those types of content, they can only be funded once, and their time cannot be increased +// A user will only broadcast their profile/report/message after it has been funded +// If we are aware of a profile/report/message hash, we know it must either be funded, or it is created by a malicious entity. + +// We keep track of the last time we checked each status +// This enables us to disregard statuses we received when the credits servers were hacked, if the servers are ever hacked +// The parameters will describe the time period during which the server(s) were hacked +// The app should disregard any information it received from the servers during this time +//TODO: Add a serverIdentifier to the hacked notification, and keep track of which server we received from, +// so we can disregard only what the compromised servers told us? + +//TODO: Build this package +// Hosts must retrieve funded status information from the account credit servers +// We should store the statuses in badgerDatabase +// If an identity status has not been checked in 1 month, it should be considered unknown + +import "seekia/internal/encoding" +import "seekia/internal/helpers" +import "seekia/internal/identity" + +import "errors" + +// This should only be used for Mate/Host identities +// Moderator identities are funded through identity scores, which use blockchain(s), not the account credit servers +//Outputs: +// -bool: Status is known +// -bool: Identity is funded +// -int64: Time of expiration +// -int64: Last time we checked +// -error +func GetVerifiedIdentityIsFundedStatus(identityHash [16]byte, networkType byte)(bool, bool, int64, int64, error){ + + identityType, err := identity.GetIdentityTypeFromIdentityHash(identityHash) + if (err != nil) { + identityHashHex := encoding.EncodeBytesToHexString(identityHash[:]) + return false, false, 0, 0, errors.New("GetVerifiedIdentityIsFundedStatus called with invalid identityHash: " + identityHashHex) + } + if (identityType != "Mate" && identityType != "Host"){ + return false, false, 0, 0, errors.New("GetVerifiedIdentityIsFundedStatus called with Host identity: " + identityType) + } + + isValid := helpers.VerifyNetworkType(networkType) + if (isValid == false){ + networkTypeString := helpers.ConvertByteToString(networkType) + return false, false, 0, 0, errors.New("GetVerifiedIdentityIsFundedStatus called with invalid networkType: " + networkTypeString) + } + + //TODO + + return true, true, 1, 100, nil +} + + +//Outputs: +// -bool: Status is known +// -bool: Profile Is Funded +// -int64: Time of expiration +// -int64: Last time we checked (the expiration time will never change once funded) +// -error +func GetVerifiedMateProfileIsFundedStatus(profileHash [28]byte)(bool, bool, int64, int64, error){ + + //TODO + + + return true, true, 100000000000, 100, nil +} + + +//Outputs: +// -bool: Status is known +// -bool: Message Is funded +// -int64: Time of expiration +// -int64: Last time we checked (the expiration time will never change once funded) +// -error +func GetVerifiedMessageIsFundedStatus(messageHash [26]byte)(bool, bool, int64, int64, error){ + + //TODO + // The size of the message is needed to get the isFunded status. + // We will use contentMetadata for this. + + return true, true, 1, 100, nil +} + + +//Outputs: +// -bool: Status is known +// -bool: Report Is funded +// -bool: Report was funded at one point in time +// -int64: Time of expiration +// -int64: Last time we checked (the expiration time will never change once funded) +// -error +func GetVerifiedReportIsFundedStatus(reportHash [30]byte)(bool, bool, int64, int64, error){ + + //TODO + + return true, true, 1, 100, nil +} + + diff --git a/internal/network/viewedHosts/viewedHosts.go b/internal/network/viewedHosts/viewedHosts.go new file mode 100644 index 0000000..5c597c9 --- /dev/null +++ b/internal/network/viewedHosts/viewedHosts.go @@ -0,0 +1,569 @@ + +// viewedHosts provides functions to generate and retrieve the viewedHosts list +// This list is used to browse hosts on the View Hosts page + +package viewedHosts + +import "seekia/internal/appMemory" +import "seekia/internal/encoding" +import "seekia/internal/helpers" +import "seekia/internal/identity" +import "seekia/internal/myDatastores/myList" +import "seekia/internal/myDatastores/myMap" +import "seekia/internal/mySettings" +import "seekia/internal/network/enabledHosts" +import "seekia/internal/profiles/viewableProfiles" + +import "slices" +import "sync" +import "errors" + +// This mutex will be locked whenever the viewed hosts list is being updated +var updatingViewedHostsMutex sync.Mutex + +var viewedHostsListDatastore *myList.MyList + +var viewedHostsFiltersMapDatastore *myMap.MyMap + +// This function must be called whenever an app user signs in +func InitializeViewedHostsDatastores()error{ + + updatingViewedHostsMutex.Lock() + defer updatingViewedHostsMutex.Unlock() + + newViewedHostsListDatastore, err := myList.CreateNewList("ViewedHosts") + if (err != nil) { return err } + + newViewedHostsFiltersMapDatastore, err := myMap.CreateNewMap("ViewedHostsFilters") + if (err != nil) { return err } + + viewedHostsListDatastore = newViewedHostsListDatastore + + viewedHostsFiltersMapDatastore = newViewedHostsFiltersMapDatastore + + return nil +} + + +func GetViewedHostsSortByAttribute()(string, error){ + + exists, currentAttribute, err := mySettings.GetSetting("ViewedHostsSortByAttribute") + if (err != nil) { return "", err } + if (exists == false){ + return "BanAdvocates", nil + } + + return currentAttribute, nil +} + +func GetViewedHostsSortDirection()(string, error){ + + exists, sortDirection, err := mySettings.GetSetting("ViewedHostsSortDirection") + if (err != nil) { return "", err } + if (exists == false){ + return "Descending", nil + } + if (sortDirection != "Ascending" && sortDirection != "Descending"){ + return "", errors.New("MySettings contains invalid ViewedHostsSortDirection: " + sortDirection) + } + + return sortDirection, nil +} + +// Will need a refresh any time a new host profile is downloaded +func CheckIfViewedHostsNeedsRefresh()(bool, error){ + + exists, needsRefresh, err := mySettings.GetSetting("ViewedHostsNeedsRefreshYesNo") + if (err != nil) { return true, err } + if (exists == true && needsRefresh == "No") { + return false, nil + } + + return true, nil +} + + +func GetViewedHostsAreReadyStatus(networkType byte)(bool, error){ + + isValid := helpers.VerifyNetworkType(networkType) + if (isValid == false){ + networkTypeString := helpers.ConvertByteToString(networkType) + return false, errors.New("GetViewedHostsAreReadyStatus called with invalid networkType: " + networkTypeString) + } + + exists, hostsGeneratedStatus, err := mySettings.GetSetting("ViewedHostsGeneratedStatus") + if (err != nil) { return false, err } + if (exists == false || hostsGeneratedStatus != "Yes"){ + return false, nil + } + + exists, hostsSortedStatus, err := mySettings.GetSetting("ViewedHostsSortedStatus") + if (err != nil) { return false, err } + if (exists == false || hostsSortedStatus != "Yes"){ + return false, nil + } + + exists, viewedHostsNetworkTypeString, err := mySettings.GetSetting("ViewedHostsNetworkType") + if (err != nil) { return false, err } + if (exists == false){ + // This should not happen, because ViewedHostsNetworkType is created whenever viewedHosts are generated + return false, errors.New("mySettings missing ViewedHostsNetworkType when ViewedHostsGeneratedStatus exists.") + } + + viewedHostsNetworkType, err := helpers.ConvertNetworkTypeStringToByte(viewedHostsNetworkTypeString) + if (err != nil) { + return false, errors.New("mySettings contains invalid ViewedHostsNetworkType: " + viewedHostsNetworkTypeString) + } + if (viewedHostsNetworkType != networkType){ + // ViewedHosts were generated for a different networkType + // This should never happen, because we will always set ViewedHostsGeneratedStatus to No when we switch network types, + // and we will always call the GetViewedHostsAreReadyStatus function with the current appNetworkType + + //TODO: Log this. + + err := mySettings.SetSetting("ViewedHostsGeneratedStatus", "No") + if (err != nil) { return false, err } + + return false, nil + } + + return true, nil +} + +// Outputs: +// -bool: Viewed hosts list is ready +// -[][16]byte: Viewed host identity hashes list +// -error +func GetViewedHostsList(networkType byte)(bool, [][16]byte, error){ + + isValid := helpers.VerifyNetworkType(networkType) + if (isValid == false){ + networkTypeString := helpers.ConvertByteToString(networkType) + return false, nil, errors.New("GetViewedHostsList called with invalid networkType: " + networkTypeString) + } + + areReady, err := GetViewedHostsAreReadyStatus(networkType) + if (err != nil) { return false, nil, err } + if (areReady == false){ + return false, nil, nil + } + + readyHostIdentityHashesList, err := viewedHostsListDatastore.GetList() + if (err != nil) { return false, nil, err } + + viewedHostsList := make([][16]byte, 0, len(readyHostIdentityHashesList)) + + for _, hostIdentityHashString := range readyHostIdentityHashesList{ + + hostIdentityHash, identityType, err := identity.ReadIdentityHashString(hostIdentityHashString) + if (err != nil){ + return false, nil, errors.New("viewedHostsListDatastore contains invalid identity hash: " + hostIdentityHashString) + } + if (identityType != "Host"){ + return false, nil, errors.New("viewedHostsListDatastore contains non-Host identity hash: " + identityType) + } + + viewedHostsList = append(viewedHostsList, hostIdentityHash) + } + + return true, viewedHostsList, nil +} + +// This function returns the number of viewed hosts +// It can be called before the list is ready (after being generated, but before being sorted) +func GetNumberOfGeneratedViewedHosts()(int, error){ + + currentViewedHostsList, err := viewedHostsListDatastore.GetList() + if (err != nil) { return 0, err } + + lengthInt := len(currentViewedHostsList) + + return lengthInt, nil +} + +//Outputs: +// -bool: Build encountered error +// -string: Error encountered +// -bool: Build is stopped (will be stopped if user went to different page) +// -bool: Viewed hosts are ready +// -float64: Progress status (0 - 1) +// -error +func GetViewedHostsBuildStatus(networkType byte)(bool, string, bool, bool, float64, error){ + + isValid := helpers.VerifyNetworkType(networkType) + if (isValid == false){ + networkTypeString := helpers.ConvertByteToString(networkType) + return false, "", false, false, 0, errors.New("GetViewedHostsBuildStatus called with invalid networkType: " + networkTypeString) + } + + exists, encounteredError := appMemory.GetMemoryEntry("ViewedHostsBuildEncounteredError") + if (exists == false){ + // No build exists. A build has not been started since Seekia was started + return false, "", false, false, 0, nil + } + if (encounteredError == "Yes"){ + exists, errorEncountered := appMemory.GetMemoryEntry("ViewedHostsBuildError") + if (exists == false){ + return false, "", false, false, 0, errors.New("Viewed Hosts build encountered error is yes, but no error exists.") + } + + return true, errorEncountered, false, false, 0, nil + } + + isStopped := CheckIfBuildViewedHostsIsStopped() + if (isStopped == true){ + return false, "", true, false, 0, nil + } + + hostsAreReadyBool, err := GetViewedHostsAreReadyStatus(networkType) + if (err != nil) { return false, "", false, false, 0, err } + if (hostsAreReadyBool == true){ + + return false, "", false, true, 1, nil + } + + exists, currentPercentageString := appMemory.GetMemoryEntry("ViewedHostsReadyProgressStatus") + if (exists == false){ + // No build exists. A build has not been started since Seekia was started + return false, "", false, false, 0, nil + } + + currentPercentageFloat, err := helpers.ConvertStringToFloat64(currentPercentageString) + if (err != nil){ + return false, "", false, false, 0, errors.New("ViewedHostsReadyProgressStatus is not a float: " + currentPercentageString) + } + + return false, "", false, false, currentPercentageFloat, nil +} + +func CheckIfBuildViewedHostsIsStopped()bool{ + + exists, buildStoppedStatus := appMemory.GetMemoryEntry("StopBuildViewedHostsYesNo") + if (exists == false || buildStoppedStatus != "No") { + return true + } + + return false +} + + +// This function will cancel the current build (if one is running) +// It will then start updating our viewed hosts +func StartUpdatingViewedHosts(networkType byte)error{ + + isValid := helpers.VerifyNetworkType(networkType) + if (isValid == false){ + networkTypeString := helpers.ConvertByteToString(networkType) + return errors.New("StartUpdatingViewedHosts called with invalid networkType: " + networkTypeString) + } + + appMemory.SetMemoryEntry("StopBuildViewedHostsYesNo", "Yes") + + // We wait for any existing build to stop + updatingViewedHostsMutex.Lock() + + appMemory.SetMemoryEntry("ViewedHostsBuildEncounteredError", "No") + appMemory.SetMemoryEntry("ViewedHostsBuildError", "") + appMemory.SetMemoryEntry("ViewedHostsReadyProgressStatus", "0") + + appMemory.SetMemoryEntry("StopBuildViewedHostsYesNo", "No") + + updateViewedHosts := func()error{ + + getViewedHostsNeedToBeGeneratedStatus := func()(bool, error){ + + exists, hostsGeneratedStatus, err := mySettings.GetSetting("ViewedHostsGeneratedStatus") + if (err != nil) { return false, err } + if (exists == false || hostsGeneratedStatus != "Yes"){ + return true, nil + } + + exists, viewedHostsNetworkTypeString, err := mySettings.GetSetting("ViewedHostsNetworkType") + if (err != nil) { return false, err } + if (exists == false){ + // This should not happen, because ViewedHostsNetworkType is set to Yes whenever viewed hosts are generated + return false, errors.New("ViewedHostsNetworkType missing when ViewedHostsGeneratedStatus exists.") + } + + viewedHostsNetworkType, err := helpers.ConvertNetworkTypeStringToByte(viewedHostsNetworkTypeString) + if (err != nil) { + return false, errors.New("mySettings contains invalid ViewedHostsNetworkType: " + viewedHostsNetworkTypeString) + } + if (viewedHostsNetworkType != networkType){ + // This should not happen, because ViewedHostsGeneratedStatus should be set to No whenever app network type is changed, + // and StartUpdatingViewedHosts should only be called with the current app network type. + return true, nil + } + + return false, nil + } + + viewedHostsNeedToBeGenerated, err := getViewedHostsNeedToBeGeneratedStatus() + if (err != nil) { return err } + if (viewedHostsNeedToBeGenerated == true){ + + err := mySettings.SetSetting("ViewedHostsSortedStatus", "No") + if (err != nil) { return err } + + enabledHostsList, err := enabledHosts.GetEnabledHostsList(false, networkType) + if (err != nil) { return err } + + numberOfEnabledHosts := len(enabledHostsList) + + generatedHostIdentityHashesList := make([]string, 0) + + for index, hostIdentityHash := range enabledHostsList{ + + hostPassesFilters, err := checkIfHostPassesFilters(hostIdentityHash) + if (err != nil) { return err } + if (hostPassesFilters == true){ + + hostIdentityHashString, identityType, err := identity.EncodeIdentityHashBytesToString(hostIdentityHash) + if (err != nil) { + hostIdentityHashHex := encoding.EncodeBytesToHexString(hostIdentityHash[:]) + return errors.New("enabledHostsList contains invalid identity hash: " + hostIdentityHashHex) + } + if (identityType != "Host"){ + return errors.New("enabledHostsList contains non-host identity hash: " + identityType) + } + + generatedHostIdentityHashesList = append(generatedHostIdentityHashesList, hostIdentityHashString) + } + + isStopped := CheckIfBuildViewedHostsIsStopped() + if (isStopped == true){ + return nil + } + + progressPercentage, err := helpers.ScaleNumberProportionally(true, index, 0, numberOfEnabledHosts-1, 0, 50) + if (err != nil) { return err } + + progressFloat := float64(progressPercentage)/100 + hostsReadyProgressPercentage := helpers.ConvertFloat64ToString(progressFloat) + + appMemory.SetMemoryEntry("ViewedHostsReadyProgressStatus", hostsReadyProgressPercentage) + } + + err = viewedHostsListDatastore.OverwriteList(generatedHostIdentityHashesList) + if (err != nil) { return err } + + err = mySettings.SetSetting("ViewedHostsGeneratedStatus", "Yes") + if (err != nil) { return err } + + networkTypeString := helpers.ConvertByteToString(networkType) + + err = mySettings.SetSetting("ViewedHostsNetworkType", networkTypeString) + if (err != nil) { return err } + } + + appMemory.SetMemoryEntry("ViewedHostsReadyProgressStatus", "0.50") + + isStopped := CheckIfBuildViewedHostsIsStopped() + if (isStopped == true){ + return nil + } + + hostsReadyStatus, err := GetViewedHostsAreReadyStatus(networkType) + if (err != nil) { return err } + if (hostsReadyStatus == false){ + + // Now we sort hosts + + currentSortByAttribute, err := GetViewedHostsSortByAttribute() + if (err != nil) { return err } + currentSortDirection, err := GetViewedHostsSortDirection() + if (err != nil) { return err } + + currentViewedHostsList, err := viewedHostsListDatastore.GetList() + if (err != nil) { return err } + + // We use this map to make sure there are no duplicate hosts + // This should never happen, unless the user's stored list was edited or there is a bug + allHostsMap := make(map[[16]byte]struct{}) + + // Map structure: Host Identity Hash -> Sort By Attribute Value + hostAttributeValuesMap := make(map[string]float64) + + maximumIndex := len(currentViewedHostsList) - 1 + + for index, hostIdentityHashString := range currentViewedHostsList{ + + hostIdentityHash, identityType, err := identity.ReadIdentityHashString(hostIdentityHashString) + if (err != nil){ + return errors.New("currentViewedHostsList contains invalid identity hash: " + hostIdentityHashString) + } + if (identityType != "Host"){ + return errors.New("currentViewedHostsList contains invalid non-Host identity: " + identityType) + } + + _, exists := allHostsMap[hostIdentityHash] + if (exists == true){ + return errors.New("currentViewedHostsList contains duplicate host identity hash.") + } + allHostsMap[hostIdentityHash] = struct{}{} + + profileExists, _, attributeExists, attributeValue, err := viewableProfiles.GetAnyAttributeFromNewestViewableUserProfile(hostIdentityHash, networkType, currentSortByAttribute, true, true, true) + if (err != nil) { return err } + if (profileExists == true && attributeExists == true){ + + attributeValueFloat, err := helpers.ConvertStringToFloat64(attributeValue) + if (err != nil) { + return errors.New("Viewed hosts attribute cannot be converted to float.") + } + + hostAttributeValuesMap[hostIdentityHashString] = attributeValueFloat + } + + isStopped := CheckIfBuildViewedHostsIsStopped() + if (isStopped == true){ + return nil + } + + newScaledPercentageInt, err := helpers.ScaleNumberProportionally(true, index, 0, maximumIndex, 50, 80) + if (err != nil) { return err } + + newProgressFloat := float64(newScaledPercentageInt)/100 + + newProgressString := helpers.ConvertFloat64ToString(newProgressFloat) + + appMemory.SetMemoryEntry("ViewedHostsReadyProgressStatus", newProgressString) + } + + compareHostsFunction := func(identityHashA string, identityHashB string)int{ + + if (identityHashA == identityHashB){ + panic("compareHostsFunction called with duplicate hosts.") + } + + attributeValueA, attributeValueAExists := hostAttributeValuesMap[identityHashA] + + attributeValueB, attributeValueBExists := hostAttributeValuesMap[identityHashB] + + if (attributeValueAExists == false && attributeValueBExists == false){ + + // We don't know the attribute value for either host + // We sort hosts in unicode order + if (identityHashA < identityHashB){ + return -1 + } + return 1 + + } else if (attributeValueAExists == true && attributeValueBExists == false){ + + // We sort unknown attribute hosts to the back of the list + + return -1 + + } else if (attributeValueAExists == false && attributeValueBExists == true){ + + return 1 + } + + // Both attribute values exist + + if (attributeValueA == attributeValueB){ + // We sort identity hashes in unicode order + if (identityHashA < identityHashB){ + return -1 + } + return 1 + } + + if (attributeValueA < attributeValueB){ + + if (currentSortDirection == "Ascending"){ + return -1 + } + return 1 + } + if (currentSortDirection == "Ascending"){ + return 1 + } + return -1 + } + + slices.SortFunc(currentViewedHostsList, compareHostsFunction) + + err = viewedHostsListDatastore.OverwriteList(currentViewedHostsList) + if (err != nil) { return err } + + err = mySettings.SetSetting("ViewedHostsSortedStatus", "Yes") + if (err != nil) { return err } + } + + appMemory.SetMemoryEntry("ViewedHostsReadyProgressStatus", "1") + + err = mySettings.SetSetting("ViewedHostsNeedsRefreshYesNo", "No") + if (err != nil) { return err } + + return nil + } + + updateFunction := func(){ + + err := updateViewedHosts() + if (err != nil){ + appMemory.SetMemoryEntry("ViewedHostsBuildEncounteredError", "Yes") + appMemory.SetMemoryEntry("ViewedHostsBuildError", err.Error()) + } + + updatingViewedHostsMutex.Unlock() + } + + go updateFunction() + + return nil +} + + +func checkIfHostPassesFilters(inputHostIdentityHash [16]byte)(bool, error){ + + //TODO + + return true, nil +} + + + +func GetNumberOfActiveHostFilters()(int, error){ + + numberOfActiveFilters := 0 + + //TODO + + return numberOfActiveFilters, nil +} + + +func SetHostFilterOnOffStatus(filterName string, filterOnOffStatus bool)error{ + + filterOnOffStatusString := helpers.ConvertBoolToYesOrNoString(filterOnOffStatus) + + err := viewedHostsFiltersMapDatastore.SetMapEntry(filterName, filterOnOffStatusString) + if (err != nil) { return err } + + err = mySettings.SetSetting("ViewedHostsGeneratedStatus", "No") + if (err != nil) { return err } + + return nil +} + + +func GetHostFilterOnOffStatus(filterName string)(bool, error){ + + filterStatusExists, currentFilterStatus, err := viewedHostsFiltersMapDatastore.GetMapEntry(filterName) + if (err != nil) { return false, err } + if (filterStatusExists == false){ + return false, nil + } + + filterOnOffStatusBool, err := helpers.ConvertYesOrNoStringToBool(currentFilterStatus) + if (err != nil) { return false, err } + + return filterOnOffStatusBool, nil +} + + + + + diff --git a/internal/parameters/createParameters/createParameters.go b/internal/parameters/createParameters/createParameters.go new file mode 100644 index 0000000..04cd3a1 --- /dev/null +++ b/internal/parameters/createParameters/createParameters.go @@ -0,0 +1,152 @@ + +// createParameters provides functions to create network parameters + +package createParameters + +// We encode fields using integers to save space + +// ParametersVersion = 1 +// NetworkType = 2 +// AdminPublicKeys = 3 +// BroadcastTime = 4 +// ParametersType = 5 + +import "seekia/internal/cryptography/blake3" +import "seekia/internal/cryptography/edwardsKeys" +import "seekia/internal/encoding" +import "seekia/internal/helpers" +import "seekia/internal/parameters/readParameters" + +import "time" +import messagepack "github.com/vmihailenco/msgpack/v5" +import "errors" + +//TODO: Make sure unnecessary rates are pruned +// For example, message cost rates for messages that are more than 2 weeks old can be pruned, if at least 2 rates exists +// This is because messages cannot be funded for longer than 2 weeks. + +//Inputs: +// -map[[32]byte][64]byte: Admin key -> Admin signature +// -[]byte: Parameters Bytes +// -error +func CreateParameters(adminSignaturesMap map[[32]byte][64]byte, contentBytes messagepack.RawMessage)([]byte, error){ + + parametersContentMap := make(map[int]messagepack.RawMessage) + + err := encoding.DecodeMessagePackBytes(false, contentBytes, ¶metersContentMap) + if (err != nil) { return nil, err } + + contentHash, err := blake3.Get32ByteBlake3Hash(contentBytes) + if (err != nil) { return nil, err } + + adminKeysListEncoded, exists := parametersContentMap[3] + if (exists == false){ + return nil, errors.New("CreateParameters called with parameters missing AdminPublicKeys") + } + + var adminKeysList [][32]byte + + err = encoding.DecodeMessagePackBytes(false, adminKeysListEncoded, &adminKeysList) + if (err != nil) { + return nil, errors.New("CreateParameters called with parameters containing invalid AdminPublicKeys") + } + + if (len(adminKeysList) != len(adminSignaturesMap)){ + return nil, errors.New("Cannot create parameters: Admin signatures map is not the same length as adminKeysList") + } + + signaturesList := make([][64]byte, 0, len(adminKeysList)) + + for _, adminPublicKey := range adminKeysList{ + + keySignature, exists := adminSignaturesMap[adminPublicKey] + if (exists == false) { + return nil, errors.New("Cannot create parameters: Admin signatures map missing adminPublicKey entry.") + } + + signatureIsValid := edwardsKeys.VerifySignature(adminPublicKey, keySignature, contentHash) + if (signatureIsValid == false){ + return nil, errors.New("Cannot create parameters: Invalid signature.") + } + + signaturesList = append(signaturesList, keySignature) + } + + signaturesListEncoded, err := encoding.EncodeMessagePackBytes(signaturesList) + if (err != nil){ return nil, err } + + parametersSlice := []messagepack.RawMessage{signaturesListEncoded, contentBytes} + + parametersBytes, err := encoding.EncodeMessagePackBytes(parametersSlice) + if (err != nil) { return nil, err } + + isValid, err := readParameters.VerifyParameters(parametersBytes) + if (err != nil) { return nil, err } + if (isValid == false) { + return nil, errors.New("Cannot create parameters: Invalid result.") + } + + return parametersBytes, nil +} + +// This returns the content that the admin(s) are signing, encoded with messagepack +// We need this so the admins can share the content prior to signing it +// Within the gui, they should be able to copy the raw Base64 encoded bytes, share them among eachother, paste them into the GUI, and sign them + +func CreateParametersContentBytes(adminPublicKeysList [][32]byte, parametersMap map[string]string)([]byte, error){ + + if (len(adminPublicKeysList) == 0){ + return nil, errors.New("CreateParametersContentBytes called with empty adminPublicKeys list") + } + + networkTypeString, exists := parametersMap["NetworkType"] + if (exists == false){ + return nil, errors.New("CreateParametersContentBytes called with parametersMap missing NetworkType") + } + + networkTypeByte, err := helpers.ConvertNetworkTypeStringToByte(networkTypeString) + if (err != nil){ + return nil, errors.New("CreateParametersContentBytes called with parametersMap containing invalid NetworkType: " + networkTypeString) + } + + parametersType, exists := parametersMap["ParametersType"] + if (exists == false){ + return nil, errors.New("CreateParametersContentBytes called with parametersMap missing ParametersType") + } + + parametersVersion := 1 + + broadcastTime := time.Now().Unix() + + parametersVersionEncoded, err := encoding.EncodeMessagePackBytes(parametersVersion) + if (err != nil) { return nil, err } + + networkTypeByteEncoded, err := encoding.EncodeMessagePackBytes(networkTypeByte) + if (err != nil) { return nil, err } + + adminPublicKeysEncoded, err := encoding.EncodeMessagePackBytes(adminPublicKeysList) + if (err != nil) { return nil, err } + + broadcastTimeEncoded, err := encoding.EncodeMessagePackBytes(broadcastTime) + if (err != nil) { return nil, err } + + parametersTypeEncoded, err := encoding.EncodeMessagePackBytes(parametersType) + if (err != nil) { return nil, err } + + parametersContentMap := map[int]messagepack.RawMessage{ + 1: parametersVersionEncoded, + 2: networkTypeByteEncoded, + 3: adminPublicKeysEncoded, + 4: broadcastTimeEncoded, + 5: parametersTypeEncoded, + } + + parametersContentBytes, err := encoding.EncodeMessagePackBytes(parametersContentMap) + if (err != nil) { return nil, err } + + return parametersContentBytes, nil +} + + + + diff --git a/internal/parameters/getParameters/getParameters.go b/internal/parameters/getParameters/getParameters.go new file mode 100644 index 0000000..0104c5f --- /dev/null +++ b/internal/parameters/getParameters/getParameters.go @@ -0,0 +1,253 @@ + +// getParameters provides functions to retrieve network parameters +// Parameters are downloaded by all Seekia clients, and contain variables that are necessary to use Seekia +// Parameters are authored and updated by the admin(s) + +package getParameters + +//TODO: Complete package +// There are many more parameters to be added, some of which are outlined in the documentation +// We must also figure out the best gold weight to use, and the correct types to store the values (math.Big?) + +//TODO: Add client-coded limits for some of the parameters +// If the limits are exceeded, we know the admin key(s) have been compromised, and the client should either wait for new permissions +// to be downloaded, or they will be instructed to download a new client. + +import "seekia/internal/encoding" +import "seekia/internal/helpers" +import "seekia/internal/identity" +import "seekia/internal/unixTime" + +import "slices" +import "errors" + + +//Outputs: +// -bool: Parameters exist +// -error +func CheckIfMessageCostParametersExist(networkType byte)(bool, error){ + + //TODO: Will check to make sure message cost parameters exist + + return true, nil +} + +// This will check to see if all necessary parameters for chatting exist +func CheckIfChatParametersExist(networkType byte)(bool, error){ + + parametersExist, err := CheckIfMessageCostParametersExist(networkType) + if (err != nil) { return false, err } + if (parametersExist == false){ + return false, nil + } + + //TODO + // secret inbox epoch parameters and more + + return true, nil +} + + +//Outputs: +// -bool: Relevant parameters exists +// -[][16]byte: List of Supermoderator identity hashes (Ranked from highest to lowest) +// -error +func GetSupermoderatorIdentityHashesList(networkType byte)(bool, [][16]byte, error){ + + //TODO: + // Will read from network parameter list of supermoderators + + emptyList := make([][16]byte, 0) + + return true, emptyList, nil +} + + +//Outputs: +// -bool: Parameters found +// -bool: Moderator is supermoderator +// -error +func CheckIfModeratorIsSupermoderator(inputIdentityHash [16]byte, networkType byte)(bool, bool, error){ + + isValid, err := identity.VerifyIdentityHash(inputIdentityHash, true, "Moderator") + if (err != nil){ return false, false, err } + if (isValid == false){ + inputIdentityHashHex := encoding.EncodeBytesToHexString(inputIdentityHash[:]) + return false, false, errors.New("CheckIfModeratorIsSupermoderator called with invalid identity hash: " + inputIdentityHashHex) + } + + isValid = helpers.VerifyNetworkType(networkType) + if (isValid == false){ + networkTypeString := helpers.ConvertByteToString(networkType) + return false, false, errors.New("CheckIfModeratorIsSupermoderator called with invalid networkType: " + networkTypeString) + } + + + parametersFound, supermoderatorIdentityHashesList, err := GetSupermoderatorIdentityHashesList(networkType) + if (err != nil) { return false, false, err } + if (parametersFound == false){ + return false, false, nil + } + + isSupermoderator := slices.Contains(supermoderatorIdentityHashesList, inputIdentityHash) + + return true, isSupermoderator, nil +} + + +func CheckIfSecretInboxEpochParametersExist(networkType byte)(bool, error){ + + //TODO + + return true, nil +} + +//Outputs: +// -bool: Parameters exist +// -int64: Epoch duration in seconds +// -error +func GetSecretInboxEpochDuration(networkType byte, timeMessageSent int64)(bool, int64, error){ + + //TODO + // Output: Amount of time after a constant start time that represents the start of a new epoch on a repeating period + // Example: If start time is Unix time 10, and duration is 100, epoch starts/ends at 110, 210, 310, 410.... + // The parameters need to contain the historical durations for at least 2 weeks (the longest time a message can be active on the network) + + return true, 604800, nil + +} + +// This is the amount of time a mate profile should be hosted before expiring from the network +// This applies to Mate profiles. +// If a user identity balance expires, then the profile can expire before this time has passed +// Moderator profiles never expire +//Outputs: +// -bool: Parameters exist +// -int64: Network expiration duration in seconds (returns a fallback value if parameters are missing) +// -error +func GetMateProfileMaximumExistenceDuration(networkType byte)(bool, int64, error){ + + //TODO + + duration := unixTime.GetMonthUnix() * 3 + + return true, duration, nil +} + +//Outputs: +// -bool: Parameters exist +// -int64: Number of seconds until host profile is dropped from network (returns a fallback value if parameters are missing) +// -error +func GetHostProfileMaximumExistenceDuration(networkType byte)(bool, int64, error){ + + //TODO: + // Output: Returns the length of time that a host profile is active + // Hosts must constantly broadcast new profiles to stay on the network + + return true, 10000, nil +} + + +//Outputs: +// -bool: Parameters exist +// -float64: Identity network cost per day / grams gold (atomic units) //TODO: change from gram to milligram? +// -error +func GetIdentityBalanceGoldCostPerDay(networkType byte, identityType string, broadcastTime int64)(bool, float64, error){ + + isValid := helpers.VerifyNetworkType(networkType) + if (isValid == false){ + networkTypeString := helpers.ConvertByteToString(networkType) + return false, 0, errors.New("GetIdentityBalanceGoldCostPerDay called with invalid networkType: " + networkTypeString) + } + + if (identityType != "Mate" && identityType != "Host"){ + // Moderator identities are funded through identity scores, not identity balances + return false, 0, errors.New("GetIdentityBalanceGoldCostPerDay called with invalid identityType: " + identityType) + } + + //TODO + // Output: Cost per day to fund an identity balance on the network + // Example: If output is 1 gram, then funding an identity for a month costs 30 grams + + return true, 100, nil +} + +//Outputs: +// -bool: Parameters exist +// -float64: Cost in grams of gold to fund 1KB message for 1 day +// -error +func GetMessageKilobyteGoldCostPerDay(networkType byte, broadcastTime int64)(bool, float64, error){ + + //TODO + // This represents the amount of gold it costs to fund a message on the network + // Output: Cost to fund a 1 kilobyte message for 1 day + + // Example: If cost is 100, and person wants to fund 10KB message for 2 weeks, they must spend 100*10*14 grams of gold to fund + + return true, 100, nil +} + +//Outputs: +// -bool: Parameters exist +// -float64: Cost in grams of gold to fund a report +// -error +func GetFundReportCostInGold(networkType byte, broadcastTime int64)(bool, float64, error){ + + //TODO + // This represents the amount of gold it costs to fund a report on the network + + return true, 150, nil +} + +//Outputs: +// -bool: Parameters exist +// -float64: Grams of gold cost to fund a mate profile +// -error +func GetFundMateProfileCostInGold(networkType byte, broadcastTime int64)(bool, float64, error){ + + //TODO + // This will return the cost in gold to fund a mate profile + // Each mate profile must be funded + // This is to prevent moderators from being spammed with new profiles to review + // The mate identity must also be funded + + return true, 100, nil +} + + +//Outputs: +// -bool: Parameters exist +// -int64: Amount of cryptocurrency atomic units to buy 1 kilogram of gold at inputTime +// -error +func GetCryptoAtomicUnitsPerKilogramOfGold(networkType byte, cryptocurrency string, inputTime int64)(bool, int64, error){ + + isValid := helpers.VerifyNetworkType(networkType) + if (isValid == false){ + networkTypeString := helpers.ConvertByteToString(networkType) + return false, 0, errors.New("GetCryptoAtomicUnitsPerKilogramOfGold called with invalid networkType: " + networkTypeString) + } + + if (cryptocurrency != "Ethereum" && cryptocurrency != "Cardano"){ + return false, 0, errors.New("GetCryptoUnitsPerKilogramOfGold called with invalid cryptocurrency: " + cryptocurrency) + } + + //TODO + + return true, 10, nil +} + +//Outputs: +// -bool: Parameters exist +// -float64: Amount of currency units worth 1 kilogram of gold +// -error +func GetAnyCurrencyUnitsPerKilogramOfGold(networkType byte, currencyCode string)(bool, float64, error){ + + //TODO + // Outputs the exchange rate of a currency to kilograms of gold + + return true, 100, nil +} + + + + diff --git a/internal/parameters/parametersStorage/parametersStorage.go b/internal/parameters/parametersStorage/parametersStorage.go new file mode 100644 index 0000000..cc0a78d --- /dev/null +++ b/internal/parameters/parametersStorage/parametersStorage.go @@ -0,0 +1,267 @@ + +// parametersStorage provides functions to store and retrieve network parameters + +package parametersStorage + +//TODO: Create a job to prune old parameters + +import "seekia/internal/parameters/readParameters" +import "seekia/internal/helpers" +import "seekia/internal/localFilesystem" +import "seekia/internal/encoding" + +import "bytes" +import goFilepath "path/filepath" +import "strings" +import "errors" +import "slices" + + +//TODO: The key of the master admin who can change the admin permissions +const masterAdminPublicKey string = "TODO" + +//Outputs: +// -bool: Parameters to add are well formed +// -bool: Admin permissions found (we cannot add parameters without these) +// -error +func AddParameters(inputParameters []byte)(bool, bool, error){ + + ableToRead, _, parametersNetworkType, listOfAdmins, parametersType, parametersBroadcastTime, _, err := readParameters.ReadParameters(true, inputParameters) + if (err != nil) { return false, false, err } + if (ableToRead == false){ + return false, false, nil + } + + adminParametersFound, parametersAreAuthorized, err := verifyParametersAreAuthorized(parametersType, parametersNetworkType, parametersBroadcastTime, listOfAdmins) + if (err != nil) { return false, false, err } + if (adminParametersFound == false){ + // We do not have admin parameters. We cannot determine if parameters to add are authorized + return false, false, nil + } + if (parametersAreAuthorized == false){ + // Parameters are not authorized. + // Could be malicious host who sent parameters, or admin permissions were updated and we/they do not have them yet + return true, true, nil + } + + // We check if parameters already exist that are newer + + parametersFound, _, _, existingParametersBroadcastTime, err := GetAuthorizedParameters(parametersType, parametersNetworkType) + if (err != nil) { return false, false, err } + if (parametersFound == true){ + if (existingParametersBroadcastTime >= parametersBroadcastTime){ + // These parameters are older than our existing stored authorized parameters. + // We will not add them. + return true, true, nil + } + } + + seekiaDirectory, err := localFilesystem.GetSeekiaDataFolderPath() + if (err != nil) { return false, false, err } + + networkTypeString := helpers.ConvertByteToString(parametersNetworkType) + + networkTypeFoldername := "Network" + networkTypeString + + parametersFolderpath := goFilepath.Join(seekiaDirectory, "Parameters", networkTypeFoldername) + parametersFilename := parametersType + ".messagepack" + + err = localFilesystem.CreateOrOverwriteFile(inputParameters, parametersFolderpath, parametersFilename) + if (err != nil) { return false, false, err } + + return true, true, nil +} + + +// This function will return parameters that are signed by known admins +//Outputs: +// -bool: Requested parameters found and signed by valid admins +// -[]byte: Parameters bytes +// -map[string]string: Parameters map +// -int64: Parameters broadcast time +// -error +func GetAuthorizedParameters(parametersType string, networkType byte)(bool, []byte, map[string]string, int64, error){ + + isValid := readParameters.VerifyParametersType(parametersType) + if (isValid == false) { + return false, nil, nil, 0, errors.New("GetAuthorizedParameters called with invalid parametersType: " + parametersType) + } + + isValid = helpers.VerifyNetworkType(networkType) + if (isValid == false){ + networkTypeString := helpers.ConvertByteToString(networkType) + return false, nil, nil, 0, errors.New("GetAuthorizedParameters called with invalid networkType: " + networkTypeString) + } + + parametersExist, storedParametersBytes, err := getStoredParameters(parametersType, networkType) + if (err != nil) { return false, nil, nil, 0, err } + if (parametersExist == false){ + return false, nil, nil, 0, nil + } + + ableToRead, _, parametersNetworkType, listOfSigners, currentParametersType, parametersBroadcastTime, parametersMap, err := readParameters.ReadParameters(true, storedParametersBytes) + if (err != nil) { return false, nil, nil, 0, err } + if (ableToRead == false) { + return false, nil, nil, 0, errors.New("Stored parameters are corrupt: storing corrupt " + parametersType + "parameter.") + } + if (parametersNetworkType != networkType){ + return false, nil, nil, 0, errors.New("getStoredParameters returning different networkType parameters") + } + if (currentParametersType != parametersType){ + return false, nil, nil, 0, errors.New("getStoredParameters returning different parametersType parameters") + } + + adminPermissionsFound, parametersAreAuthorized, err := verifyParametersAreAuthorized(parametersType, networkType, parametersBroadcastTime, listOfSigners) + if (err != nil) { return false, nil, nil, 0, err } + if (adminPermissionsFound == false || parametersAreAuthorized == false){ + return false, nil, nil, 0, nil + } + + return true, storedParametersBytes, parametersMap, parametersBroadcastTime, nil +} + +//Outputs: +// -bool: Admin permissions found +// -bool: Parameters are authorized +// -error +func verifyParametersAreAuthorized(parametersType string, networkType byte, parametersBroadcastTime int64, parametersSigners [][32]byte)(bool, bool, error){ + + containsDuplicates, _ := helpers.CheckIfListContainsDuplicates(parametersSigners) + if (containsDuplicates == true){ + return false, false, errors.New("verifyParametersAreAuthorized called with parametersSigners list containing duplicates.") + } + + parametersExist, adminPermissionsParameters, err := getStoredParameters("AdminPermissions", networkType) + if (err != nil) { return false, false, err } + if (parametersExist == false){ + return false, false, nil + } + + ableToRead, _, parametersNetworkType, listOfAdmins, currentParametersType, _, adminPermissionsMap, err := readParameters.ReadParameters(true, adminPermissionsParameters) + if (err != nil) { return false, false, err } + if (ableToRead == false){ + return false, false, errors.New("Parameters storage corrupt: Storing invalid AdminPermissions.") + } + if (parametersNetworkType != networkType){ + return false, false, errors.New("getStoredParameters returning parameters of a different networkType") + } + if (currentParametersType != "AdminPermissions"){ + return false, false, errors.New("getStoredParameters returning parameters of a different parametersType") + } + if (len(listOfAdmins) != 1){ + return false, false, errors.New("Parameters storage is corrupt: storing AdminPermissions with more than 1 signer.") + } + + masterAdminPublicKeyBytes, err := encoding.DecodeHexStringToBytes(masterAdminPublicKey) + if (err != nil){ + return false, false, errors.New("Master admin public key is malformed: " + masterAdminPublicKey + ". Reason: " + err.Error()) + } + + areEqual := bytes.Equal(listOfAdmins[0][:], masterAdminPublicKeyBytes) + if (areEqual == false){ + return false, false, errors.New("Parameters storage is corrupt: storing AdminPermissions with different admin key.") + } + + authorizedAdminsListString, exists := adminPermissionsMap[parametersType + "_AuthorizedAdmins"] + if (exists == false) { + return false, false, errors.New("Stored AdminPermissions parameters missing " + parametersType + "_AuthorizedAdmins") + } + + authorizedAdminsList := strings.Split(authorizedAdminsListString, "+") + if (len(authorizedAdminsList) == 0){ + return false, false, errors.New("Stored AdminPermissions parameters contains empty authorizedAdmins list") + } + + authorizedAdminKeysList := make([][32]byte, 0, len(authorizedAdminsList)) + + for _, adminPublicKeyHex := range authorizedAdminsList{ + + adminPublicKey, err := encoding.DecodeHexStringToBytes(adminPublicKeyHex) + if (err != nil){ + return false, false, errors.New("Stored AdminPermissions parameters contains empty authorizedAdmins list: Key not hex.") + } + + if (len(adminPublicKey) != 32){ + return false, false, errors.New("Stored AdminPermissions parameters contains empty authorizedAdmins list: Key is invalid length.") + } + + adminPublicKeyArray := [32]byte(adminPublicKey) + + authorizedAdminKeysList = append(authorizedAdminKeysList, adminPublicKeyArray) + } + + minimumSigners, exists := adminPermissionsMap[parametersType + "_MinimumSigners"] + if (exists == false) { + return false, false, errors.New("Parameters storage is corrupt: AdminPermissions missing MinimumSigners but contains AuthorizedAdmins") + } + + minimumNumberOfSigners, err := helpers.ConvertStringToInt(minimumSigners) + if (err != nil) { + return false, false, errors.New("Parameters storage is corrupt: AdminPermissions contains invalid minimumSigners: " + minimumSigners) + } + + if (len(authorizedAdminKeysList) < minimumNumberOfSigners){ + return false, false, errors.New("Parameters storage is corrupt: AdminPermissions contains less authorizedAdmins than minimumSigners") + } + + minimumBroadcastTime, exists := adminPermissionsMap[parametersType + "_MinimumBroadcastTime"] + if (exists == false) { + return false, false, errors.New("Parameters storage is corrupt: AdminPermissions missing minimumBroadcastTime but contains AuthorizedAdmins for parameterType.") + } + + minimumBroadcastTimeInt64, err := helpers.ConvertStringToInt64(minimumBroadcastTime) + if (err != nil) { + return false, false, errors.New("Parameters storage is corrupt: AdminPermissions contains invalid minimumBroadcastTime: " + minimumBroadcastTime) + } + + if (parametersBroadcastTime < minimumBroadcastTimeInt64){ + return false, false, nil + } + + verifiedAdminsCount := 0 + + for _, adminPublicKey := range parametersSigners{ + + isInList := slices.Contains(authorizedAdminKeysList, adminPublicKey) + if (isInList == true){ + verifiedAdminsCount += 1 + } + } + + if (verifiedAdminsCount < minimumNumberOfSigners){ + // This parameters file does not have enough authorized signers + // Either the provider of the parameters file is malicious, or the permissions file has updated + // and the provider/we have not received the newest file + return true, false, nil + } + + return true, true, nil +} + +//Outputs: +// -bool: Parameters exist +// -[]byte: Parameters bytes +// -error +func getStoredParameters(parametersType string, networkType byte)(bool, []byte, error){ + + seekiaDirectory, err := localFilesystem.GetSeekiaDataFolderPath() + if (err != nil) { return false, nil, err } + + networkTypeString := helpers.ConvertByteToString(networkType) + + networkTypeFoldername := "Network" + networkTypeString + + parametersFilename := parametersType + ".messagepack" + parametersFilepath := goFilepath.Join(seekiaDirectory, "Parameters", networkTypeFoldername, parametersFilename) + + exists, fileBytes, err := localFilesystem.GetFileContents(parametersFilepath) + if (err != nil) { return false, nil, err } + if (exists == false){ + return false, nil, nil + } + + return true, fileBytes, nil +} + + + diff --git a/internal/parameters/readParameters/readParameters.go b/internal/parameters/readParameters/readParameters.go new file mode 100644 index 0000000..9ed4dc5 --- /dev/null +++ b/internal/parameters/readParameters/readParameters.go @@ -0,0 +1,284 @@ + +// readParameters provides functions to read and verify parameters + +package readParameters + + +import "seekia/internal/cryptography/blake3" +import "seekia/internal/cryptography/edwardsKeys" +import "seekia/internal/helpers" +import "seekia/internal/encoding" + +import messagepack "github.com/vmihailenco/msgpack/v5" +import "strings" +import "slices" + +func VerifyParametersType(parametersType string)bool{ + + allParametersTypesList := GetAllParametersTypesList() + + isValid := slices.Contains(allParametersTypesList, parametersType) + if (isValid == true){ + return true + } + + return false +} + +func VerifyParametersHash(inputHash [31]byte)bool{ + + metadataByte := inputHash[30] + + if (metadataByte != 1){ + return false + } + + return true +} + +func GetAllParametersTypesList()[]string{ + + //TODO: Add more + + typesList := []string{"AdminPermissions", "GeneralParameters"} + + return typesList +} + +func VerifyParameters(inputParameters []byte)(bool, error){ + + ableToRead, _, _, _, _, _, _, err := ReadParameters(true, inputParameters) + if (err != nil) { return false, err } + if (ableToRead == false){ + return false, nil + } + + return true, nil +} + +//Outputs: +// -bool: Parameters are valid +// -[31]byte: Parameters Hash +// -int: Parameters version +// -byte: Parameters network type byte (1 == Mainnet, 2 == Testnet1) +// -[][32]byte: List of admins who signed the parameters +// -string: Parameters type +// -int64: Parameters broadcast time +// -map[string]string: Parameters Map +// -error (will return error if there is a bug in the function) +func ReadParametersAndHash(verifyParameters bool, inputParameters []byte)(bool, [31]byte, int, byte, [][32]byte, string, int64, map[string]string, error){ + + ableToRead, parametersVersion, parametersNetworkType, parametersAuthorsList, parametersType, parametersBroadcastTime, parametersMap, err := ReadParameters(verifyParameters, inputParameters) + if (err != nil) { return false, [31]byte{}, 0, 0, nil, "", 0, nil, err } + if (ableToRead == false){ + return false, [31]byte{}, 0, 0, nil, "", 0, nil, nil + } + + parametersHashWithoutMetadataByte, err := blake3.GetBlake3HashAsBytes(30, inputParameters) + if (err != nil) { return false, [31]byte{}, 0, 0, nil, "", 0, nil, err } + + //TODO: Use this byte for something useful + metadataByte := byte(1) + + parametersHashSlice := append(parametersHashWithoutMetadataByte, metadataByte) + + parametersHash := [31]byte(parametersHashSlice) + + return true, parametersHash, parametersVersion, parametersNetworkType, parametersAuthorsList, parametersType, parametersBroadcastTime, parametersMap, nil + +} + +// This function reads the parameters without computing the hash, thus saving computational time. +//Outputs: +// -bool: Parameters are valid +// -int: Parameters version +// -byte: Parameters network type byte (1 == Mainnet, 2 == Testnet1) +// -[][32]byte: List of admins who signed the parameters +// -string: Parameters type +// -int64: Parameters broadcast time +// -map[string]string: Parameters Map +// -error (will return error if there is a bug in the function) +func ReadParameters(verifyParameters bool, inputParameters []byte)(bool, int, byte, [][32]byte, string, int64, map[string]string, error){ + + var parametersSlice []messagepack.RawMessage + + err := encoding.DecodeMessagePackBytes(false, inputParameters, ¶metersSlice) + if (err != nil){ + // Invalid parameters: Invalid messagepack + return false, 0, 0, nil, "", 0, nil, nil + } + + if (len(parametersSlice) != 2){ + // Invalid parameters: Invalid messagepack + return false, 0, 0, nil, "", 0, nil, nil + } + + parametersSignaturesEncoded := parametersSlice[0] + parametersContentMessagepack := parametersSlice[1] + + var adminSignaturesList [][64]byte + + err = encoding.DecodeMessagePackBytes(false, parametersSignaturesEncoded, &adminSignaturesList) + if (err != nil){ + // Invalid parameters: Invalid parameters signatures + return false, 0, 0, nil, "", 0, nil, nil + } + + rawParametersMap := make(map[int]messagepack.RawMessage) + + err = encoding.DecodeMessagePackBytes(false, parametersContentMessagepack, &rawParametersMap) + if (err != nil){ + // Invalid parameters: Invalid content + return false, 0, 0, nil, "", 0, nil, nil + } + + parametersVersionEncoded, exists := rawParametersMap[1] + if (exists == false){ + // Invalid parameters: Missing parameters version + return false, 0, 0, nil, "", 0, nil, nil + } + + parametersVersion, err := encoding.DecodeRawMessagePackToInt(parametersVersionEncoded) + if (err != nil){ + // Invalid parameters: Contains invalid parameters version + return false, 0, 0, nil, "", 0, nil, nil + } + + if (parametersVersion != 1){ + + // Parameters must have been created by a different Seekia version. We cannot read them. + return false, 0, 0, nil, "", 0, nil, nil + } + + networkTypeEncoded, exists := rawParametersMap[2] + if (exists == false){ + // Invalid parameters: Missing network type + return false, 0, 0, nil, "", 0, nil, nil + } + + networkTypeByte, err := encoding.DecodeRawMessagePackToByte(networkTypeEncoded) + if (err != nil){ + // Invalid parameters: Contains invalid network type + return false, 0, 0, nil, "", 0, nil, nil + } + + isValid := helpers.VerifyNetworkType(networkTypeByte) + if (isValid == false){ + // Invalid parameters: Contains invalid network type + return false, 0, 0, nil, "", 0, nil, nil + } + + adminPublicKeysListEncoded, exists := rawParametersMap[3] + if (exists == false){ + // Invalid parameters: Missing admin public keys list + return false, 0, 0, nil, "", 0, nil, nil + } + + var adminPublicKeysList [][32]byte + + err = encoding.DecodeMessagePackBytes(false, adminPublicKeysListEncoded, &adminPublicKeysList) + if (err != nil){ + // Invalid parameters: Invalid admin public keys list + return false, 0, 0, nil, "", 0, nil, nil + } + + if (len(adminPublicKeysList) == 0){ + return false, 0, 0, nil, "", 0, nil, nil + } + + if (verifyParameters == true){ + + contentHash, err := blake3.Get32ByteBlake3Hash(parametersContentMessagepack) + if (err != nil) { return false, 0, 0, nil, "", 0, nil, err } + + if (len(adminSignaturesList) != len(adminPublicKeysList)){ + // Invalid parameters: Admin signatures do not match admin public keys. + return false, 0, 0, nil, "", 0, nil, nil + } + + for index, adminPublicKey := range adminPublicKeysList{ + + if (len(adminPublicKey) != 32){ + // Invalid parameters: Invalid admin public key + return false, 0, 0, nil, "", 0, nil, nil + } + + keySignature := adminSignaturesList[index] + + signatureIsValid := edwardsKeys.VerifySignature(adminPublicKey, keySignature, contentHash) + if (signatureIsValid == false){ + // Cannot read parameters: Signature is invalid + return false, 0, 0, nil, "", 0, nil, nil + } + } + } + + broadcastTimeEncoded, exists := rawParametersMap[4] + if (exists == false){ + // Invalid parameters: Missing BroadcastTime + return false, 0, 0, nil, "", 0, nil, nil + } + + broadcastTime, err := encoding.DecodeRawMessagePackToInt64(broadcastTimeEncoded) + if (err != nil){ + // Invalid parameters: Contains invalid BroadcastTime + return false, 0, 0, nil, "", 0, nil, nil + } + + parametersTypeEncoded, exists := rawParametersMap[5] + if (exists == false){ + // Invalid parameters: Missing ParametersType + return false, 0, 0, nil, "", 0, nil, nil + } + + parametersType, err := encoding.DecodeRawMessagePackToString(parametersTypeEncoded) + if (err != nil){ + // Invalid parameters: Invalid ParametersType + return false, 0, 0, nil, "", 0, nil, nil + } + + if (verifyParameters == true){ + + isValid := helpers.VerifyBroadcastTime(broadcastTime) + if (isValid == false){ + // Invalid parameters: Invalid BroadcastTime + return false, 0, 0, nil, "", 0, nil, nil + } + + isValid = VerifyParametersType(parametersType) + if (isValid == false){ + // Invalid parameters: Invalid ParametersType + return false, 0, 0, nil, "", 0, nil, nil + } + } + + networkTypeString := helpers.ConvertByteToString(networkTypeByte) + + broadcastTimeString := helpers.ConvertInt64ToString(broadcastTime) + + adminPublicKeyStringsList := make([]string, 0, len(adminPublicKeysList)) + + for _, adminPublicKeyBytes := range adminPublicKeysList{ + + adminPublicKeyString := encoding.EncodeBytesToHexString(adminPublicKeyBytes[:]) + + adminPublicKeyStringsList = append(adminPublicKeyStringsList, adminPublicKeyString) + } + + adminPublicKeysJoined := strings.Join(adminPublicKeyStringsList, "+") + + parametersMap := map[string]string{ + "ParametersVersion": "1", + "NetworkType": networkTypeString, + "ParametersType": parametersType, + "BroadcastTime": broadcastTimeString, + "AdminPublicKeys": adminPublicKeysJoined, + } + + //TODO: Add more + + return true, parametersVersion, networkTypeByte, adminPublicKeysList, parametersType, broadcastTime, parametersMap, nil +} + + + diff --git a/internal/profiles/attributeDisplay/attributeDisplay.go b/internal/profiles/attributeDisplay/attributeDisplay.go new file mode 100644 index 0000000..c37a937 --- /dev/null +++ b/internal/profiles/attributeDisplay/attributeDisplay.go @@ -0,0 +1,953 @@ + +// attributeDisplay provides a function to format profile attributes for display in the GUI +// Example: Raw Distance is formatted as a distance in kilometers, and we may need to convert it to metric + +package attributeDisplay + +//TODO: Deal with singular/multiple values and how that changes an attribute value's units +// For example: 1 variant, 2 variants + +import "seekia/resources/worldLocations" +import "seekia/resources/worldLanguages" +import "seekia/resources/currencies" + +import "seekia/internal/convertCurrencies" +import "seekia/internal/globalSettings" +import "seekia/internal/helpers" +import "seekia/internal/network/appNetworkType/getAppNetworkType" +import "seekia/internal/translation" + +import "strings" +import "errors" + +// This function will format the values into the user's currently selected language or measurement system +// This function also supports calculated attributes +//Outputs: +// -string: Attribute Title (translated) +// -bool: Attribute is numerical +// -func(string)(string, error): Function to convert attribute value to a displayable, translated value +// -Inputs: +// -string: Raw attribute value +// -Outputs: +// -string: Formatted attribute value +// -error +// -string: Units of the attribute value (translated) +// -string: Translated text to show if data is unavailable ("No Response"/"Unknown") +// -Example: Height is missing = "No Response", Identity Score is missing = "Unknown" +// - If it is impossible for the data to be unavailable, we return "". Example: IsMyContact +// -error +func GetProfileAttributeDisplayInfo(attributeName string)(string, bool, func(string)(string, error), string, string, error){ + + passValueFunction := func(input string)(string, error){ + return input, nil + } + + formatPercentageFunction := func(input string)(string, error){ + valueFloat64, err := helpers.ConvertStringToFloat64(input) + if (err != nil){ + return "", errors.New("formatPercentageFunction called with non-numeric " + attributeName + " value: " + input) + } + + if (valueFloat64 < 0 || valueFloat64 > 100){ + return "", errors.New("formatPercentageFunction called with invalid " + attributeName + " percentage value: " + input) + } + + result := input + "%" + return result, nil + } + + roundNumberFunction_Float64 := func(input float64)(string, error){ + + // We check to see if we need to round the number + // We need this for readability + // We also convert the number to show numeric units (Example: 5500000 -> 5.5 million) + + if (input < 1000){ + + numberIsInteger := helpers.CheckIfFloat64IsInteger(input) + if (numberIsInteger == true){ + + valueRounded := helpers.ConvertFloat64ToStringRounded(input, 0) + return valueRounded, nil + } + + valueRounded := helpers.ConvertFloat64ToStringRounded(input, 2) + return valueRounded, nil + } + + translatedNumber, err := helpers.ConvertFloat64ToRoundedStringWithTranslatedUnits(input) + if (err != nil) { return "", err } + + return translatedNumber, nil + } + + roundNumberFunction := func(input string)(string, error){ + + valueFloat64, err := helpers.ConvertStringToFloat64(input) + if (err != nil){ + return "", errors.New("roundNumberFunction called with non-numeric " + attributeName + " value: " + input) + } + + result, err := roundNumberFunction_Float64(valueFloat64) + if (err != nil) { return "", err } + + return result, nil + } + + translateValueFunction := func(input string)(string, error){ + + result := translation.TranslateTextFromEnglishToMyLanguage(input) + return result, nil + } + + noResponseTranslated := translation.TranslateTextFromEnglishToMyLanguage("No Response") + unknownTranslated := translation.TranslateTextFromEnglishToMyLanguage("Unknown") + + switch attributeName { + + case "ProfileVersion":{ + + titleTranslated := translation.TranslateTextFromEnglishToMyLanguage("Profile Version") + + return titleTranslated, true, passValueFunction, "", "", nil + } + case "NetworkType":{ + + titleTranslated := translation.TranslateTextFromEnglishToMyLanguage("Network Type") + + return titleTranslated, true, passValueFunction, "", "", nil + } + case "IdentityKey":{ + + titleTranslated := translation.TranslateTextFromEnglishToMyLanguage("Identity Key") + + return titleTranslated, false, passValueFunction, "", "", nil + } + case "IdentityHash":{ + + titleTranslated := translation.TranslateTextFromEnglishToMyLanguage("Identity Hash") + + return titleTranslated, false, passValueFunction, "", "", nil + } + case "ProfileType":{ + + titleTranslated := translation.TranslateTextFromEnglishToMyLanguage("Profile Type") + + return titleTranslated, false, translateValueFunction, "", "", nil + } + case "BroadcastTime":{ + + titleTranslated := translation.TranslateTextFromEnglishToMyLanguage("Broadcast Time") + + return titleTranslated, true, passValueFunction, "", "", nil + } + case "Disabled":{ + + titleTranslated := translation.TranslateTextFromEnglishToMyLanguage("Disabled") + + // This attribute will either have the value "Yes" or it won't exist + + return titleTranslated, false, translateValueFunction, "", "", nil + } + case "DeviceIdentifier":{ + + titleTranslated := translation.TranslateTextFromEnglishToMyLanguage("Device Identifier") + + return titleTranslated, false, passValueFunction, "", "", nil + } + case "NaclKey":{ + + titleTranslated := translation.TranslateTextFromEnglishToMyLanguage("Nacl Key") + + return titleTranslated, false, passValueFunction, "", "", nil + } + case "KyberKey":{ + + titleTranslated := translation.TranslateTextFromEnglishToMyLanguage("Kyber Key") + + return titleTranslated, false, passValueFunction, "", "", nil + } + case "ChatKeysLatestUpdateTime":{ + + titleTranslated := translation.TranslateTextFromEnglishToMyLanguage("Chat Keys Latest Update Time") + + return titleTranslated, true, passValueFunction, "", "", nil + } + case "Username", "Description", "Hobbies", "Job", "Beliefs":{ + + // These are attributes which are not numerical and do not need their value translated + // These attributes also have a title which is identical to their name + // They are also all user-supplied, so they have a possible No Response value. + + titleTranslated := translation.TranslateTextFromEnglishToMyLanguage(attributeName) + + return titleTranslated, false, passValueFunction, "", noResponseTranslated, nil + } + case "Photos", "Avatar", "Language", "Questionnaire", "Tags":{ + + // These are attributes which should not be displayed as strings in the GUI + // They must be displayed in a custom way + // These attributes also have a title which is identical to their name + // They are also all user-supplied, so they have a possible No Response value. + + // Photos: a list of base64 webp images, separated by "+" + // Avatar: an integer which references an emoji (see /resources/imageFiles/emojis.go) + + titleTranslated := translation.TranslateTextFromEnglishToMyLanguage(attributeName) + + return titleTranslated, false, passValueFunction, "", noResponseTranslated, nil + } + case "CatsRating":{ + + titleTranslated := translation.TranslateTextFromEnglishToMyLanguage("Cats Rating") + + return titleTranslated, true, passValueFunction, "/10", noResponseTranslated, nil + } + case "DogsRating":{ + + titleTranslated := translation.TranslateTextFromEnglishToMyLanguage("Dogs Rating") + + return titleTranslated, true, passValueFunction, "/10", noResponseTranslated, nil + } + case "PetsRating":{ + + titleTranslated := translation.TranslateTextFromEnglishToMyLanguage("Pets Rating") + + return titleTranslated, true, passValueFunction, "/10", noResponseTranslated, nil + } + case "GenderIdentity":{ + + titleTranslated := translation.TranslateTextFromEnglishToMyLanguage("Gender Identity") + + translateGenderFunction := func(input string)(string, error){ + + if (input == "Man" || input == "Woman"){ + result := translation.TranslateTextFromEnglishToMyLanguage(input) + return result, nil + } + return input, nil + } + + return titleTranslated, false, translateGenderFunction, "", noResponseTranslated, nil + } + case "FruitRating", + "VegetablesRating", + "NutsRating", + "GrainsRating", + "DairyRating", + "SeafoodRating", + "BeefRating", + "PorkRating", + "PoultryRating", + "EggsRating", + "BeansRating":{ + + foodName := strings.TrimSuffix(attributeName, "Rating") + + attributeTitle := foodName + " Rating" + + titleTranslated := translation.TranslateTextFromEnglishToMyLanguage(attributeTitle) + + return titleTranslated, true, passValueFunction, "/10", noResponseTranslated, nil + } + case "AlcoholFrequency":{ + + titleTranslated := translation.TranslateTextFromEnglishToMyLanguage("Alcohol Frequency") + + return titleTranslated, true, passValueFunction, "/10", noResponseTranslated, nil + } + case "TobaccoFrequency":{ + + titleTranslated := translation.TranslateTextFromEnglishToMyLanguage("Tobacco Frequency") + + return titleTranslated, true, passValueFunction, "/10", noResponseTranslated, nil + } + case "CannabisFrequency":{ + + titleTranslated := translation.TranslateTextFromEnglishToMyLanguage("Cannabis Frequency") + + return titleTranslated, true, passValueFunction, "/10", noResponseTranslated, nil + } + case "Fame":{ + + titleTranslated := translation.TranslateTextFromEnglishToMyLanguage("Fame") + + return titleTranslated, true, passValueFunction, "/10", noResponseTranslated, nil + } + case "Wealth":{ + + // This attribute should not be displayed to the user, use WealthInGold instead. + + titleTranslated := translation.TranslateTextFromEnglishToMyLanguage("Wealth") + + return titleTranslated, true, passValueFunction, "", noResponseTranslated, nil + } + case "WealthCurrency":{ + + // This attribute should not be displayed to the user, use WealthInGold to display wealth in a user's currency instead. + + titleTranslated := translation.TranslateTextFromEnglishToMyLanguage("Wealth Currency") + + return titleTranslated, false, passValueFunction, "", noResponseTranslated, nil + } + case "WealthIsLowerBound":{ + + titleTranslated := translation.TranslateTextFromEnglishToMyLanguage("Wealth Is Lower Bound") + + // Value is either "Yes" or "No" + + return titleTranslated, false, translateValueFunction, "", noResponseTranslated, nil + } + case "WealthInGold":{ + + // We use this attribute to display a user's Wealth + // We have to convert it to the user's currency + + titleTranslated := translation.TranslateTextFromEnglishToMyLanguage("Wealth") + + getAppCurrency := func()(string, error){ + + exists, appCurrencyCode, err := globalSettings.GetSetting("Currency") + if (err != nil) { return "", err } + if (exists == false){ + return "USD", nil + } + return appCurrencyCode, nil + } + appCurrencyCode, err := getAppCurrency() + if (err != nil) { return "", false, nil, "", "", err } + + _, appCurrencySymbol, err := currencies.GetCurrencyInfoFromCurrencyCode(appCurrencyCode) + if (err != nil) { return "", false, nil, "", "", err } + + appNetworkType, err := getAppNetworkType.GetAppNetworkType() + if (err != nil){ return "", false, nil, "", "", err } + + formatValueFunction := func(profileWealthInGold string)(string, error){ + + wealthInGoldFloat64, err := helpers.ConvertStringToFloat64(profileWealthInGold) + if (err != nil) { return "", err } + + _, convertedProfileWealth, err := convertCurrencies.ConvertKilogramsOfGoldToAnyCurrency(appNetworkType, wealthInGoldFloat64, appCurrencyCode) + if (err != nil) { return "", err } + + convertedResult, err := roundNumberFunction_Float64(convertedProfileWealth) + if (err != nil) { return "", err } + + resultWithSymbol := appCurrencySymbol + convertedResult + + return resultWithSymbol, nil + } + + currencyCodePadded := " " + appCurrencyCode + + return titleTranslated, true, formatValueFunction, currencyCodePadded, noResponseTranslated, nil + } + case "HasGenitalHerpes":{ + + titleTranslated := translation.TranslateTextFromEnglishToMyLanguage("Has Genital Herpes") + + return titleTranslated, false, passValueFunction, "", noResponseTranslated, nil + } + case "HasHIV":{ + + titleTranslated := translation.TranslateTextFromEnglishToMyLanguage("Has HIV") + + return titleTranslated, false, passValueFunction, "", noResponseTranslated, nil + } + case "HairColor":{ + + titleTranslated := translation.TranslateTextFromEnglishToMyLanguage("Hair Color") + + return titleTranslated, false, passValueFunction, "", noResponseTranslated, nil + } + case "HairTexture":{ + + titleTranslated := translation.TranslateTextFromEnglishToMyLanguage("Hair Texture") + + formatHairTextureValuesFunction := func(input string)(string, error){ + + //TODO: Translate + + if (input == "1"){ + result := "1/6 - Straight" + return result, nil + } + if (input == "2"){ + result := "2/6 - Slightly Wavy" + return result, nil + } + if (input == "3"){ + result := "3/6 - Wavy" + return result, nil + } + if (input == "4"){ + result := "4/6 - Big Curls" + return result, nil + } + if (input == "5"){ + result := "5/6 - Small Curls" + return result, nil + } + if (input == "6"){ + result := "6/6 - Very Tight Curls" + return result, nil + } + return "", errors.New("HairTexture formatValuesFunction called with invalid input: " + input) + } + + return titleTranslated, true, formatHairTextureValuesFunction, "", noResponseTranslated, nil + } + case "EyeColor":{ + + titleTranslated := translation.TranslateTextFromEnglishToMyLanguage("Eye Color") + + return titleTranslated, false, passValueFunction, "", noResponseTranslated, nil + } + case "SkinColor":{ + + titleTranslated := translation.TranslateTextFromEnglishToMyLanguage("Skin Color") + + return titleTranslated, true, passValueFunction, "", noResponseTranslated, nil + } + case "BodyMuscle":{ + + titleTranslated := translation.TranslateTextFromEnglishToMyLanguage("Body Muscle") + + return titleTranslated, true, passValueFunction, "/4", noResponseTranslated, nil + } + case "BodyFat":{ + + titleTranslated := translation.TranslateTextFromEnglishToMyLanguage("Body Fat") + + return titleTranslated, true, passValueFunction, "/4", noResponseTranslated, nil + } + case "Height":{ + + titleTranslated := translation.TranslateTextFromEnglishToMyLanguage("Height") + + unitsTranslated := translation.TranslateTextFromEnglishToMyLanguage("centimeters") + unitsWithPadding := " " + unitsTranslated + + return titleTranslated, true, roundNumberFunction, unitsWithPadding, noResponseTranslated, nil + } + case "Sex":{ + + titleTranslated := translation.TranslateTextFromEnglishToMyLanguage("Sex") + + return titleTranslated, false, translateValueFunction, "", noResponseTranslated, nil + } + case "Age":{ + + titleTranslated := translation.TranslateTextFromEnglishToMyLanguage("Age") + + unitsTranslated := translation.TranslateTextFromEnglishToMyLanguage("Years Old") + unitsWithPadding := " " + unitsTranslated + + return titleTranslated, true, passValueFunction, unitsWithPadding, noResponseTranslated, nil + } + case "IsSameSex":{ + + titleTranslated := translation.TranslateTextFromEnglishToMyLanguage("Is Same Sex") + + return titleTranslated, false, translateValueFunction, "", unknownTranslated, nil + } + case "OffspringProbabilityOfAnyMonogenicDisease":{ + + titleTranslated := translation.TranslateTextFromEnglishToMyLanguage("Offspring Probability Of Any Monogenic Disease") + + return titleTranslated, true, formatPercentageFunction, "", unknownTranslated, nil + } + case "TotalPolygenicDiseaseRiskScore":{ + + titleTranslated := translation.TranslateTextFromEnglishToMyLanguage("Total Polygenic Disease Risk Score") + + return titleTranslated, true, passValueFunction, "/100", noResponseTranslated, nil + } + case "OffspringTotalPolygenicDiseaseRiskScore":{ + + titleTranslated := translation.TranslateTextFromEnglishToMyLanguage("Offspring Total Polygenic Disease Risk Score") + + return titleTranslated, true, passValueFunction, "/100", unknownTranslated, nil + } + case "23andMe_AncestryComposition":{ + // There is no way to display this as text, we use the gui instead + + titleTranslated := translation.TranslateTextFromEnglishToMyLanguage("23andMe Ancestry Composition") + + return titleTranslated, false, passValueFunction, "", noResponseTranslated, nil + } + case "23andMe_NeanderthalVariants":{ + + titleTranslated := translation.TranslateTextFromEnglishToMyLanguage("23andMe Neanderthal Variants") + + unitsTranslated := translation.TranslateTextFromEnglishToMyLanguage("Variants") + + unitsWithPadding := " " + unitsTranslated + + return titleTranslated, true, passValueFunction, unitsWithPadding, noResponseTranslated, nil + } + case "23andMe_OffspringNeanderthalVariants":{ + + titleTranslated := translation.TranslateTextFromEnglishToMyLanguage("Offspring 23andMe Neanderthal Variants") + + unitsTranslated := translation.TranslateTextFromEnglishToMyLanguage("Variants") + + unitsWithPadding := " " + unitsTranslated + + return titleTranslated, true, passValueFunction, unitsWithPadding, unknownTranslated, nil + } + case "23andMe_MaternalHaplogroup":{ + + titleTranslated := translation.TranslateTextFromEnglishToMyLanguage("23andMe Maternal Haplogroup") + + return titleTranslated, false, passValueFunction, "", noResponseTranslated, nil + } + case "23andMe_PaternalHaplogroup":{ + + titleTranslated := translation.TranslateTextFromEnglishToMyLanguage("23andMe Paternal Haplogroup") + + return titleTranslated, false, passValueFunction, "", noResponseTranslated, nil + } + case "IsMyContact":{ + + titleTranslated := translation.TranslateTextFromEnglishToMyLanguage("Is My Contact") + + return titleTranslated, false, translateValueFunction, "", "", nil + } + case "IsLiked":{ + + titleTranslated := translation.TranslateTextFromEnglishToMyLanguage("Is Liked") + + return titleTranslated, false, translateValueFunction, "", "", nil + } + case "IsIgnored":{ + + titleTranslated := translation.TranslateTextFromEnglishToMyLanguage("Is Ignored") + + return titleTranslated, false, translateValueFunction, "", "", nil + } + case "HasRejectedMe":{ + + titleTranslated := translation.TranslateTextFromEnglishToMyLanguage("Has Rejected Me") + + return titleTranslated, false, translateValueFunction, "", "", nil + } + case "IHaveMessaged":{ + + titleTranslated := translation.TranslateTextFromEnglishToMyLanguage("I Have Messaged") + + return titleTranslated, false, translateValueFunction, "", "", nil + } + case "HasMessagedMe":{ + + titleTranslated := translation.TranslateTextFromEnglishToMyLanguage("Has Messaged Me") + + return titleTranslated, false, translateValueFunction, "", "", nil + } + case "SearchTermsCount":{ + + titleTranslated := translation.TranslateTextFromEnglishToMyLanguage("Search Terms Count") + + unitsTranslated := translation.TranslateTextFromEnglishToMyLanguage("Terms") + unitsWithPadding := " " + unitsTranslated + + return titleTranslated, true, passValueFunction, unitsWithPadding, "", nil + } + case "Sexuality":{ + + titleTranslated := translation.TranslateTextFromEnglishToMyLanguage("Sexuality") + + return titleTranslated, false, translateValueFunction, "", noResponseTranslated, nil + } + case "PrimaryLocationCountry", "SecondaryLocationCountry":{ + + getTitleTranslated := func()string{ + + if (attributeName == "PrimaryLocationCountry"){ + + titleTranslated := translation.TranslateTextFromEnglishToMyLanguage("Primary Country") + + return titleTranslated + } + + titleTranslated := translation.TranslateTextFromEnglishToMyLanguage("Secondary Country") + + return titleTranslated + } + + titleTranslated := getTitleTranslated() + + formatValueFunction := func(inputValue string)(string, error){ + + inputCountryIdentifier, err := helpers.ConvertStringToInt(inputValue) + if (err != nil) { return "", err } + + countryObject, err := worldLocations.GetCountryObjectFromCountryIdentifier(inputCountryIdentifier) + if (err != nil) { return "", err } + + countryPrimaryName := countryObject.NamesList[0] + + countryPrimaryNameTranslated := translation.TranslateTextFromEnglishToMyLanguage(countryPrimaryName) + + return countryPrimaryNameTranslated, nil + } + + return titleTranslated, false, formatValueFunction, "", noResponseTranslated, nil + } + case "PrimaryLocationLatitude":{ + + titleTranslated := translation.TranslateTextFromEnglishToMyLanguage("Primary Location Latitude") + + return titleTranslated, true, roundNumberFunction, "°", noResponseTranslated, nil + } + case "PrimaryLocationLongitude":{ + + titleTranslated := translation.TranslateTextFromEnglishToMyLanguage("Primary Location Longitude") + + return titleTranslated, true, roundNumberFunction, "°", noResponseTranslated, nil + } + case "SecondaryLocationLatitude":{ + + titleTranslated := translation.TranslateTextFromEnglishToMyLanguage("Secondary Location Latitude") + + return titleTranslated, true, roundNumberFunction, "°", noResponseTranslated, nil + } + case "SecondaryLocationLongitude":{ + + titleTranslated := translation.TranslateTextFromEnglishToMyLanguage("Secondary Location Longitude") + + return titleTranslated, true, roundNumberFunction, "°", noResponseTranslated, nil + } + case "Distance":{ + + titleTranslated := translation.TranslateTextFromEnglishToMyLanguage("Distance") + + getMetricOrImperial := func()(string, error){ + + exists, metricOrImperial, err := globalSettings.GetSetting("MetricOrImperial") + if (err != nil) { return "", err } + if (exists == false){ + return "Metric", nil + } + if (metricOrImperial != "Metric" && metricOrImperial != "Imperial"){ + return "", errors.New("Global settings contains invalid metricOrImperial: " + metricOrImperial) + } + return metricOrImperial, nil + } + + metricOrImperial, err := getMetricOrImperial() + if (err != nil) { return "", false, nil, "", "", err } + + if (metricOrImperial == "Metric"){ + + unitsTranslated := translation.TranslateTextFromEnglishToMyLanguage("Kilometers") + unitsWithPadding := " " + unitsTranslated + + return "Distance", true, roundNumberFunction, unitsWithPadding, unknownTranslated, nil + } + + // We convert to imperial + + formatValueFunction := func(inputValue string)(string, error){ + + distanceKilometersFloat64, err := helpers.ConvertStringToFloat64(inputValue) + if (err != nil){ + return "", errors.New("formatValueFunction called with invalid distance: " + inputValue) + } + + distanceMiles, err := helpers.ConvertKilometersToMiles(distanceKilometersFloat64) + if (err != nil) { return "", err } + + distanceMilesRounded, err := roundNumberFunction_Float64(distanceMiles) + if (err != nil) { return "", err } + + return distanceMilesRounded, nil + } + + unitsTranslated := translation.TranslateTextFromEnglishToMyLanguage("Miles") + unitsWithPadding := " " + unitsTranslated + + return titleTranslated, true, formatValueFunction, unitsWithPadding, unknownTranslated, nil + } + case "ProfileLanguage":{ + + titleTranslated := translation.TranslateTextFromEnglishToMyLanguage("Profile Language") + + formatValueFunction := func(inputValue string)(string, error){ + + languageInt, err := helpers.ConvertStringToInt(inputValue) + if (err != nil){ + return "", errors.New("Database corrupt: Contains invalid profile language identifier: " + inputValue) + } + + languageObject, err := worldLanguages.GetLanguageObjectFromLanguageIdentifier(languageInt) + if (err != nil){ + return "", errors.New("Database corrupt: Contains invalid profile language identifier: " + inputValue) + } + + languagePrimaryName := languageObject.NamesList[0] + + languageNameTranslated := translation.TranslateTextFromEnglishToMyLanguage(languagePrimaryName) + + return languageNameTranslated, nil + } + + return titleTranslated, true, formatValueFunction, "", noResponseTranslated, nil + } + case "SeekiaVersion":{ + + titleTranslated := translation.TranslateTextFromEnglishToMyLanguage("Seekia Version") + + return titleTranslated, false, passValueFunction, "", "", nil + } + case "IdentityScore":{ + + titleTranslated := translation.TranslateTextFromEnglishToMyLanguage("Identity Score") + + return titleTranslated, true, passValueFunction, "", unknownTranslated, nil + } + case "Controversy":{ + + titleTranslated := translation.TranslateTextFromEnglishToMyLanguage("Controversy") + + return titleTranslated, true, passValueFunction, "", unknownTranslated, nil + } + case "BanAdvocates":{ + + titleTranslated := translation.TranslateTextFromEnglishToMyLanguage("Ban Advocates") + + unitsTranslated := translation.TranslateTextFromEnglishToMyLanguage("Advocates") + + unitsWithPadding := " " + unitsTranslated + + return titleTranslated, true, passValueFunction, unitsWithPadding, unknownTranslated, nil + } + case "NumberOfReviews":{ + + titleTranslated := translation.TranslateTextFromEnglishToMyLanguage("Number Of Reviews") + + unitsTranslated := translation.TranslateTextFromEnglishToMyLanguage("Reviews") + + unitsWithPadding := " " + unitsTranslated + + return titleTranslated, true, passValueFunction, unitsWithPadding, unknownTranslated, nil + } + case "MatchScore":{ + + titleTranslated := translation.TranslateTextFromEnglishToMyLanguage("Match Score") + + return titleTranslated, true, passValueFunction, "", unknownTranslated, nil + } + case "RacialSimilarity":{ + + titleTranslated := translation.TranslateTextFromEnglishToMyLanguage("Racial Similarity") + + return titleTranslated, true, passValueFunction, "", unknownTranslated, nil + } + case "EyeColorSimilarity":{ + + titleTranslated := translation.TranslateTextFromEnglishToMyLanguage("Eye Color Similarity") + + return titleTranslated, true, passValueFunction, "%", unknownTranslated, nil + } + case "EyeColorGenesSimilarity":{ + + titleTranslated := translation.TranslateTextFromEnglishToMyLanguage("Eye Color Genes Similarity") + + return titleTranslated, true, passValueFunction, "%", unknownTranslated, nil + } + case "HairColorSimilarity":{ + + titleTranslated := translation.TranslateTextFromEnglishToMyLanguage("Hair Color Similarity") + + return titleTranslated, true, passValueFunction, "%", unknownTranslated, nil + } + case "HairColorGenesSimilarity":{ + + titleTranslated := translation.TranslateTextFromEnglishToMyLanguage("Hair Color Genes Similarity") + + return titleTranslated, true, passValueFunction, "%", unknownTranslated, nil + } + case "SkinColorSimilarity":{ + + titleTranslated := translation.TranslateTextFromEnglishToMyLanguage("Skin Color Similarity") + + return titleTranslated, true, passValueFunction, "%", unknownTranslated, nil + } + case "SkinColorGenesSimilarity":{ + + titleTranslated := translation.TranslateTextFromEnglishToMyLanguage("Skin Color Genes Similarity") + + return titleTranslated, true, passValueFunction, "%", unknownTranslated, nil + } + case "HairTextureSimilarity":{ + + titleTranslated := translation.TranslateTextFromEnglishToMyLanguage("Hair Texture Similarity") + + return titleTranslated, true, passValueFunction, "%", unknownTranslated, nil + } + case "HairTextureGenesSimilarity":{ + + titleTranslated := translation.TranslateTextFromEnglishToMyLanguage("Hair Texture Genes Similarity") + + return titleTranslated, true, passValueFunction, "%", unknownTranslated, nil + } + case "FacialStructureGenesSimilarity":{ + + titleTranslated := translation.TranslateTextFromEnglishToMyLanguage("Facial Structure Genes Similarity") + + return titleTranslated, true, passValueFunction, "%", unknownTranslated, nil + } + case "EyeColorGenesSimilarity_NumberOfSimilarAlleles":{ + + titleTranslated := translation.TranslateTextFromEnglishToMyLanguage("Eye Color Genes Similarity - Number Of Similar Alleles") + + unitsTranslated := translation.TranslateTextFromEnglishToMyLanguage("Alleles") + + unitsWithPadding := " " + unitsTranslated + + return titleTranslated, true, passValueFunction, unitsWithPadding, unknownTranslated, nil + } + case "SkinColorGenesSimilarity_NumberOfSimilarAlleles":{ + + titleTranslated := translation.TranslateTextFromEnglishToMyLanguage("Skin Color Genes Similarity - Number Of Similar Alleles") + + unitsTranslated := translation.TranslateTextFromEnglishToMyLanguage("Alleles") + + unitsWithPadding := " " + unitsTranslated + + return titleTranslated, true, passValueFunction, unitsWithPadding, unknownTranslated, nil + } + case "HairColorGenesSimilarity_NumberOfSimilarAlleles":{ + + titleTranslated := translation.TranslateTextFromEnglishToMyLanguage("Hair Color Genes Similarity - Number Of Similar Alleles") + + unitsTranslated := translation.TranslateTextFromEnglishToMyLanguage("Alleles") + + unitsWithPadding := " " + unitsTranslated + + return titleTranslated, true, passValueFunction, unitsWithPadding, unknownTranslated, nil + } + case "HairTextureGenesSimilarity_NumberOfSimilarAlleles":{ + + titleTranslated := translation.TranslateTextFromEnglishToMyLanguage("Hair Texture Genes Similarity - Number Of Similar Alleles") + + unitsTranslated := translation.TranslateTextFromEnglishToMyLanguage("Alleles") + + unitsWithPadding := " " + unitsTranslated + + return titleTranslated, true, passValueFunction, unitsWithPadding, unknownTranslated, nil + } + case "FacialStructureGenesSimilarity_NumberOfSimilarAlleles":{ + + titleTranslated := translation.TranslateTextFromEnglishToMyLanguage("Facial Structure Genes Similarity - Number Of Similar Alleles") + + unitsTranslated := translation.TranslateTextFromEnglishToMyLanguage("Alleles") + + unitsWithPadding := " " + unitsTranslated + + return titleTranslated, true, passValueFunction, unitsWithPadding, unknownTranslated, nil + } + case "23andMe_AncestralSimilarity":{ + + titleTranslated := translation.TranslateTextFromEnglishToMyLanguage("23andMe Ancestral Similarity") + + return titleTranslated, true, passValueFunction, "%", unknownTranslated, nil + } + case "23andMe_MaternalHaplogroupSimilarity":{ + + titleTranslated := translation.TranslateTextFromEnglishToMyLanguage("23andMe Maternal Haplogroup Similarity") + + return titleTranslated, true, passValueFunction, "%", unknownTranslated, nil + } + case "23andMe_PaternalHaplogroupSimilarity":{ + + titleTranslated := translation.TranslateTextFromEnglishToMyLanguage("23andMe Paternal Haplogroup Similarity") + + return titleTranslated, true, passValueFunction, "%", unknownTranslated, nil + } + case "OffspringProbabilityOfAnyMonogenicDisease_NumberOfDiseasesTested":{ + + titleTranslated := translation.TranslateTextFromEnglishToMyLanguage("Offspring Probability Of Any Monogenic Disease - Number Of Diseases Tested") + + unitsTranslated := translation.TranslateTextFromEnglishToMyLanguage("Diseases") + + unitsWithPadding := " " + unitsTranslated + + return titleTranslated, true, passValueFunction, unitsWithPadding, "", nil + } + case "TotalPolygenicDiseaseRiskScore_NumberOfDiseasesTested":{ + + titleTranslated := translation.TranslateTextFromEnglishToMyLanguage("Total Polygenic Disease Risk Score - Number Of Diseases Tested") + + unitsTranslated := translation.TranslateTextFromEnglishToMyLanguage("Diseases") + + unitsWithPadding := " " + unitsTranslated + + return titleTranslated, true, passValueFunction, unitsWithPadding, "", nil + } + case "OffspringTotalPolygenicDiseaseRiskScore_NumberOfDiseasesTested":{ + + titleTranslated := translation.TranslateTextFromEnglishToMyLanguage("Offspring Total Polygenic Disease Risk Score - Number Of Diseases Tested") + + unitsTranslated := translation.TranslateTextFromEnglishToMyLanguage("Diseases") + + unitsWithPadding := " " + unitsTranslated + + return titleTranslated, true, passValueFunction, unitsWithPadding, "", nil + } + } + + attributeHasMonogenicDiseasePrefix := strings.HasPrefix(attributeName, "MonogenicDisease_") + if (attributeHasMonogenicDiseasePrefix == true){ + + attributeNameWithoutPrefix := strings.TrimPrefix(attributeName, "MonogenicDisease_") + + hasSuffix := strings.HasSuffix(attributeNameWithoutPrefix, "_ProbabilityOfPassingAVariant") + if (hasSuffix == true){ + + monogenicDiseaseNameWithUnderscores := strings.TrimSuffix(attributeNameWithoutPrefix, "_ProbabilityOfPassingAVariant") + + monogenicDiseaseName := strings.ReplaceAll(monogenicDiseaseNameWithUnderscores, "_", " ") + + title := monogenicDiseaseName + " Probability Of Passing A Variant" + titleTranslated := translation.TranslateTextFromEnglishToMyLanguage(title) + + return titleTranslated, true, formatPercentageFunction, "", noResponseTranslated, nil + } + + hasSuffix = strings.HasSuffix(attributeNameWithoutPrefix, "_NumberOfVariantsTested") + if (hasSuffix == true){ + + monogenicDiseaseNameWithUnderscores := strings.TrimSuffix(attributeNameWithoutPrefix, "_NumberOfVariantsTested") + + monogenicDiseaseName := strings.ReplaceAll(monogenicDiseaseNameWithUnderscores, "_", " ") + + title := monogenicDiseaseName + " Number Of Variants Tested" + titleTranslated := translation.TranslateTextFromEnglishToMyLanguage(title) + + return titleTranslated, true, passValueFunction, "Variants", noResponseTranslated, nil + } + + return "", false, nil, "", "", errors.New("GetProfileAttributeDisplayInfo called with unknown attributeName: " + attributeName) + } + + hasLocusValuePrefix := strings.HasPrefix(attributeName, "LocusValue_rs") + if (hasLocusValuePrefix == true){ + + locusRSID := strings.TrimPrefix(attributeName, "LocusValue_") + + locusTranslated := translation.TranslateTextFromEnglishToMyLanguage("Locus") + valueTranslated := translation.TranslateTextFromEnglishToMyLanguage("Value") + + titleTranslated := locusTranslated + " " + locusRSID + " " + valueTranslated + + return titleTranslated, false, passValueFunction, "", noResponseTranslated, nil + } + + + return "", false, nil, "", "", errors.New("GetProfileAttributeDisplayInfo called with unknown attributeName: " + attributeName) +} + + diff --git a/internal/profiles/attributeDisplay/attributeDisplay_test.go b/internal/profiles/attributeDisplay/attributeDisplay_test.go new file mode 100644 index 0000000..fef9b2a --- /dev/null +++ b/internal/profiles/attributeDisplay/attributeDisplay_test.go @@ -0,0 +1,50 @@ +package attributeDisplay_test + +import "seekia/internal/globalSettings" +import "seekia/internal/profiles/attributeDisplay" +import "seekia/internal/profiles/calculatedAttributes" +import "seekia/internal/profiles/profileFormat" + +import "testing" + + +func TestGetAttributeDisplayInfo(t *testing.T){ + + err := globalSettings.InitializeGlobalSettingsDatastore() + if (err != nil) { + t.Fatalf("InitializeGlobalSettingsDatastore failed: " + err.Error()) + } + + err = profileFormat.InitializeProfileFormatVariables() + if (err != nil) { + t.Fatalf("InitializeProfileFormatVariables failed: " + err.Error()) + } + + attributeObjectsList, err := profileFormat.GetProfileAttributeObjectsList() + if (err != nil){ + t.Fatalf("GetProfileAttributeObjectsList failed: " + err.Error()) + } + + for _, attributeObject := range attributeObjectsList{ + + attributeName := attributeObject.AttributeName + + _, _, _, _, _, err := attributeDisplay.GetProfileAttributeDisplayInfo(attributeName) + if (err != nil){ + t.Fatalf("GetProfileAttributeDisplayInfo failed for attribute: " + attributeName + ". Reason: " + err.Error()) + } + } + + calculatedAttributesList := calculatedAttributes.GetCalculatedAttributesList() + + for _, attributeName := range calculatedAttributesList{ + + _, _, _, _, _, err := attributeDisplay.GetProfileAttributeDisplayInfo(attributeName) + if (err != nil){ + t.Fatalf("GetProfileAttributeDisplayInfo failed for attribute: " + attributeName + ". Reason: " + err.Error()) + } + } +} + + + diff --git a/internal/profiles/calculatedAttributes/calculatedAttributes.go b/internal/profiles/calculatedAttributes/calculatedAttributes.go new file mode 100644 index 0000000..83d9ee2 --- /dev/null +++ b/internal/profiles/calculatedAttributes/calculatedAttributes.go @@ -0,0 +1,1651 @@ + +// calculatedAttributes provides functions to retrieve calculated profile attributes +// These are attributes that are constructed, rather than being retrieved from a profileMap directly. +// They can be constructed using profile attributes, a user's desires, and other information. +// An example is Distance, which requires calculating the distance between another user's coordinates and our own + +package calculatedAttributes + +import "seekia/imported/geodist" + +import "seekia/resources/geneticReferences/monogenicDiseases" +import "seekia/resources/geneticReferences/polygenicDiseases" +import "seekia/resources/geneticReferences/traits" + +import "seekia/internal/badgerDatabase" +import "seekia/internal/convertCurrencies" +import "seekia/internal/desires/myLocalDesires" +import "seekia/internal/desires/myMateDesires" +import "seekia/internal/encoding" +import "seekia/internal/genetics/companyAnalysis" +import "seekia/internal/genetics/createGeneticAnalysis" +import "seekia/internal/genetics/myChosenAnalysis" +import "seekia/internal/genetics/readGeneticAnalysis" +import "seekia/internal/helpers" +import "seekia/internal/identity" +import "seekia/internal/messaging/myChatMessages" +import "seekia/internal/moderation/moderatorControversy" +import "seekia/internal/moderation/moderatorScores" +import "seekia/internal/moderation/reviewStorage" +import "seekia/internal/myContacts" +import "seekia/internal/myIdentity" +import "seekia/internal/myIgnoredUsers" +import "seekia/internal/myLikedUsers" +import "seekia/internal/myMatchScore" +import "seekia/internal/profiles/myLocalProfiles" +import "seekia/internal/profiles/readProfiles" + +import messagepack "github.com/vmihailenco/msgpack/v5" + +import "sync" +import "strings" +import "errors" +import "slices" + +//TODO: +// -LastActive +// -OffspringLactoseToleranceProbability +// Used to sort users based on probability of lactose tolerance +// This allows the user to sort matches based on whose offspring is most likely to be lactose tolerant +// -Offspring Probability for all traits +// -DietSimilarity + + +var calculatedAttributesList = []string{ + "IdentityHash", + "IdentityScore", + "MatchScore", + "Controversy", + "BanAdvocates", + "Distance", + "IsSameSex", + "23andMe_OffspringNeanderthalVariants", + "OffspringProbabilityOfAnyMonogenicDisease", + "OffspringProbabilityOfAnyMonogenicDisease_NumberOfDiseasesTested", + "TotalPolygenicDiseaseRiskScore", + "TotalPolygenicDiseaseRiskScore_NumberOfDiseasesTested", + "OffspringTotalPolygenicDiseaseRiskScore", + "OffspringTotalPolygenicDiseaseRiskScore_NumberOfDiseasesTested", + "SearchTermsCount", + "HasMessagedMe", + "IHaveMessaged", + "HasRejectedMe", + "IsLiked", + "IsIgnored", + "IsMyContact", + "WealthInGold", + "RacialSimilarity", + "EyeColorSimilarity", + "EyeColorGenesSimilarity", + "EyeColorGenesSimilarity_NumberOfSimilarAlleles", + "HairColorSimilarity", + "HairColorGenesSimilarity", + "HairColorGenesSimilarity_NumberOfSimilarAlleles", + "SkinColorSimilarity", + "SkinColorGenesSimilarity", + "SkinColorGenesSimilarity_NumberOfSimilarAlleles", + "HairTextureSimilarity", + "HairTextureGenesSimilarity", + "HairTextureGenesSimilarity_NumberOfSimilarAlleles", + "FacialStructureGenesSimilarity", + "FacialStructureGenesSimilarity_NumberOfSimilarAlleles", + "23andMe_AncestralSimilarity", + "23andMe_MaternalHaplogroupSimilarity", + "23andMe_PaternalHaplogroupSimilarity", + "NumberOfReviews", +} + +// We use a map for faster lookups +var calculatedAttributesMap map[string]struct{} + +// We use this function to initialize the calculatedAttributesMap +func init(){ + + calculatedAttributesMap = make(map[string]struct{}) + + for _, attributeName := range calculatedAttributesList{ + + calculatedAttributesMap[attributeName] = struct{}{} + } +} + +// We only use this function for testing +func GetCalculatedAttributesList()[]string{ + + return calculatedAttributesList +} + + +func GetRetrieveAnyProfileAttributeIncludingCalculatedFunction(profileVersion int, rawProfileMap map[int]messagepack.RawMessage)(func(string)(bool, int, string, error), error){ + + // We use this map to store formatted profile values + // We do this so we only have to format values once for all uses of the getAnyAttributeFunction iteration + formattedProfileMap := make(map[string]string) + + // We use this mutex so it is safe to call the getAnyAttributeFunction concurrently + var formattedProfileMapMutex sync.RWMutex + + getAttributeFunction := func(attributeName string)(bool, int, string, error){ + + formattedProfileMapMutex.RLock() + formattedAttributeValue, exists := formattedProfileMap[attributeName] + formattedProfileMapMutex.RUnlock() + if (exists == true){ + return true, profileVersion, formattedAttributeValue, nil + } + + // Now we check the rawProfileMap + + attributeExists, formattedAttributeValue, err := readProfiles.GetFormattedProfileAttributeFromRawProfileMap(rawProfileMap, attributeName) + if (err != nil) { return false, 0, "", err } + if (attributeExists == false){ + return false, profileVersion, "", nil + } + + // We write the formatted value to the formattedProfileMap and return it + + formattedProfileMapMutex.Lock() + formattedProfileMap[attributeName] = formattedAttributeValue + formattedProfileMapMutex.Unlock() + + return true, profileVersion, formattedAttributeValue, nil + } + + getAnyAttributeFunction := func(attributeName string)(bool, int, string, error){ + + attributeExists, _, attributeValue, err := GetAnyProfileAttributeIncludingCalculated(attributeName, getAttributeFunction) + if (err != nil) { return false, 0, "", err } + if (attributeExists == false){ + return false, profileVersion, "", nil + } + return true, profileVersion, attributeValue, nil + } + + return getAnyAttributeFunction, nil +} + +//Outputs: +// -bool: Attribute value is known +// -int: Profile version that attribute was derived from. +// This does not matter for calculated attributes, which will always return the same kind of result, regardless of profile versions. +// -string: Attribute value +// -error +func GetAnyProfileAttributeIncludingCalculated(attributeName string, getProfileAttributesFunction func(string)(bool, int, string, error))(bool, int, string, error){ + + _, isCalulatedAttribute := calculatedAttributesMap[attributeName] + if (isCalulatedAttribute == false){ + + exists, profileVersion, retrievedAttribute, err := getProfileAttributesFunction(attributeName) + if (err != nil) { return false, 0, "", err } + if (exists == false) { + return false, 0, "", nil + } + + return true, profileVersion, retrievedAttribute, nil + } + + exists, profileVersion, profileType, err := getProfileAttributesFunction("ProfileType") + if (err != nil) { return false, 0, "", err } + if (exists == false) { + return false, 0, "", errors.New("GetAnyProfileAttributeIncludingCalculated called with profile missing ProfileType") + } + + getUserIdentityHash := func()([16]byte, error){ + + exists, _, identityKeyHex, err := getProfileAttributesFunction("IdentityKey") + if (err != nil) { return [16]byte{}, err } + if (exists == false) { + return [16]byte{}, errors.New("GetAnyProfileAttributeIncludingCalculated called with profile missing IdentityKey") + } + + identityKeyBytes, err := encoding.DecodeHexStringToBytes(identityKeyHex) + if (err != nil){ + return [16]byte{}, errors.New("GetAnyProfileAttributeIncludingCalculated called with profile containing non-hex IdentityKey: " + identityKeyHex) + } + + if (len(identityKeyBytes) != 32){ + return [16]byte{}, errors.New("GetAnyProfileAttributeIncludingCalculated called with profile containing invalid IdentityKey: Invalid length: " + identityKeyHex) + } + + identityKeyArray := [32]byte(identityKeyBytes) + + identityHash, err := identity.ConvertIdentityKeyToIdentityHash(identityKeyArray, profileType) + if (err != nil) { return [16]byte{}, err } + + return identityHash, nil + } + + getUserProfileNetworkType := func()(byte, error){ + + exists, _, networkTypeString, err := getProfileAttributesFunction("NetworkType") + if (err != nil) { return 0, err } + if (exists == false) { + return 0, errors.New("GetAnyProfileAttributeIncludingCalculated called with profile missing NetworkType") + } + + networkType, err := helpers.ConvertNetworkTypeStringToByte(networkTypeString) + if (err != nil) { + return 0, errors.New("GetAnyProfileAttributeIncludingCalculated called with profile containing invalid NetworkType: " + networkTypeString) + } + + return networkType, nil + } + + switch attributeName{ + + case "IdentityHash":{ + + identityHash, err := getUserIdentityHash() + if (err != nil) { return false, 0, "", err } + + identityHashString, _, err := identity.EncodeIdentityHashBytesToString(identityHash) + if (err != nil){ return false, 0, "", err } + + return true, profileVersion, identityHashString, nil + } + case "IdentityScore":{ + + if (profileType != "Moderator"){ + return false, 0, "", errors.New("Trying to get identity score for non-moderator identity.") + } + + identityHash, err := getUserIdentityHash() + if (err != nil) { return false, 0, "", err } + + scoreIsKnown, moderatorScore, _, _, _, err := moderatorScores.GetModeratorIdentityScore(identityHash) + if (err != nil) { return false, 0, "", err } + if (scoreIsKnown == false){ + return false, 0, "", nil + } + + moderatorScoreString := helpers.ConvertFloat64ToString(moderatorScore) + + return true, profileVersion, moderatorScoreString, nil + } + case "MatchScore":{ + + if (profileType != "Mate"){ + return false, 0, "", errors.New("Trying to get match score for non mate identity.") + } + + // Calculating match score requires retrieving other calculated attributes + // We have to recursively call this function + + getAnyAttributeForMatchScore := func(inputAttributeName string)(bool, int, string, error){ + + // We make sure infinite recursion will not happen accidentally + if (inputAttributeName == "MatchScore"){ + return false, 0, "", errors.New("Infinite recursion occurs during match score attribute retrieval.") + } + + exists, profileVersion, attributeValue, err := GetAnyProfileAttributeIncludingCalculated(inputAttributeName, getProfileAttributesFunction) + + return exists, profileVersion, attributeValue, err + } + + allMyDesiresList := myMateDesires.GetAllMyDesiresList(false) + + matchScore := 0 + + for _, desireName := range allMyDesiresList{ + + myDesireExists, statusIsKnown, fulfillsDesire, err := myMateDesires.CheckIfMateProfileFulfillsMyDesire(desireName, getAnyAttributeForMatchScore) + if (err != nil){ return false, 0, "", err } + if (myDesireExists == false || statusIsKnown == false || fulfillsDesire == false){ + continue + } + + pointsToAdd, err := myMatchScore.GetMyMatchScoreDesirePoints(desireName) + if (err != nil){ return false, 0, "", err } + + matchScore += pointsToAdd + } + + matchScoreString := helpers.ConvertIntToString(matchScore) + + return true, profileVersion, matchScoreString, nil + } + case "Controversy":{ + + if (profileType != "Moderator"){ + return false, 0, "", errors.New("Trying to get Controversy for non-Moderator identity.") + } + + identityHash, err := getUserIdentityHash() + if (err != nil) { return false, 0, "", err } + + profileNetworkType, err := getUserProfileNetworkType() + if (err != nil) { return false, 0, "", err } + + isKnown, controversyRating, err := moderatorControversy.GetModeratorControversyRating(identityHash, profileNetworkType) + if (err != nil) { return false, 0, "", err } + if (isKnown == false){ + return false, 0, "", nil + } + + ratingString := helpers.ConvertInt64ToString(controversyRating) + + return true, profileVersion, ratingString, nil + } + case "BanAdvocates":{ + + // This is the number of moderators who have banned this identity + // We will return both eligible and banned ban advocates + + identityHash, err := getUserIdentityHash() + if (err != nil) { return false, 0, "", err } + + profileNetworkType, err := getUserProfileNetworkType() + if (err != nil) { return false, 0, "", err } + + downloadingRequiredReviews, numberOfBanAdvocates, err := reviewStorage.GetNumberOfBanAdvocatesForIdentity(identityHash, profileNetworkType) + if (err != nil) { return false, 0, "", err } + if (downloadingRequiredReviews == false){ + return false, 0, "", nil + } + + numberOfBanAdvocatesString := helpers.ConvertIntToString(numberOfBanAdvocates) + + return true, profileVersion, numberOfBanAdvocatesString, nil + } + case "Distance":{ + + if (profileType != "Mate"){ + return false, 0, "", errors.New("Trying to get Distance for non-Mate identity.") + } + + exists, _, theirLocationLatitudeString, err := getProfileAttributesFunction("PrimaryLocationLatitude") + if (err != nil) { return false, 0, "", err } + if (exists == false){ + return false, 0, "", nil + } + + exists, _, theirLocationLongitudeString, err := getProfileAttributesFunction("PrimaryLocationLongitude") + if (err != nil) { return false, 0, "", err } + if (exists == false){ + return false, 0, "", errors.New("Profile malformed when trying to calculate distance: contains PrimaryLocationLatitude and not PrimaryLocationLongitude") + } + + theirLocationLatitudeFloat64, err := helpers.ConvertStringToFloat64(theirLocationLatitudeString) + if (err != nil) { + return false, 0, "", errors.New("Profile malformed when trying to calculate distance: contains invalid PrimaryLocationLatitude: " + theirLocationLatitudeString) + } + + theirLocationLongitudeFloat64, err := helpers.ConvertStringToFloat64(theirLocationLongitudeString) + if (err != nil) { + return false, 0, "", errors.New("Profile malformed when trying to calculate distance: contains invalid PrimaryLocationLongitude: " + theirLocationLongitudeString) + } + + exists, myLocationLatitudeString, err := myLocalProfiles.GetProfileData("Mate", "PrimaryLocationLatitude") + if (err != nil) { return false, 0, "", err } + if (exists == false){ + return false, 0, "", nil + } + + exists, myLocationLongitudeString, err := myLocalProfiles.GetProfileData("Mate", "PrimaryLocationLongitude") + if (err != nil) { return false, 0, "", err } + if (exists == false){ + return false, 0, "", errors.New("MyLocalProfiles contains PrimaryLocationLatitude but not PrimaryLocationLongitude") + } + + myLocationLatitudeFloat64, err := helpers.ConvertStringToFloat64(myLocationLatitudeString) + if (err != nil) { + return false, 0, "", errors.New("MyLocalProfiles contains invalid PrimaryLocationLatitude: " + myLocationLatitudeString) + } + myLocationLongitudeFloat64, err := helpers.ConvertStringToFloat64(myLocationLongitudeString) + if (err != nil) { + return false, 0, "", errors.New("MyLocalProfiles contains invalid PrimaryLocationLongitude: " + myLocationLongitudeString) + } + + distanceInKilometers, err := geodist.GetDistanceBetweenCoordinates(myLocationLatitudeFloat64, myLocationLongitudeFloat64, theirLocationLatitudeFloat64, theirLocationLongitudeFloat64) + if (err != nil){ return false, 0, "", err } + + distanceString := helpers.ConvertFloat64ToString(distanceInKilometers) + + return true, profileVersion, distanceString, nil + } + case "IsSameSex":{ + + // We use this to see if the user is the same sex as us + // We then know if we should display offspring attributes + // If they are the same sex, reproduction is impossible, so showing the offspring data is pointless + // We return Unknown if either person is of one of the intersex sexes + // This way, offspring data will be shown unless a Male is viewing a Female, or vice versa. + + mySexExists, mySex, err := myLocalProfiles.GetProfileData("Mate", "Sex") + if (err != nil) { return false, 0, "", err } + if (mySexExists == false){ + return false, 0, "", nil + } + if (mySex != "Male" && mySex != "Female"){ + return false, 0, "", nil + } + userSexExists, _, userSex, err := getProfileAttributesFunction("Sex") + if (err != nil) { return false, 0, "", err } + if (userSexExists == false){ + return false, 0, "", nil + } + if (userSex != "Male" && userSex != "Female"){ + return false, 0, "", nil + } + if (mySex == userSex){ + return true, profileVersion, "Yes", nil + } + return true, profileVersion, "No", nil + } + case "23andMe_OffspringNeanderthalVariants":{ + + myNeanderthalVariantsExist, myNeanderthalVariants, err := myLocalProfiles.GetProfileData("Mate", "23andMe_NeanderthalVariants") + if (err != nil) { return false, 0, "", err } + if (myNeanderthalVariantsExist == false){ + return false, 0, "", nil + } + + myNeanderthalVariantsInt, err := helpers.ConvertStringToInt(myNeanderthalVariants) + if (err != nil) { + return false, 0, "", errors.New("MyLocalProfiles malformed: Contains invalid 23andMe_NeanderthalVariants: " + myNeanderthalVariants) + } + + userNeanderthalVariantsExist, _, userNeanderthalVariants, err := getProfileAttributesFunction("23andMe_NeanderthalVariants") + if (err != nil) { return false, 0, "", err } + if (userNeanderthalVariantsExist == false){ + return false, 0, "", nil + } + + userNeanderthalVariantsInt, err := helpers.ConvertStringToInt(userNeanderthalVariants) + if (err != nil){ + return false, 0, "", errors.New("GetAnyProfileAttributeIncludingCalculated called with profile containing invalid 23andMe_NeanderthalVariants: " + userNeanderthalVariants) + } + + // We take the average of both + offspringNeanderthalVariants := (myNeanderthalVariantsInt + userNeanderthalVariantsInt)/2 + + offspringNeanderthalVariantsString := helpers.ConvertIntToString(offspringNeanderthalVariants) + + return true, profileVersion, offspringNeanderthalVariantsString, nil + } + case "OffspringProbabilityOfAnyMonogenicDisease", + "OffspringProbabilityOfAnyMonogenicDisease_NumberOfDiseasesTested":{ + + // We say the probability is known if: + // 1. At least 1 tested variant exists for both people for the same monogenic disease + // 2. For any monogenic diseases where either person has a non-zero probability, we know the offspring has a 0 probability + // For users using the 0% desire, they can be sure that: + // 1. Their matches have at least been analyzed by 1 company. + // 2. Their matches will never be carriers for the same monogenic disease(s) as them. + + // TODO Users should eventually be able to filter users based on how many variants the offspring has been tested for + // For example, If a user has had their entire genome sequenced, they will be able to only show other users who have done the same + + myPersonChosen, myGenomesExist, myAnalysisIsReady, myGeneticAnalysisMapList, myGenomeIdentifier, iHaveMultipleGenomes, err := myChosenAnalysis.GetMyChosenMateGeneticAnalysis() + if (err != nil) { return false, 0, "", err } + if (myPersonChosen == false || myGenomesExist == false || myAnalysisIsReady == false){ + + // We have not linked a genome person and performed a genetic analysis + // The total monogenic disease risk is unknown + // We can still predict disease risk for individual recessive disorders when one person has no variants, but + // not the total probability for all monogenic diseases + // This is because everyone is a carrier for at least some recessive monogenic disorders + // + return false, profileVersion, "", nil + } + + monogenicDiseaseObjectsList, err := monogenicDiseases.GetMonogenicDiseaseObjectsList() + if (err != nil) { return false, 0, "", err } + + // This will be true if either we or the user have a non-zero probability of passing a variant, + // but the opposite person has an unknown probability of passing a variant + // This is a much higher risk scenario + // For monogenic diseases where both user probabilities are unknown, this does not apply + // Basically, we want to exercise a higher level of caution if we are aware of a non-zero potential risk + // This will also be true if either user has any dominant monogenic diseases + // Thus, if our final disease probability is 0%, and this bool is true, we will say probability is Unknown + nonZeroUnknownDiseaseRiskExists := false + + // This will store the number of diseases we can test for + // This is displayed to the user in the viewProfileGui + numberOfDiseasesTested := 0 + + // We use this bool to track if the user has provided any monogenic disease probabilities + // If they have not, then we will return "Unknown" for the same reason we described for when our own analysis does not exist + //TODO: Require both probabilities to exist for the same disease at least once? + anyUserProbabilityIsKnown := false + + allDiseaseProbabilitiesList := make([]float64, 0) + + for _, diseaseObject := range monogenicDiseaseObjectsList{ + + monogenicDiseaseName := diseaseObject.DiseaseName + diseaseIsDominantOrRecessive := diseaseObject.DominantOrRecessive + + diseaseNameWithUnderscores := strings.ReplaceAll(monogenicDiseaseName, " ", "_") + + probabilityOfPassingAVariantAttributeName := "MonogenicDisease_" + diseaseNameWithUnderscores + "_ProbabilityOfPassingAVariant" + + userProbabilityIsKnown, _, userProbabilityOfPassingADiseaseVariant, err := getProfileAttributesFunction(probabilityOfPassingAVariantAttributeName) + if (err != nil) { return false, 0, "", err } + + if (userProbabilityIsKnown == true){ + anyUserProbabilityIsKnown = true + } + + myProbabilityIsKnown, _, _, myProbabilityOfPassingADiseaseVariant, _, _, _, err := readGeneticAnalysis.GetPersonMonogenicDiseaseInfoFromGeneticAnalysis(myGeneticAnalysisMapList, monogenicDiseaseName, myGenomeIdentifier, iHaveMultipleGenomes) + if (err != nil) { return false, 0, "", err } + + if (userProbabilityIsKnown == false && myProbabilityIsKnown == false){ + continue + } + + // Outputs: + // -bool: Probability is known + // -float64: Probability offspring will have the disease (0-1) + // -error + getOffspringProbabilityOfDisease := func()(bool, float64, error){ + + if (userProbabilityIsKnown == true && myProbabilityIsKnown == false){ + + if (diseaseIsDominantOrRecessive == "Dominant"){ + + if (userProbabilityOfPassingADiseaseVariant == "100"){ + // Offspring will always have disease (double dominant) + return true, 1, nil + } + + if (userProbabilityOfPassingADiseaseVariant != "0"){ + nonZeroUnknownDiseaseRiskExists = true + } + + return false, 0, nil + } + + // diseaseIsDominantOrRecessive == "Recessive" + + if (userProbabilityOfPassingADiseaseVariant == "0"){ + // There is a 0% probability of offspring having this disease + return true, 0, nil + } + + // User has a non-zero probability of passing variant + nonZeroUnknownDiseaseRiskExists = true + + return false, 0, nil + } + if (userProbabilityIsKnown == false && myProbabilityIsKnown == true){ + + if (diseaseIsDominantOrRecessive == "Dominant"){ + + if (myProbabilityOfPassingADiseaseVariant == 100){ + // Offspring will have disease (we are double dominant, all of our offspring will have this disease) + return true, 1, nil + } + if (myProbabilityOfPassingADiseaseVariant != 0){ + nonZeroUnknownDiseaseRiskExists = true + } + return false, 0, nil + } + + // diseaseIsDominantOrRecessive == "Recessive" + + if (myProbabilityOfPassingADiseaseVariant == 0){ + // There is a 0% probability of offspring having this disease + return true, 0, nil + } + // We have a non-zero probability of passing a variant + nonZeroUnknownDiseaseRiskExists = true + return false, 0, nil + } + + userProbabilityOfPassingADiseaseVariantInt, err := helpers.ConvertStringToInt(userProbabilityOfPassingADiseaseVariant) + if (err != nil){ + return false, 0, errors.New("GetAnyProfileAttributeIncludingCalculated called with profile containing invalid " + probabilityOfPassingAVariantAttributeName + ": " + userProbabilityOfPassingADiseaseVariant) + } + + offspringPercentageProbabilityOfDisease, _, err := createGeneticAnalysis.GetOffspringMonogenicDiseaseProbabilities(diseaseIsDominantOrRecessive, myProbabilityOfPassingADiseaseVariant, userProbabilityOfPassingADiseaseVariantInt) + if (err != nil) { return false, 0, err } + + offspringProbabilityOfDisease := float64(offspringPercentageProbabilityOfDisease)/100 + + return true, offspringProbabilityOfDisease, nil + } + + offspringProbabilityIsKnown, offspringProbabilityOfDisease, err := getOffspringProbabilityOfDisease() + if (err != nil) { return false, 0, "", err } + if (offspringProbabilityIsKnown == false){ + continue + } + + numberOfDiseasesTested += 1 + + if (attributeName == "OffspringProbabilityOfAnyMonogenicDisease_NumberOfDiseasesTested"){ + // We don't care about retrieving the total probability, so we can skip to the next disease + continue + } + + if (offspringProbabilityOfDisease == 100){ + // Probability that offspring will have a disease is 100% + + return true, profileVersion, "100", nil + } + + allDiseaseProbabilitiesList = append(allDiseaseProbabilitiesList, offspringProbabilityOfDisease) + } + + if (anyUserProbabilityIsKnown == false){ + // We need at least 1 disease probability from the user for this attribute's status to be known + return false, profileVersion, "", nil + } + + if (attributeName == "OffspringProbabilityOfAnyMonogenicDisease_NumberOfDiseasesTested"){ + + numberOfDiseasesTestedString := helpers.ConvertIntToString(numberOfDiseasesTested) + + return true, profileVersion, numberOfDiseasesTestedString, nil + } + + if (len(allDiseaseProbabilitiesList) == 0){ + return false, profileVersion, "", nil + } + + // We need to find the probability of any of the probabilities occurring + // Inclusive OR for all probabilities in the list + // P(At least one disease) = 1 − P(No disease) + + probabilityOfNoMonogenicDiseases := float64(1) + + for _, diseaseProbability := range allDiseaseProbabilitiesList{ + + // We multiply by the probability of no disease + probabilityOfNoMonogenicDiseases *= 1 - diseaseProbability + } + + probabilityOfAtLeast1Disease := 1 - probabilityOfNoMonogenicDiseases + + if (probabilityOfAtLeast1Disease == 0 && nonZeroUnknownDiseaseRiskExists == true){ + return false, profileVersion, "", nil + } + + totalRiskProbabilityPercentage := probabilityOfAtLeast1Disease * 100 + + totalRiskProbabilityPercentageString := helpers.ConvertFloat64ToStringRounded(totalRiskProbabilityPercentage, 0) + + return true, profileVersion, totalRiskProbabilityPercentageString, nil + } + case "TotalPolygenicDiseaseRiskScore", + "TotalPolygenicDiseaseRiskScore_NumberOfDiseasesTested":{ + + // This attribute describes the user's total polygenic disease risk score + // This enables users to choose a mate who has a low risk of polygenic diseases + // The value is a number between 0 and 100 + + //TODO: Users should be able to filter by the number of loci and diseases tested + + polygenicDiseaseObjectsList, err := polygenicDiseases.GetPolygenicDiseaseObjectsList() + if (err != nil) { return false, 0, "", err } + + numberOfDiseasesTested := 0 + + // This variable adds up the risk score for each disease + // Each risk score is a number between 0 and 1 + allDiseasesAverageRiskScoreNumerator := float64(0) + + for _, diseaseObject := range polygenicDiseaseObjectsList{ + + diseaseLociList := diseaseObject.LociList + + userRiskWeightSum := 0 + userMinimumPossibleRiskWeightSum := 0 + userMaximumPossibleRiskWeightSum := 0 + userNumberOfLociTested := 0 + + for _, locusObject := range diseaseLociList{ + + locusRSID := locusObject.LocusRSID + + locusRSIDString := helpers.ConvertInt64ToString(locusRSID) + + locusRiskWeightsMap := locusObject.RiskWeightsMap + locusMinimumRiskWeight := locusObject.MinimumRiskWeight + locusMaximumRiskWeight := locusObject.MaximumRiskWeight + + locusValueAttributeName := "LocusValue_rs" + locusRSIDString + + userLocusBasePairExists, _, userLocusBasePair, err := getProfileAttributesFunction(locusValueAttributeName) + if (err != nil) { return false, 0, "", err } + if (userLocusBasePairExists == false){ + continue + } + + userNumberOfLociTested += 1 + + userMinimumPossibleRiskWeightSum += locusMinimumRiskWeight + userMaximumPossibleRiskWeightSum += locusMaximumRiskWeight + + userLocusRiskWeight, exists := locusRiskWeightsMap[userLocusBasePair] + if (exists == false){ + // We do not know the risk weight for this base pair + // We treat this as a 0 risk weight + } else { + userRiskWeightSum += userLocusRiskWeight + } + } + + if (userNumberOfLociTested == 0){ + continue + } + + numberOfDiseasesTested += 1 + + userDiseaseRiskScore, err := helpers.ScaleNumberProportionally(true, userRiskWeightSum, userMinimumPossibleRiskWeightSum, userMaximumPossibleRiskWeightSum, 0, 100) + if (err != nil) { return false, 0, "", err } + + userRiskScoreFraction := float64(userDiseaseRiskScore)/float64(100) + + allDiseasesAverageRiskScoreNumerator += userRiskScoreFraction + } + + if (attributeName == "TotalPolygenicDiseaseRiskScore_NumberOfDiseasesTested"){ + numberOfDiseasesTestedString := helpers.ConvertIntToString(numberOfDiseasesTested) + + return true, profileVersion, numberOfDiseasesTestedString, nil + } + + if (numberOfDiseasesTested == 0){ + return false, profileVersion, "", nil + } + + allDiseasesAverageRiskScore := (float64(allDiseasesAverageRiskScoreNumerator)/float64(numberOfDiseasesTested)) * 100 + + allDiseasesAverageRiskScoreString := helpers.ConvertFloat64ToStringRounded(allDiseasesAverageRiskScore, 0) + + return true, profileVersion, allDiseasesAverageRiskScoreString, nil + } + case "OffspringTotalPolygenicDiseaseRiskScore", + "OffspringTotalPolygenicDiseaseRiskScore_NumberOfDiseasesTested":{ + + // This attribute is used to show the offspring's total risk score for all polygenic diseases + // This enables users to choose a mate whose offspring will have the lowest polygenic disease risk + // The value is a number between 0 and 100 + + // TODO Users should eventually be able to filter users based on how many loci/diseases the offspring has been tested for + // For example, if a user has had their entire genome sequenced, they will be able to only show other users who have done the same + + //TODO: We should also be weighting the diseases based on how bad they are. + // For example, Breast Cancer is not as bad as Epilepsy + // LifeView.com weights each disease in their polygenic disease risk score calculation + + myPersonChosen, myGenomesExist, myAnalysisIsReady, myGeneticAnalysisMapList, myGenomeIdentifier, _, err := myChosenAnalysis.GetMyChosenMateGeneticAnalysis() + if (err != nil) { return false, 0, "", err } + if (myPersonChosen == false || myGenomesExist == false || myAnalysisIsReady == false){ + + // We have not linked a genome person and performed a genetic analysis + // The total monogenic disease risk is unknown + // We can still predict disease risk for individual recessive disorders when one person has no variants, but + // not the total probability for all monogenic diseases + // This is because everyone is a carrier for at least some recessive monogenic disorders + // + return false, profileVersion, "", nil + } + + polygenicDiseaseObjectsList, err := polygenicDiseases.GetPolygenicDiseaseObjectsList() + if (err != nil) { return false, 0, "", err } + + numberOfDiseasesTested := 0 + + // This variable adds up the risk score for each disease + // Each risk score is a number between 0 and 1 + allDiseasesAverageRiskScoreNumerator := float64(0) + + for _, diseaseObject := range polygenicDiseaseObjectsList{ + + diseaseName := diseaseObject.DiseaseName + diseaseLociList := diseaseObject.LociList + + offspringRiskWeightSum := 0 + offspringMinimumPossibleRiskWeightSum := 0 + offspringMaximumPossibleRiskWeightSum := 0 + offspringNumberOfLociTested := 0 + + for _, locusObject := range diseaseLociList{ + + locusIdentifier := locusObject.LocusIdentifier + locusRSID := locusObject.LocusRSID + + locusRSIDString := helpers.ConvertInt64ToString(locusRSID) + + locusRiskWeightsMap := locusObject.RiskWeightsMap + locusOddsRatiosMap := locusObject.OddsRatiosMap + locusMinimumRiskWeight := locusObject.MinimumRiskWeight + locusMaximumRiskWeight := locusObject.MaximumRiskWeight + + locusValueAttributeName := "LocusValue_rs" + locusRSIDString + + myLocusInfoIsKnown, _, myLocusBasePair, _, _, _, err := readGeneticAnalysis.GetPersonPolygenicDiseaseLocusInfoFromGeneticAnalysis(myGeneticAnalysisMapList, diseaseName, locusIdentifier, myGenomeIdentifier) + if (err != nil) { return false, 0, "", err } + if (myLocusInfoIsKnown == false){ + continue + } + + userLocusBasePairExists, _, userLocusBasePair, err := getProfileAttributesFunction(locusValueAttributeName) + if (err != nil) { return false, 0, "", err } + if (userLocusBasePairExists == false){ + continue + } + + offspringLocusRiskWeight, _, _, _, err := createGeneticAnalysis.GetOffspringPolygenicDiseaseLocusInfo(locusRiskWeightsMap, locusOddsRatiosMap, myLocusBasePair, userLocusBasePair) + if (err != nil) { return false, 0, "", err } + + offspringNumberOfLociTested += 1 + + offspringMinimumPossibleRiskWeightSum += locusMinimumRiskWeight + offspringMaximumPossibleRiskWeightSum += locusMaximumRiskWeight + + offspringRiskWeightSum += offspringLocusRiskWeight + } + + if (offspringNumberOfLociTested == 0){ + continue + } + + numberOfDiseasesTested += 1 + + offspringRiskScore, err := helpers.ScaleNumberProportionally(true, offspringRiskWeightSum, offspringMinimumPossibleRiskWeightSum, offspringMaximumPossibleRiskWeightSum, 0, 100) + if (err != nil) { return false, 0, "", err } + + offspringRiskScoreFraction := float64(offspringRiskScore)/float64(100) + + allDiseasesAverageRiskScoreNumerator += offspringRiskScoreFraction + } + + if (attributeName == "OffspringTotalPolygenicDiseaseRiskScore_NumberOfDiseasesTested"){ + numberOfDiseasesTestedString := helpers.ConvertIntToString(numberOfDiseasesTested) + + return true, profileVersion, numberOfDiseasesTestedString, nil + } + + if (numberOfDiseasesTested == 0){ + return false, profileVersion, "", nil + } + + allDiseasesAverageRiskScore := (float64(allDiseasesAverageRiskScoreNumerator)/float64(numberOfDiseasesTested)) * 100 + + allDiseasesAverageRiskScoreString := helpers.ConvertFloat64ToStringRounded(allDiseasesAverageRiskScore, 0) + + return true, profileVersion, allDiseasesAverageRiskScoreString, nil + } + case "SearchTermsCount":{ + + myDesireExists, myDesiredChoicesListString, err := myLocalDesires.GetDesire("SearchTerms") + if (err != nil) { return false, 0, "", err } + if (myDesireExists == false){ + // No terms are selected + return true, profileVersion, "0", nil + } + myDesiredChoicesList := strings.Split(myDesiredChoicesListString, "+") + + myDesiredSearchTermsList := make([]string, 0, len(myDesiredChoicesList)) + + for _, searchTermBase64 := range myDesiredChoicesList{ + + searchTerm, err := encoding.DecodeBase64StringToUnicodeString(searchTermBase64) + if (err != nil){ + return false, 0, "", errors.New("My search term desires contains invalid term: " + searchTermBase64) + } + + myDesiredSearchTermsList = append(myDesiredSearchTermsList, searchTerm) + } + + // We count the number of occurances within the user's profile + + searchTermCount := 0 + + profileAttributesToCheckList := []string{"Description", "Tags", "Hobbies", "Beliefs"} + + for _, attributeName := range profileAttributesToCheckList{ + + attributeExists, _, attributeValue, err := getProfileAttributesFunction(attributeName) + if (err != nil) { return false, 0, "", err } + if (attributeExists == false){ + continue + } + for _, searchTerm := range myDesiredSearchTermsList{ + numberOfOccurances := strings.Count(attributeValue, searchTerm) + searchTermCount += numberOfOccurances + } + } + + searchTermCountString := helpers.ConvertIntToString(searchTermCount) + + return true, profileVersion, searchTermCountString, nil + } + case "HasMessagedMe":{ + + userIdentityHash, err := getUserIdentityHash() + if (err != nil) { return false, 0, "", err } + + profileNetworkType, err := getUserProfileNetworkType() + if (err != nil) { return false, 0, "", err } + + hasMessagedMe, err := myChatMessages.CheckIfUserHasMessagedMe(userIdentityHash, profileNetworkType) + if (err != nil) { return false, 0, "", err } + + if (hasMessagedMe == false){ + return true, profileVersion, "No", nil + } + return true, profileVersion, "Yes", nil + } + case "IHaveMessaged":{ + + userIdentityHash, err := getUserIdentityHash() + if (err != nil) { return false, 0, "", err } + + profileNetworkType, err := getUserProfileNetworkType() + if (err != nil) { return false, 0, "", err } + + iHaveMessaged, err := myChatMessages.CheckIfIHaveMessagedUser(userIdentityHash, profileNetworkType) + if (err != nil) { return false, 0, "", err } + + if (iHaveMessaged == false){ + return true, profileVersion, "No", nil + } + return true, profileVersion, "Yes", nil + } + case "HasRejectedMe":{ + + if (profileType != "Mate"){ + // Only Mate users can reject other users + return true, profileVersion, "No", nil + } + + myIdentityExists, myIdentityHash, err := myIdentity.GetMyIdentityHash(profileType) + if (err != nil) { return false, 0, "", err } + if (myIdentityExists == false){ + return true, profileVersion, "No", nil + } + + userIdentityHash, err := getUserIdentityHash() + if (err != nil) { return false, 0, "", err } + + profileNetworkType, err := getUserProfileNetworkType() + if (err != nil) { return false, 0, "", err } + + myIdentityFound, anyMessagesFound, _, _, _, _, _, theyHaveContactedMe, theyHaveRejectedMe, _, err := myChatMessages.GetMyConversationInfoAndSortedMessagesList(myIdentityHash, userIdentityHash, profileNetworkType) + if (err != nil) { return false, 0, "", err } + if (myIdentityFound == false){ + return false, 0, "", errors.New("My Identity not found after being found already.") + } + if (anyMessagesFound == false){ + // No messages exist between us + return true, profileVersion, "No", nil + } + if (theyHaveContactedMe == true && theyHaveRejectedMe == true){ + return true, profileVersion, "Yes", nil + } + return true, profileVersion, "No", nil + } + case "IsLiked":{ + + userIdentityHash, err := getUserIdentityHash() + if (err != nil) { return false, 0, "", err } + + isLiked, _, err := myLikedUsers.CheckIfUserIsLiked(userIdentityHash) + if (err != nil) { return false, 0, "", err } + if (isLiked == true){ + return true, profileVersion, "Yes", nil + } + return true, profileVersion, "No", nil + } + case "IsIgnored":{ + + userIdentityHash, err := getUserIdentityHash() + if (err != nil) { return false, 0, "", err } + + isIgnored, _, _, _, err := myIgnoredUsers.CheckIfUserIsIgnored(userIdentityHash) + if (err != nil) { return false, 0, "", err } + if (isIgnored == true){ + return true, profileVersion, "Yes", nil + } + return true, profileVersion, "No", nil + } + case "IsMyContact":{ + + userIdentityHash, err := getUserIdentityHash() + if (err != nil) { return false, 0, "", err } + + isMyContact, err := myContacts.CheckIfUserIsMyContact(userIdentityHash) + if (err != nil) { return false, 0, "", err } + if (isMyContact == true){ + return true, profileVersion, "Yes", nil + } + return true, profileVersion, "No", nil + } + case "WealthInGold":{ + + wealthExists, _, userWealth, err := getProfileAttributesFunction("Wealth") + if (err != nil) { return false, 0, "", err } + if (wealthExists == false){ + return false, 0, "", nil + } + + currencyExists, _, userCurrency, err := getProfileAttributesFunction("WealthCurrency") + if (err != nil) { return false, 0, "", err } + if (currencyExists == false){ + return false, 0, "", errors.New("Database corrupt: Contains mate profile with Wealth but missing WealthCurrency") + } + + userWealthFloat64, err := helpers.ConvertStringToFloat64(userWealth) + if (err != nil){ + return false, 0, "", errors.New("Database corrupt: Contains mate profile with invalid wealth: " + userWealth) + } + + profileNetworkType, err := getUserProfileNetworkType() + if (err != nil) { return false, 0, "", err } + + _, wealthInGold, err := convertCurrencies.ConvertCurrencyToKilogramsOfGold(profileNetworkType, userCurrency, userWealthFloat64) + if (err != nil){ return false, 0, "", err } + + wealthInGoldString := helpers.ConvertFloat64ToString(wealthInGold) + + return true, profileVersion, wealthInGoldString, nil + } + case "RacialSimilarity":{ + + // Calculating racial similarity requires retrieving other calculated attributes + // We have to recursively call this function + + getProfileAttributesFunctionForRacialSimilarity := func(inputAttributeName string)(bool, int, string, error){ + + // We make sure infinite recursion will not happen accidentally + if (inputAttributeName == "RacialSimilarity"){ + return false, 0, "", errors.New("Infinite recursion occurs during racial similarity attribute retrieval.") + } + + exists, profileVersion, attributeValue, err := getProfileAttributesFunction(inputAttributeName) + + return exists, profileVersion, attributeValue, err + } + + // RacialSimilarity is a value which aims to represent how racially similar 2 users are + // + // The calculation currently only adds points when the user has shared racial information such as genes and ancestry + // If users have not shared their genes, they may still be racially similar + // We want to prevent users who have not shared genes from being ranked too low by users who are sorting by racial similarity + // We should accomplish this by deducting points if similarity is low, and adding points if similarity exists + // Fine-tuning the algorithm requires real-world testing + // We can see how well the algorithm is working by creating fake profiles with real photos of people along with their genomes + racialSimilarity := float64(0) + + anyValueIsKnown := false + + addSimilarityAttributeToTotal := func(similarityAttributeName string, importanceFactor int)error{ + + valueIsKnown, _, similarityValueString, err := GetAnyProfileAttributeIncludingCalculated(similarityAttributeName, getProfileAttributesFunctionForRacialSimilarity) + if (err != nil) { return err } + if (valueIsKnown == true){ + + anyValueIsKnown = true + + // The input attribute value is a percentage between 0 - 100 + + similarityValue, err := helpers.ConvertStringToFloat64(similarityValueString) + if (err != nil){ + return errors.New("GetAnyProfileAttributeIncludingCalculated returning invalid " + similarityAttributeName + ": " + similarityValueString) + } + + valueToAdd := similarityValue * float64(importanceFactor) + + racialSimilarity += valueToAdd + } + + return nil + } + + err = addSimilarityAttributeToTotal("EyeColorSimilarity", 4) + if (err != nil) { return false, 0, "", err } + + err = addSimilarityAttributeToTotal("EyeColorGenesSimilarity", 4) + if (err != nil) { return false, 0, "", err } + + err := addSimilarityAttributeToTotal("HairColorSimilarity", 4) + if (err != nil) { return false, 0, "", err } + + err = addSimilarityAttributeToTotal("HairColorGenesSimilarity", 4) + if (err != nil) { return false, 0, "", err } + + err = addSimilarityAttributeToTotal("SkinColorSimilarity", 4) + if (err != nil) { return false, 0, "", err } + + err = addSimilarityAttributeToTotal("SkinColorGenesSimilarity", 4) + if (err != nil) { return false, 0, "", err } + + err = addSimilarityAttributeToTotal("HairTextureSimilarity", 2) + if (err != nil) { return false, 0, "", err } + + err = addSimilarityAttributeToTotal("HairTextureGenesSimilarity", 2) + if (err != nil) { return false, 0, "", err } + + // TODO: Facial structure similarity (Comparison between user profile photos is needed) + + err = addSimilarityAttributeToTotal("FacialStructureGenesSimilarity", 3) + if (err != nil) { return false, 0, "", err } + + err = addSimilarityAttributeToTotal("23andMe_AncestralSimilarity", 5) + if (err != nil) { return false, 0, "", err } + + err = addSimilarityAttributeToTotal("23andMe_MaternalHaplogroupSimilarity", 1) + if (err != nil) { return false, 0, "", err } + + err = addSimilarityAttributeToTotal("23andMe_PaternalHaplogroupSimilarity", 1) + if (err != nil) { return false, 0, "", err } + + if (anyValueIsKnown == false){ + return false, profileVersion, "", nil + } + + racialSimilarityInt, err := helpers.FloorFloat64ToInt(racialSimilarity) + if (err != nil) { return false, 0, "", err } + + racialSimilarityString := helpers.ConvertIntToString(racialSimilarityInt) + + return true, profileVersion, racialSimilarityString, nil + } + case "HairColorSimilarity":{ + + // HairColor is a "+" delimited string consisting of "Brown", "Black", "Blonde", "Orange" + // The list must contain at least 1 color and cannot contain more than 2 colors. Repeats are not allowed. + + myHairColorExists, myHairColorAttribute, err := myLocalProfiles.GetProfileData("Mate", "HairColor") + if (err != nil) { return false, 0, "", err } + if (myHairColorExists == false){ + return false, 0, "", nil + } + + userHairColorExists, _, userHairColorAttribute, err := getProfileAttributesFunction("HairColor") + if (err != nil) { return false, 0, "", err } + if (userHairColorExists == false){ + return false, 0, "", nil + } + + if (myHairColorAttribute == userHairColorAttribute){ + return true, profileVersion, "100", nil + } + + myHairColorList := strings.Split(myHairColorAttribute, "+") + userHairColorList := strings.Split(userHairColorAttribute, "+") + + areEqual := helpers.CheckIfTwoListsContainIdenticalItems(myHairColorList, userHairColorList) + if (areEqual == true){ + return true, profileVersion, "100", nil + } + + // Lists are not equal + // 1 shared color == 50% similar. + // No shared colors == 0% similar. + + for _, colorName := range myHairColorList{ + + containsColor := slices.Contains(userHairColorList, colorName) + if (containsColor == true){ + return true, profileVersion, "50", nil + } + } + + return true, profileVersion, "0", nil + } + case "SkinColorSimilarity":{ + + // Skin color is an integer between 1-6 + + mySkinColorExists, mySkinColorAttribute, err := myLocalProfiles.GetProfileData("Mate", "SkinColor") + if (err != nil) { return false, 0, "", err } + if (mySkinColorExists == false){ + return false, 0, "", nil + } + + userSkinColorExists, _, userSkinColorAttribute, err := getProfileAttributesFunction("SkinColor") + if (err != nil) { return false, 0, "", err } + if (userSkinColorExists == false){ + return false, 0, "", nil + } + + mySkinColorInt, err := helpers.ConvertStringToInt(mySkinColorAttribute) + if (err != nil){ + return false, 0, "", errors.New("myLocalProfiles contains invalid SkinColor: " + mySkinColorAttribute) + } + + userSkinColorInt, err := helpers.ConvertStringToInt(userSkinColorAttribute) + if (err != nil){ + return false, 0, "", errors.New("User profile contains invalid SkinColor: " + userSkinColorAttribute) + } + + getSkinColorSimilarity := func()int{ + + if (mySkinColorInt == userSkinColorInt){ + return 100 + } + + lesserValue := min(mySkinColorInt, userSkinColorInt) + greaterValue := max(mySkinColorInt, userSkinColorInt) + + difference := greaterValue - lesserValue + + if (difference == 1){ + return 80 + + } else if (difference == 2){ + return 60 + + } else if (difference == 3){ + return 20 + } + + return 0 + } + + skinColorSimilarity := getSkinColorSimilarity() + + skinColorSimilarityString := helpers.ConvertIntToString(skinColorSimilarity) + + return true, profileVersion, skinColorSimilarityString, nil + } + case "EyeColorSimilarity":{ + + // The EyeColor attribute is a "+" delimited string consisting of: "Blue", "Green", "Hazel", Brown" + // It must contain at least 1 color, cannot contain more than 4 colors. Repeats are not allowed. + // Example: "Blue+Green", "Blue" + + myEyeColorExists, myEyeColorAttribute, err := myLocalProfiles.GetProfileData("Mate", "EyeColor") + if (err != nil) { return false, 0, "", err } + if (myEyeColorExists == false){ + return false, 0, "", nil + } + + userEyeColorExists, _, userEyeColorAttribute, err := getProfileAttributesFunction("EyeColor") + if (err != nil) { return false, 0, "", err } + if (userEyeColorExists == false){ + return false, 0, "", nil + } + + getEyeColorSimilarity := func()string{ + + if (myEyeColorAttribute == userEyeColorAttribute){ + return "100" + } + + myEyeColorAttributeList := strings.Split(myEyeColorAttribute, "+") + userEyeColorAttributeList := strings.Split(userEyeColorAttribute, "+") + + listsContainIdenticalItems := helpers.CheckIfTwoListsContainIdenticalItems(myEyeColorAttributeList, userEyeColorAttributeList) + if (listsContainIdenticalItems == true){ + return "100" + } + + // This method could possibly be improved. + + numberOfSharedColors := 0 + + for _, colorName := range myEyeColorAttributeList{ + + containsColor := slices.Contains(userEyeColorAttributeList, colorName) + if (containsColor == true){ + numberOfSharedColors += 1 + } + } + + if (numberOfSharedColors == 0){ + return "0" + } + if (numberOfSharedColors == 1){ + return "50" + } + if (numberOfSharedColors == 2){ + return "75" + } + // numberOfSharedColors == 3 + // numberOfSharedColors cannot be 4 + // If it was, then the lists would contain identical items, which we already checked for + return "90" + } + + eyeColorSimilarity := getEyeColorSimilarity() + + return true, profileVersion, eyeColorSimilarity, nil + } + case "HairTextureSimilarity":{ + + // Hair Texture is an integer between 1-6 + + myHairTextureExists, myHairTextureAttribute, err := myLocalProfiles.GetProfileData("Mate", "HairTexture") + if (err != nil) { return false, 0, "", err } + if (myHairTextureExists == false){ + return false, 0, "", nil + } + + userHairTextureExists, _, userHairTextureAttribute, err := getProfileAttributesFunction("HairTexture") + if (err != nil) { return false, 0, "", err } + if (userHairTextureExists == false){ + return false, 0, "", nil + } + + myHairTextureInt, err := helpers.ConvertStringToInt(myHairTextureAttribute) + if (err != nil){ + return false, 0, "", errors.New("myLocalProfiles contains invalid HairTexture: " + myHairTextureAttribute) + } + + userHairTextureInt, err := helpers.ConvertStringToInt(userHairTextureAttribute) + if (err != nil){ + return false, 0, "", errors.New("User profile contains invalid HairTexture: " + userHairTextureAttribute) + } + + getHairTextureSimilarity := func()int{ + + if (myHairTextureInt == userHairTextureInt){ + return 100 + } + + lesserValue := min(myHairTextureInt, userHairTextureInt) + greaterValue := max(myHairTextureInt, userHairTextureInt) + + difference := greaterValue - lesserValue + + if (difference == 1){ + return 80 + + } else if (difference == 2){ + return 70 + + } else if (difference == 3){ + return 20 + } + + return 0 + } + + hairTextureSimilarity := getHairTextureSimilarity() + + hairTextureSimilarityString := helpers.ConvertIntToString(hairTextureSimilarity) + + return true, profileVersion, hairTextureSimilarityString, nil + } + case "EyeColorGenesSimilarity", + "EyeColorGenesSimilarity_NumberOfSimilarAlleles", + "HairColorGenesSimilarity", + "HairColorGenesSimilarity_NumberOfSimilarAlleles", + "SkinColorGenesSimilarity", + "SkinColorGenesSimilarity_NumberOfSimilarAlleles", + "HairTextureGenesSimilarity", + "HairTextureGenesSimilarity_NumberOfSimilarAlleles", + "FacialStructureGenesSimilarity", + "FacialStructureGenesSimilarity_NumberOfSimilarAlleles":{ + + // Disclaimer: I'm not sure how well this comparison will work + // TODO Compare magnitude of rsIDs, because some rsIDs are more causal than others. + // We may want to also calculate the outcomes for each user's genes, not just the raw genes. + // That calcuation would be a lot slower. + // + // We want this similarity comparison to aid users in their search for mates who look like them and with whom + // they are likely to produce offspring who look like them + + myPersonChosen, myGenomesExist, myAnalysisIsReady, myGeneticAnalysisMapList, myGenomeIdentifier, iHaveMultipleGenomes, err := myChosenAnalysis.GetMyChosenMateGeneticAnalysis() + if (err != nil) { return false, 0, "", err } + if (myPersonChosen == false || myGenomesExist == false || myAnalysisIsReady == false){ + + // We have not linked a genome person and performed a genetic analysis + // All genetic information is unknown + return false, profileVersion, "", nil + } + + getTraitName := func()string{ + + isEyeColor := strings.HasPrefix(attributeName, "EyeColor") + if (isEyeColor == true){ + + return "Eye Color" + } + isHairColor := strings.HasPrefix(attributeName, "HairColor") + if (isHairColor == true){ + + return "Hair Color" + } + isSkinColor := strings.HasPrefix(attributeName, "SkinColor") + if (isSkinColor == true){ + + return "Skin Color" + } + isHairTexture := strings.HasPrefix(attributeName, "HairTexture") + if (isHairTexture == true){ + + return "Hair Texture" + } + + // attributeName prefix == "FacialStructure" + + return "Facial Structure" + } + + traitName := getTraitName() + + myTraitLociMap, _, _, _, _, err := readGeneticAnalysis.GetPersonTraitInfoFromGeneticAnalysis(myGeneticAnalysisMapList, traitName, myGenomeIdentifier, iHaveMultipleGenomes) + if (err != nil) { return false, 0, "", err } + + traitObject, err := traits.GetTraitObject(traitName) + if (err != nil) { return false, 0, "", err } + + traitLociList := traitObject.LociList + + // This keeps track of the number of alleles for which both us and the user have a known value + numberOfKnownAlleles := 0 + + // This keeps track of the number of alleles for which us and the user have the same value + numberOfSimilarAlleles := 0 + + for _, rsID := range traitLociList{ + + myLocusValue, myLocusValueExists := myTraitLociMap[rsID] + if (myLocusValueExists == false){ + continue + } + + rsIDString := helpers.ConvertInt64ToString(rsID) + + userLocusValueExists, _, userLocusValue, err := getProfileAttributesFunction("LocusValue_rs" + rsIDString) + if (err != nil) { return false, 0, "", err } + if (userLocusValueExists == false){ + continue + } + + numberOfKnownAlleles += 2 + + //TODO: Deal with locus base pair phase information + // We may want to require all loci to be phased for the comparison + // Users who want to use this feature would have to get a genetic sequence which has phase information + // This would save space on profiles because we wouldn't have to share an IsPhased bool for each locus (because all loci would be phased) + // People who are more knowledgeable about genetics should share their opinions. + + myLocusValueBase1 := myLocusValue.Base1 + myLocusValueBase2 := myLocusValue.Base2 + + userLocusValueBase1, userLocusValueBase2, semicolonExists := strings.Cut(userLocusValue, ";") + if (semicolonExists == false){ + return false, 0, "", errors.New("Database contains invalid profile: Profile contains invalid LocusValue_rs" + rsIDString + " value: " + userLocusValue) + } + + // We are counting how many shared alleles we have for this locus, irrespective of locus phase + // + // For example: + // + // User1: TT, User2: GT = 1 shared allele + // User1: AG, User2: GA = 2 shared alleles + // User1: CC, User2: GG = 0 shared alleles + // + // We will possibly change this strategy once we include locus phase information. + + if (myLocusValueBase1 == userLocusValueBase1){ + + numberOfSimilarAlleles += 1 + + if (myLocusValueBase2 == userLocusValueBase2){ + numberOfSimilarAlleles += 1 + } + + continue + } + if (myLocusValueBase1 == userLocusValueBase2){ + + numberOfSimilarAlleles += 1 + + if (myLocusValueBase2 == userLocusValueBase1){ + numberOfSimilarAlleles += 1 + } + continue + } + if (myLocusValueBase2 == userLocusValueBase1){ + + numberOfSimilarAlleles += 1 + + // We already know that myLocusValueBase1 != userLocusValueBase2 + continue + } + if (myLocusValueBase2 == userLocusValueBase2){ + + numberOfSimilarAlleles += 1 + + // We already know that myLocusValueBase1 != userLocusValueBase1 + continue + } + } + + if (numberOfKnownAlleles < 15){ + // We don't know enough loci values to be able to calculate genetic similarity + return false, 0, "", nil + } + + attributeIsNumberOfSimilarAlleles := strings.HasSuffix(attributeName, "_NumberOfSimilarAlleles") + if (attributeIsNumberOfSimilarAlleles == true){ + + numberOfSimilarAllelesString := helpers.ConvertIntToString(numberOfSimilarAlleles) + numberOfKnownAllelesString := helpers.ConvertIntToString(numberOfKnownAlleles) + + result := numberOfSimilarAllelesString + "/" + numberOfKnownAllelesString + + return true, profileVersion, result, nil + } + + genesSimilarity := (float64(numberOfSimilarAlleles)/float64(numberOfKnownAlleles)) * 100 + + genesSimilarityInt, err := helpers.FloorFloat64ToInt(genesSimilarity) + if (err != nil) { return false, 0, "", err } + + if (genesSimilarityInt > 100){ + return true, profileVersion, "100", nil + } + + genesSimilarityString := helpers.ConvertIntToString(genesSimilarityInt) + + return true, profileVersion, genesSimilarityString, nil + } + case "23andMe_AncestralSimilarity":{ + + // Ancestral similarity aims to compare how closely related a user's ancestors are. + + // Ancestral similarity could be improved by comparing categories based on their genetic distance. + // If one pair of categories only diverged genetically 5,000 years ago, and the other diverged 30,000 years ago, + // we would count the more recently diverged group as being more similar. + // This may not always be the best measurement, because we must also take into account the genetic + // similarity between different groups. + // For example, a population which diverged a longer time ago might still be more similar due to factors + // such as gene flow and a more similar evolution. + // People who are more knowledgeable about these topics should share their thoughts. + + myAncestryCompositionExists, myAncestryCompositionAttribute, err := myLocalProfiles.GetProfileData("Mate", "23andMe_AncestryComposition") + if (err != nil){ return false, 0, "", err } + if (myAncestryCompositionExists == false){ + return false, 0, "", nil + } + + userAncestryCompositionExists, _, userAncestryCompositionAttribute, err := getProfileAttributesFunction("23andMe_AncestryComposition") + if (err != nil) { return false, 0, "", err } + if (userAncestryCompositionExists == false){ + return false, 0, "", nil + } + + ancestralSimilarity, err := companyAnalysis.GetAncestralSimilarity_23andMe(false, myAncestryCompositionAttribute, userAncestryCompositionAttribute) + if (err != nil){ + return false, 0, "", errors.New("GetAncestralSimilarity_23andMe failed when calculating my 23andMe_AncestralSimilarity with another user: " + err.Error()) + } + + ancestralSimilarityString := helpers.ConvertIntToString(ancestralSimilarity) + + return true, profileVersion, ancestralSimilarityString, nil + } + case "23andMe_MaternalHaplogroupSimilarity", + "23andMe_PaternalHaplogroupSimilarity":{ + + profileAttributeName := strings.TrimSuffix(attributeName, "Similarity") + + myHaplogroupExists, myHaplogroup, err := myLocalProfiles.GetProfileData("Mate", profileAttributeName) + if (err != nil){ return false, 0, "", err } + if (myHaplogroupExists == false){ + return false, 0, "", nil + } + + userHaplogroupExists, _, userHaplogroup, err := getProfileAttributesFunction(profileAttributeName) + if (err != nil){ return false, 0, "", err } + if (userHaplogroupExists == false){ + return false, 0, "", nil + } + + //TODO: Rank haplogroups based on genetic similarity and incorporate into calculation + + if (myHaplogroup == userHaplogroup){ + return true, profileVersion, "100", nil + } + return true, profileVersion, "0", nil + } + case "NumberOfReviews":{ + + if (profileType != "Moderator"){ + return false, 0, "", errors.New("GetAnyProfileAttributeIncludingCalculated called with NumberOfReviews attribute for non-Moderator profileType: " + profileType) + } + + userIdentityHash, err := getUserIdentityHash() + if (err != nil) { return false, 0, "", err } + + numberOfReviews := 0 + + anyExist, identityReviewHashesList, err := badgerDatabase.GetReviewerReviewHashesList(userIdentityHash, "Identity") + if (err != nil) { return false, 0, "", err } + if (anyExist == true){ + numberOfReviews += len(identityReviewHashesList) + } + + anyExist, profileReviewHashesList, err := badgerDatabase.GetReviewerReviewHashesList(userIdentityHash, "Profile") + if (err != nil) { return false, 0, "", err } + if (anyExist == true){ + numberOfReviews += len(profileReviewHashesList) + } + + anyExist, attributeReviewHashesList, err := badgerDatabase.GetReviewerReviewHashesList(userIdentityHash, "Attribute") + if (err != nil) { return false, 0, "", err } + if (anyExist == true){ + numberOfReviews += len(attributeReviewHashesList) + } + + anyExist, messageReviewHashesList, err := badgerDatabase.GetReviewerReviewHashesList(userIdentityHash, "Message") + if (err != nil) { return false, 0, "", err } + if (anyExist == true){ + numberOfReviews += len(messageReviewHashesList) + } + + numberOfReviewsString := helpers.ConvertIntToString(numberOfReviews) + + return true, profileVersion, numberOfReviewsString, nil + } + } + + return false, 0, "", errors.New("GetAnyProfileAttributeIncludingCalculated called with unknown attribute: " + attributeName) +} + + + + + diff --git a/internal/profiles/createProfiles/createProfiles.go b/internal/profiles/createProfiles/createProfiles.go new file mode 100644 index 0000000..1ae1291 --- /dev/null +++ b/internal/profiles/createProfiles/createProfiles.go @@ -0,0 +1,204 @@ + +// createProfiles provides a function to create user profiles + +package createProfiles + +import "seekia/internal/appValues" +import "seekia/internal/cryptography/blake3" +import "seekia/internal/cryptography/edwardsKeys" +import "seekia/internal/encoding" +import "seekia/internal/helpers" +import "seekia/internal/profiles/profileFormat" +import "seekia/internal/profiles/readProfiles" + +import messagepack "github.com/vmihailenco/msgpack/v5" + +import "strings" +import "time" +import "errors" + + +func CreateProfile(identityPublicKey [32]byte, identityPrivateKey [64]byte, inputProfileMap map[string]string)([]byte, error){ + + networkTypeString, exists := inputProfileMap["NetworkType"] + if (exists == false){ + return nil, errors.New("CreateProfile called with profileMap missing NetworkType") + } + + networkType, err := helpers.ConvertNetworkTypeStringToByte(networkTypeString) + if (err != nil){ + return nil, errors.New("CreateProfile called with profileMap containing invalid NetworkType: " + networkTypeString) + } + + profileType, exists := inputProfileMap["ProfileType"] + if (exists == false) { + return nil, errors.New("CreateProfile called with profile map missing profile type.") + } + + if (profileType != "Mate" && profileType != "Host" && profileType != "Moderator"){ + return nil, errors.New("CreateProfile called with profile map containing invalid profileType: " + profileType) + } + + networkTypeEncoded, err := encoding.EncodeMessagePackBytes(networkType) + if (err != nil){ return nil, err } + + profileTypeEncoded, err := encoding.EncodeMessagePackBytes(profileType) + if (err != nil) { return nil, err } + + profileVersion, err := appValues.GetProfileVersion(profileType) + if (err != nil) { return nil, err } + + profileVersionEncoded, err := encoding.EncodeMessagePackBytes(profileVersion) + if (err != nil) { return nil, err } + + identityPublicKeyEncoded, err := encoding.EncodeMessagePackBytes(identityPublicKey) + if (err != nil) { return nil, err } + + broadcastTime := time.Now().Unix() + + broadcastTimeEncoded, err := encoding.EncodeMessagePackBytes(broadcastTime) + if (err != nil) { return nil, err } + + profileContentMap := map[int]messagepack.RawMessage{ + 51: networkTypeEncoded, + 1: profileVersionEncoded, + 2: identityPublicKeyEncoded, + 3: profileTypeEncoded, + 4: broadcastTimeEncoded, + } + + getProfileIsDisabledBool := func()bool{ + + iAmDisabled, exists := inputProfileMap["Disabled"] + if (exists == true && iAmDisabled == "Yes"){ + return true + } + return false + } + + profileIsDisabled := getProfileIsDisabledBool() + if (profileIsDisabled == true){ + + trueEncoded, err := encoding.EncodeMessagePackBytes(true) + if (err != nil) { return nil, err } + + profileContentMap[5] = trueEncoded + } + + if (profileIsDisabled == false){ + + for attributeName, attributeValue := range inputProfileMap{ + + if (attributeName == ""){ + return nil, errors.New("CreateProfile called with profileMap containing empty key.") + } + if (attributeValue == ""){ + return nil, errors.New("CreateProfile called with profileMap containing empty value.") + } + + if (attributeName == "BroadcastTime" || attributeName == "ProfileVersion"){ + // This function should be called with profile maps which do not contain these attributes. + return nil, errors.New("CreateProfile called with profileMap containing unwanted attribute: " + attributeName) + } + + if (attributeName == "NetworkType" || attributeName == "ProfileType" || attributeName == "Disabled"){ + // These are attributes we already added to the profileContentMap + continue + } + + getAttributeEncodedBytes := func()(messagepack.RawMessage, error){ + + if (attributeName == "NaclKey" || attributeName == "KyberKey"){ + + // We encode the raw bytes of the keys to save space + + keyBytes, err := encoding.DecodeBase64StringToBytes(attributeValue) + if (err != nil){ + return nil, errors.New("CreateProfile called with profileMap containing invalid " + attributeName + ": " + attributeValue) + } + + keyBytesEncoded, err := encoding.EncodeMessagePackBytes(keyBytes) + if (err != nil){ return nil, err } + + return keyBytesEncoded, nil + } + if (attributeName == "Photos"){ + + // We encode the raw bytes a list of []byte + + base64PhotosList := strings.Split(attributeValue, "+") + + rawPhotoBytesList := make([][]byte, 0, len(base64PhotosList)) + + for _, base64Photo := range base64PhotosList{ + + base64Bytes, err := encoding.DecodeBase64StringToBytes(base64Photo) + if (err != nil) { + return nil, errors.New("CreateProfile called with profile containing invalid photos attribute: Item not Base64: " + base64Photo) + } + rawPhotoBytesList = append(rawPhotoBytesList, base64Bytes) + } + + rawPhotoBytesListEncoded, err := encoding.EncodeMessagePackBytes(rawPhotoBytesList) + if (err != nil){ return nil, err } + + return rawPhotoBytesListEncoded, nil + } + + //TODO: Add more attributes + // We can encode all numbers and hex-encoded strings using messagepack to reduce size + + // //TODO: Encode base pairs using a number + // There are 36 total base pair possibilities + // This will reduce the base pair encoded size by 1/3 + // This will be significant once each profile has thousands of base pairs + + // We encode the attribute value as a string + + attributeEncoded, err := encoding.EncodeMessagePackBytes(attributeValue) + if (err != nil){ return nil, err } + + return attributeEncoded, nil + } + + attributeEncoded, err := getAttributeEncodedBytes() + if (err != nil) { return nil, err } + + attributeIdentifier, err := profileFormat.GetAttributeIdentifierFromAttributeName(attributeName) + if (err != nil){ + return nil, errors.New("CreateProfile failed: " + err.Error()) + } + + profileContentMap[attributeIdentifier] = attributeEncoded + } + } + + profileContentMessagepack, err := encoding.EncodeMessagePackBytes(profileContentMap) + if (err != nil) { + return nil, errors.New("CreateProfile failed: " + err.Error()) + } + + contentHash, err := blake3.Get32ByteBlake3Hash(profileContentMessagepack) + if (err != nil) { return nil, err } + + profileSignature := edwardsKeys.CreateSignature(identityPrivateKey, contentHash) + + signatureEncoded, err := encoding.EncodeMessagePackBytes(profileSignature) + if (err != nil) { return nil, err } + + profileSlice := []messagepack.RawMessage{signatureEncoded, profileContentMessagepack} + + profileBytes, err := encoding.EncodeMessagePackBytes(profileSlice) + if (err != nil) { return nil, err } + + profileIsValid, err := readProfiles.VerifyProfile(profileBytes) + if (err != nil){ return nil, err } + if (profileIsValid == false){ + return nil, errors.New("CreateProfile failed: Invalid result profile.") + } + + return profileBytes, nil +} + + + diff --git a/internal/profiles/myLocalProfiles/myLocalProfiles.go b/internal/profiles/myLocalProfiles/myLocalProfiles.go new file mode 100644 index 0000000..ea6a1ed --- /dev/null +++ b/internal/profiles/myLocalProfiles/myLocalProfiles.go @@ -0,0 +1,141 @@ + +// myLocalProfiles provides functions to store and access a user's local Mate, Host, and Moderator profiles. +// A local profile is a map of Attribute Name -> Attribute Value, stored within a myMap datastore. +// Some of these attributes are special, and define how the profile will be created. +// See myProfileExports.go to see how a profile is exported. +// To access broadcasted profiles, use myBroadcasts.go. + +package myLocalProfiles + +import "seekia/internal/myDatastores/myMap" + +import "errors" + +var myMateProfileMapDatastore *myMap.MyMap +var myHostProfileMapDatastore *myMap.MyMap +var myModeratorProfileMapDatastore *myMap.MyMap + +// This function must be called whenever we sign in to an app user +func InitializeMyLocalProfileDatastores()error{ + + newMateProfileMapDatastore, err := myMap.CreateNewMap("MyLocalMateProfile") + if (err != nil) { return err } + + newHostProfileMapDatastore, err := myMap.CreateNewMap("MyLocalHostProfile") + if (err != nil) { return err } + + newModeratorProfileMapDatastore, err := myMap.CreateNewMap("MyLocalModeratorProfile") + if (err != nil) { return err } + + myMateProfileMapDatastore = newMateProfileMapDatastore + myHostProfileMapDatastore = newHostProfileMapDatastore + myModeratorProfileMapDatastore = newModeratorProfileMapDatastore + + return nil +} + +func SetProfileData(profileType string, attributeName string, content string)error{ + + if (attributeName == ""){ + return errors.New("SetProfileData called with empty attribute.") + } + + if (content == ""){ + return errors.New("SetProfileData called with empty content for attribute: " + attributeName) + } + + if (profileType == "Mate"){ + err := myMateProfileMapDatastore.SetMapEntry(attributeName, content) + if (err != nil) { return err } + + return nil + } + if (profileType == "Host"){ + err := myHostProfileMapDatastore.SetMapEntry(attributeName, content) + if (err != nil) { return err } + + return nil + } + if (profileType == "Moderator"){ + err := myModeratorProfileMapDatastore.SetMapEntry(attributeName, content) + if (err != nil) { return err } + + return nil + } + + return errors.New("SetProfileData called with invalid profile type: " + profileType) +} + +//Outputs: +// -bool: Attribute exists +// -string: Profile attribute data +// -error +func GetProfileData(profileType string, attributeName string)(bool, string, error){ + + if (attributeName == ""){ + return false, "", errors.New("GetProfileData called with empty attribute.") + } + + if (profileType == "Mate"){ + exists, value, err := myMateProfileMapDatastore.GetMapEntry(attributeName) + if (err != nil) { return false, "", err } + if (exists == false){ + return false, "", nil + } + + return true, value, nil + } + if (profileType == "Host"){ + exists, value, err := myHostProfileMapDatastore.GetMapEntry(attributeName) + if (err != nil) { return false, "", err } + if (exists == false){ + return false, "", nil + } + + return true, value, nil + } + if (profileType == "Moderator"){ + exists, value, err := myModeratorProfileMapDatastore.GetMapEntry(attributeName) + if (err != nil) { return false, "", err } + if (exists == false){ + return false, "", nil + } + + return true, value, nil + } + + return false, "", errors.New("GetProfileData called with invalid profile type: " + profileType) +} + + +func DeleteProfileData(profileType string, attributeName string)error{ + + if (attributeName == ""){ + return errors.New("DeleteProfileData called with empty attributeName.") + } + + if (profileType == "Mate"){ + err := myMateProfileMapDatastore.DeleteMapEntry(attributeName) + if (err != nil) { return err } + + return nil + } + if (profileType == "Host"){ + err := myHostProfileMapDatastore.DeleteMapEntry(attributeName) + if (err != nil) { return err } + + return nil + } + if (profileType == "Moderator"){ + err := myModeratorProfileMapDatastore.DeleteMapEntry(attributeName) + if (err != nil) { return err } + + return nil + } + + return errors.New("DeleteProfileData called with invalid profile type: " + profileType) +} + + + + diff --git a/internal/profiles/myProfileExports/myProfileExports.go b/internal/profiles/myProfileExports/myProfileExports.go new file mode 100644 index 0000000..5a3aed3 --- /dev/null +++ b/internal/profiles/myProfileExports/myProfileExports.go @@ -0,0 +1,431 @@ + +// myProfileExports provides functions to manage a user's exported profiles +// Exported profiles exist so users can view their profiles before broadcasting them +// They are stored in their own folder, waiting to be broadcasted + +package myProfileExports + +import "seekia/resources/geneticReferences/monogenicDiseases" +import "seekia/resources/geneticReferences/polygenicDiseases" +import "seekia/resources/geneticReferences/traits" + +import "seekia/internal/genetics/readGeneticAnalysis" + +import "seekia/internal/encoding" +import "seekia/internal/genetics/myChosenAnalysis" +import "seekia/internal/helpers" +import "seekia/internal/localFilesystem" +import "seekia/internal/messaging/myChatKeys" +import "seekia/internal/myDevice" +import "seekia/internal/myIdentity" +import "seekia/internal/profiles/createProfiles" +import "seekia/internal/profiles/myLocalProfiles" +import "seekia/internal/profiles/profileFormat" +import "seekia/internal/profiles/readProfiles" + +import messagepack "github.com/vmihailenco/msgpack/v5" + +import "strings" +import "time" +import "errors" +import "path/filepath" +import "sync" +import "slices" + +// We lock this when we are writing any exported profile files +var writingExportedProfileFilesMutex sync.Mutex + +func UpdateMyExportedProfile(myProfileType string, networkType byte)error{ + + if (myProfileType != "Mate" && myProfileType != "Host" && myProfileType != "Moderator"){ + return errors.New("UpdateMyExportedProfile called with invalid myProfileType: " + myProfileType) + } + + isValid := helpers.VerifyNetworkType(networkType) + if (isValid == false){ + networkTypeString := helpers.ConvertByteToString(networkType) + return errors.New("UpdateMyExportedProfile called with invalid networkType: " + networkTypeString) + } + + networkTypeString := helpers.ConvertByteToString(networkType) + + myIdentityFound, myIdentityHash, err := myIdentity.GetMyIdentityHash(myProfileType) + if (err != nil) { return err } + if (myIdentityFound == false) { + return errors.New("UpdateMyExportedProfile called when my identity is missing.") + } + + getNewProfileMap := func()(map[string]string, error){ + + profileMap := map[string]string{ + "ProfileType": myProfileType, + "NetworkType": networkTypeString, + } + + attributeExists, profileIsDisabled, err := myLocalProfiles.GetProfileData(myProfileType, "Disabled") + if (err != nil) { return nil, err } + if (attributeExists == true && profileIsDisabled == "Yes"){ + + profileMap["Disabled"] = "Yes" + return profileMap, nil + } + + // This is a map of all possible profile attributes + // This map contains some attributes that are calculated, and thus will not be found when querying the user profile + // An example is a user's genetic analysis, which is added in its own function + // Map Structure: Attribute name -> Attribute object + profileAttributeObjectsList, err := profileFormat.GetProfileAttributeObjectsList() + if (err != nil) { return nil, err } + + for _, attributeObject := range profileAttributeObjectsList{ + + attributeName := attributeObject.AttributeName + + if (attributeName == "ProfileType" || attributeName == "BroadcastTime" || attributeName == "IdentityKey" || attributeName == "ProfileVersion" || attributeName == "Disabled"){ + continue + } + + profileVersion := 1 + + attributeProfileTypesList, err := attributeObject.GetProfileTypes(profileVersion) + if (err != nil) { return nil, err } + + attributeIsRelevant := slices.Contains(attributeProfileTypesList, myProfileType) + if (attributeIsRelevant == false){ + continue + } + + attributeValueExists, attributeValue, err := myLocalProfiles.GetProfileData(myProfileType, attributeName) + if (err != nil){ return nil, err } + if (attributeValueExists == false){ + continue + } + + if (attributeName == "23andMe_AncestryComposition"){ + + // The user has the option of hiding this attribute from their profile + // The user can then view offspring ancestry compositions without sharing that information on their profile + // This is the only attribute (other than genetic analysis attributes) where hiding the attribute is possible + + exists, visibilityStatus, err := myLocalProfiles.GetProfileData("Mate", "VisibilityStatus_23andMe_AncestryComposition") + if (err != nil) { return nil, err } + if (exists == true && visibilityStatus == "No"){ + // The user has opted to not share this attribute + continue + } + } + + if (attributeName == "PrimaryLocationLatitude" || attributeName == "PrimaryLocationLongitude" || attributeName == "PrimaryLocationCountry" || attributeName == "SecondaryLocationLatitude" || attributeName == "SecondaryLocationLongitude" || attributeName == "SecondaryLocationCountry"){ + + exists, visibilityStatus, err := myLocalProfiles.GetProfileData("Mate", "VisibilityStatus_Location") + if (err != nil){ return nil, err } + if (exists == true && visibilityStatus == "No"){ + // The user has opted to not share their location + continue + } + } + + profileMap[attributeName] = attributeValue + } + + addGeneticAnalysisToProfileMap := func()error{ + + if (myProfileType != "Mate"){ + // Only mate profiles can contain a genetic analysis + return nil + } + + myGenomePersonChosen, anyGenomesExist, personAnalysisIsReady, myGeneticAnalysisMapList, genomeIdentifierToShare, multipleGenomesExist, err := myChosenAnalysis.GetMyChosenMateGeneticAnalysis() + if (err != nil) { return err } + if (myGenomePersonChosen == false){ + // User has not linked a genome person. + // No genetic analysis needs to be added. + return nil + } + if (anyGenomesExist == false){ + // The profile has a genome person, but the genome person has no genomes + return nil + } + if (personAnalysisIsReady == false){ + return errors.New("UpdateMyExportedProfile called when profile genetic analysis is not ready.") + } + + monogenicDiseaseNamesList, err := monogenicDiseases.GetMonogenicDiseaseNamesList() + if (err != nil) { return err } + + for _, monogenicDiseaseName := range monogenicDiseaseNamesList{ + + shareDiseaseInfoAttributeName := "ShareMonogenicDiseaseInfo_" + monogenicDiseaseName + + currentShareDiseaseInfoAttributeExists, currentShareDiseaseInfoAttribute, err := myLocalProfiles.GetProfileData("Mate", shareDiseaseInfoAttributeName) + if (err != nil) { return err } + if (currentShareDiseaseInfoAttributeExists == false){ + continue + } + if (currentShareDiseaseInfoAttribute != "Yes"){ + continue + } + + probabilitiesKnown, _, _, probabilityOfPassingADiseaseVariant, _, numberOfVariantsTested, _, err := readGeneticAnalysis.GetPersonMonogenicDiseaseInfoFromGeneticAnalysis(myGeneticAnalysisMapList, monogenicDiseaseName, genomeIdentifierToShare, multipleGenomesExist) + if (err != nil) { return err } + if (probabilitiesKnown == false){ + continue + } + + diseaseNameWithUnderscores := strings.ReplaceAll(monogenicDiseaseName, " ", "_") + + probabilityOfPassingVariantAttributeName := "MonogenicDisease_" + diseaseNameWithUnderscores + "_ProbabilityOfPassingAVariant" + + probabilityOfPassingADiseaseVariantString := helpers.ConvertIntToString(probabilityOfPassingADiseaseVariant) + + variantsTestedAttributeName := "MonogenicDisease_" + diseaseNameWithUnderscores + "_NumberOfVariantsTested" + + numberOfVariantsTestedString := helpers.ConvertIntToString(numberOfVariantsTested) + + profileMap[probabilityOfPassingVariantAttributeName] = probabilityOfPassingADiseaseVariantString + + profileMap[variantsTestedAttributeName] = numberOfVariantsTestedString + } + + polygenicDiseaseObjectsList, err := polygenicDiseases.GetPolygenicDiseaseObjectsList() + if (err != nil) { return err } + + for _, diseaseObject := range polygenicDiseaseObjectsList{ + + diseaseName := diseaseObject.DiseaseName + + shareDiseaseInfoAttributeName := "SharePolygenicDiseaseInfo_" + diseaseName + + currentShareDiseaseInfoAttributeExists, currentShareDiseaseInfoAttribute, err := myLocalProfiles.GetProfileData("Mate", shareDiseaseInfoAttributeName) + if (err != nil) { return err } + if (currentShareDiseaseInfoAttributeExists == false){ + continue + } + if (currentShareDiseaseInfoAttribute != "Yes"){ + continue + } + + diseaseLociList := diseaseObject.LociList + + for _, locusObject := range diseaseLociList{ + + locusIdentifier := locusObject.LocusIdentifier + + locusValueKnown, _, locusBasePair, _, _, _, err := readGeneticAnalysis.GetPersonPolygenicDiseaseLocusInfoFromGeneticAnalysis(myGeneticAnalysisMapList, diseaseName, locusIdentifier, genomeIdentifierToShare) + if (err != nil) { return err } + if (locusValueKnown == false){ + continue + } + + locusRSID := locusObject.LocusRSID + + locusRSIDString := helpers.ConvertInt64ToString(locusRSID) + + profileMap["LocusValue_rs" + locusRSIDString] = locusBasePair + } + } + + traitObjectsList, err := traits.GetTraitObjectsList() + if (err != nil) { return err } + + for _, traitObject := range traitObjectsList{ + + traitName := traitObject.TraitName + + shareTraitInfoAttributeName := "ShareTraitInfo_" + traitName + + currentShareTraitInfoAttributeExists, currentShareTraitInfoAttribute, err := myLocalProfiles.GetProfileData("Mate", shareTraitInfoAttributeName) + if (err != nil) { return err } + if (currentShareTraitInfoAttributeExists == false){ + continue + } + if (currentShareTraitInfoAttribute != "Yes"){ + continue + } + + myTraitLocusValuesMap, _, _, _, _, err := readGeneticAnalysis.GetPersonTraitInfoFromGeneticAnalysis(myGeneticAnalysisMapList, traitName, genomeIdentifierToShare, multipleGenomesExist) + if (err != nil) { return err } + + for rsID, locusValueObject := range myTraitLocusValuesMap{ + + rsIDString := helpers.ConvertInt64ToString(rsID) + + locusBase1 := locusValueObject.Base1 + locusBase2 := locusValueObject.Base2 + + basePairValue := locusBase1 + ";" + locusBase2 + + profileMap["LocusValue_rs" + rsIDString] = basePairValue + } + } + + return nil + } + + err = addGeneticAnalysisToProfileMap() + if (err != nil) { return nil, err } + + if (myProfileType != "Host"){ + + // We add the chat keys to the Mate/Moderator profile + // Host profiles do not have chat keys + + identityExists, myNaclKey, myKyberKey, err := myChatKeys.GetMyNewestPublicChatKeys(myIdentityHash, networkType) + if (err != nil) { return nil, err } + if (identityExists == false){ + return nil, errors.New("My identity not found after being found already.") + } + + myIdentityFound, myDeviceIdentifier, err := myDevice.GetMyDeviceIdentifier(myIdentityHash, networkType) + if (err != nil) { return nil, err } + if (myIdentityFound == false){ + return nil, errors.New("My identity not found after being found already.") + } + + myDeviceIdentifierHex := encoding.EncodeBytesToHexString(myDeviceIdentifier[:]) + + myNaclKeyString := encoding.EncodeBytesToBase64String(myNaclKey[:]) + myKyberKeyString := encoding.EncodeBytesToBase64String(myKyberKey[:]) + + profileMap["DeviceIdentifier"] = myDeviceIdentifierHex + profileMap["NaclKey"] = myNaclKeyString + profileMap["KyberKey"] = myKyberKeyString + + // We get the chatKeysLatestUpdateTime, assuming that this profile will be broadcast + // We will only update the chatKeysLatestUpdateTime we have saved locally if the profile is actually broadcast + getChatKeysLatestUpdateTime := func()(int64, error){ + + myIdentityExists, anyKeysExist, existingNaclKey, existingKyberKey, err := myChatKeys.GetMyNewestBroadcastPublicChatKeys(myIdentityHash, networkType) + if (err != nil){ return 0, err } + if (myIdentityExists == false){ + return 0, errors.New("My identity not found after being found already.") + } + if (anyKeysExist == true){ + if (existingNaclKey != myNaclKey || existingKyberKey != myKyberKey){ + // This profile contains new keys. + // Our latestChatKeysUpdateTime is now the creation time of the profile we are currently creating + // We will get the time now, which may be a second older than the broadcastTime of the profile, which doesn't matter + currentTime := time.Now().Unix() + return currentTime, nil + } + } + updateTimeExists, existingUpdateTime, err := myChatKeys.GetMyChatKeysLatestUpdateTime(myIdentityHash, networkType) + if (err != nil) { return 0, err } + if (updateTimeExists == false){ + // We have not broadcast a profile yet. + // Our current exported profile will contain the newest chat keys + currentTime := time.Now().Unix() + return currentTime, nil + } + // We have broadcasted a profile before, and the current exported profile is not broadcasting any novel chat keys + return existingUpdateTime, nil + } + + chatKeysLatestUpdateTime, err := getChatKeysLatestUpdateTime() + if (err != nil) { return nil, err } + + chatKeysLatestUpdateTimeString := helpers.ConvertInt64ToString(chatKeysLatestUpdateTime) + + profileMap["ChatKeysLatestUpdateTime"] = chatKeysLatestUpdateTimeString + } + + return profileMap, nil + } + + profileMap, err := getNewProfileMap() + if (err != nil){ return err } + + myIdentityExists, myIdentityPublicKey, myIdentityPrivateKey, err := myIdentity.GetMyPublicPrivateIdentityKeys(myProfileType) + if (err != nil) { return err } + if (myIdentityExists == false){ + return errors.New("UpdateMyExportedProfile called when my identity is missing.") + } + + profileBytes, err := createProfiles.CreateProfile(myIdentityPublicKey, myIdentityPrivateKey, profileMap) + if (err != nil) { return err } + + userDirectory, err := localFilesystem.GetAppUserFolderPath() + if (err != nil ) { return err } + + exportedProfilesDirectory := filepath.Join(userDirectory, "MyExportedProfiles") + + networkTypeExportedProfilesDirectory := filepath.Join(exportedProfilesDirectory, "Network" + networkTypeString) + + writingExportedProfileFilesMutex.Lock() + defer writingExportedProfileFilesMutex.Unlock() + + _, err = localFilesystem.CreateFolder(exportedProfilesDirectory) + if (err != nil) {return err } + + _, err = localFilesystem.CreateFolder(networkTypeExportedProfilesDirectory) + if (err != nil) {return err } + + profileFileName := myProfileType + "Profile.messagepack" + + err = localFilesystem.CreateOrOverwriteFile(profileBytes, networkTypeExportedProfilesDirectory, profileFileName) + if (err != nil) { return err } + + return nil +} + + +//Outputs: +// -bool: Profile found +// -[28]byte: Profile hash +// -[]byte: Profile bytes +// -map[int]messagepack.RawMessage: Raw profile map +// -error +func GetMyExportedProfile(myProfileType string, networkType byte)(bool, [28]byte, []byte, map[int]messagepack.RawMessage, error){ + + if (myProfileType != "Mate" && myProfileType != "Host" && myProfileType != "Moderator"){ + return false, [28]byte{}, nil, nil, errors.New("GetMyExportedProfile called with invalid profileType: " + myProfileType) + } + + isValid := helpers.VerifyNetworkType(networkType) + if (isValid == false){ + networkTypeString := helpers.ConvertByteToString(networkType) + return false, [28]byte{}, nil, nil, errors.New("GetMyExportedProfile called with invalid networkType: " + networkTypeString) + } + + myIdentityFound, myIdentityHash, err := myIdentity.GetMyIdentityHash(myProfileType) + if (err != nil) { return false, [28]byte{}, nil, nil, err } + if (myIdentityFound == false) { + return false, [28]byte{}, nil, nil, errors.New("GetMyExportedProfile called when my identity is missing.") + } + + userDirectory, err := localFilesystem.GetAppUserFolderPath() + if (err != nil ) { return false, [28]byte{}, nil, nil, err } + + networkTypeString := helpers.ConvertByteToString(networkType) + + profileFilePath := filepath.Join(userDirectory, "MyExportedProfiles", "Network" + networkTypeString, myProfileType + "Profile.messagepack") + + exists, profileBytes, err := localFilesystem.GetFileContents(profileFilePath) + if (err != nil) { return false, [28]byte{}, nil, nil, err } + if (exists == false) { + return false, [28]byte{}, nil, nil, nil + } + + ableToRead, profileHash, _, profileNetworkType, profileAuthor, _, _, rawProfileMap, err := readProfiles.ReadProfileAndHash(true, profileBytes) + if (err != nil) { return false, [28]byte{}, nil, nil, err } + if (ableToRead == false){ + return false, [28]byte{}, nil, nil, errors.New("MyExportedProfiles folder contains invalid profile.") + } + if (profileNetworkType != networkType){ + return false, [28]byte{}, nil, nil, errors.New("MyExportedProfiles folder contains profile for different networkType.") + } + + if (profileAuthor != myIdentityHash){ + // This profile must be authored by an old identity. + // We will not return error, instead just return profile missing + // Once we update the exported profile again, this old profile will be overwritten + return false, [28]byte{}, nil, nil, nil + } + + return true, profileHash, profileBytes, rawProfileMap, nil +} + + + + + diff --git a/internal/profiles/myProfileStatus/myProfileStatus.go b/internal/profiles/myProfileStatus/myProfileStatus.go new file mode 100644 index 0000000..9b31fe0 --- /dev/null +++ b/internal/profiles/myProfileStatus/myProfileStatus.go @@ -0,0 +1,181 @@ + +// myProfileStatus provides functions to check if a user's profile is active on the network + +package myProfileStatus + +// For a profile to be active, multiple conditions must be met: +// 1. User's identity is funded +// 2. User's broadcast profile exists (the user has uploaded their profile to the network) +// 3. User's broadcast profile is funded (if profileType == "Mate") +// 4. User's broadcast profile is not disabled +// A disabled profile will exist on the network until the identity's balance expires, or the maximum profile existence time has passed +// 5. User's broadcast profile Disabled status matches their local profile. + +import "seekia/internal/helpers" +import "seekia/internal/moderation/myIdentityScore" +import "seekia/internal/myIdentity" +import "seekia/internal/network/myBroadcasts" +import "seekia/internal/network/myFundedStatus" +import "seekia/internal/network/myIdentityBalance" +import "seekia/internal/parameters/getParameters" +import "seekia/internal/profiles/myLocalProfiles" + +import "time" +import "errors" + +//Outputs: +// -bool: My identity exists +// -bool: My profile is active +// -error +func GetMyProfileIsActiveStatus(myIdentityHash [16]byte, networkType byte)(bool, bool, error){ + + isValid := helpers.VerifyNetworkType(networkType) + if (isValid == false){ + networkTypeString := helpers.ConvertByteToString(networkType) + return false, false, errors.New("GetMyProfileIsActiveStatus called with invalid networkType: " + networkTypeString) + } + + isMine, myIdentityType, err := myIdentity.CheckIfIdentityHashIsMine(myIdentityHash) + if (err != nil) { return false, false, err } + if (isMine == false){ + return false, false, nil + } + + identityExists, profileExists, broadcastProfileHash, getAnyAttributeFromMyBroadcastProfile, err := myBroadcasts.GetRetrieveAnyAttributeFromMyBroadcastProfileFunction(myIdentityHash, networkType) + if (err != nil) { return false, false, err } + if (identityExists == false) { + return false, false, errors.New("My identity not found after being found already.") + } + if (profileExists == false){ + // There is no broadcast profile. + // Profile has never been broadcasted from this device. + // Profile is not active. + return true, false, nil + } + + attributeExists, _, broadcastProfileIsDisabled, err := getAnyAttributeFromMyBroadcastProfile("Disabled") + if (err != nil) { return false, false, err } + if (attributeExists == true && broadcastProfileIsDisabled == "Yes"){ + // Current broadcast profile is disabled. + // Profile is not active. + + return true, false, nil + } + + // We check if the local profile is disabled. + + exists, iAmDisabled, err := myLocalProfiles.GetProfileData(myIdentityType, "Disabled") + if (err != nil) { return false, false, err } + if (exists == true && iAmDisabled == "Yes"){ + // The current broadcast profile is not disabled, but the local profile is. + // This should only occur if the profile was disabled on a different network, + // funding the new profile failed, or is currently being attempted. + // We check for this because we don't want to allow the sending of messages when this occurs + + // TODO: Perform the automatic updating and broadcasting of disabled profiles when the user + // disables their profile and switches to a different networkType + + return true, false, nil + } + + // Now we check if profile is inactive due to lack of identity balance funds, or expiration + // Moderator profiles never expire + + if (myIdentityType == "Mate" || myIdentityType == "Host"){ + + attributeExists, _, broadcastTimeString, err := getAnyAttributeFromMyBroadcastProfile("BroadcastTime") + if (err != nil) { return false, false, err } + if (attributeExists == false) { + return false, false, errors.New("Malformed broadcast profile: Missing BroadcastTime.") + } + + broadcastTimeInt64, err := helpers.ConvertBroadcastTimeStringToInt64(broadcastTimeString) + if (err != nil) { + return false, false, errors.New("Malformed broadcast profile: Contains invalid BroadcastTime: " + broadcastTimeString) + } + + identityExists, myIdentityIsActivated, myBalanceIsSufficient, balanceIsSufficientStartTime, _, err := myIdentityBalance.GetMyIdentityBalanceStatus(myIdentityHash, networkType) + if (err != nil){ return false, false, err } + if (identityExists == false) { + return false, false, errors.New("My identity not found after being found already.") + } + if (myIdentityIsActivated == false || myBalanceIsSufficient == false){ + return true, false, nil + } + if (balanceIsSufficientStartTime > broadcastTimeInt64){ + // Our balance is sufficient now, but wasn't at the time of the broadcast of our current broadcast profile + // This should not happen... unless we got invalid identity deposit data from hosts either now or earlier. + // We say profile is not active + return true, false, nil + } + + if (myIdentityType == "Mate" || myIdentityType == "Host"){ + + // We check to see if profile has expired from the network + // Moderator profiles never expire + + getMaximumExistenceDuration := func()(int64, error){ + + if (myIdentityType == "Mate"){ + + _, maximumExistenceTime, err := getParameters.GetMateProfileMaximumExistenceDuration(networkType) + if (err != nil){ return 0, err } + + return maximumExistenceTime, nil + } + + _, maximumExistenceTime, err := getParameters.GetHostProfileMaximumExistenceDuration(networkType) + if (err != nil) { return 0, err } + + return maximumExistenceTime, nil + } + + maximumExistenceDuration, err := getMaximumExistenceDuration() + if (err != nil){ return false, false, err } + + currentTime := time.Now().Unix() + + timeElapsed := currentTime - broadcastTimeInt64 + + if (maximumExistenceDuration <= timeElapsed){ + // Profile has expired from network + return true, false, nil + } + } + } + + if (myIdentityType == "Moderator"){ + + // We check if identity score is sufficient + + identityExists, _, scoreIsSufficient, _, _, err := myIdentityScore.GetMyIdentityScore() + if (err != nil) { return false, false, err } + if (identityExists == false) { + return false, false, errors.New("My identity not found after being found.") + } + + if (scoreIsSufficient == false){ + return true, false, nil + } + } + + if (myIdentityType == "Mate"){ + + // We make sure profile is funded + + statusIsKnown, profileIsFunded, _, err := myFundedStatus.CheckIfMyMateProfileIsFunded(broadcastProfileHash) + if (err != nil) { return false, false, err } + if (statusIsKnown == false || profileIsFunded == false){ + return true, false, nil + } + } + + // Profile should be active on the network! + + return true, true, nil +} + + + + + diff --git a/internal/profiles/profileFormat/profileFormat.go b/internal/profiles/profileFormat/profileFormat.go new file mode 100644 index 0000000..7d7215c --- /dev/null +++ b/internal/profiles/profileFormat/profileFormat.go @@ -0,0 +1,2545 @@ + +// profileFormat provides a specification of user profile format and syntax +// It is used by readProfiles to verify profiles + +package profileFormat + +//TODO: Add Host and Moderator attributes + +//TODO: Change all attribute identifiers so they are in ascending order +// The order was tarnished after I added and removed some attributes + +import "seekia/resources/currencies" +import "seekia/resources/imageFiles" +import "seekia/resources/worldLanguages" +import "seekia/resources/worldLocations" + +import "seekia/internal/allowedText" +import "seekia/internal/cryptography/kyber" +import "seekia/internal/cryptography/nacl" +import "seekia/internal/encoding" +import "seekia/internal/genetics/companyAnalysis" +import "seekia/internal/helpers" +import "seekia/internal/identity" +import "seekia/internal/imagery" +import "seekia/internal/mateQuestionnaire" + +import "slices" +import "strings" +import "errors" + +func VerifyProfileType(profileType string)bool{ + + if (profileType == "Mate" || profileType == "Host" || profileType == "Moderator"){ + return true + } + + return false +} + +func GetSupportedVersionsList()[]int{ + + supportedVersionsList := []int{1} + + return supportedVersionsList +} + +type AttributeObject struct{ + + // A list of the profile versions that this attribute is allowed for + ProfileVersions []int + + // This is an integer that is used as the key for the entry in the raw profile + // We do this to save space. 16 (1 byte encoded MessagePack) is smaller than "SecondaryLocationCountry" (24 bytes). + // It will always be the same for all profile versions + // It must be a number between 0 and 4294967295 + AttributeIdentifier int + + // The name will always be the same across all profile versions + // We can change what is stored in the attribute for a new profile version, so we never need to worry about "using up" a name + AttributeName string + + // Is true if this attribute is required for all profileTypes returned by GetProfileTypes + // Example: "BroadcastTime" and "ProfileType" are required attributes for all profiles + // Input: Profile Version + // Output: Attribute is required, error + GetIsRequired func(int)(bool, error) + + // List of attributes that must come accompanied with this attribute + // An example is PrimaryLocationCountry, which must be accompanied by PrimaryLocationLatitude and PrimaryLocationLongitude + // If isRequired == true, this will be an empty list. All of the IsRequired==true attributes are part of the same mandatory group + // Input: Profile Version + // Output: Attribute mandatory attributes, error + GetMandatoryAttributes func(int)([]string, error) + + // List of profileTypes that the attribute is allowed to exist for + // Example: "Height" is only permitted for Mate profiles + // Input: Profile Version + // Output: Attribute profile types, error + GetProfileTypes func(int)([]string, error) + + // Returns "Always"/"Sometimes"/"Never" + // Example: "Age" is always canonical (a number), "GenderIdentity" is sometimes canonical, "Description" is never canonical. + // Input: Profile Version + // Output: Attribute Is Canonical, error + GetIsCanonical func(int)(string, error) + + // This function is used to check if profile values are valid + // Input: Profile Version, ProfileType, Attribute Name + // Output: IsValid, IsCanonical, error + CheckValueFunction func(int, string, string)(bool, bool, error) +} + +// This must be run once upon application startup +func InitializeProfileFormatVariables()error{ + + initializeProfileAttributeObjectsList() + + profileAttributeObjectsList, err := GetProfileAttributeObjectsList() + if (err != nil) { return err } + + // Map Structure: Attribute name -> Attribute Object + profileAttributeObjectsByIdentifierMap = make(map[int]AttributeObject) + + // Map Structure: Attribute name -> Attribute Object + profileAttributeObjectsByNameMap = make(map[string]AttributeObject) + + // Map Structure: Attribute Identifier -> Attribute Name + profileAttributeNamesByIdentifierMap = make(map[int]string) + + // Map Structure: Attribute Name -> Attribute Identifier + profileAttributeIdentifiersByNameMap = make(map[string]int) + + for _, attributeObject := range profileAttributeObjectsList{ + + attributeIdentifier := attributeObject.AttributeIdentifier + attributeName := attributeObject.AttributeName + + profileAttributeObjectsByIdentifierMap[attributeIdentifier] = attributeObject + profileAttributeObjectsByNameMap[attributeName] = attributeObject + + profileAttributeNamesByIdentifierMap[attributeIdentifier] = attributeName + profileAttributeIdentifiersByNameMap[attributeName] = attributeIdentifier + } + + return nil +} + +func GetAttributeNameFromAttributeIdentifier(attributeIdentifier int)(string, error){ + + if (profileAttributeNamesByIdentifierMap == nil){ + return "", errors.New("GetAttributeNameFromAttributeIdentifier called when map is not initialized.") + } + + attributeName, exists := profileAttributeNamesByIdentifierMap[attributeIdentifier] + if (exists == false){ + attributeIdentifierString := helpers.ConvertIntToString(attributeIdentifier) + return "", errors.New("GetAttributeNameFromAttributeIdentifier called with unknown attributeIdentifier: " + attributeIdentifierString) + } + + return attributeName, nil +} + +func GetAttributeIdentifierFromAttributeName(attributeName string)(int, error){ + + if (profileAttributeIdentifiersByNameMap == nil){ + return 0, errors.New("GetAttributeIdentifierFromAttributeName called when map is not initialized.") + } + + attributeIdentifier, exists := profileAttributeIdentifiersByNameMap[attributeName] + if (exists == false){ + return 0, errors.New("GetAttributeIdentifierFromAttributeName called with unknown attributeName: " + attributeName) + } + + return attributeIdentifier, nil +} + +func GetAttributeObjectFromAttributeIdentifier(attributeIdentifier int)(AttributeObject, error){ + + profileAttributeObjectsMap, err := GetProfileAttributeObjectsByIdentifierMap() + if (err != nil) { return AttributeObject{}, err } + + attributeObject, exists := profileAttributeObjectsMap[attributeIdentifier] + if (exists == false){ + attributeIdentifierString := helpers.ConvertIntToString(attributeIdentifier) + return AttributeObject{}, errors.New("GetAttributeObjectFromAttributeIdentifier called with unknown attribute identifier: " + attributeIdentifierString) + } + + return attributeObject, nil +} + + + +// We read data into these variables to make retrieval faster + +// Map Structure: Attribute Identifier -> Attribute Name +var profileAttributeNamesByIdentifierMap map[int]string + +// Map Structure: Attribute Name -> Attribute Identifier +var profileAttributeIdentifiersByNameMap map[string]int + +// Map Structure: Attribute Identifier -> Attribute Object +var profileAttributeObjectsByIdentifierMap map[int]AttributeObject + +// Map Structure: Attribute Name -> Attribute Object +var profileAttributeObjectsByNameMap map[string]AttributeObject + +var profileAttributeObjectsList []AttributeObject + + + +// Outputs: +// -map[int]AttributeObject: Attribute Identifier -> Attribute Object +func GetProfileAttributeObjectsByIdentifierMap()(map[int]AttributeObject, error){ + + if (profileAttributeObjectsByIdentifierMap == nil){ + return nil, errors.New("GetProfileAttributeObjectsByIdentifierMap called when map is not initialized.") + } + + return profileAttributeObjectsByIdentifierMap, nil +} + +// Outputs: +// -map[string]AttributeObject: Attribute name -> Attribute Object +func GetProfileAttributeObjectsByNameMap()(map[string]AttributeObject, error){ + + if (profileAttributeObjectsByNameMap == nil){ + return nil, errors.New("GetProfileAttributeObjectsByNameMap called when map is not initialized.") + } + + return profileAttributeObjectsByNameMap, nil +} + +func GetProfileAttributeObjectsList()([]AttributeObject, error){ + + if (profileAttributeObjectsList == nil){ + return nil, errors.New("GetProfileAttributeObjectsList called when list is not initialized.") + } + + return profileAttributeObjectsList, nil +} + +func initializeProfileAttributeObjectsList(){ + + // Below are some standard getAttribute functions + + getIsRequired_Yes := func(profileVersion int)(bool, error){ + if (profileVersion != 1){ + return false, errors.New("Trying to retrieve isRequired status for unknown profile version.") + } + return true, nil + } + + getIsRequired_No := func(profileVersion int)(bool, error){ + if (profileVersion != 1){ + return false, errors.New("Trying to retrieve isRequired status for unknown profile version.") + } + return false, nil + } + + getMandatoryAttributes_None := func(profileVersion int)([]string, error){ + if (profileVersion != 1){ + return nil, errors.New("Trying to retrieve mandatory attributes for unknown profile version.") + } + emptyList := make([]string, 0) + return emptyList, nil + } + + getProfileTypes_All := func(profileVersion int)([]string, error){ + if (profileVersion != 1){ + return nil, errors.New("Trying to retrieve profileTypes for unknown profile version.") + } + profileTypesList := []string{"Mate", "Host", "Moderator"} + return profileTypesList, nil + } + + getProfileTypes_Mate := func(profileVersion int)([]string, error){ + if (profileVersion != 1){ + return nil, errors.New("Trying to retrieve profileTypes for unknown profile version.") + } + profileTypesList := []string{"Mate"} + return profileTypesList, nil + } + + getProfileTypes_MateOrModerator := func(profileVersion int)([]string, error){ + if (profileVersion != 1){ + return nil, errors.New("Trying to retrieve profileTypes for unknown profile version.") + } + profileTypesList := []string{"Mate", "Moderator"} + return profileTypesList, nil + } + + getIsCanonical_Always := func(profileVersion int)(string, error){ + if (profileVersion != 1){ + return "", errors.New("Trying to retrieve IsCanonical for unknown profile version.") + } + return "Always", nil + } + + getIsCanonical_Sometimes := func(profileVersion int)(string, error){ + if (profileVersion != 1){ + return "", errors.New("Trying to retrieve IsCanonical for unknown profile version.") + } + return "Sometimes", nil + } + + getIsCanonical_Never := func(profileVersion int)(string, error){ + if (profileVersion != 1){ + return "", errors.New("Trying to retrieve IsCanonical for unknown profile version.") + } + return "Never", nil + } + + checkValueFunction_Mate1To10Rating := func(profileVersion int, profileType string, input string)(bool, bool, error){ + + if (profileVersion != 1){ + return false, false, errors.New("Trying to check profile value for unknown profile version.") + } + + if (profileType != "Mate"){ + return false, false, errors.New("Invalid profile type when trying to verify Mate 1to10 rating: " + profileType) + } + + isValid, err := helpers.VerifyStringIsIntWithinRange(input, 1, 10) + if (err != nil) { return false, false, err } + if (isValid == false){ + return false, false, nil + } + + return true, true, nil + } + + checkValueFunction_MateYesOrNo := func(profileVersion int, profileType string, input string)(bool, bool, error){ + + if (profileVersion != 1){ + return false, false, errors.New("Trying to check profile value for unknown profile version.") + } + + if (profileType != "Mate"){ + return false, false, errors.New("Invalid profile type when trying to verify MateYesOrNo: " + profileType) + } + + if (input != "Yes" && input != "No"){ + return false, false, nil + } + + return true, true, nil + } + + + attributeObjectsList := make([]AttributeObject, 0, 322) + + checkValueFunction_ProfileVersion := func(profileVersion int, profileType string, input string)(bool, bool, error){ + + if (profileVersion != 1){ + return false, false, errors.New("Trying to check profile value for unknown profile version.") + } + + if (profileType != "Mate" && profileType != "Host" && profileType != "Moderator"){ + return false, false, errors.New("Invalid profile type when trying to check ProfileVersion: " + profileType) + } + + if (input != "1"){ + return false, false, nil + } + return true, true, nil + } + + attributeObject_ProfileVersion := AttributeObject{ + ProfileVersions: []int{1}, + AttributeIdentifier: 1, + AttributeName: "ProfileVersion", + GetIsRequired: getIsRequired_Yes, + GetMandatoryAttributes: getMandatoryAttributes_None, + GetProfileTypes: getProfileTypes_All, + GetIsCanonical: getIsCanonical_Always, + CheckValueFunction: checkValueFunction_ProfileVersion, + } + attributeObjectsList = append(attributeObjectsList, attributeObject_ProfileVersion) + + checkValueFunction_NetworkType := func(profileVersion int, profileType string, input string)(bool, bool, error){ + + if (profileVersion != 1){ + return false, false, errors.New("Trying to check profile value for unknown profile version.") + } + + if (profileType != "Mate" && profileType != "Host" && profileType != "Moderator"){ + return false, false, errors.New("Invalid profile type when trying to check NetworkType: " + profileType) + } + + if (input != "1" && input != "2"){ + return false, false, nil + } + return true, true, nil + } + + attributeObject_NetworkType := AttributeObject{ + ProfileVersions: []int{1}, + AttributeIdentifier: 51, + AttributeName: "NetworkType", + GetIsRequired: getIsRequired_Yes, + GetMandatoryAttributes: getMandatoryAttributes_None, + GetProfileTypes: getProfileTypes_All, + GetIsCanonical: getIsCanonical_Always, + CheckValueFunction: checkValueFunction_NetworkType, + } + attributeObjectsList = append(attributeObjectsList, attributeObject_NetworkType) + + checkValueFunction_IdentityKey := func(profileVersion int, profileType string, input string)(bool, bool, error){ + + if (profileVersion != 1){ + return false, false, errors.New("Trying to check profile value for unknown profile version.") + } + + if (profileType != "Mate" && profileType != "Host" && profileType != "Moderator"){ + return false, false, errors.New("Invalid profile type when trying to check IdentityKey: " + profileType) + } + + isValid := identity.VerifyIdentityKeyHex(input) + if (isValid == false){ + return false, false, nil + } + + return true, true, nil + } + + attributeObject_IdentityKey := AttributeObject{ + ProfileVersions: []int{1}, + AttributeIdentifier: 2, + AttributeName: "IdentityKey", + GetIsRequired: getIsRequired_Yes, + GetMandatoryAttributes: getMandatoryAttributes_None, + GetProfileTypes: getProfileTypes_All, + GetIsCanonical: getIsCanonical_Always, + CheckValueFunction: checkValueFunction_IdentityKey, + } + attributeObjectsList = append(attributeObjectsList, attributeObject_IdentityKey) + + checkValueFunction_ProfileType := func(profileVersion int, profileType string, input string)(bool, bool, error){ + + if (profileVersion != 1){ + return false, false, errors.New("Trying to check profile value for unknown profile version.") + } + + if (profileType != "Mate" && profileType != "Host" && profileType != "Moderator"){ + return false, false, errors.New("Invalid profile type when trying to check ProfileType: " + profileType) + } + + if (input != profileType){ + return false, false, nil + } + + return true, true, nil + } + + attributeObject_ProfileType := AttributeObject{ + ProfileVersions: []int{1}, + AttributeIdentifier: 3, + AttributeName: "ProfileType", + GetIsRequired: getIsRequired_Yes, + GetMandatoryAttributes: getMandatoryAttributes_None, + GetProfileTypes: getProfileTypes_All, + GetIsCanonical: getIsCanonical_Always, + CheckValueFunction: checkValueFunction_ProfileType, + } + attributeObjectsList = append(attributeObjectsList, attributeObject_ProfileType) + + checkValueFunction_BroadcastTime := func(profileVersion int, profileType string, input string)(bool, bool, error){ + + if (profileVersion != 1){ + return false, false, errors.New("Trying to check profile value for unknown profile version.") + } + + if (profileType != "Mate" && profileType != "Host" && profileType != "Moderator"){ + return false, false, errors.New("Invalid profile type when trying to check BroadcastTime: " + profileType) + } + + _, err := helpers.ConvertBroadcastTimeStringToInt64(input) + if (err != nil){ + return false, false, nil + } + + return true, true, nil + } + + attributeObject_BroadcastTime := AttributeObject{ + ProfileVersions: []int{1}, + AttributeIdentifier: 4, + AttributeName: "BroadcastTime", + GetIsRequired: getIsRequired_Yes, + GetMandatoryAttributes: getMandatoryAttributes_None, + GetProfileTypes: getProfileTypes_All, + GetIsCanonical: getIsCanonical_Always, + CheckValueFunction: checkValueFunction_BroadcastTime, + } + attributeObjectsList = append(attributeObjectsList, attributeObject_BroadcastTime) + + checkValueFunction_Disabled := func(profileVersion int, profileType string, input string)(bool, bool, error){ + + if (profileVersion != 1){ + return false, false, errors.New("Trying to check profile value for unknown profile version.") + } + + if (profileType != "Mate" && profileType != "Host" && profileType != "Moderator"){ + return false, false, errors.New("Invalid profile type when trying to check attribute value: " + profileType) + } + + if (input != "Yes"){ + return false, false, nil + } + + return true, true, nil + } + + attributeObject_Disabled := AttributeObject{ + ProfileVersions: []int{1}, + AttributeIdentifier: 5, + AttributeName: "Disabled", + GetIsRequired: getIsRequired_No, + GetMandatoryAttributes: getMandatoryAttributes_None, + GetProfileTypes: getProfileTypes_All, + GetIsCanonical: getIsCanonical_Always, + CheckValueFunction: checkValueFunction_Disabled, + } + attributeObjectsList = append(attributeObjectsList, attributeObject_Disabled) + + checkValueFunction_ProfileLanguage := func(profileVersion int, profileType string, input string)(bool, bool, error){ + + if (profileVersion != 1){ + return false, false, errors.New("Trying to check profile value for unknown profile version.") + } + + if (profileType != "Mate" && profileType != "Host" && profileType != "Moderator"){ + return false, false, errors.New("Invalid profile type when trying to check ProfileLanguage: " + profileType) + } + + languageIdentifier, err := helpers.ConvertStringToInt(input) + if (err != nil){ + return false, false, nil + } + + isValid := worldLanguages.VerifyLanguageIdentifier(languageIdentifier) + if (isValid == false){ + return false, false, nil + } + + return true, true, nil + } + + attributeObject_ProfileLanguage := AttributeObject{ + ProfileVersions: []int{1}, + AttributeIdentifier: 107, + AttributeName: "ProfileLanguage", + GetIsRequired: getIsRequired_No, + GetMandatoryAttributes: getMandatoryAttributes_None, + GetProfileTypes: getProfileTypes_All, + GetIsCanonical: getIsCanonical_Always, + CheckValueFunction: checkValueFunction_ProfileLanguage, + } + attributeObjectsList = append(attributeObjectsList, attributeObject_ProfileLanguage) + + checkValueFunction_Height := func(profileVersion int, profileType string, input string)(bool, bool, error){ + + if (profileVersion != 1){ + return false, false, errors.New("Trying to check profile value for unknown profile version.") + } + + if (profileType != "Mate"){ + return false, false, errors.New("Trying to check if Height is valid for non-mate profileType: " + profileType) + } + + isValid, err := helpers.VerifyStringIsFloatWithinRange(input, 30, 400) + if (err != nil) { return false, false, err } + if (isValid == true){ + return true, true, nil + } + + return false, false, nil + } + + attributeObject_Height := AttributeObject{ + ProfileVersions: []int{1}, + AttributeIdentifier: 6, + AttributeName: "Height", + GetIsRequired: getIsRequired_No, + GetMandatoryAttributes: getMandatoryAttributes_None, + GetProfileTypes: getProfileTypes_Mate, + GetIsCanonical: getIsCanonical_Always, + CheckValueFunction: checkValueFunction_Height, + } + attributeObjectsList = append(attributeObjectsList, attributeObject_Height) + + checkValueFunction_Sex := func(profileVersion int, profileType string, input string)(bool, bool, error){ + + if (profileVersion != 1){ + return false, false, errors.New("Trying to check profile value for unknown profile version.") + } + + if (profileType != "Mate"){ + return false, false, errors.New("Trying to check if Sex is valid for non-mate profileType: " + profileType) + } + + if (input == "Male" || input == "Female" || input == "Intersex Male" || input == "Intersex Female" || input == "Intersex"){ + return true, true, nil + } + + return false, false, nil + } + + attributeObject_Sex := AttributeObject{ + ProfileVersions: []int{1}, + AttributeIdentifier: 7, + AttributeName: "Sex", + GetIsRequired: getIsRequired_No, + GetMandatoryAttributes: getMandatoryAttributes_None, + GetProfileTypes: getProfileTypes_Mate, + GetIsCanonical: getIsCanonical_Always, + CheckValueFunction: checkValueFunction_Sex, + } + attributeObjectsList = append(attributeObjectsList, attributeObject_Sex) + + checkValueFunction_Age := func(profileVersion int, profileType string, input string)(bool, bool, error){ + + if (profileVersion != 1){ + return false, false, errors.New("Trying to check profile value for unknown profile version.") + } + + if (profileType != "Mate"){ + return false, false, errors.New("Trying to check if age is valid for non-mate profileType: " + profileType) + } + + isValid, err := helpers.VerifyStringIsIntWithinRange(input, 18, 150) + if (err != nil) { return false, false, err } + if (isValid == true){ + return true, true, nil + } + + return false, false, nil + } + + attributeObject_Age := AttributeObject{ + ProfileVersions: []int{1}, + AttributeIdentifier: 8, + AttributeName: "Age", + GetIsRequired: getIsRequired_No, + GetMandatoryAttributes: getMandatoryAttributes_None, + GetProfileTypes: getProfileTypes_Mate, + GetIsCanonical: getIsCanonical_Always, + CheckValueFunction: checkValueFunction_Age, + } + attributeObjectsList = append(attributeObjectsList, attributeObject_Age) + + checkValueFunction_Description := func(profileVersion int, profileType string, input string)(bool, bool, error){ + + if (profileVersion != 1){ + return false, false, errors.New("Trying to check profile value for unknown profile version.") + } + + if (profileType != "Mate" && profileType != "Host" && profileType != "Moderator"){ + return false, false, errors.New("Invalid profile type when trying to verify Description: " + profileType) + } + + if (input == ""){ + return false, false, nil + } + + isAllowed := allowedText.VerifyStringIsAllowed(input) + if (isAllowed == false){ + return false, false, nil + } + + descriptionLength := len(input) + + if (profileType == "Mate" && descriptionLength <= 3000){ + return true, false, nil + } + if (profileType == "Host" && descriptionLength <= 300){ + return true, false, nil + } + if (profileType == "Moderator" && descriptionLength <= 500){ + return true, false, nil + } + + return false, false, nil + } + + attributeObject_Description := AttributeObject{ + ProfileVersions: []int{1}, + AttributeIdentifier: 9, + AttributeName: "Description", + GetIsRequired: getIsRequired_No, + GetMandatoryAttributes: getMandatoryAttributes_None, + GetProfileTypes: getProfileTypes_All, + GetIsCanonical: getIsCanonical_Never, + CheckValueFunction: checkValueFunction_Description, + } + attributeObjectsList = append(attributeObjectsList, attributeObject_Description) + + checkValueFunction_Username := func(profileVersion int, profileType string, input string)(bool, bool, error){ + + if (profileVersion != 1){ + return false, false, errors.New("Trying to check profile value for unknown profile version.") + } + + if (profileType != "Mate" && profileType != "Host" && profileType != "Moderator"){ + return false, false, errors.New("Invalid profile type when trying to verify Username: " + profileType) + } + + if (input == ""){ + return false, false, nil + } + + isAllowed := allowedText.VerifyStringIsAllowed(input) + if (isAllowed == false){ + return false, false, nil + } + + containsTabsOrNewlines := helpers.CheckIfStringContainsTabsOrNewlines(input) + if (containsTabsOrNewlines == true){ + return false, false, nil + } + + usernameByteLength := len(input) + + if (usernameByteLength <= 25){ + return true, false, nil + } + + return false, false, nil + } + + attributeObject_Username := AttributeObject{ + ProfileVersions: []int{1}, + AttributeIdentifier: 10, + AttributeName: "Username", + GetIsRequired: getIsRequired_No, + GetMandatoryAttributes: getMandatoryAttributes_None, + GetProfileTypes: getProfileTypes_All, + GetIsCanonical: getIsCanonical_Never, + CheckValueFunction: checkValueFunction_Username, + } + attributeObjectsList = append(attributeObjectsList, attributeObject_Username) + + checkValueFunction_LocationLatitude := func(profileVersion int, profileType string, input string)(bool, bool, error){ + + if (profileVersion != 1){ + return false, false, errors.New("Trying to check profile value for unknown profile version.") + } + + if (profileType != "Mate"){ + return false, false, errors.New("Invalid profile type when trying to verify LocationLatitude: " + profileType) + } + + latitudeFloat64, err := helpers.ConvertStringToFloat64(input) + if (err != nil){ + return false, false, nil + } + + isValid := helpers.VerifyLatitude(latitudeFloat64) + if (isValid == false){ + return false, false, nil + } + + return true, true, nil + } + + getMandatoryAttributes_PrimaryLocationLatitude := func(profileVersion int)([]string, error){ + + if (profileVersion != 1){ + return nil, errors.New("Invalid profile version when trying to get mandatory attributes.") + } + mandatoryAttributesList := []string{"PrimaryLocationLongitude"} + return mandatoryAttributesList, nil + } + + attributeObject_PrimaryLocationLatitude := AttributeObject{ + ProfileVersions: []int{1}, + AttributeIdentifier: 11, + AttributeName: "PrimaryLocationLatitude", + GetIsRequired: getIsRequired_No, + GetMandatoryAttributes: getMandatoryAttributes_PrimaryLocationLatitude, + GetProfileTypes: getProfileTypes_Mate, + GetIsCanonical: getIsCanonical_Always, + CheckValueFunction: checkValueFunction_LocationLatitude, + } + attributeObjectsList = append(attributeObjectsList, attributeObject_PrimaryLocationLatitude) + + getMandatoryAttributes_PrimaryLocationLongitude := func(profileVersion int)([]string, error){ + + if (profileVersion != 1){ + return nil, errors.New("Invalid profile version when trying to get mandatory attributes.") + } + mandatoryAttributesList := []string{"PrimaryLocationLatitude"} + return mandatoryAttributesList, nil + } + + checkValueFunction_LocationLongitude := func(profileVersion int, profileType string, input string)(bool, bool, error){ + + if (profileVersion != 1){ + return false, false, errors.New("Trying to check profile value for unknown profile version.") + } + + if (profileType != "Mate"){ + return false, false, errors.New("Invalid profile type when trying to verify LocationLongitude: " + profileType) + } + + longitudeFloat64, err := helpers.ConvertStringToFloat64(input) + if (err != nil){ + return false, false, nil + } + + isValid := helpers.VerifyLongitude(longitudeFloat64) + if (isValid == false){ + return false, false, nil + } + + return true, true, nil + } + + attributeObject_PrimaryLocationLongitude := AttributeObject{ + ProfileVersions: []int{1}, + AttributeIdentifier: 12, + AttributeName: "PrimaryLocationLongitude", + GetIsRequired: getIsRequired_No, + GetMandatoryAttributes: getMandatoryAttributes_PrimaryLocationLongitude, + GetProfileTypes: getProfileTypes_Mate, + GetIsCanonical: getIsCanonical_Always, + CheckValueFunction: checkValueFunction_LocationLongitude, + } + attributeObjectsList = append(attributeObjectsList, attributeObject_PrimaryLocationLongitude) + + getMandatoryAttributes_PrimaryLocationCountry := func(profileVersion int)([]string, error){ + + if (profileVersion != 1){ + return nil, errors.New("Invalid profile version when trying to get mandatory attributes.") + } + mandatoryAttributesList := []string{"PrimaryLocationLatitude", "PrimaryLocationLongitude"} + return mandatoryAttributesList, nil + } + + checkValueFunction_LocationCountry := func(profileVersion int, profileType string, input string)(bool, bool, error){ + + if (profileVersion != 1){ + return false, false, errors.New("Trying to check profile value for unknown profile version.") + } + + if (profileType != "Mate"){ + return false, false, errors.New("Invalid profile type when trying to verify LocationCountry: " + profileType) + } + + countryIdentifier, err := helpers.ConvertStringToInt(input) + if (err != nil){ + return false, false, nil + } + + isValid := worldLocations.VerifyCountryIdentifier(countryIdentifier) + if (isValid == false){ + return false, false, nil + } + + return true, true, nil + } + + attributeObject_PrimaryLocationCountry := AttributeObject{ + ProfileVersions: []int{1}, + AttributeIdentifier: 13, + AttributeName: "PrimaryLocationCountry", + GetIsRequired: getIsRequired_No, + GetMandatoryAttributes: getMandatoryAttributes_PrimaryLocationCountry, + GetProfileTypes: getProfileTypes_Mate, + GetIsCanonical: getIsCanonical_Always, + CheckValueFunction: checkValueFunction_LocationCountry, + } + attributeObjectsList = append(attributeObjectsList, attributeObject_PrimaryLocationCountry) + + getMandatoryAttributes_SecondaryLocationLatitude := func(profileVersion int)([]string, error){ + + if (profileVersion != 1){ + return nil, errors.New("Invalid profile version when trying to get mandatory attributes.") + } + mandatoryAttributesList := []string{"PrimaryLocationLatitude", "PrimaryLocationLongitude", "SecondaryLocationLongitude"} + return mandatoryAttributesList, nil + } + + attributeObject_SecondaryLocationLatitude := AttributeObject{ + ProfileVersions: []int{1}, + AttributeIdentifier: 14, + AttributeName: "SecondaryLocationLatitude", + GetIsRequired: getIsRequired_No, + GetMandatoryAttributes: getMandatoryAttributes_SecondaryLocationLatitude, + GetProfileTypes: getProfileTypes_Mate, + GetIsCanonical: getIsCanonical_Always, + CheckValueFunction: checkValueFunction_LocationLatitude, + } + attributeObjectsList = append(attributeObjectsList, attributeObject_SecondaryLocationLatitude) + + getMandatoryAttributes_SecondaryLocationLongitude := func(profileVersion int)([]string, error){ + + if (profileVersion != 1){ + return nil, errors.New("Invalid profile version when trying to get mandatory attributes.") + } + mandatoryAttributesList := []string{"PrimaryLocationLatitude", "PrimaryLocationLongitude", "SecondaryLocationLatitude"} + return mandatoryAttributesList, nil + } + + attributeObject_SecondaryLocationLongitude := AttributeObject{ + ProfileVersions: []int{1}, + AttributeIdentifier: 15, + AttributeName: "SecondaryLocationLongitude", + GetIsRequired: getIsRequired_No, + GetMandatoryAttributes: getMandatoryAttributes_SecondaryLocationLongitude, + GetProfileTypes: getProfileTypes_Mate, + GetIsCanonical: getIsCanonical_Always, + CheckValueFunction: checkValueFunction_LocationLongitude, + } + attributeObjectsList = append(attributeObjectsList, attributeObject_SecondaryLocationLongitude) + + getMandatoryAttributes_SecondaryLocationCountry := func(profileVersion int)([]string, error){ + + if (profileVersion != 1){ + return nil, errors.New("Invalid profile version when trying to get mandatory attributes.") + } + mandatoryAttributesList := []string{"PrimaryLocationLatitude", "PrimaryLocationLongitude", "SecondaryLocationLatitude", "SecondaryLocationLongitude"} + return mandatoryAttributesList, nil + } + + attributeObject_SecondaryLocationCountry := AttributeObject{ + ProfileVersions: []int{1}, + AttributeIdentifier: 16, + AttributeName: "SecondaryLocationCountry", + GetIsRequired: getIsRequired_No, + GetMandatoryAttributes: getMandatoryAttributes_SecondaryLocationCountry, + GetProfileTypes: getProfileTypes_Mate, + GetIsCanonical: getIsCanonical_Always, + CheckValueFunction: checkValueFunction_LocationCountry, + } + attributeObjectsList = append(attributeObjectsList, attributeObject_SecondaryLocationCountry) + + checkValueFunction_Tags := func(profileVersion int, profileType string, input string)(bool, bool, error){ + + if (profileVersion != 1){ + return false, false, errors.New("Trying to check profile value for unknown profile version.") + } + + if (profileType != "Mate"){ + return false, false, errors.New("Invalid profile type when trying to verify Tags: " + profileType) + } + + tagsList := strings.Split(input, "+&") + + if (len(tagsList) > 30){ + return false, false, nil + } + + // We use this map to detect duplicates + allTagsMap := make(map[string]struct{}) + + totalTagByteLength := 0 + + for _, tagString := range tagsList{ + + if (tagString == ""){ + return false, false, nil + } + + _, exists := allTagsMap[tagString] + if (exists == true){ + // Duplicate exists + return false, false, nil + } + allTagsMap[tagString] = struct{}{} + + isAllowed := allowedText.VerifyStringIsAllowed(tagString) + if (isAllowed == false){ + return false, false, nil + } + + containsTabOrNewline := helpers.CheckIfStringContainsTabsOrNewlines(tagString) + if (containsTabOrNewline == true){ + return false, false, nil + } + + tagByteLength := len(tagString) + + if (tagByteLength > 40){ + return false, false, nil + } + totalTagByteLength += tagByteLength + } + + if (totalTagByteLength > 500){ + return false, false, nil + } + + return true, false, nil + } + + attributeObject_Tags := AttributeObject{ + ProfileVersions: []int{1}, + AttributeIdentifier: 17, + AttributeName: "Tags", + GetIsRequired: getIsRequired_No, + GetMandatoryAttributes: getMandatoryAttributes_None, + GetProfileTypes: getProfileTypes_Mate, + GetIsCanonical: getIsCanonical_Never, + CheckValueFunction: checkValueFunction_Tags, + } + attributeObjectsList = append(attributeObjectsList, attributeObject_Tags) + + checkValueFunction_Photos := func(profileVersion int, profileType string, input string)(bool, bool, error){ + + if (profileVersion != 1){ + return false, false, errors.New("Trying to check profile value for unknown profile version.") + } + + if (profileType != "Mate"){ + return false, false, errors.New("Invalid profile type when trying to verify Photos: " + profileType) + } + + base64PhotosList := strings.Split(input, "+") + + if (len(base64PhotosList) > 5){ + return false, false, nil + } + + for _, base64Photo := range base64PhotosList{ + + photoBytes, err := encoding.DecodeBase64StringToBytes(base64Photo) + if (err != nil){ + return false, false, nil + } + + if (base64Photo == ""){ + return false, false, nil + } + + isValid, err := imagery.VerifyStandardImageBytes(photoBytes) + if (err != nil){ + return false, false, err + } + + if (isValid == false){ + return false, false, nil + } + } + + return true, false, nil + } + + attributeObject_Photos := AttributeObject{ + ProfileVersions: []int{1}, + AttributeIdentifier: 18, + AttributeName: "Photos", + GetIsRequired: getIsRequired_No, + GetMandatoryAttributes: getMandatoryAttributes_None, + GetProfileTypes: getProfileTypes_Mate, + GetIsCanonical: getIsCanonical_Never, + CheckValueFunction: checkValueFunction_Photos, + } + attributeObjectsList = append(attributeObjectsList, attributeObject_Photos) + + checkValueFunction_Questionnaire := func(profileVersion int, profileType string, input string)(bool, bool, error){ + + if (profileVersion != 1){ + return false, false, errors.New("Trying to check profile value for unknown profile version.") + } + + if (profileType != "Mate"){ + return false, false, errors.New("Invalid profile type when trying to verify Questionnaire: " + profileType) + } + + _, err := mateQuestionnaire.ReadQuestionnaireString(input) + if (err != nil){ + return false, false, nil + } + return true, false, nil + } + + attributeObject_Questionnaire := AttributeObject{ + ProfileVersions: []int{1}, + AttributeIdentifier: 19, + AttributeName: "Questionnaire", + GetIsRequired: getIsRequired_No, + GetMandatoryAttributes: getMandatoryAttributes_None, + GetProfileTypes: getProfileTypes_Mate, + GetIsCanonical: getIsCanonical_Never, + CheckValueFunction: checkValueFunction_Questionnaire, + } + attributeObjectsList = append(attributeObjectsList, attributeObject_Questionnaire) + + checkValueFunction_Avatar := func(profileVersion int, profileType string, input string)(bool, bool, error){ + + if (profileVersion != 1){ + return false, false, errors.New("Trying to check profile value for unknown profile version.") + } + + if (profileType != "Mate" && profileType != "Host" && profileType != "Moderator"){ + return false, false, errors.New("Invalid profile type when trying to verify Avatar: " + profileType) + } + + emojiIdentifier, err := helpers.ConvertStringToInt(input) + if (err != nil){ + return false, false, nil + } + + isValid := imageFiles.VerifyEmojiIdentifier(emojiIdentifier) + if (isValid == false){ + return false, false, nil + } + return true, true, nil + } + + attributeObject_Avatar := AttributeObject{ + ProfileVersions: []int{1}, + AttributeIdentifier: 20, + AttributeName: "Avatar", + GetIsRequired: getIsRequired_No, + GetMandatoryAttributes: getMandatoryAttributes_None, + GetProfileTypes: getProfileTypes_All, + GetIsCanonical: getIsCanonical_Always, + CheckValueFunction: checkValueFunction_Avatar, + } + attributeObjectsList = append(attributeObjectsList, attributeObject_Avatar) + + checkValueFunction_Sexuality := func(profileVersion int, profileType string, input string)(bool, bool, error){ + + if (profileVersion != 1){ + return false, false, errors.New("Trying to check profile value for unknown profile version.") + } + + if (profileType != "Mate"){ + return false, false, errors.New("Invalid profile type when trying to verify Sexuality: " + profileType) + } + + if (input != "Male" && input != "Female" && input != "Male And Female"){ + return false, false, nil + } + return true, true, nil + } + + attributeObject_Sexuality := AttributeObject{ + ProfileVersions: []int{1}, + AttributeIdentifier: 21, + AttributeName: "Sexuality", + GetIsRequired: getIsRequired_No, + GetMandatoryAttributes: getMandatoryAttributes_None, + GetProfileTypes: getProfileTypes_Mate, + GetIsCanonical: getIsCanonical_Always, + CheckValueFunction: checkValueFunction_Sexuality, + } + attributeObjectsList = append(attributeObjectsList, attributeObject_Sexuality) + + checkValueFunction_23andMe_AncestryComposition := func(profileVersion int, profileType string, input string)(bool, bool, error){ + + if (profileVersion != 1){ + return false, false, errors.New("Trying to check profile value for unknown profile version.") + } + + if (profileType != "Mate"){ + return false, false, errors.New("Invalid profile type when trying to verify 23andMe_AncestryComposition: " + profileType) + } + + isValid, _, _, _, err := companyAnalysis.ReadAncestryCompositionAttribute_23andMe(true, input) + if (err != nil) { return false, false, err } + if (isValid == false){ + return false, false, nil + } + return true, true, nil + } + + attributeObject_23andMe_AncestryComposition := AttributeObject{ + ProfileVersions: []int{1}, + AttributeIdentifier: 22, + AttributeName: "23andMe_AncestryComposition", + GetIsRequired: getIsRequired_No, + GetMandatoryAttributes: getMandatoryAttributes_None, + GetProfileTypes: getProfileTypes_Mate, + GetIsCanonical: getIsCanonical_Always, + CheckValueFunction: checkValueFunction_23andMe_AncestryComposition, + } + attributeObjectsList = append(attributeObjectsList, attributeObject_23andMe_AncestryComposition) + + checkValueFunction_23andMe_NeanderthalVariants := func(profileVersion int, profileType string, input string)(bool, bool, error){ + + if (profileVersion != 1){ + return false, false, errors.New("Trying to check profile value for unknown profile version.") + } + + if (profileType != "Mate"){ + return false, false, errors.New("Invalid profile type when trying to verify 23andMe_NeanderthalVariants: " + profileType) + } + + neanderthalVariantsInt, err := helpers.ConvertStringToInt(input) + if (err != nil){ + return false, false, nil + } + + isValid := companyAnalysis.VerifyNeanderthalVariants_23andMe(neanderthalVariantsInt) + if (isValid == false){ + return false, false, nil + } + return true, true, nil + } + + attributeObject_23andMe_NeanderthalVariants := AttributeObject{ + ProfileVersions: []int{1}, + AttributeIdentifier: 23, + AttributeName: "23andMe_NeanderthalVariants", + GetIsRequired: getIsRequired_No, + GetMandatoryAttributes: getMandatoryAttributes_None, + GetProfileTypes: getProfileTypes_Mate, + GetIsCanonical: getIsCanonical_Always, + CheckValueFunction: checkValueFunction_23andMe_NeanderthalVariants, + } + attributeObjectsList = append(attributeObjectsList, attributeObject_23andMe_NeanderthalVariants) + + checkValueFunction_23andMe_MaternalHaplogroup := func(profileVersion int, profileType string, input string)(bool, bool, error){ + + if (profileVersion != 1){ + return false, false, errors.New("Trying to check profile value for unknown profile version.") + } + + if (profileType != "Mate"){ + return false, false, errors.New("Invalid profile type when trying to verify 23andMe_MaternalHaplogroup: " + profileType) + } + + isValid, isCanonical := companyAnalysis.VerifyMaternalHaplogroup_23AndMe(input) + + return isValid, isCanonical, nil + } + + attributeObject_23andMe_MaternalHaplogroup := AttributeObject{ + ProfileVersions: []int{1}, + AttributeIdentifier: 24, + AttributeName: "23andMe_MaternalHaplogroup", + GetIsRequired: getIsRequired_No, + GetMandatoryAttributes: getMandatoryAttributes_None, + GetProfileTypes: getProfileTypes_Mate, + GetIsCanonical: getIsCanonical_Sometimes, + CheckValueFunction: checkValueFunction_23andMe_MaternalHaplogroup, + } + attributeObjectsList = append(attributeObjectsList, attributeObject_23andMe_MaternalHaplogroup) + + checkValueFunction_23andMe_PaternalHaplogroup := func(profileVersion int, profileType string, input string)(bool, bool, error){ + + if (profileVersion != 1){ + return false, false, errors.New("Trying to check profile value for unknown profile version.") + } + + if (profileType != "Mate"){ + return false, false, errors.New("Invalid profile type when trying to verify 23andMe_PaternalHaplogroup: " + profileType) + } + + isValid, isCanonical := companyAnalysis.VerifyPaternalHaplogroup_23AndMe(input) + + return isValid, isCanonical, nil + } + + attributeObject_23andMe_PaternalHaplogroup := AttributeObject{ + ProfileVersions: []int{1}, + AttributeIdentifier: 25, + AttributeName: "23andMe_PaternalHaplogroup", + GetIsRequired: getIsRequired_No, + GetMandatoryAttributes: getMandatoryAttributes_None, + GetProfileTypes: getProfileTypes_Mate, + GetIsCanonical: getIsCanonical_Sometimes, + CheckValueFunction: checkValueFunction_23andMe_PaternalHaplogroup, + } + attributeObjectsList = append(attributeObjectsList, attributeObject_23andMe_PaternalHaplogroup) + + checkValueFunction_BodyFat := func(profileVersion int, profileType string, input string)(bool, bool, error){ + + if (profileVersion != 1){ + return false, false, errors.New("Trying to check profile value for unknown profile version.") + } + + if (profileType != "Mate"){ + return false, false, errors.New("Invalid profile type when trying to verify BodyFat: " + profileType) + } + + isValid, err := helpers.VerifyStringIsIntWithinRange(input, 1, 4) + if (err != nil) { return false, false, err } + if (isValid == false){ + return false, false, nil + } + + return true, true, nil + } + + attributeObject_BodyFat := AttributeObject{ + ProfileVersions: []int{1}, + AttributeIdentifier: 26, + AttributeName: "BodyFat", + GetIsRequired: getIsRequired_No, + GetMandatoryAttributes: getMandatoryAttributes_None, + GetProfileTypes: getProfileTypes_Mate, + GetIsCanonical: getIsCanonical_Always, + CheckValueFunction: checkValueFunction_BodyFat, + } + attributeObjectsList = append(attributeObjectsList, attributeObject_BodyFat) + + checkValueFunction_BodyMuscle := func(profileVersion int, profileType string, input string)(bool, bool, error){ + + if (profileVersion != 1){ + return false, false, errors.New("Trying to check profile value for unknown profile version.") + } + + if (profileType != "Mate"){ + return false, false, errors.New("Invalid profile type when trying to verify BodyMuscle: " + profileType) + } + + isValid, err := helpers.VerifyStringIsIntWithinRange(input, 1, 4) + if (err != nil) { return false, false, err } + if (isValid == false){ + return false, false, nil + } + + return true, true, nil + } + + attributeObject_BodyMuscle := AttributeObject{ + ProfileVersions: []int{1}, + AttributeIdentifier: 27, + AttributeName: "BodyMuscle", + GetIsRequired: getIsRequired_No, + GetMandatoryAttributes: getMandatoryAttributes_None, + GetProfileTypes: getProfileTypes_Mate, + GetIsCanonical: getIsCanonical_Always, + CheckValueFunction: checkValueFunction_BodyMuscle, + } + attributeObjectsList = append(attributeObjectsList, attributeObject_BodyMuscle) + + checkValueFunction_EyeColor := func(profileVersion int, profileType string, input string)(bool, bool, error){ + + if (profileVersion != 1){ + return false, false, errors.New("Trying to check profile value for unknown profile version.") + } + + if (profileType != "Mate"){ + return false, false, errors.New("Invalid profile type when trying to verify EyeColor: " + profileType) + } + + if (input == ""){ + return false, false, nil + } + + eyeColorsList := strings.Split(input, "+") + + if (len(eyeColorsList) > 4){ + return false, false, nil + } + + // We use this map to check for duplicates + addedColorsMap := make(map[string]struct{}) + + for _, colorName := range eyeColorsList{ + + if (colorName != "Blue" && colorName != "Green" && colorName != "Amber" && colorName != "Brown"){ + return false, false, nil + } + + _, exists := addedColorsMap[colorName] + if (exists == true){ + return false, false, nil + } + addedColorsMap[colorName] = struct{}{} + } + + return true, true, nil + } + + attributeObject_EyeColor := AttributeObject{ + ProfileVersions: []int{1}, + AttributeIdentifier: 28, + AttributeName: "EyeColor", + GetIsRequired: getIsRequired_No, + GetMandatoryAttributes: getMandatoryAttributes_None, + GetProfileTypes: getProfileTypes_Mate, + GetIsCanonical: getIsCanonical_Always, + CheckValueFunction: checkValueFunction_EyeColor, + } + attributeObjectsList = append(attributeObjectsList, attributeObject_EyeColor) + + checkValueFunction_HairColor := func(profileVersion int, profileType string, input string)(bool, bool, error){ + + if (profileVersion != 1){ + return false, false, errors.New("Trying to check profile value for unknown profile version.") + } + + if (profileType != "Mate"){ + return false, false, errors.New("Invalid profile type when trying to verify HairColor: " + profileType) + } + + if (input == ""){ + return false, false, nil + } + + hairColorsList := strings.Split(input, "+") + + if (len(hairColorsList) > 2){ + return false, false, nil + } + + // We use this map to check for duplicates + addedColorsMap := make(map[string]struct{}) + + for _, colorName := range hairColorsList{ + + if (colorName != "Black" && colorName != "Brown" && colorName != "Blonde" && colorName != "Orange"){ + return false, false, nil + } + + _, exists := addedColorsMap[colorName] + if (exists == true){ + return false, false, nil + } + addedColorsMap[colorName] = struct{}{} + } + + return true, true, nil + } + + attributeObject_HairColor := AttributeObject{ + ProfileVersions: []int{1}, + AttributeIdentifier: 29, + AttributeName: "HairColor", + GetIsRequired: getIsRequired_No, + GetMandatoryAttributes: getMandatoryAttributes_None, + GetProfileTypes: getProfileTypes_Mate, + GetIsCanonical: getIsCanonical_Always, + CheckValueFunction: checkValueFunction_HairColor, + } + attributeObjectsList = append(attributeObjectsList, attributeObject_HairColor) + + checkValueFunction_HairTexture := func(profileVersion int, profileType string, input string)(bool, bool, error){ + + if (profileVersion != 1){ + return false, false, errors.New("Trying to check profile value for unknown profile version.") + } + + if (profileType != "Mate"){ + return false, false, errors.New("Invalid profile type when trying to verify HairTexture: " + profileType) + } + + if (input == ""){ + return false, false, nil + } + + // 1 == Straight, 2 == Slightly Wavy, 3 == Wavy, 4 == Big Curls, 5 == Small Curls, 6 == Very Tight Curls + // These descriptions are taken from 23andMe. + + if (input == "1" || input == "2" || input == "3" || input == "4" || input == "5" || input == "6"){ + return true, true, nil + } + + return false, false, nil + } + + attributeObject_HairTexture := AttributeObject{ + ProfileVersions: []int{1}, + AttributeIdentifier: 63, + AttributeName: "HairTexture", + GetIsRequired: getIsRequired_No, + GetMandatoryAttributes: getMandatoryAttributes_None, + GetProfileTypes: getProfileTypes_Mate, + GetIsCanonical: getIsCanonical_Always, + CheckValueFunction: checkValueFunction_HairTexture, + } + attributeObjectsList = append(attributeObjectsList, attributeObject_HairTexture) + + checkValueFunction_SkinColor := func(profileVersion int, profileType string, input string)(bool, bool, error){ + + if (profileVersion != 1){ + return false, false, errors.New("Trying to check profile value for unknown profile version.") + } + + if (profileType != "Mate"){ + return false, false, errors.New("Invalid profile type when trying to verify SkinColor: " + profileType) + } + + if (input == ""){ + return false, false, nil + } + + if (input == "1" || input == "2" || input == "3" || input == "4" || input == "5" || input == "6"){ + return true, true, nil + } + + return false, false, nil + } + + attributeObject_SkinColor := AttributeObject{ + ProfileVersions: []int{1}, + AttributeIdentifier: 108, + AttributeName: "SkinColor", + GetIsRequired: getIsRequired_No, + GetMandatoryAttributes: getMandatoryAttributes_None, + GetProfileTypes: getProfileTypes_Mate, + GetIsCanonical: getIsCanonical_Always, + CheckValueFunction: checkValueFunction_SkinColor, + } + attributeObjectsList = append(attributeObjectsList, attributeObject_SkinColor) + + attributeObject_HasHIV := AttributeObject{ + ProfileVersions: []int{1}, + AttributeIdentifier: 30, + AttributeName: "HasHIV", + GetIsRequired: getIsRequired_No, + GetMandatoryAttributes: getMandatoryAttributes_None, + GetProfileTypes: getProfileTypes_Mate, + GetIsCanonical: getIsCanonical_Always, + CheckValueFunction: checkValueFunction_MateYesOrNo, + } + attributeObjectsList = append(attributeObjectsList, attributeObject_HasHIV) + + attributeObject_HasGenitalHerpes := AttributeObject{ + ProfileVersions: []int{1}, + AttributeIdentifier: 31, + AttributeName: "HasGenitalHerpes", + GetIsRequired: getIsRequired_No, + GetMandatoryAttributes: getMandatoryAttributes_None, + GetProfileTypes: getProfileTypes_Mate, + GetIsCanonical: getIsCanonical_Always, + CheckValueFunction: checkValueFunction_MateYesOrNo, + } + attributeObjectsList = append(attributeObjectsList, attributeObject_HasGenitalHerpes) + + checkValueFunction_Hobbies := func(profileVersion int, profileType string, input string)(bool, bool, error){ + + if (profileVersion != 1){ + return false, false, errors.New("Trying to check profile value for unknown profile version.") + } + + if (profileType != "Mate"){ + return false, false, errors.New("Invalid profile type when trying to verify Hobbies: " + profileType) + } + + if (input == ""){ + return false, false, nil + } + + isAllowed := allowedText.VerifyStringIsAllowed(input) + if (isAllowed == false){ + return false, false, nil + } + + if (len(input) <= 1000){ + return true, false, nil + } + + return false, false, nil + } + + attributeObject_Hobbies := AttributeObject{ + ProfileVersions: []int{1}, + AttributeIdentifier: 32, + AttributeName: "Hobbies", + GetIsRequired: getIsRequired_No, + GetMandatoryAttributes: getMandatoryAttributes_None, + GetProfileTypes: getProfileTypes_Mate, + GetIsCanonical: getIsCanonical_Never, + CheckValueFunction: checkValueFunction_Hobbies, + } + attributeObjectsList = append(attributeObjectsList, attributeObject_Hobbies) + + getMandatoryAttributes_Wealth := func(profileVersion int)([]string, error){ + + if (profileVersion != 1){ + return nil, errors.New("Invalid profile version when trying to get mandatory attributes.") + } + mandatoryAttributesList := []string{"WealthCurrency", "WealthIsLowerBound"} + return mandatoryAttributesList, nil + } + + checkValueFunction_Wealth := func(profileVersion int, profileType string, input string)(bool, bool, error){ + + if (profileVersion != 1){ + return false, false, errors.New("Trying to check profile value for unknown profile version.") + } + + if (profileType != "Mate"){ + return false, false, errors.New("Invalid profile type when trying to verify Wealth: " + profileType) + } + + isValid, err := helpers.VerifyStringIsIntWithinRange(input, 0, 9223372036854775807) + if (err != nil) { return false, false, err } + + if (isValid == false){ + return false, false, nil + } + + return true, true, nil + } + + attributeObject_Wealth := AttributeObject{ + ProfileVersions: []int{1}, + AttributeIdentifier: 33, + AttributeName: "Wealth", + GetIsRequired: getIsRequired_No, + GetMandatoryAttributes: getMandatoryAttributes_Wealth, + GetProfileTypes: getProfileTypes_Mate, + GetIsCanonical: getIsCanonical_Always, + CheckValueFunction: checkValueFunction_Wealth, + } + attributeObjectsList = append(attributeObjectsList, attributeObject_Wealth) + + getMandatoryAttributes_WealthCurrency := func(profileVersion int)([]string, error){ + + if (profileVersion != 1){ + return nil, errors.New("Invalid profile version when trying to get mandatory attributes.") + } + mandatoryAttributesList := []string{"Wealth", "WealthIsLowerBound"} + return mandatoryAttributesList, nil + } + + checkValueFunction_WealthCurrency := func(profileVersion int, profileType string, input string)(bool, bool, error){ + + if (profileVersion != 1){ + return false, false, errors.New("Trying to check profile value for unknown profile version.") + } + + if (profileType != "Mate"){ + return false, false, errors.New("Invalid profile type when trying to verify WealthCurrency: " + profileType) + } + + isValid, err := currencies.VerifyCurrencyCode(input) + if (err != nil) { return false, false, err } + if (isValid == false){ + return false, false, nil + } + + return true, true, nil + } + + attributeObject_WealthCurrency := AttributeObject{ + ProfileVersions: []int{1}, + AttributeIdentifier: 34, + AttributeName: "WealthCurrency", + GetIsRequired: getIsRequired_No, + GetMandatoryAttributes: getMandatoryAttributes_WealthCurrency, + GetProfileTypes: getProfileTypes_Mate, + GetIsCanonical: getIsCanonical_Always, + CheckValueFunction: checkValueFunction_WealthCurrency, + } + attributeObjectsList = append(attributeObjectsList, attributeObject_WealthCurrency) + + getMandatoryAttributes_WealthIsLowerBound := func(profileVersion int)([]string, error){ + + if (profileVersion != 1){ + return nil, errors.New("Invalid profile version when trying to get mandatory attributes.") + } + mandatoryAttributesList := []string{"Wealth", "WealthCurrency"} + return mandatoryAttributesList, nil + } + + attributeObject_WealthIsLowerBound := AttributeObject{ + ProfileVersions: []int{1}, + AttributeIdentifier: 35, + AttributeName: "WealthIsLowerBound", + GetIsRequired: getIsRequired_No, + GetMandatoryAttributes: getMandatoryAttributes_WealthIsLowerBound, + GetProfileTypes: getProfileTypes_Mate, + GetIsCanonical: getIsCanonical_Always, + CheckValueFunction: checkValueFunction_MateYesOrNo, + } + attributeObjectsList = append(attributeObjectsList, attributeObject_WealthIsLowerBound) + + checkValueFunction_Job := func(profileVersion int, profileType string, input string)(bool, bool, error){ + + if (profileVersion != 1){ + return false, false, errors.New("Trying to check profile value for unknown profile version.") + } + + if (profileType != "Mate"){ + return false, false, errors.New("Invalid profile type when trying to verify Job: " + profileType) + } + + if (input == ""){ + return false, false, nil + } + + isAllowed := allowedText.VerifyStringIsAllowed(input) + if (isAllowed == false){ + return false, false, nil + } + + if (len(input) <= 100){ + return true, false, nil + } + + return false, false, nil + } + + attributeObject_Job := AttributeObject{ + ProfileVersions: []int{1}, + AttributeIdentifier: 36, + AttributeName: "Job", + GetIsRequired: getIsRequired_No, + GetMandatoryAttributes: getMandatoryAttributes_None, + GetProfileTypes: getProfileTypes_Mate, + GetIsCanonical: getIsCanonical_Never, + CheckValueFunction: checkValueFunction_Job, + } + attributeObjectsList = append(attributeObjectsList, attributeObject_Job) + + + addFoodRatingAttributeObject := func(attributeIdentifier int, attributeName string){ + + attributeObject_FoodRating := AttributeObject{ + ProfileVersions: []int{1}, + AttributeIdentifier: attributeIdentifier, + AttributeName: attributeName, + GetIsRequired: getIsRequired_No, + GetMandatoryAttributes: getMandatoryAttributes_None, + GetProfileTypes: getProfileTypes_Mate, + GetIsCanonical: getIsCanonical_Always, + CheckValueFunction: checkValueFunction_Mate1To10Rating, + } + + attributeObjectsList = append(attributeObjectsList, attributeObject_FoodRating) + } + + addFoodRatingAttributeObject(37, "FruitRating") + addFoodRatingAttributeObject(38, "VegetablesRating") + addFoodRatingAttributeObject(39, "NutsRating") + addFoodRatingAttributeObject(40, "GrainsRating") + addFoodRatingAttributeObject(41, "DairyRating") + addFoodRatingAttributeObject(42, "SeafoodRating") + addFoodRatingAttributeObject(43, "BeefRating") + addFoodRatingAttributeObject(44, "PorkRating") + addFoodRatingAttributeObject(45, "PoultryRating") + addFoodRatingAttributeObject(46, "EggsRating") + addFoodRatingAttributeObject(47, "BeansRating") + + //TODO: Add Potatoes? + + + attributeObject_Fame := AttributeObject{ + ProfileVersions: []int{1}, + AttributeIdentifier: 48, + AttributeName: "Fame", + GetIsRequired: getIsRequired_No, + GetMandatoryAttributes: getMandatoryAttributes_None, + GetProfileTypes: getProfileTypes_Mate, + GetIsCanonical: getIsCanonical_Always, + CheckValueFunction: checkValueFunction_Mate1To10Rating, + } + attributeObjectsList = append(attributeObjectsList, attributeObject_Fame) + + + attributeObject_AlcoholFrequency := AttributeObject{ + ProfileVersions: []int{1}, + AttributeIdentifier: 49, + AttributeName: "AlcoholFrequency", + GetIsRequired: getIsRequired_No, + GetMandatoryAttributes: getMandatoryAttributes_None, + GetProfileTypes: getProfileTypes_Mate, + GetIsCanonical: getIsCanonical_Always, + CheckValueFunction: checkValueFunction_Mate1To10Rating, + } + attributeObjectsList = append(attributeObjectsList, attributeObject_AlcoholFrequency) + + attributeObject_TobaccoFrequency := AttributeObject{ + ProfileVersions: []int{1}, + AttributeIdentifier: 50, + AttributeName: "TobaccoFrequency", + GetIsRequired: getIsRequired_No, + GetMandatoryAttributes: getMandatoryAttributes_None, + GetProfileTypes: getProfileTypes_Mate, + GetIsCanonical: getIsCanonical_Always, + CheckValueFunction: checkValueFunction_Mate1To10Rating, + } + attributeObjectsList = append(attributeObjectsList, attributeObject_TobaccoFrequency) + + attributeObject_CannabisFrequency := AttributeObject{ + ProfileVersions: []int{1}, + AttributeIdentifier: 52, + AttributeName: "CannabisFrequency", + GetIsRequired: getIsRequired_No, + GetMandatoryAttributes: getMandatoryAttributes_None, + GetProfileTypes: getProfileTypes_Mate, + GetIsCanonical: getIsCanonical_Always, + CheckValueFunction: checkValueFunction_Mate1To10Rating, + } + attributeObjectsList = append(attributeObjectsList, attributeObject_CannabisFrequency) + + checkValueFunction_Language := func(profileVersion int, profileType string, input string)(bool, bool, error){ + + if (profileVersion != 1){ + return false, false, errors.New("Trying to check profile value for unknown profile version.") + } + + if (profileType != "Mate"){ + return false, false, errors.New("Invalid profile type when trying to verify Language: " + profileType) + } + + languageItemsList := strings.Split(input, "+&") + + if (len(languageItemsList) > 100){ + return false, false, nil + } + + allLanguageObjectsMap, err := worldLanguages.GetWorldLanguageObjectsMap() + if (err != nil) { return false, false, err } + + // We use this map to detect duplicates + languageNamesMap := make(map[string]struct{}) + + customLanguageExists := false + + for _, languageItem := range languageItemsList{ + + languageName, languageRating, delimiterFound := strings.Cut(languageItem, "$") + if (delimiterFound == false){ + return false, false, nil + } + + _, exists := languageNamesMap[languageName] + if (exists == true){ + return false, false, nil + } + languageNamesMap[languageName] = struct{}{} + + ratingIsValid, err := helpers.VerifyStringIsIntWithinRange(languageRating, 1, 5) + if (err != nil) { return false, false, err } + if (ratingIsValid == false){ + return false, false, nil + } + + _, exists = allLanguageObjectsMap[languageName] + if (exists == false){ + + // Language must be custom + + customLanguageExists = true + + if (languageName == ""){ + return false, false, nil + } + + isAllowed := allowedText.VerifyStringIsAllowed(languageName) + if (isAllowed == false){ + return false, false, nil + } + + containsTabsOrNewlines := helpers.CheckIfStringContainsTabsOrNewlines(languageName) + if (containsTabsOrNewlines == true){ + return false, false, nil + } + + if (len(languageName) > 30){ + return false, false, nil + } + } + } + + if (customLanguageExists == false){ + return true, true, nil + } + + return true, false, nil + } + + attributeObject_Language := AttributeObject{ + ProfileVersions: []int{1}, + AttributeIdentifier: 53, + AttributeName: "Language", + GetIsRequired: getIsRequired_No, + GetMandatoryAttributes: getMandatoryAttributes_None, + GetProfileTypes: getProfileTypes_Mate, + GetIsCanonical: getIsCanonical_Sometimes, + CheckValueFunction: checkValueFunction_Language, + } + attributeObjectsList = append(attributeObjectsList, attributeObject_Language) + + checkValueFunction_Beliefs := func(profileVersion int, profileType string, input string)(bool, bool, error){ + + if (profileVersion != 1){ + return false, false, errors.New("Trying to check profile value for unknown profile version.") + } + + if (profileType != "Mate"){ + return false, false, errors.New("Invalid profile type when trying to verify Beliefs: " + profileType) + } + + if (input == ""){ + return false, false, nil + } + + isAllowed := allowedText.VerifyStringIsAllowed(input) + if (isAllowed == false){ + return false, false, nil + } + + if (len(input) <= 1000){ + return true, false, nil + } + + return false, false, nil + } + + attributeObject_Beliefs := AttributeObject{ + ProfileVersions: []int{1}, + AttributeIdentifier: 54, + AttributeName: "Beliefs", + GetIsRequired: getIsRequired_No, + GetMandatoryAttributes: getMandatoryAttributes_None, + GetProfileTypes: getProfileTypes_Mate, + GetIsCanonical: getIsCanonical_Never, + CheckValueFunction: checkValueFunction_Beliefs, + } + attributeObjectsList = append(attributeObjectsList, attributeObject_Beliefs) + + checkValueFunction_GenderIdentity := func(profileVersion int, profileType string, input string)(bool, bool, error){ + + if (profileVersion != 1){ + return false, false, errors.New("Trying to check profile value for unknown profile version.") + } + + if (profileType != "Mate"){ + return false, false, errors.New("Invalid profile type when trying to verify GenderIdentity: " + profileType) + } + + if (input == ""){ + return false, false, nil + } + + isAllowed := allowedText.VerifyStringIsAllowed(input) + if (isAllowed == false){ + return false, false, nil + } + + containsTabsOrNewlines := helpers.CheckIfStringContainsTabsOrNewlines(input) + if (containsTabsOrNewlines == true){ + return false, false, nil + } + + if (input == "Man" || input == "Woman"){ + return true, true, nil + } + + if (len(input) <= 50){ + return true, false, nil + } + + return false, false, nil + } + + attributeObject_GenderIdentity := AttributeObject{ + ProfileVersions: []int{1}, + AttributeIdentifier: 55, + AttributeName: "GenderIdentity", + GetIsRequired: getIsRequired_No, + GetMandatoryAttributes: getMandatoryAttributes_None, + GetProfileTypes: getProfileTypes_Mate, + GetIsCanonical: getIsCanonical_Sometimes, + CheckValueFunction: checkValueFunction_GenderIdentity, + } + attributeObjectsList = append(attributeObjectsList, attributeObject_GenderIdentity) + + attributeObject_PetsRating := AttributeObject{ + ProfileVersions: []int{1}, + AttributeIdentifier: 56, + AttributeName: "PetsRating", + GetIsRequired: getIsRequired_No, + GetMandatoryAttributes: getMandatoryAttributes_None, + GetProfileTypes: getProfileTypes_Mate, + GetIsCanonical: getIsCanonical_Always, + CheckValueFunction: checkValueFunction_Mate1To10Rating, + } + attributeObjectsList = append(attributeObjectsList, attributeObject_PetsRating) + + attributeObject_DogsRating := AttributeObject{ + ProfileVersions: []int{1}, + AttributeIdentifier: 57, + AttributeName: "DogsRating", + GetIsRequired: getIsRequired_No, + GetMandatoryAttributes: getMandatoryAttributes_None, + GetProfileTypes: getProfileTypes_Mate, + GetIsCanonical: getIsCanonical_Always, + CheckValueFunction: checkValueFunction_Mate1To10Rating, + } + attributeObjectsList = append(attributeObjectsList, attributeObject_DogsRating) + + attributeObject_CatsRating := AttributeObject{ + ProfileVersions: []int{1}, + AttributeIdentifier: 58, + AttributeName: "CatsRating", + GetIsRequired: getIsRequired_No, + GetMandatoryAttributes: getMandatoryAttributes_None, + GetProfileTypes: getProfileTypes_Mate, + GetIsCanonical: getIsCanonical_Always, + CheckValueFunction: checkValueFunction_Mate1To10Rating, + } + attributeObjectsList = append(attributeObjectsList, attributeObject_CatsRating) + + getMandatoryAttributes_NaclKey := func(profileVersion int)([]string, error){ + + if (profileVersion != 1){ + return nil, errors.New("Trying to check mandatory attributes for unknown profile version.") + } + + mandatoryAttributesList := []string{"KyberKey", "DeviceIdentifier", "ChatKeysLatestUpdateTime"} + return mandatoryAttributesList, nil + } + + checkValueFunction_NaclKey := func(profileVersion int, profileType string, input string)(bool, bool, error){ + + if (profileVersion != 1){ + return false, false, errors.New("Trying to check profile value for unknown profile version.") + } + + if (profileType != "Mate" && profileType != "Moderator"){ + return false, false, errors.New("Invalid profile type when trying to verify NaclKey: " + profileType) + } + + isValid := nacl.VerifyNaclPublicKeyString(input) + if (isValid == false){ + return false, false, nil + } + return true, true, nil + } + + attributeObject_NaclKey := AttributeObject{ + ProfileVersions: []int{1}, + AttributeIdentifier: 103, + AttributeName: "NaclKey", + GetIsRequired: getIsRequired_Yes, + GetMandatoryAttributes: getMandatoryAttributes_NaclKey, + GetProfileTypes: getProfileTypes_MateOrModerator, + GetIsCanonical: getIsCanonical_Always, + CheckValueFunction: checkValueFunction_NaclKey, + } + attributeObjectsList = append(attributeObjectsList, attributeObject_NaclKey) + + getMandatoryAttributes_KyberKey := func(profileVersion int)([]string, error){ + + if (profileVersion != 1){ + return nil, errors.New("Trying to check mandatory attributes for unknown profile version.") + } + + mandatoryAttributesList := []string{"NaclKey", "DeviceIdentifier", "ChatKeysLatestUpdateTime"} + return mandatoryAttributesList, nil + } + + checkValueFunction_KyberKey := func(profileVersion int, profileType string, input string)(bool, bool, error){ + + if (profileVersion != 1){ + return false, false, errors.New("Trying to check profile value for unknown profile version.") + } + + if (profileType != "Mate" && profileType != "Moderator"){ + return false, false, errors.New("Invalid profile type when trying to verify KyberKey: " + profileType) + } + + isValid := kyber.VerifyKyberPublicKeyString(input) + if (isValid == false){ + return false, false, nil + } + return true, true, nil + } + + attributeObject_KyberKey := AttributeObject{ + ProfileVersions: []int{1}, + AttributeIdentifier: 104, + AttributeName: "KyberKey", + GetIsRequired: getIsRequired_Yes, + GetMandatoryAttributes: getMandatoryAttributes_KyberKey, + GetProfileTypes: getProfileTypes_MateOrModerator, + GetIsCanonical: getIsCanonical_Always, + CheckValueFunction: checkValueFunction_KyberKey, + } + attributeObjectsList = append(attributeObjectsList, attributeObject_KyberKey) + + getMandatoryAttributes_DeviceIdentifier := func(profileVersion int)([]string, error){ + + if (profileVersion != 1){ + return nil, errors.New("Trying to check mandatory attributes for unknown profile version.") + } + + mandatoryAttributesList := []string{"NaclKey", "KyberKey", "ChatKeysLatestUpdateTime"} + return mandatoryAttributesList, nil + } + + checkValueFunction_DeviceIdentifier := func(profileVersion int, profileType string, input string)(bool, bool, error){ + + if (profileVersion != 1){ + return false, false, errors.New("Trying to check profile value for unknown profile version.") + } + + if (profileType != "Mate" && profileType != "Moderator"){ + return false, false, errors.New("Invalid profile type when trying to verify DeviceIdentifier: " + profileType) + } + + isValid := helpers.VerifyHexString(11, input) + if (isValid == false){ + return false, false, nil + } + + return true, true, nil + } + + attributeObject_DeviceIdentifier := AttributeObject{ + ProfileVersions: []int{1}, + AttributeIdentifier: 105, + AttributeName: "DeviceIdentifier", + GetIsRequired: getIsRequired_Yes, + GetMandatoryAttributes: getMandatoryAttributes_DeviceIdentifier, + GetProfileTypes: getProfileTypes_MateOrModerator, + GetIsCanonical: getIsCanonical_Always, + CheckValueFunction: checkValueFunction_DeviceIdentifier, + } + attributeObjectsList = append(attributeObjectsList, attributeObject_DeviceIdentifier) + + getMandatoryAttributes_ChatKeysLatestUpdateTime := func(profileVersion int)([]string, error){ + + if (profileVersion != 1){ + return nil, errors.New("Trying to check mandatory attributes for unknown profile version.") + } + + mandatoryAttributesList := []string{"NaclKey", "KyberKey", "DeviceIdentifier"} + return mandatoryAttributesList, nil + } + + checkValueFunction_ChatKeysLatestUpdateTime := func(profileVersion int, profileType string, input string)(bool, bool, error){ + + if (profileVersion != 1){ + return false, false, errors.New("Trying to check profile value for unknown profile version.") + } + + if (profileType != "Mate" && profileType != "Moderator"){ + return false, false, errors.New("Invalid profile type when trying to verify ChatKeysLatestUpdateTime: " + profileType) + } + + _, err := helpers.ConvertBroadcastTimeStringToInt64(input) + if (err != nil){ + return false, false, nil + } + + return true, true, nil + } + + attributeObject_ChatKeysLatestUpdateTime := AttributeObject{ + ProfileVersions: []int{1}, + AttributeIdentifier: 106, + AttributeName: "ChatKeysLatestUpdateTime", + GetIsRequired: getIsRequired_Yes, + GetMandatoryAttributes: getMandatoryAttributes_ChatKeysLatestUpdateTime, + GetProfileTypes: getProfileTypes_MateOrModerator, + GetIsCanonical: getIsCanonical_Always, + CheckValueFunction: checkValueFunction_ChatKeysLatestUpdateTime, + } + attributeObjectsList = append(attributeObjectsList, attributeObject_ChatKeysLatestUpdateTime) + + checkValueFunction_MonogenicDiseaseVariantProbability := func(profileVersion int, profileType string, input string)(bool, bool, error){ + + if (profileVersion != 1){ + return false, false, errors.New("Trying to check profile value for unknown profile version.") + } + + if (profileType != "Mate"){ + return false, false, errors.New("Invalid profile type when trying to verify MonogenicDiseaseVariantProbability: " + profileType) + } + + isValid, err := helpers.VerifyStringIsIntWithinRange(input, 0, 100) + if (err != nil) { return false, false, err } + if (isValid == false){ + return false, false, nil + } + + return true, true, nil + } + + addMonogenicDiseaseVariantProbabilityAttribute := func(attributeIdentifier int, attributeName string){ + + attributePrefix := strings.TrimSuffix(attributeName, "ProbabilityOfPassingAVariant") + + getMandatoryAttributes_MonogenicDisease := func(profileVersion int)([]string, error){ + if (profileVersion != 1){ + return nil, errors.New("Trying to retrieve mandatory attributes for unknown profile version.") + } + + mandatoryAttributeName := attributePrefix + "NumberOfVariantsTested" + mandatoryAttributesList := []string{mandatoryAttributeName} + + return mandatoryAttributesList, nil + } + + attributeObject_MonogenicDiseaseVariantProbability := AttributeObject{ + ProfileVersions: []int{1}, + AttributeIdentifier: attributeIdentifier, + AttributeName: attributeName, + GetIsRequired: getIsRequired_No, + GetMandatoryAttributes: getMandatoryAttributes_MonogenicDisease, + GetProfileTypes: getProfileTypes_Mate, + GetIsCanonical: getIsCanonical_Always, + CheckValueFunction: checkValueFunction_MonogenicDiseaseVariantProbability, + } + attributeObjectsList = append(attributeObjectsList, attributeObject_MonogenicDiseaseVariantProbability) + } + + checkValueFunction_MonogenicDiseaseNumberOfVariantsTested := func(profileVersion int, profileType string, input string)(bool, bool, error){ + + if (profileVersion != 1){ + return false, false, errors.New("Trying to check profile value for unknown profile version.") + } + + if (profileType != "Mate"){ + return false, false, errors.New("Invalid profile type when trying to verify MonogenicDiseaseNumberOfVariantsTested: " + profileType) + } + + isValid, err := helpers.VerifyStringIsIntWithinRange(input, 1, 31) + if (err != nil) { return false, false, err } + if (isValid == false){ + return false, false, nil + } + + return true, true, nil + } + + addMonogenicDiseaseNumberOfVariantsTestedAttribute := func(attributeIdentifier int, attributeName string){ + + attributePrefix := strings.TrimSuffix(attributeName, "NumberOfVariantsTested") + + getMandatoryAttributes_MonogenicDisease := func(profileVersion int)([]string, error){ + if (profileVersion != 1){ + return nil, errors.New("Trying to retrieve mandatory attributes for unknown profile version.") + } + + mandatoryAttributeName := attributePrefix + "ProbabilityOfPassingAVariant" + mandatoryAttributesList := []string{mandatoryAttributeName} + return mandatoryAttributesList, nil + } + + attributeObject_MonogenicDiseaseVariantsTested := AttributeObject{ + ProfileVersions: []int{1}, + AttributeIdentifier: attributeIdentifier, + AttributeName: attributeName, + GetIsRequired: getIsRequired_No, + GetMandatoryAttributes: getMandatoryAttributes_MonogenicDisease, + GetProfileTypes: getProfileTypes_Mate, + GetIsCanonical: getIsCanonical_Always, + CheckValueFunction: checkValueFunction_MonogenicDiseaseNumberOfVariantsTested, + } + + attributeObjectsList = append(attributeObjectsList, attributeObject_MonogenicDiseaseVariantsTested) + } + + addMonogenicDiseaseNumberOfVariantsTestedAttribute(59, "MonogenicDisease_Cystic_Fibrosis_NumberOfVariantsTested") + addMonogenicDiseaseVariantProbabilityAttribute(60, "MonogenicDisease_Cystic_Fibrosis_ProbabilityOfPassingAVariant") + + addMonogenicDiseaseNumberOfVariantsTestedAttribute(61, "MonogenicDisease_Sickle_Cell_Anemia_NumberOfVariantsTested") + addMonogenicDiseaseVariantProbabilityAttribute(62, "MonogenicDisease_Sickle_Cell_Anemia_ProbabilityOfPassingAVariant") + + validBasesList := []string{"C", "A", "T", "G", "I", "D"} + + checkValueFunction_GenomeBasePair := func(profileVersion int, profileType string, input string)(bool, bool, error){ + + if (profileVersion != 1){ + return false, false, errors.New("Trying to check profile value for unknown profile version.") + } + + if (profileType != "Mate"){ + return false, false, errors.New("Invalid profile type when trying to verify MonogenicDiseaseNumberOfVariantsTested: " + profileType) + } + + base1, base2, delimiterFound := strings.Cut(input, ";") + if (delimiterFound == false){ + return false, false, nil + } + + baseIsValid := slices.Contains(validBasesList, base1) + if (baseIsValid == false){ + return false, false, nil + } + + baseIsValid = slices.Contains(validBasesList, base2) + if (baseIsValid == false){ + return false, false, nil + } + + return true, true, nil + } + + addLocusValueAttributeObject := func(attributeIdentifier int, attributeName string){ + + attributeObject_LocusValueBasePair := AttributeObject{ + ProfileVersions: []int{1}, + AttributeIdentifier: attributeIdentifier, + AttributeName: attributeName, + GetIsRequired: getIsRequired_No, + GetMandatoryAttributes: getMandatoryAttributes_None, + GetProfileTypes: getProfileTypes_Mate, + GetIsCanonical: getIsCanonical_Always, + CheckValueFunction: checkValueFunction_GenomeBasePair, + } + + attributeObjectsList = append(attributeObjectsList, attributeObject_LocusValueBasePair) + } + + // TODO: Add LocusIsPhased to each rsID + // Change attributeIdentifiers so: + // -Polygenic diseases are allotted the range: 1000 - 1999 + // -Monogenic diseases are allotted the range: 2000 - 9,999 + // -rsIDs are allotted the range: 10,000 - 3,000,000 (profiles will probably never share more than 500,000 loci) + + addLocusValueAttributeObject(500, "LocusValue_rs16942") + addLocusValueAttributeObject(501, "LocusValue_rs1045485") + addLocusValueAttributeObject(502, "LocusValue_rs34330") + addLocusValueAttributeObject(503, "LocusValue_rs144848") + addLocusValueAttributeObject(504, "LocusValue_rs766173") + addLocusValueAttributeObject(505, "LocusValue_rs1799950") + addLocusValueAttributeObject(506, "LocusValue_rs4986850") + addLocusValueAttributeObject(507, "LocusValue_rs2227945") + addLocusValueAttributeObject(508, "LocusValue_rs1799966") + addLocusValueAttributeObject(509, "LocusValue_rs4987117") + addLocusValueAttributeObject(510, "LocusValue_rs1799954") + addLocusValueAttributeObject(511, "LocusValue_rs11571746") + addLocusValueAttributeObject(512, "LocusValue_rs4987047") + addLocusValueAttributeObject(513, "LocusValue_rs11571833") + addLocusValueAttributeObject(514, "LocusValue_rs1801426") + addLocusValueAttributeObject(515, "LocusValue_rs3218707") + addLocusValueAttributeObject(516, "LocusValue_rs4987945") + addLocusValueAttributeObject(517, "LocusValue_rs4986761") + addLocusValueAttributeObject(518, "LocusValue_rs3218695") + addLocusValueAttributeObject(519, "LocusValue_rs1800056") + addLocusValueAttributeObject(520, "LocusValue_rs1800057") + addLocusValueAttributeObject(521, "LocusValue_rs3092856") + addLocusValueAttributeObject(522, "LocusValue_rs1800058") + addLocusValueAttributeObject(523, "LocusValue_rs1801673") + addLocusValueAttributeObject(524, "LocusValue_rs17879961") + addLocusValueAttributeObject(525, "LocusValue_rs182549") + addLocusValueAttributeObject(526, "LocusValue_rs4988235") + addLocusValueAttributeObject(527, "LocusValue_rs7349332") + addLocusValueAttributeObject(528, "LocusValue_rs11803731") + addLocusValueAttributeObject(529, "LocusValue_rs17646946") + addLocusValueAttributeObject(530, "LocusValue_rs11571747") + addLocusValueAttributeObject(531, "LocusValue_rs7779616") + addLocusValueAttributeObject(532, "LocusValue_rs892839") + addLocusValueAttributeObject(533, "LocusValue_rs1003719") + addLocusValueAttributeObject(534, "LocusValue_rs7617069") + addLocusValueAttributeObject(535, "LocusValue_rs7174027") + addLocusValueAttributeObject(536, "LocusValue_rs989869") + addLocusValueAttributeObject(537, "LocusValue_rs2342494") + addLocusValueAttributeObject(538, "LocusValue_rs1158810") + addLocusValueAttributeObject(539, "LocusValue_rs1800414") + addLocusValueAttributeObject(540, "LocusValue_rs1540771") + addLocusValueAttributeObject(541, "LocusValue_rs26722") + addLocusValueAttributeObject(542, "LocusValue_rs1939707") + addLocusValueAttributeObject(543, "LocusValue_rs1800401") + addLocusValueAttributeObject(544, "LocusValue_rs17184180") + addLocusValueAttributeObject(545, "LocusValue_rs35051352") + addLocusValueAttributeObject(546, "LocusValue_rs1800422") + addLocusValueAttributeObject(547, "LocusValue_rs784416") + addLocusValueAttributeObject(548, "LocusValue_rs7803030") + addLocusValueAttributeObject(549, "LocusValue_rs16977009") + addLocusValueAttributeObject(550, "LocusValue_rs622330") + addLocusValueAttributeObject(551, "LocusValue_rs16863422") + addLocusValueAttributeObject(552, "LocusValue_rs12896399") + addLocusValueAttributeObject(553, "LocusValue_rs2422239") + addLocusValueAttributeObject(554, "LocusValue_rs7495174") + addLocusValueAttributeObject(555, "LocusValue_rs13016869") + addLocusValueAttributeObject(556, "LocusValue_rs2835630") + addLocusValueAttributeObject(557, "LocusValue_rs3809761") + addLocusValueAttributeObject(558, "LocusValue_rs11636232") + addLocusValueAttributeObject(559, "LocusValue_rs1805008") + addLocusValueAttributeObject(560, "LocusValue_rs3212368") + addLocusValueAttributeObject(561, "LocusValue_rs894883") + addLocusValueAttributeObject(562, "LocusValue_rs10266101") + addLocusValueAttributeObject(563, "LocusValue_rs911015") + addLocusValueAttributeObject(564, "LocusValue_rs974448") + addLocusValueAttributeObject(565, "LocusValue_rs6950754") + addLocusValueAttributeObject(566, "LocusValue_rs28777") + addLocusValueAttributeObject(567, "LocusValue_rs11855019") + addLocusValueAttributeObject(568, "LocusValue_rs1042602") + addLocusValueAttributeObject(569, "LocusValue_rs1887276") + addLocusValueAttributeObject(570, "LocusValue_rs147068120") + addLocusValueAttributeObject(571, "LocusValue_rs9971729") + addLocusValueAttributeObject(572, "LocusValue_rs4911442") + addLocusValueAttributeObject(573, "LocusValue_rs6910861") + addLocusValueAttributeObject(574, "LocusValue_rs12543326") + addLocusValueAttributeObject(575, "LocusValue_rs10424065") + addLocusValueAttributeObject(576, "LocusValue_rs1978859") + addLocusValueAttributeObject(577, "LocusValue_rs6462562") + addLocusValueAttributeObject(578, "LocusValue_rs6020957") + addLocusValueAttributeObject(579, "LocusValue_rs2733832") + addLocusValueAttributeObject(580, "LocusValue_rs8039195") + addLocusValueAttributeObject(581, "LocusValue_rs2034128") + addLocusValueAttributeObject(582, "LocusValue_rs4353811") + addLocusValueAttributeObject(583, "LocusValue_rs7965082") + addLocusValueAttributeObject(584, "LocusValue_rs10265937") + addLocusValueAttributeObject(585, "LocusValue_rs12437560") + addLocusValueAttributeObject(586, "LocusValue_rs1019212") + addLocusValueAttributeObject(587, "LocusValue_rs805693") + addLocusValueAttributeObject(588, "LocusValue_rs6828137") + addLocusValueAttributeObject(589, "LocusValue_rs805694") + addLocusValueAttributeObject(590, "LocusValue_rs397723") + addLocusValueAttributeObject(591, "LocusValue_rs62330021") + addLocusValueAttributeObject(592, "LocusValue_rs1572037") + addLocusValueAttributeObject(593, "LocusValue_rs7219915") + addLocusValueAttributeObject(594, "LocusValue_rs112747614") + addLocusValueAttributeObject(595, "LocusValue_rs10237838") + addLocusValueAttributeObject(596, "LocusValue_rs138777265") + addLocusValueAttributeObject(597, "LocusValue_rs6918152") + addLocusValueAttributeObject(598, "LocusValue_rs3212369") + addLocusValueAttributeObject(599, "LocusValue_rs1005999") + addLocusValueAttributeObject(600, "LocusValue_rs1393350") + addLocusValueAttributeObject(601, "LocusValue_rs7176696") + addLocusValueAttributeObject(602, "LocusValue_rs4778241") + addLocusValueAttributeObject(603, "LocusValue_rs3940272") + addLocusValueAttributeObject(604, "LocusValue_rs2835621") + addLocusValueAttributeObject(605, "LocusValue_rs2034127") + addLocusValueAttributeObject(606, "LocusValue_rs9858909") + addLocusValueAttributeObject(607, "LocusValue_rs6020940") + addLocusValueAttributeObject(608, "LocusValue_rs2168809") + addLocusValueAttributeObject(609, "LocusValue_rs4433629") + addLocusValueAttributeObject(610, "LocusValue_rs16977002") + addLocusValueAttributeObject(611, "LocusValue_rs10843104") + addLocusValueAttributeObject(612, "LocusValue_rs3794604") + addLocusValueAttributeObject(613, "LocusValue_rs2854746") + addLocusValueAttributeObject(614, "LocusValue_rs10237488") + addLocusValueAttributeObject(615, "LocusValue_rs9971100") + addLocusValueAttributeObject(616, "LocusValue_rs2095645") + addLocusValueAttributeObject(617, "LocusValue_rs2385028") + addLocusValueAttributeObject(618, "LocusValue_rs6997494") + addLocusValueAttributeObject(619, "LocusValue_rs2422241") + addLocusValueAttributeObject(620, "LocusValue_rs6039272") + addLocusValueAttributeObject(621, "LocusValue_rs1105879") + addLocusValueAttributeObject(622, "LocusValue_rs4911414") + addLocusValueAttributeObject(623, "LocusValue_rs72928978") + addLocusValueAttributeObject(624, "LocusValue_rs73488486") + addLocusValueAttributeObject(625, "LocusValue_rs141318671") + addLocusValueAttributeObject(626, "LocusValue_rs4778211") + addLocusValueAttributeObject(627, "LocusValue_rs10237319") + addLocusValueAttributeObject(628, "LocusValue_rs4793389") + addLocusValueAttributeObject(629, "LocusValue_rs7183877") + addLocusValueAttributeObject(630, "LocusValue_rs12552712") + addLocusValueAttributeObject(631, "LocusValue_rs7628370") + addLocusValueAttributeObject(632, "LocusValue_rs1562005") + addLocusValueAttributeObject(633, "LocusValue_rs1015092") + addLocusValueAttributeObject(634, "LocusValue_rs7214306") + addLocusValueAttributeObject(635, "LocusValue_rs6056126") + addLocusValueAttributeObject(636, "LocusValue_rs11957757") + addLocusValueAttributeObject(637, "LocusValue_rs805722") + addLocusValueAttributeObject(638, "LocusValue_rs7277820") + addLocusValueAttributeObject(639, "LocusValue_rs12821256") + addLocusValueAttributeObject(640, "LocusValue_rs7552331") + addLocusValueAttributeObject(641, "LocusValue_rs17447439") + addLocusValueAttributeObject(642, "LocusValue_rs3935591") + addLocusValueAttributeObject(643, "LocusValue_rs3768056") + addLocusValueAttributeObject(644, "LocusValue_rs12913832") + addLocusValueAttributeObject(645, "LocusValue_rs7640340") + addLocusValueAttributeObject(646, "LocusValue_rs12155314") + addLocusValueAttributeObject(647, "LocusValue_rs9782955") + addLocusValueAttributeObject(648, "LocusValue_rs351385") + addLocusValueAttributeObject(649, "LocusValue_rs4790309") + addLocusValueAttributeObject(650, "LocusValue_rs937171") + addLocusValueAttributeObject(651, "LocusValue_rs4552364") + addLocusValueAttributeObject(652, "LocusValue_rs11191909") + addLocusValueAttributeObject(653, "LocusValue_rs728405") + addLocusValueAttributeObject(654, "LocusValue_rs1325127") + addLocusValueAttributeObject(655, "LocusValue_rs72777200") + addLocusValueAttributeObject(656, "LocusValue_rs2762462") + addLocusValueAttributeObject(657, "LocusValue_rs6749293") + addLocusValueAttributeObject(658, "LocusValue_rs7807181") + addLocusValueAttributeObject(659, "LocusValue_rs7966317") + addLocusValueAttributeObject(660, "LocusValue_rs2238289") + addLocusValueAttributeObject(661, "LocusValue_rs16891982") + addLocusValueAttributeObject(662, "LocusValue_rs2748901") + addLocusValueAttributeObject(663, "LocusValue_rs4053148") + addLocusValueAttributeObject(664, "LocusValue_rs116359091") + addLocusValueAttributeObject(665, "LocusValue_rs1129038") + addLocusValueAttributeObject(666, "LocusValue_rs7516150") + addLocusValueAttributeObject(667, "LocusValue_rs4648379") + addLocusValueAttributeObject(668, "LocusValue_rs13097965") + addLocusValueAttributeObject(669, "LocusValue_rs11237982") + addLocusValueAttributeObject(670, "LocusValue_rs2252893") + addLocusValueAttributeObject(671, "LocusValue_rs12906280") + addLocusValueAttributeObject(672, "LocusValue_rs11604811") + addLocusValueAttributeObject(673, "LocusValue_rs12335410") + addLocusValueAttributeObject(674, "LocusValue_rs6555969") + addLocusValueAttributeObject(675, "LocusValue_rs6478394") + addLocusValueAttributeObject(676, "LocusValue_rs2274107") + addLocusValueAttributeObject(677, "LocusValue_rs74409360") + addLocusValueAttributeObject(678, "LocusValue_rs10278187") + addLocusValueAttributeObject(679, "LocusValue_rs4633993") + addLocusValueAttributeObject(680, "LocusValue_rs2832438") + addLocusValueAttributeObject(681, "LocusValue_rs2894450") + addLocusValueAttributeObject(682, "LocusValue_rs875143") + addLocusValueAttributeObject(683, "LocusValue_rs916977") + addLocusValueAttributeObject(684, "LocusValue_rs341147") + addLocusValueAttributeObject(685, "LocusValue_rs1999527") + addLocusValueAttributeObject(686, "LocusValue_rs10234405") + addLocusValueAttributeObject(687, "LocusValue_rs2327101") + addLocusValueAttributeObject(688, "LocusValue_rs8028689") + addLocusValueAttributeObject(689, "LocusValue_rs717463") + addLocusValueAttributeObject(690, "LocusValue_rs8079498") + addLocusValueAttributeObject(691, "LocusValue_rs12593929") + addLocusValueAttributeObject(692, "LocusValue_rs12203592") + addLocusValueAttributeObject(693, "LocusValue_rs4521336") + addLocusValueAttributeObject(694, "LocusValue_rs1834640") + addLocusValueAttributeObject(695, "LocusValue_rs13098099") + addLocusValueAttributeObject(696, "LocusValue_rs975633") + addLocusValueAttributeObject(697, "LocusValue_rs13297008") + addLocusValueAttributeObject(698, "LocusValue_rs2240203") + addLocusValueAttributeObject(699, "LocusValue_rs3829241") + addLocusValueAttributeObject(700, "LocusValue_rs12694574") + addLocusValueAttributeObject(701, "LocusValue_rs2034129") + addLocusValueAttributeObject(702, "LocusValue_rs1800407") + addLocusValueAttributeObject(703, "LocusValue_rs348613") + addLocusValueAttributeObject(704, "LocusValue_rs7182710") + addLocusValueAttributeObject(705, "LocusValue_rs142317543") + addLocusValueAttributeObject(706, "LocusValue_rs7781059") + addLocusValueAttributeObject(707, "LocusValue_rs4778138") + addLocusValueAttributeObject(708, "LocusValue_rs1126809") + addLocusValueAttributeObject(709, "LocusValue_rs1408799") + addLocusValueAttributeObject(710, "LocusValue_rs1562006") + addLocusValueAttributeObject(711, "LocusValue_rs12452184") + addLocusValueAttributeObject(712, "LocusValue_rs10209564") + addLocusValueAttributeObject(713, "LocusValue_rs12913823") + addLocusValueAttributeObject(714, "LocusValue_rs11631797") + addLocusValueAttributeObject(715, "LocusValue_rs6944702") + addLocusValueAttributeObject(716, "LocusValue_rs6693258") + addLocusValueAttributeObject(717, "LocusValue_rs642742") + addLocusValueAttributeObject(718, "LocusValue_rs6795519") + addLocusValueAttributeObject(719, "LocusValue_rs6039266") + addLocusValueAttributeObject(720, "LocusValue_rs2070959") + addLocusValueAttributeObject(721, "LocusValue_rs6420484") + addLocusValueAttributeObject(722, "LocusValue_rs2835660") + addLocusValueAttributeObject(723, "LocusValue_rs12358982") + addLocusValueAttributeObject(724, "LocusValue_rs16977008") + addLocusValueAttributeObject(725, "LocusValue_rs1667394") + addLocusValueAttributeObject(726, "LocusValue_rs1426654") + addLocusValueAttributeObject(727, "LocusValue_rs1939697") + addLocusValueAttributeObject(728, "LocusValue_rs7170852") + addLocusValueAttributeObject(729, "LocusValue_rs121908120") + addLocusValueAttributeObject(730, "LocusValue_rs2327089") + addLocusValueAttributeObject(731, "LocusValue_rs911020") + addLocusValueAttributeObject(732, "LocusValue_rs6058017") + addLocusValueAttributeObject(733, "LocusValue_rs6462544") + addLocusValueAttributeObject(734, "LocusValue_rs2108166") + addLocusValueAttributeObject(735, "LocusValue_rs17252053") + addLocusValueAttributeObject(736, "LocusValue_rs9301973") + addLocusValueAttributeObject(737, "LocusValue_rs35264875") + addLocusValueAttributeObject(738, "LocusValue_rs9894429") + addLocusValueAttributeObject(739, "LocusValue_rs10485860") + addLocusValueAttributeObject(740, "LocusValue_rs1008591") + addLocusValueAttributeObject(741, "LocusValue_rs6056119") + addLocusValueAttributeObject(742, "LocusValue_rs3912104") + addLocusValueAttributeObject(743, "LocusValue_rs790464") + addLocusValueAttributeObject(744, "LocusValue_rs4778218") + addLocusValueAttributeObject(745, "LocusValue_rs1747677") + addLocusValueAttributeObject(746, "LocusValue_rs6056066") + addLocusValueAttributeObject(747, "LocusValue_rs12614022") + addLocusValueAttributeObject(748, "LocusValue_rs7799331") + addLocusValueAttributeObject(749, "LocusValue_rs1805007") + addLocusValueAttributeObject(750, "LocusValue_rs4648477") + addLocusValueAttributeObject(751, "LocusValue_rs4648478") + addLocusValueAttributeObject(752, "LocusValue_rs9692219") + + profileAttributeObjectsList = attributeObjectsList +} + + + + diff --git a/internal/profiles/profileFormat/profileFormat_test.go b/internal/profiles/profileFormat/profileFormat_test.go new file mode 100644 index 0000000..0c2bedc --- /dev/null +++ b/internal/profiles/profileFormat/profileFormat_test.go @@ -0,0 +1,259 @@ +package profileFormat_test + +import "seekia/resources/geneticReferences/monogenicDiseases" +import "seekia/resources/geneticReferences/polygenicDiseases" +import "seekia/resources/geneticReferences/traits" + +import "seekia/internal/profiles/profileFormat" +import "seekia/internal/helpers" + +import "testing" +import "strings" + +func TestProfileFormat(t *testing.T){ + + err := profileFormat.InitializeProfileFormatVariables() + if (err != nil){ + t.Fatalf("Failed to initialize profile format variables: " + err.Error()) + } + + // We use this map to detect duplicate attribute identifiers + attributeIdentifiersMap := make(map[int]struct{}) + + // We use this map to detect duplicate attribute names + attributeNamesMap := make(map[string]struct{}) + + // We store all mandatory attributes in this map + allMandatoryAttributesMap := make(map[string]struct{}) + + profileAttributeObjectsList, err := profileFormat.GetProfileAttributeObjectsList() + if (err != nil){ + t.Fatalf("GetProfileAttributeObjectsList failed: " + err.Error()) + } + + for _, attributeObject := range profileAttributeObjectsList{ + + profileVersionsList := attributeObject.ProfileVersions + attributeIdentifier := attributeObject.AttributeIdentifier + attributeName := attributeObject.AttributeName + getIsRequiredFunction := attributeObject.GetIsRequired + getMandatoryAttributesFunction := attributeObject.GetMandatoryAttributes + getProfileTypesFunction := attributeObject.GetProfileTypes + getIsCanonicalFunction := attributeObject.GetIsCanonical + checkValueFunction := attributeObject.CheckValueFunction + + if (profileVersionsList == nil){ + t.Fatalf(attributeName + " Attribute object missing profile versions list.") + } + if (attributeIdentifier == 0){ + t.Fatalf(attributeName + " Attribute object missing attribute identifier.") + } + if (attributeIdentifier < 1 || attributeIdentifier > 4294967295){ + t.Fatalf(attributeName + " Attribute object contains attribute identifier out of range.") + } + if (attributeName == ""){ + t.Fatalf(attributeName + " Attribute object missing attribute name.") + } + if (getIsRequiredFunction == nil){ + t.Fatalf(attributeName + " Attribute object missing getIsRequiredFunction") + } + if (getMandatoryAttributesFunction == nil){ + t.Fatalf(attributeName + " Attribute object missing getMandatoryAttributesFunction") + } + if (getProfileTypesFunction == nil){ + t.Fatalf(attributeName + " Attribute object missing getProfileTypesFunction") + } + if (getIsCanonicalFunction == nil){ + t.Fatalf(attributeName + " Attribute object missing getIsCanonicalFunction") + } + if (checkValueFunction == nil){ + t.Fatalf(attributeName + " Attribute object missing checkValueFunction") + } + + + if (len(profileVersionsList) != 1){ + t.Fatalf(attributeName + " Attribute object contains invalid profileVersionsList.") + } + supportedProfileVersion := profileVersionsList[0] + + if (supportedProfileVersion != 1){ + t.Fatalf(attributeName + " Attribute object contains invalid profileVersionsList.") + } + + _, exists := attributeIdentifiersMap[attributeIdentifier] + if (exists == true){ + attributeIdentifierString := helpers.ConvertIntToString(attributeIdentifier) + t.Fatalf("Duplicate attribute identifier found: " + attributeIdentifierString) + } + attributeIdentifiersMap[attributeIdentifier] = struct{}{} + + _, exists = attributeNamesMap[attributeName] + if (exists == true){ + t.Fatalf("Duplicate attribute name found: " + attributeName) + } + attributeNamesMap[attributeName] = struct{}{} + + + mandatoryAttributesList, err := getMandatoryAttributesFunction(1) + if (err != nil){ + t.Fatalf(attributeName + " Get Mandatory attributes function failed: " + err.Error()) + } + + containsDuplicates, _ := helpers.CheckIfListContainsDuplicates(mandatoryAttributesList) + if (containsDuplicates == true){ + t.Fatalf(attributeName + " Mandatory attributes list contains duplicates.") + } + + for _, attributeString := range mandatoryAttributesList{ + allMandatoryAttributesMap[attributeString] = struct{}{} + } + + profileTypesList, err := getProfileTypesFunction(1) + if (err != nil){ + t.Fatalf(attributeName + " GetProfileTypes function failed: " + err.Error()) + } + + if (len(profileTypesList) == 0 || len(profileTypesList) > 3){ + t.Fatalf(attributeName + " GetProfileTypes function returning invalid list.") + } + + // We use this map to detect duplicates + profileTypesMap := make(map[string]struct{}) + + for _, profileType := range profileTypesList{ + + _, exists := profileTypesMap[profileType] + if (exists == true){ + t.Fatalf(attributeName + " GetProfileTypes function returning list with duplicates.") + } + profileTypesMap[profileType] = struct{}{} + + if (profileType != "Mate" && profileType != "Host" && profileType != "Moderator"){ + t.Fatalf(attributeName + " GetProfileTypes function returning invalid profile type: " + profileType) + } + } + + isCanonical, err := getIsCanonicalFunction(1) + if (err != nil){ + t.Fatalf(attributeName + " GetIsCanonical failed to return:" + err.Error()) + } + if (isCanonical != "Always" && isCanonical != "Sometimes" && isCanonical != "Never"){ + t.Fatalf(attributeName + " GetIsCanonical returning invalid isCanonical: " + isCanonical) + } + } + + for mandatoryAttribute, _ := range allMandatoryAttributesMap{ + + _, exists := attributeNamesMap[mandatoryAttribute] + if (exists == false){ + t.Fatalf("Mandatory attribute object not found: " + mandatoryAttribute) + } + } +} + + +func TestProfileGeneticReferences(t *testing.T){ + + // We use this to make sure all genetic attributes exist. + + err := profileFormat.InitializeProfileFormatVariables() + if (err != nil){ + t.Fatalf("Failed to initialize profile format variables: " + err.Error()) + } + + attributeObjectsByNameMap, err := profileFormat.GetProfileAttributeObjectsByNameMap() + if (err != nil){ + t.Fatalf("GetProfileAttributeObjectsByNameMap failed: " + err.Error()) + } + + monogenicDiseases.InitializeMonogenicDiseaseVariables() + + monogenicDiseaseNamesList, err := monogenicDiseases.GetMonogenicDiseaseNamesList() + if (err != nil){ + t.Fatalf("GetMonogenicDiseaseNamesList failed: " + err.Error()) + } + + for _, monogenicDiseaseName := range monogenicDiseaseNamesList{ + + diseaseNameWithUnderscores := strings.ReplaceAll(monogenicDiseaseName, " ", "_") + + probabilityOfPassingVariantAttributeName := "MonogenicDisease_" + diseaseNameWithUnderscores + "_ProbabilityOfPassingAVariant" + + variantsTestedAttributeName := "MonogenicDisease_" + diseaseNameWithUnderscores + "_NumberOfVariantsTested" + + _, attributeExists := attributeObjectsByNameMap[probabilityOfPassingVariantAttributeName] + if (attributeExists == false){ + t.Fatalf("Profile format missing attribute:" + probabilityOfPassingVariantAttributeName) + } + + _, attributeExists = attributeObjectsByNameMap[variantsTestedAttributeName] + if (attributeExists == false){ + t.Fatalf("Profile format missing attribute:" + variantsTestedAttributeName) + } + } + + polygenicDiseases.InitializePolygenicDiseaseVariables() + + polygenicDiseaseObjectsList, err := polygenicDiseases.GetPolygenicDiseaseObjectsList() + if (err != nil) { + t.Fatalf("GetPolygenicDiseaseObjectsList failed: " + err.Error()) + } + + for _, diseaseObject := range polygenicDiseaseObjectsList{ + + diseaseLociList := diseaseObject.LociList + + for _, locusObject := range diseaseLociList{ + + locusRSID := locusObject.LocusRSID + + locusRSIDString := helpers.ConvertInt64ToString(locusRSID) + + locusValueAttributeName := "LocusValue_rs" + locusRSIDString + + _, exists := attributeObjectsByNameMap[locusValueAttributeName] + if (exists == false){ + t.Fatalf("Profile format missing attribute: " + locusValueAttributeName) + } + } + } + + traits.InitializeTraitVariables() + + traitObjectsList, err := traits.GetTraitObjectsList() + if (err != nil){ + t.Fatalf("GetTraitObjectsList failed: " + err.Error()) + } + + // This map stores the rsID attributes which are missing from the profile format + missingLociMap := make(map[string]struct{}) + + for _, traitObject := range traitObjectsList{ + + traitLociList := traitObject.LociList + + for _, rsID := range traitLociList{ + + locusRSIDString := helpers.ConvertInt64ToString(rsID) + + locusValueAttributeName := "LocusValue_rs" + locusRSIDString + + _, exists := attributeObjectsByNameMap[locusValueAttributeName] + if (exists == false){ + missingLociMap[locusValueAttributeName] = struct{}{} + } + } + } + + if (len(missingLociMap) != 0){ + + missingLociList := helpers.GetListOfMapKeys(missingLociMap) + + missingLociListString := strings.Join(missingLociList, ", ") + + t.Fatalf("Profile format missing rsID attribute(s): " + missingLociListString) + } +} + + + diff --git a/internal/profiles/profileStorage/profileStorage.go b/internal/profiles/profileStorage/profileStorage.go new file mode 100644 index 0000000..601491f --- /dev/null +++ b/internal/profiles/profileStorage/profileStorage.go @@ -0,0 +1,333 @@ + +// profileStorage provides functions to store and retrieve downloaded profiles + +package profileStorage + +//TODO: Add a job to prune database entries for profiles/attributes which have been deleted + +import "seekia/internal/badgerDatabase" +import "seekia/internal/contentMetadata" +import "seekia/internal/encoding" +import "seekia/internal/helpers" +import "seekia/internal/identity" +import "seekia/internal/mySettings" +import "seekia/internal/profiles/readProfiles" + +import messagepack "github.com/vmihailenco/msgpack/v5" + +import "errors" +import "slices" + +func GetNumberOfStoredProfiles()(int64, error){ + + numberOfProfiles, err := badgerDatabase.GetNumberOfUserProfiles() + if (err != nil) { return 0, err } + + return numberOfProfiles, nil +} + +// Returns all identity hashes of all authors of profiles in the database +func GetAllStoredProfileIdentityHashes()([][16]byte, error){ + + mateProfileIdentityHashesList, err := badgerDatabase.GetAllProfileIdentityHashes("Mate") + if (err != nil) { return nil, err } + + hostProfileIdentityHashesList, err := badgerDatabase.GetAllProfileIdentityHashes("Host") + if (err != nil) { return nil, err } + + moderatorProfileIdentityHashesList, err := badgerDatabase.GetAllProfileIdentityHashes("Moderator") + if (err != nil) { return nil, err } + + allProfileIdentityHashesList := slices.Concat(mateProfileIdentityHashesList, hostProfileIdentityHashesList, moderatorProfileIdentityHashesList) + + return allProfileIdentityHashesList, nil +} + +func GetAllStoredProfileHashes()([][28]byte, error){ + + allMateProfileHashesList, err := badgerDatabase.GetAllProfileHashes("Mate") + if (err != nil) { return nil, err } + + allHostProfileHashesList, err := badgerDatabase.GetAllProfileHashes("Host") + if (err != nil) { return nil, err } + + allModeratorProfileHashesList, err := badgerDatabase.GetAllProfileHashes("Moderator") + if (err != nil) { return nil, err } + + allProfileHashesList := slices.Concat(allMateProfileHashesList, allHostProfileHashesList, allModeratorProfileHashesList) + + return allProfileHashesList, nil +} + + +//Outputs: +// -bool: Profile exists +// -[]byte: Profile Bytes +// -error +func GetStoredProfile(profileHash [28]byte)(bool, []byte, error){ + + profileType, _, err := readProfiles.ReadProfileHashMetadata(profileHash) + if (err != nil) { + profileHashHex := encoding.EncodeBytesToHexString(profileHash[:]) + return false, nil, errors.New("GetStoredProfile called with invalid profileHash: " + profileHashHex) + } + + exists, profileBytes, err := badgerDatabase.GetUserProfile(profileType, profileHash) + if (err != nil) { return false, nil, err } + if (exists == false){ + return false, nil, nil + } + + return true, profileBytes, nil +} + + +// This function verifies the profile file is valid and adds the profile to the database +//Outputs: +// -bool: Profile is well formed +// -[28]byte: Profile hash of added profile +// -error +func AddUserProfile(inputProfile []byte)(bool, [28]byte, error){ + + ableToRead, newProfileHash, newProfileVersion, newProfileNetworkType, userIdentityHash, _, _, rawProfileMap, err := readProfiles.ReadProfileAndHash(true, inputProfile) + if (err != nil) { return false, [28]byte{}, err } + if (ableToRead == false){ + // Profile is invalid, host that we downloaded from must be malicious, or profile version is newer than our own. + return false, [28]byte{}, nil + } + + userIdentityType, err := identity.GetIdentityTypeFromIdentityHash(userIdentityHash) + if (err != nil) { + identityHashHex := encoding.EncodeBytesToHexString(userIdentityHash[:]) + return false, [28]byte{}, errors.New("IdentityHash invalid after profile has been verified: " + identityHashHex) + } + + exists, _, err := badgerDatabase.GetUserProfile(userIdentityType, newProfileHash) + if (err != nil) { return false, [28]byte{}, err } + if (exists == true){ + // Profile already imported, skip it. + return true, newProfileHash, nil + } + + err = badgerDatabase.AddUserProfile(userIdentityType, newProfileHash, inputProfile) + if (err != nil) { return false, [28]byte{}, err } + + err = badgerDatabase.AddIdentityProfileHash(userIdentityHash, newProfileHash) + if (err != nil) { return false, [28]byte{}, err } + + profileAttributeHashesMap, _, err := readProfiles.GetProfileAttributeHashesMap(userIdentityHash, newProfileVersion, newProfileNetworkType, rawProfileMap) + if (err != nil) { return false, [28]byte{}, err } + + for _, attributeHash := range profileAttributeHashesMap{ + + err := badgerDatabase.AddAttributeProfile(attributeHash, newProfileHash) + if (err != nil) { return false, [28]byte{}, err } + } + + if (userIdentityType == "Mate"){ + + err = mySettings.SetSetting("MyMatchesNeedRefreshYesNo", "Yes") + if (err != nil) { return false, [28]byte{}, err } + + } else if (userIdentityType == "Host"){ + + err = mySettings.SetSetting("ViewedHostsNeedsRefreshYesNo", "Yes") + if (err != nil) { return false, [28]byte{}, err } + + } else if (userIdentityType == "Moderator"){ + + err = mySettings.SetSetting("ViewedModeratorsNeedsRefreshYesNo", "Yes") + if (err != nil) { return false, [28]byte{}, err } + } + + err = mySettings.SetSetting("ViewedContentNeedsRefreshYesNo", "Yes") + if (err != nil) { return false, [28]byte{}, err } + + return true, newProfileHash, nil +} + +// This function will return a user's newest profile. Viewable status (approved/banned status) is ignored +//Outputs: +// -bool: Profile exists +// -int: Profile version +// -[28]byte: Profile hash +// -[]byte: Profile bytes +// -int64: Profile broadcast time +// -map[int]messagepack.RawMessage: Raw profile map +// -error +func GetNewestUserProfile(userIdentityHash [16]byte, networkType byte)(bool, int, [28]byte, []byte, int64, map[int]messagepack.RawMessage, error){ + + identityType, err := identity.GetIdentityTypeFromIdentityHash(userIdentityHash) + if (err != nil) { + userIdentityHashHex := encoding.EncodeBytesToHexString(userIdentityHash[:]) + return false, 0, [28]byte{}, nil, 0, nil, errors.New("GetNewestUserProfile called with invalid userIdentityHash: " + userIdentityHashHex) + } + + isValid := helpers.VerifyNetworkType(networkType) + if (isValid == false){ + networkTypeString := helpers.ConvertByteToString(networkType) + return false, 0, [28]byte{}, nil, 0, nil, errors.New("GetNewestUserProfile called with invalid networkType: " + networkTypeString) + } + + exists, profileHashesList, err := badgerDatabase.GetIdentityProfileHashesList(userIdentityHash) + if (err != nil) { return false, 0, [28]byte{}, nil, 0, nil, err } + if (exists == false){ + return false, 0, [28]byte{}, nil, 0, nil, nil + } + + anyProfileFound := false + + //TODO: Deal with 2 profiles with identical broadcastTimes + // We must compare the profile hashes + + var newestProfileHash [28]byte + newestProfileVersion := 0 + newestProfileBroadcastTime := int64(0) + newestProfileBytes := make([]byte, 0) + newestProfileRawProfileMap := make(map[int]messagepack.RawMessage) + + for _, profileHash := range profileHashesList{ + + exists, profileBytes, err := badgerDatabase.GetUserProfile(identityType, profileHash) + if (err != nil) { return false, 0, [28]byte{}, nil, 0, nil, err } + if (exists == false){ + // Identity profile hashes list is outdated, it will be updated by databaseJobs + continue + } + + ableToRead, profileVersion, profileNetworkType, profileIdentityHash, profileBroadcastTime, _, rawProfileMap, err := readProfiles.ReadProfile(false, profileBytes) + if (err != nil) { return false, 0, [28]byte{}, nil, 0, nil, err } + if (ableToRead == false){ + return false, 0, [28]byte{}, nil, 0, nil, errors.New("Database corrupt: Contains invalid profile.") + } + if (profileIdentityHash != userIdentityHash){ + return false, 0, [28]byte{}, nil, 0, nil, errors.New("Database corrupt: identity profiles list contains profile with different identityHash") + } + if (profileNetworkType != networkType){ + // This profile belongs to a different network type + // Example of network types: Mainnet, Testnet1 + continue + } + + if (anyProfileFound == false || profileBroadcastTime > newestProfileBroadcastTime){ + newestProfileVersion = profileVersion + newestProfileHash = profileHash + newestProfileBroadcastTime = profileBroadcastTime + newestProfileRawProfileMap = rawProfileMap + newestProfileBytes = profileBytes + } + + anyProfileFound = true + } + + if (anyProfileFound == false){ + return false, 0, [28]byte{}, nil, 0, nil, nil + } + + return true, newestProfileVersion, newestProfileHash, newestProfileBytes, newestProfileBroadcastTime, newestProfileRawProfileMap, nil +} + + + + +// This function is used to find metadata about a profile attribute +// We search through all stored profiles to find a profile that the attribute belongs to +// We may be able to verify the profile author while we are missing the actual profile content +// That would happen if we have saved the profile metadata and deleted the profile +// +//Outputs: +// -bool: Metadata exists +// -int: Attribute identifier +// -[16]byte: Attribute author identity hash +// -byte: Attribute network type +// -[][28]byte: List of profile hashes with this attribute (they may be deleted/expired) +// -error +func GetProfileAttributeMetadata(inputAttributeHash [27]byte)(bool, int, [16]byte, byte, [][28]byte, error){ + + isValid, err := readProfiles.VerifyAttributeHash(inputAttributeHash, false, "", false, false) + if (err != nil) { return false, 0, [16]byte{}, 0, nil, err } + if (isValid == false){ + inputAttributeHashHex := encoding.EncodeBytesToHexString(inputAttributeHash[:]) + return false, 0, [16]byte{}, 0, nil, errors.New("GetProfileAttributeMetadata called with invalid attributeHash: " + inputAttributeHashHex) + } + + anyExist, attributeProfileHashesList, err := badgerDatabase.GetAttributeProfilesList(inputAttributeHash) + if (err != nil) { return false, 0, [16]byte{}, 0, nil, err } + if (anyExist == false){ + return false, 0, [16]byte{}, 0, nil, nil + } + + for _, profileHash := range attributeProfileHashesList{ + + profileMetadataExists, _, profileNetworkType, profileAuthor, _, profileIsDisabled, _, profileAttributeHashesMap, err := contentMetadata.GetProfileMetadata(profileHash) + if (err != nil) { return false, 0, [16]byte{}, 0, nil, err } + if (profileMetadataExists == false){ + continue + } + if (profileIsDisabled == true){ + // The profile is disabled, the attribute hash cannot belong to it. + return false, 0, [16]byte{}, 0, nil, errors.New("AttributeProfilesList contains disabled profile.") + } + + for attributeIdentifier, profileAttributeHash := range profileAttributeHashesMap{ + + if (profileAttributeHash == inputAttributeHash){ + return true, attributeIdentifier, profileAuthor, profileNetworkType, attributeProfileHashesList, nil + } + } + + // This is only reached if the profile did not contain the attribute + return false, 0, [16]byte{}, 0, nil, errors.New("AttributeProfilesList contains profile which does not contain the attribute.") + } + + // We could not find the attribute's metadata + + return false, 0, [16]byte{}, 0, attributeProfileHashesList, nil +} + + +// This function is the same as GetProfileAttributeMetadata, except it attempts to find a full profile which contains the attribute +//Outputs: +// -bool: Metadata exists +// -int: Attribute identifier +// -[16]byte: Attribute author identity hash +// -byte: Attribute network type +// -[][28]byte: List of profile hashes with this attribute (they may be deleted/expired) +// -bool: Full Profile exists +// -[28]byte: Full profile hash +// -[]byte: Full Profile bytes +// -error +func GetProfileAttributeMetadataAndProfile(inputAttributeHash [27]byte)(bool, int, [16]byte, byte, [][28]byte, bool, [28]byte, []byte, error){ + + isValid, err := readProfiles.VerifyAttributeHash(inputAttributeHash, false, "", false, false) + if (err != nil) { return false, 0, [16]byte{}, 0, nil, false, [28]byte{}, nil, err } + if (isValid == false){ + inputAttributeHashHex := encoding.EncodeBytesToHexString(inputAttributeHash[:]) + return false, 0, [16]byte{}, 0, nil, false, [28]byte{}, nil, errors.New("GetProfileAttributeMetadataAndProfile called with invalid attributeHash: " + inputAttributeHashHex) + } + + metadataExists, attributeIdentifier, attributeAuthor, attributeNetworkType, attributeProfileHashesList, err := GetProfileAttributeMetadata(inputAttributeHash) + if (err != nil) { return false, 0, [16]byte{}, 0, nil, false, [28]byte{}, nil, err } + if (metadataExists == false){ + + // Attribute metadata does not exist. + + return false, 0, [16]byte{}, 0, nil, false, [28]byte{}, nil, nil + } + + // Now we try to find a full profile which contains this attribute + + for _, profileHash := range attributeProfileHashesList{ + + profileExists, profileBytes, err := GetStoredProfile(profileHash) + if (err != nil) { return false, 0, [16]byte{}, 0, nil, false, [28]byte{}, nil, err } + if (profileExists == true){ + return true, attributeIdentifier, attributeAuthor, attributeNetworkType, attributeProfileHashesList, true, profileHash, profileBytes, nil + } + } + + // We cannot find a full profile containing this attribute + + return true, attributeIdentifier, attributeAuthor, attributeNetworkType, attributeProfileHashesList, false, [28]byte{}, nil, nil +} + diff --git a/internal/profiles/readProfiles/readProfiles.go b/internal/profiles/readProfiles/readProfiles.go new file mode 100644 index 0000000..b81e466 --- /dev/null +++ b/internal/profiles/readProfiles/readProfiles.go @@ -0,0 +1,822 @@ + +// readProfiles provides functions to read and validate user profiles + +package readProfiles + + +import "seekia/internal/cryptography/blake3" +import "seekia/internal/cryptography/edwardsKeys" +import "seekia/internal/encoding" +import "seekia/internal/helpers" +import "seekia/internal/identity" +import "seekia/internal/profiles/profileFormat" + +import messagepack "github.com/vmihailenco/msgpack/v5" + +import "strings" +import "encoding/binary" +import "errors" +import "slices" + +// Verifies a profile +//Outputs: +// -bool: Is valid +// -error: Will return error if there is a bug in the function +func VerifyProfile(inputProfile []byte)(bool, error){ + + ableToRead, _, _, _, _, _, _, err := ReadProfile(true, inputProfile) + if (err != nil) { return false, err } + if (ableToRead == false){ + return false, nil + } + + return true, nil +} + +func VerifyProfileHash(inputHash [28]byte, profileTypeProvided bool, expectedProfileType string, isDisabledProvided bool, expectedIsDisabled bool)(bool, error){ + + if (profileTypeProvided == true){ + if (expectedProfileType != "Mate" && expectedProfileType != "Host" && expectedProfileType != "Moderator"){ + return false, errors.New("VerifyProfileHash called with invalid provided expectedProfileType: " + expectedProfileType) + } + } + + profileType, profileIsDisabled, err := ReadProfileHashMetadata(inputHash) + if (err != nil){ + return false, nil + } + if (profileTypeProvided == true){ + if (profileType != expectedProfileType){ + return false, nil + } + } + if (isDisabledProvided == true){ + if (profileIsDisabled != expectedIsDisabled){ + return false, nil + } + } + + return true, nil +} + + +func VerifyAttributeHash(inputHash [27]byte, profileTypeProvided bool, expectedProfileType string, isCanonicalProvided bool, expectedIsCanonical bool)(bool, error){ + + if (profileTypeProvided == true){ + if (expectedProfileType != "Mate" && expectedProfileType != "Host" && expectedProfileType != "Moderator"){ + return false, errors.New("VerifyAttributeHash called with invalid provided expectedProfileType: " + expectedProfileType) + } + } + + profileType, attributeIsCanonical, err := ReadAttributeHashMetadata(inputHash) + if (err != nil){ + return false, nil + } + if (profileTypeProvided == true){ + if (profileType != expectedProfileType){ + return false, nil + } + } + if (isCanonicalProvided == true){ + if (attributeIsCanonical != expectedIsCanonical){ + return false, nil + } + } + + return true, nil +} + +//Outputs: +// -string: Profile Type +// -bool: Profile is disabled +// -error: +func ReadProfileHashMetadata(profileHash [28]byte)(string, bool, error){ + + metadataByte := profileHash[27] + + switch metadataByte{ + + case 1:{ + return "Mate", true, nil + } + case 2:{ + return "Mate", false, nil + } + case 3:{ + return "Host", true, nil + } + case 4:{ + return "Host", false, nil + } + case 5:{ + return "Moderator", true, nil + } + case 6:{ + return "Moderator", false, nil + } + } + + profileHashHex := encoding.EncodeBytesToHexString(profileHash[:]) + + return "", false, errors.New("ReadProfileHashMetadata called with invalid profileHash: " + profileHashHex) +} + +//Outputs: +// -string: Identity Type of author +// -bool: Attribute is canonical +// -error: +func ReadAttributeHashMetadata(attributeHash [27]byte)(string, bool, error){ + + metadataByte := attributeHash[26] + + switch metadataByte{ + + case 1:{ + return "Mate", true, nil + } + case 2:{ + return "Mate", false, nil + } + case 3:{ + return "Host", true, nil + } + case 4:{ + return "Host", false, nil + } + case 5:{ + return "Moderator", true, nil + } + case 6:{ + return "Moderator", false, nil + } + } + + attributeHashHex := encoding.EncodeBytesToHexString(attributeHash[:]) + + return "", false, errors.New("ReadAttributeHashMetadata called with invalid attributeHash: " + attributeHashHex) +} + +// This function will read a profile and compute its hash. +//Outputs: +// -bool: Able to read profile +// -[28]byte: Profile hash +// -int: Profile version +// -byte: Network type (1 == Mainnet, 2 == Testnet1) +// -[16]byte: Profile author identity hash +// -int64: Profile broadcast time (alleged, can be faked) +// -bool: Profile is disabled +// -map[int]messagepack.RawMessage: Raw profile map (Attribute Identifier -> Attribute messagepack bytes value) +// -error (will return err if there is a bug) +func ReadProfileAndHash(verifyProfile bool, inputProfile []byte)(bool, [28]byte, int, byte, [16]byte, int64, bool, map[int]messagepack.RawMessage, error){ + + ableToRead, profileVersion, networkType, profileAuthor, profileBroadcastTime, profileIsDisabled, rawProfileMap, err := ReadProfile(verifyProfile, inputProfile) + if (err != nil) { return false, [28]byte{}, 0, 0, [16]byte{}, 0, false, nil, err } + if (ableToRead == false){ + return false, [28]byte{}, 0, 0, [16]byte{}, 0, false, nil, nil + } + + profileHashWithoutMetadataByte, err := blake3.GetBlake3HashAsBytes(27, inputProfile) + if (err != nil) { return false, [28]byte{}, 0, 0, [16]byte{}, 0, false, nil, err } + + profileAuthorIdentityType, err := identity.GetIdentityTypeFromIdentityHash(profileAuthor) + if (err != nil) { return false, [28]byte{}, 0, 0, [16]byte{}, 0, false, nil, err } + + getProfileHashMetadataByte := func()byte{ + + if (profileAuthorIdentityType == "Mate"){ + + if (profileIsDisabled == true){ + return 1 + } + + return 2 + + } else if (profileAuthorIdentityType == "Host"){ + + if (profileIsDisabled == true){ + return 3 + } + + return 4 + } + // profileAuthorIdentityType == "Moderator" + + if (profileIsDisabled == true){ + return 5 + } + + return 6 + } + + profileHashMetadataByte := getProfileHashMetadataByte() + + profileHashSlice := append(profileHashWithoutMetadataByte, profileHashMetadataByte) + + profileHash := [28]byte(profileHashSlice) + + return true, profileHash, profileVersion, networkType, profileAuthor, profileBroadcastTime, profileIsDisabled, rawProfileMap, nil +} + + +// This function reads a profile without computing the profile's hash +// This is faster because the profile's bytes do not need to be hashed +//Outputs: +// -bool: Able to read profile +// -int: Profile version +// -byte: Network type (1 == Mainnet, 2 == Testnet1) +// -[16]byte: Profile author identity hash +// -int64: Profile broadcast time (alleged, can be faked) +// -bool: Profile is disabled +// -map[int]messagepack.RawMessage: Raw profile map (Attribute identifier -> Attribute raw bytes) +// -error (will return err if there is a bug) +func ReadProfile(verifyProfile bool, inputProfile []byte)(bool, int, byte, [16]byte, int64, bool, map[int]messagepack.RawMessage, error){ + + var profileSlice []messagepack.RawMessage + + err := messagepack.Unmarshal(inputProfile, &profileSlice) + if (err != nil){ + // Profile is malformed: Invalid profile messagepack + return false, 0, 0, [16]byte{}, 0, false, nil, nil + } + + if (len(profileSlice) != 2){ + // Profile is malformed: Invalid profile messagepack + return false, 0, 0, [16]byte{}, 0, false, nil, nil + } + + profileSignatureEncoded := profileSlice[0] + profileContentEncoded := profileSlice[1] + + profileSignature, err := encoding.DecodeRawMessagePackTo64ByteArray(profileSignatureEncoded) + if (err != nil){ + // Profile is malformed: Invalid profile signature + return false, 0, 0, [16]byte{}, 0, false, nil, nil + } + + // This map will contain the profile content + // The fields are encoded as identifiers, which need to be converted to names + // Identifiers are integers, which we use instead of encoding the full name of the attribute + // We do this to make profiles smaller + // For example, 3 == "ProfileType" + rawProfileMap := make(map[int]messagepack.RawMessage) + + err = encoding.DecodeMessagePackBytes(false, profileContentEncoded, &rawProfileMap) + if (err != nil) { + // Profile is malformed: Profile content map is invalid + return false, 0, 0, [16]byte{}, 0, false, nil, nil + } + + profileVersionEncoded, exists := rawProfileMap[1] + if (exists == false) { + // Profile is malformed: Missing profileVersion + return false, 0, 0, [16]byte{}, 0, false, nil, nil + } + + profileVersion, err := encoding.DecodeRawMessagePackToInt(profileVersionEncoded) + if (err != nil){ + // Profile is malformed: Invalid profile version + return false, 0, 0, [16]byte{}, 0, false, nil, nil + } + + if (profileVersion != 1){ + // We cannot read this profile. It was created by an newer version of Seekia. + return false, 0, 0, [16]byte{}, 0, false, nil, nil + } + + networkTypeEncoded, exists := rawProfileMap[51] + if (exists == false) { + // Profile is malformed: Missing NetworkType + return false, 0, 0, [16]byte{}, 0, false, nil, nil + } + + networkType, err := encoding.DecodeRawMessagePackToByte(networkTypeEncoded) + if (err != nil){ + // Profile is malformed: Invalid network type + return false, 0, 0, [16]byte{}, 0, false, nil, nil + } + + isValid := helpers.VerifyNetworkType(networkType) + if (isValid == false){ + // Profile is malformed: Invalid network type + return false, 0, 0, [16]byte{}, 0, false, nil, nil + } + + identityKeyEncoded, exists := rawProfileMap[2] + if (exists == false) { + // Profile is malformed: Missing identity key. + return false, 0, 0, [16]byte{}, 0, false, nil, nil + } + + identityKey, err := encoding.DecodeRawMessagePackTo32ByteArray(identityKeyEncoded) + if (err != nil){ + // Profile is malformed: Invalid identity key + return false, 0, 0, [16]byte{}, 0, false, nil, nil + } + + profileTypeEncoded, exists := rawProfileMap[3] + if (exists == false) { + // Profile is malformed: missing ProfileType. + return false, 0, 0, [16]byte{}, 0, false, nil, nil + } + + profileType, err := encoding.DecodeRawMessagePackToString(profileTypeEncoded) + if (err != nil){ + // Profile is malformed: Invalid ProfileType. + return false, 0, 0, [16]byte{}, 0, false, nil, nil + } + + if (profileType != "Mate" && profileType != "Host" && profileType != "Moderator"){ + // Profile is malformed: Invalid ProfileType. + return false, 0, 0, [16]byte{}, 0, false, nil, nil + } + + identityHash, err := identity.ConvertIdentityKeyToIdentityHash(identityKey, profileType) + if (err != nil) { return false, 0, 0, [16]byte{}, 0, false, nil, err } + + broadcastTimeEncoded, exists := rawProfileMap[4] + if (exists == false) { + // Profile is malformed: missing BroadcastTime + return false, 0, 0, [16]byte{}, 0, false, nil, nil + } + + broadcastTimeInt64, err := encoding.DecodeRawMessagePackToInt64(broadcastTimeEncoded) + if (err != nil) { + // Profile is malformed: Contains invalid BroadcastTime + return false, 0, 0, [16]byte{}, 0, false, nil, nil + } + + if (verifyProfile == true){ + + isValid := helpers.VerifyBroadcastTime(broadcastTimeInt64) + if (isValid == false){ + // Profile is malformed: Contains invalid BroadcastTime + return false, 0, 0, [16]byte{}, 0, false, nil, nil + } + } + + if (verifyProfile == true){ + + contentHash, err := blake3.Get32ByteBlake3Hash(profileContentEncoded) + if (err != nil) { return false, 0, 0, [16]byte{}, 0, false, nil, err } + + signatureIsValid := edwardsKeys.VerifySignature(identityKey, profileSignature, contentHash) + if (signatureIsValid == false) { + // Profile is malformed: Invalid signature. + return false, 0, 0, [16]byte{}, 0, false, nil, nil + } + } + + isDisabledEncoded, exists := rawProfileMap[5] + if (exists == true){ + + // Profile is disabled. + + isDisabledBool, err := encoding.DecodeRawMessagePackToBool(isDisabledEncoded) + if (err != nil){ + // Profile is malformed: Invalid IsDisabled attribute. + return false, 0, 0, [16]byte{}, 0, false, nil, nil + } + + if (isDisabledBool == false){ + // Profile is malformed: Invalid Disabled attribute. + return false, 0, 0, [16]byte{}, 0, false, nil, nil + } + + if (len(rawProfileMap) != 6){ + // Profile is malformed: Invalid Disabled profile. + return false, 0, 0, [16]byte{}, 0, false, nil, nil + } + + return true, profileVersion, networkType, identityHash, broadcastTimeInt64, true, rawProfileMap, nil + } + + if (verifyProfile == true){ + + profileAttributeObjectsList, err := profileFormat.GetProfileAttributeObjectsList() + if (err != nil) { return false, 0, 0, [16]byte{}, 0, false, nil, err } + + for _, attributeObject := range profileAttributeObjectsList{ + + attributeName := attributeObject.AttributeName + + if (attributeName == "Disabled" || attributeName == "ProfileType" || attributeName == "IdentityKey" || attributeName == "ProfileVersion" || attributeName == "NetworkType" || attributeName == "BroadcastTime"){ + // We already verified these attributes + continue + } + + attributeProfileVersions := attributeObject.ProfileVersions + + isRelevantVersion := slices.Contains(attributeProfileVersions, profileVersion) + if (isRelevantVersion == false){ + // This attribute does not belong to this profile version + // Profiles created in this version do not have this attribute + continue + } + + attributeProfileTypes, err := attributeObject.GetProfileTypes(profileVersion) + if (err != nil) { return false, 0, 0, [16]byte{}, 0, false, nil, err } + + isRelevantProfileType := slices.Contains(attributeProfileTypes, profileType) + if (isRelevantProfileType == false){ + continue + } + + attributeIdentifier := attributeObject.AttributeIdentifier + + attributeIsRequired, err := attributeObject.GetIsRequired(profileVersion) + if (err != nil) { return false, 0, 0, [16]byte{}, 0, false, nil, err } + + attributeValueBytes, exists := rawProfileMap[attributeIdentifier] + if (exists == false){ + + if (attributeIsRequired == true){ + // Profile is malformed: Profile is missing a required attribute + return false, 0, 0, [16]byte{}, 0, false, nil, nil + } + + continue + } + + attributeIsValid, attributeValueString, err := formatProfileAttributeRawMessagePackToString(attributeName, attributeValueBytes) + if (err != nil) { return false, 0, 0, [16]byte{}, 0, false, nil, err } + if (attributeIsValid == false){ + // Profile is malformed: Profile contains an invalid attribute value + return false, 0, 0, [16]byte{}, 0, false, nil, nil + } + + attributeValueIsValid, _, err := attributeObject.CheckValueFunction(profileVersion, profileType, attributeValueString) + if (err != nil){ return false, 0, 0, [16]byte{}, 0, false, nil, err } + if (attributeValueIsValid == false){ + // Profile is malformed: Profile contains an invalid attribute value + return false, 0, 0, [16]byte{}, 0, false, nil, nil + } + + if (attributeIsRequired == false){ + + // We make sure profile has all the mandatory attributes for this attribute + // We don't need to perform this check if the attribute is required + + mandatoryAttributesList, err := attributeObject.GetMandatoryAttributes(profileVersion) + if (err != nil){ return false, 0, 0, [16]byte{}, 0, false, nil, err } + + for _, mandatoryAttributeName := range mandatoryAttributesList{ + + mandatoryAttributeIdentifier, err := profileFormat.GetAttributeIdentifierFromAttributeName(mandatoryAttributeName) + if (err != nil){ + return false, 0, 0, [16]byte{}, 0, false, nil, errors.New("GetMandatoryAttributes returning unknown attribute name: " + mandatoryAttributeName) + } + + _, exists := rawProfileMap[mandatoryAttributeIdentifier] + if (exists == false){ + // Profile is malformed: Missing a mandatory attribute. + return false, 0, 0, [16]byte{}, 0, false, nil, nil + } + } + } + } + } + + return true, profileVersion, networkType, identityHash, broadcastTimeInt64, false, rawProfileMap, nil +} + +// This function must be called on profiles which have already been verified +// Outputs: +// -bool: Attribute exists +// -string: Attribute Value (Formatted) +// -error +func GetFormattedProfileAttributeFromRawProfileMap(rawProfileMap map[int]messagepack.RawMessage, attributeName string)(bool, string, error){ + + attributeIdentifier, err := profileFormat.GetAttributeIdentifierFromAttributeName(attributeName) + if (err != nil) { + return false, "", errors.New("GetFormattedProfileAttributeFromRawProfileMap failed: " + err.Error()) + } + + attributeValueBytes, exists := rawProfileMap[attributeIdentifier] + if (exists == false){ + return false, "", nil + } + + attributeIsValid, attributeFormatted, err := formatProfileAttributeRawMessagePackToString(attributeName, attributeValueBytes) + if (err != nil){ return false, "", err } + if (attributeIsValid == false){ + // Profile is malformed. + // This should never happen, because this function should only be called after the profile has been verified by ReadProfile + + return false, "", errors.New("GetFormattedProfileAttributeFromRawProfileMap called with raw profile map containing invalid " + attributeName + " attribute value.") + } + + return true, attributeFormatted, nil +} + + +// We use this function to convert attributes from raw MessagePack to string +//Outputs: +// -bool: Attribute is valid +// -Be aware that this function does not fully verify attributes +// -string: Attribute formatted +// -error +func formatProfileAttributeRawMessagePackToString(attributeName string, attributeValueBytes messagepack.RawMessage)(bool, string, error){ + + switch attributeName{ + + // Some attributes are encoded in special ways in MessagePack to save space + // For example, BroadcastTime is encoded as a int64 rather than a string + + case "ProfileVersion":{ + + profileVersion, err := encoding.DecodeRawMessagePackToInt(attributeValueBytes) + if (err != nil){ + // Profile is malformed: Invalid profile version + return false, "", nil + } + + if (profileVersion != 1){ + // We cannot read this profile. It was created by an newer version of Seekia. + // This should never happen, because this function should only be called after the profile's network type has been verified + return false, "", errors.New("formatProfileAttributeRawMessagePackToString called with unknown ProfileVersion.") + } + + return true, "1", nil + } + case "NetworkType":{ + + networkType, err := encoding.DecodeRawMessagePackToByte(attributeValueBytes) + if (err != nil){ + // Profile is malformed: Invalid network type + // This should never happen, because this function should only be called after the profile's networkType has been verified + return false, "", errors.New("formatProfileAttributeRawMessagePackToString called with invalid NetworkType.") + } + + isValid := helpers.VerifyNetworkType(networkType) + if (isValid == false){ + // Profile malformed: Invalid network type + // This should never happen, because this function should only be called after the profile's networkType has been verified + return false, "", errors.New("formatProfileAttributeRawMessagePackToString called with invalid NetworkType.") + } + + networkTypeString := helpers.ConvertByteToString(networkType) + + return true, networkTypeString, nil + } + case "IdentityKey":{ + + identityKey, err := encoding.DecodeRawMessagePackTo32ByteArray(attributeValueBytes) + if (err != nil){ + // Profile is malformed: Invalid identity key + // This should never happen, because this function should only be called after the profile's IdentityKey has been verified + return false, "", errors.New("formatProfileAttributeRawMessagePackToString called with invalid IdentityKey.") + } + + identityKeyHexString := encoding.EncodeBytesToHexString(identityKey[:]) + + return true, identityKeyHexString, nil + } + case "BroadcastTime":{ + + broadcastTimeInt64, err := encoding.DecodeRawMessagePackToInt64(attributeValueBytes) + if (err != nil) { + // Profile is malformed: Contains invalid BroadcastTime + // This should never happen, because this function should only be called after the profile's BroadcastTime has been verified + return false, "", errors.New("formatProfileAttributeRawMessagePackToString called with invalid BroadcastTime.") + } + + broadcastTimeString := helpers.ConvertInt64ToString(broadcastTimeInt64) + + return true, broadcastTimeString, nil + } + case "Disabled":{ + + isDisabledBool, err := encoding.DecodeRawMessagePackToBool(attributeValueBytes) + if (err != nil){ + // Profile is malformed: Contains invalid Disabled attribute + // This should never happen, because this function should only be called after the profile's Disabled has been verified + return false, "", errors.New("formatProfileAttributeRawMessagePackToString called with invalid Disabled.") + } + + if (isDisabledBool == false){ + // Profile is malformed: Contains invalid Disabled attribute + // This should never happen, because this function should only be called after the profile's Disabled has been verified + return false, "", errors.New("formatProfileAttributeRawMessagePackToString called with invalid Disabled.") + } + + return true, "Yes", nil + } + case "NaclKey", "KyberKey":{ + + keyBytes, err := encoding.DecodeRawMessagePackToBytes(attributeValueBytes) + if (err != nil) { + // Profile is malformed: Contains invalid NaclKey/KyberKey attribute + return false, "", nil + } + + keyString := encoding.EncodeBytesToBase64String(keyBytes) + + return true, keyString, nil + } + case "Photos":{ + + var rawPhotoBytesList [][]byte + + err := encoding.DecodeMessagePackBytes(false, attributeValueBytes, &rawPhotoBytesList) + if (err != nil) { + // Profile is malformed: Contains invalid Photos attribute + return false, "", nil + } + + if (len(rawPhotoBytesList) == 0){ + // Profile is malformed: Contains invalid Photos attribute + return false, "", nil + } + + // We use this to build the output + // The output is a "+" delimited string of each photo's bytes encoded in Base64 + var attributeBuilder strings.Builder + + finalIndex := len(rawPhotoBytesList) - 1 + + for index, photoBytes := range rawPhotoBytesList{ + + photoBase64 := encoding.EncodeBytesToBase64String(photoBytes) + + attributeBuilder.WriteString(photoBase64) + + if (index != finalIndex){ + attributeBuilder.WriteString("+") + } + } + + photoAttributeString := attributeBuilder.String() + + return true, photoAttributeString, nil + } + } + + // Attribute is not encoded in a special way. + // It is encoded as a unicode string. + + attributeValueString, err := encoding.DecodeRawMessagePackToString(attributeValueBytes) + if (err != nil) { return false, "", err } + + return true, attributeValueString, nil +} + +// Attribute hashes are used for moderation +// A profile's attribute hash can be reviewed without reviewing the whole profile +// This function does not verify the raw profile map. The raw profile map must first be verified +// Outputs: +// -map[int][27]byte: Profile attribute hashes map (Attribute identifier -> Attribute hash) +// -bool: Profile is canonical (all attributes are canonical) +// -error +func GetProfileAttributeHashesMap(profileAuthor [16]byte, profileVersion int, profileNetworkType byte, rawProfileMap map[int]messagepack.RawMessage)(map[int][27]byte, bool, error){ + + profileType, err := identity.GetIdentityTypeFromIdentityHash(profileAuthor) + if (err != nil){ + profileAuthorHex := encoding.EncodeBytesToHexString(profileAuthor[:]) + return nil, false, errors.New("GetProfileAttributeHashesMap called with invalid profileAuthor: " + profileAuthorHex) + } + + _, exists := rawProfileMap[5] + if (exists == true){ + + // Profile is disabled. + // A disabled profile has an empty attribute hashes map. + // All disabled profile are also canonical + + emptyMap := make(map[int][27]byte) + + return emptyMap, true, nil + } + + // We use a prefix when creating attribute hashes + // The prefix starts with the string "profileattributehashsalt" decoded from base32 to bytes + attributeHashInputPrefix := []byte{124, 92, 84, 44, 128, 156, 226, 128, 210, 100, 56, 36, 121, 1, 115} + + attributeHashInputPrefix = append(attributeHashInputPrefix, profileAuthor[:]...) + attributeHashInputPrefix = append(attributeHashInputPrefix, profileNetworkType) + + + // Map Structure: Attribute Identifier -> Attribute hash + profileAttributeHashesMap := make(map[int][27]byte) + + // We set this variable to false if any attribute is not canonical + profileIsCanonical := true + + for attributeIdentifier, attributeRawValueBytes := range rawProfileMap{ + + attributeObject, err := profileFormat.GetAttributeObjectFromAttributeIdentifier(attributeIdentifier) + if (err != nil) { return nil, false, err } + + attributeName := attributeObject.AttributeName + + if (attributeName == "ProfileType" || attributeName == "IdentityKey" || attributeName == "ProfileVersion" || attributeName == "NetworkType"){ + // We don't need to store these attributes in the attribute hashes map + continue + } + + //Outputs: + // -bool: Attribute is canonical + // -error + getAttributeIsCanonical := func()(bool, error){ + + getIsCanonicalFunction := attributeObject.GetIsCanonical + + attributeIsCanonicalInfo, err := getIsCanonicalFunction(profileVersion) + if (err != nil) { return false, err } + + if (attributeIsCanonicalInfo == "Always"){ + return true, nil + } + if (attributeIsCanonicalInfo == "Never"){ + return false, nil + } + // attributeIsCanonicalInfo == "Sometimes" + // We have to manually check to see if the attribute's value is canonical + + valueIsValid, attributeValueString, err := formatProfileAttributeRawMessagePackToString(attributeName, attributeRawValueBytes) + if (err != nil) { return false, err } + if (valueIsValid == false){ + // Profile contains an invalid attribute value + return false, errors.New("GetProfileAttributeHashesMap called with profile containing invalid attribute value for attribute: " + attributeName) + } + + valueIsValid, attributeIsCanonical, err := attributeObject.CheckValueFunction(profileVersion, profileType, attributeValueString) + if (err != nil){ return false, err } + if (valueIsValid == false){ + // Profile contains an invalid attribute value + return false, errors.New("GetProfileAttributeHashesMap called with profile containing invalid attribute value for attribute: " + attributeName + ". Attribute value: " + attributeValueString) + } + + return attributeIsCanonical, nil + } + + attributeIsCanonical, err := getAttributeIsCanonical() + if (err != nil) { return nil, false, err } + if (attributeIsCanonical == false){ + profileIsCanonical = false + } + + // We now create the attribute hash + // Each attribute hash represents an attribute value created by a specific author + // + // We use the messagePack encoded bytes to create the attribute hash + // I'm not sure, but it may be possible to encode the same value using multiple different messagepack encodings + // For example, a number could be encoded in two different ways using raw messagepack + // This would result in two different attribute hashes for the same attribute value + // This is fine + // It would only be a problem if an attribute hash represented two different attribute values + + if (attributeIdentifier < 1 || attributeIdentifier > 4294967295){ + return nil, false, errors.New("profileFormat contains invalid attribute identifier.") + } + + attributeIdentifierUint32 := uint32(attributeIdentifier) + + // We copy the prefix and append the attribute identifier and attribute raw value bytes + // We then feed that into blake3 + + hashInputBytes := slices.Clone(attributeHashInputPrefix) + + hashInputBytes = binary.LittleEndian.AppendUint32(hashInputBytes, attributeIdentifierUint32) + + hashInputBytes = append(hashInputBytes, attributeRawValueBytes...) + + attributeHashWithoutMetadata, err := blake3.GetBlake3HashAsBytes(26, hashInputBytes) + if (err != nil) { return nil, false, err } + + getMetadataByte := func()(byte, error){ + + if (profileType == "Mate"){ + if (attributeIsCanonical == true){ + return 1, nil + } + return 2, nil + } + if (profileType == "Host"){ + if (attributeIsCanonical == true){ + return 3, nil + } + return 4, nil + } + if (profileType == "Moderator"){ + if (attributeIsCanonical == true){ + return 5, nil + } + return 6, nil + } + return 0, errors.New("GetIdentityTypeFromIdentityHash returning invalid profileType: " + profileType) + } + + attributeHashMetadataByte, err := getMetadataByte() + if (err != nil) { return nil, false, err } + + attributeHashBytes := append(attributeHashWithoutMetadata, attributeHashMetadataByte) + + attributeHash := [27]byte(attributeHashBytes) + + profileAttributeHashesMap[attributeIdentifier] = attributeHash + } + + return profileAttributeHashesMap, profileIsCanonical, nil +} + + diff --git a/internal/profiles/userStatistics/userStatistics.go b/internal/profiles/userStatistics/userStatistics.go new file mode 100644 index 0000000..3d4d516 --- /dev/null +++ b/internal/profiles/userStatistics/userStatistics.go @@ -0,0 +1,800 @@ + +// userStatistics provides functions to generate statistics about stored user profiles. +// The user chooses the attribute(s), and the gui displays a chart with a button to view the statistics data. +// See statisticsGui.go to see the gui code. + +package userStatistics + +//TODO: Add the ability to control for confounding variables +// Example: Wealth, controlled for age and sex + +import "seekia/internal/badgerDatabase" +import "seekia/internal/helpers" +import "seekia/internal/profiles/calculatedAttributes" +import "seekia/internal/profiles/profileStorage" +import "seekia/internal/profiles/attributeDisplay" +import "seekia/internal/translation" + +import "slices" +import "strings" +import "errors" +import "math" + + +type StatisticsItem struct{ + + // The label for the statistics item + // For a bar chart, this represents the name of an X axis bar. + // For a donut chart, this represents the name of a donut slice + // Example: "Man", "100-200" + // This will never be translated, unless it is the Unknown/No Response item, in which case + // the label will be "Unknown"/"No Response" translated to the user's current app language + Label string + + // This is the formatted, human readable version of the label + // This will be translated into the application language + // Sometimes, the LabelFormatted will be identical to Label + // This does not include units (Example: " days", " users") + // Example: + // -"1000000-2000000" -> "1 million-2 million" + LabelFormatted string + + // The value corresponding to the label + // For a bar chart, this represents the Y axis value for a bar. + // For a donut chart, this represents the value (size) of one of the donut slices + // This will never be translated + // For example, the value could be 500 if 500 men responded Yes. + Value float64 + + // This is the formatted version of the value + // This does not include units (Example: " days", " users") + // Examples: + // -5 -> "5/10" + // -1500000000000 -> "1.5 trillion" + ValueFormatted string +} + + +//Outputs: +// -int: Number of users analyzed in statistics +// -[]StatisticsItem: Statistics items list (sorted, not grouped) +// -bool: Grouping performed +// -[]StatisticsItem: Grouped items list +// -func(float64)(string, error): Function to format y axis values +// -This is used because the values must be passed to the chart code as pure floats, but they must be formatted after to be human readable +// -Example: "1000000" -> "1 million" +// -error +func GetUserStatisticsItemsLists_BarChart(identityType string, + networkType byte, + xAxisAttribute string, + xAxisIsNumerical bool, + formatXAxisValuesFunction func(string)(string, error), + xAxisUnknownLabel string, + yAxisAttribute string)(int, []StatisticsItem, bool, []StatisticsItem, func(float64)(string, error), error){ + + isValid := helpers.VerifyNetworkType(networkType) + if (isValid == false){ + networkTypeString := helpers.ConvertByteToString(networkType) + return 0, nil, false, nil, nil, errors.New("GetUserStatisticsItemsLists_BarChart called with invalid networkType: " + networkTypeString) + } + + getYAxisRoundingPrecision := func()int{ + if (yAxisAttribute == "Number Of Users"){ + return 0 + } + if (yAxisAttribute == "Average Height"){ + return 0 + } + if (yAxisAttribute == "Average Age"){ + return 0 + } + return 1 + } + + yAxisRoundingPrecision := getYAxisRoundingPrecision() + + //Outputs: + // -int: Total analyzed users + // -[]StatisticsItem: Statistics items list + // -map[string]int: Response Counts map (X axis attribute response -> Number of y axis responses) + // -bool: yAxisIsAverage + // -map[string]float64: Response Sums map (X axis attribute response -> all y axis responses summed) (If yAxisIsAverage == true) + // -func(float64)(string, error): Function to format statistics values + // -bool: Include a No Response/Unknown value item + // -This is only needed if at least 1 user did not respond to the X axis attribute on their profile. + // -StatisticsItem: The unknown value item. + // -error + getStatisticsItemsList := func()(int, []StatisticsItem, map[string]int, bool, map[string]float64, func(float64)(string, error), bool, StatisticsItem, error){ + + //TODO: Add "Probability Of ..." + // This will allow viewing of choice attribute probabilities + // For example: Probability of being Male, probability of being Female, etc. + // We need to add this for each canonical option + + if (yAxisAttribute == "Number Of Users"){ + + totalAnalyzedUsers, statisticsItemsList, responseCountsMap, numberOfUnknownValueUsers, err := getProfileAttributeCountStatisticsItemsList(identityType, networkType, xAxisAttribute, formatXAxisValuesFunction) + if (err != nil) { return 0, nil, nil, false, nil, nil, false, StatisticsItem{}, err } + + formatValuesFunction := func(input float64)(string, error){ + + // input will be an whole number representing the number of users + + result := helpers.ConvertFloat64ToStringRounded(input, 0) + + return result, nil + } + + if (numberOfUnknownValueUsers == 0){ + return totalAnalyzedUsers, statisticsItemsList, responseCountsMap, false, nil, formatValuesFunction, false, StatisticsItem{}, nil + } + + unknownValueFormatted := helpers.ConvertIntToString(numberOfUnknownValueUsers) + + unknownItem := StatisticsItem{ + Label: xAxisUnknownLabel, + LabelFormatted: xAxisUnknownLabel, + Value: float64(numberOfUnknownValueUsers), + ValueFormatted: unknownValueFormatted, + } + + return totalAnalyzedUsers, statisticsItemsList, responseCountsMap, false, nil, formatValuesFunction, true, unknownItem, nil + } + + userIdentityHashesToAnalyzeList, err := getUserIdentityHashesToAnalyzeList(identityType) + if (err != nil) { return 0, nil, nil, false, nil, nil, false, StatisticsItem{}, err } + + yAxisIsAverage := strings.HasPrefix(yAxisAttribute, "Average ") + if (yAxisIsAverage == true){ + + // This function will return the attribute name + // We must convert the attribute title to the attribute name + getAttributeToGetAverageFor := func()string{ + + attributeTitle := strings.TrimPrefix(yAxisAttribute, "Average ") + + if (attributeTitle == "Wealth"){ + return "WealthInGold" + } + if (attributeTitle == "23andMe Neanderthal Variants"){ + return "23andMe_NeanderthalVariants" + } + if (attributeTitle == "Body Fat"){ + return "BodyFat" + } + if (attributeTitle == "Body Muscle"){ + return "BodyMuscle" + } + if (attributeTitle == "Fruit Rating"){ + return "FruitRating" + } + if (attributeTitle == "Vegetables Rating"){ + return "VegetablesRating" + } + if (attributeTitle == "Nuts Rating"){ + return "NutsRating" + } + if (attributeTitle == "Grains Rating"){ + return "GrainsRating" + } + if (attributeTitle == "Dairy Rating"){ + return "DairyRating" + } + if (attributeTitle == "Seafood Rating"){ + return "SeafoodRating" + } + if (attributeTitle == "Beef Rating"){ + return "BeefRating" + } + if (attributeTitle == "Pork Rating"){ + return "PorkRating" + } + if (attributeTitle == "Poultry Rating"){ + return "PoultryRating" + } + if (attributeTitle == "Eggs Rating"){ + return "EggsRating" + } + if (attributeTitle == "Beans Rating"){ + return "BeansRating" + } + if (attributeTitle == "Alcohol Frequency"){ + return "AlcoholFrequency" + } + if (attributeTitle == "Tobacco Frequency"){ + return "TobaccoFrequency" + } + if (attributeTitle == "Cannabis Frequency"){ + return "CannabisFrequency" + } + if (attributeTitle == "Pets Rating"){ + return "PetsRating" + } + if (attributeTitle == "Dogs Rating"){ + return "DogsRating" + } + if (attributeTitle == "Cats Rating"){ + return "CatsRating" + } + + return attributeTitle + } + + attributeToGetAverageFor := getAttributeToGetAverageFor() + + totalAnalyzedUsers := 0 + + //Map structure: X-Axis attribute -> Number of users with this x-axis attribute who have a y-axis attribute response + responseCountsMap := make(map[string]int) + //Map structure: X-axis attribute response -> All Y-axis attribute responses summed for users with this x-axis attribute + responseSumsMap := make(map[string]float64) + + // This stores a count of the number of users whose X axis value is unknown + // For example, if X axis is Age, this is the number of users who did not respond + numberOfUsersWithUnknownXAxisValue := 0 + // This stores the total sum of all y axis values for users who have an unknown X axis value + // For example, if the X axis attribute is age, and the Y axis attribute is average height, + // this stores the sum of heights of all users who have no age on their profile. + usersWithUnknownXAxisValueYAxisValuesSum := float64(0) + + for _, userIdentityHash := range userIdentityHashesToAnalyzeList{ + + profileFound, getAnyUserAttributeValueFunction, err := getRetrieveAnyAttributeFromUserNewestProfileFunction(userIdentityHash, networkType) + if (err != nil) { return 0, nil, nil, false, nil, nil, false, StatisticsItem{}, err } + if (profileFound == false){ + continue + } + + userIsDisabled, _, _, err := getAnyUserAttributeValueFunction("Disabled") + if (err != nil) { return 0, nil, nil, false, nil, nil, false, StatisticsItem{}, err } + if (userIsDisabled == true){ + continue + } + + attributeExists, _, userAttributeToGetAverageForValue, err := getAnyUserAttributeValueFunction(attributeToGetAverageFor) + if (err != nil) { return 0, nil, nil, false, nil, nil, false, StatisticsItem{}, err } + if (attributeExists == false){ + // This user did not respond to the attribute we are getting the average for + // We will not add them to the statistics maps + continue + } + + totalAnalyzedUsers += 1 + + userAttributeToGetAverageForValueFloat64, err := helpers.ConvertStringToFloat64(userAttributeToGetAverageForValue) + if (err != nil) { + return 0, nil, nil, false, nil, nil, false, StatisticsItem{}, errors.New("Database corrupt: Contains invalid " + userAttributeToGetAverageForValue + " value: " + userAttributeToGetAverageForValue) + } + + attributeFound, _, userXAxisAttributeValue, err := getAnyUserAttributeValueFunction(xAxisAttribute) + if (err != nil) { return 0, nil, nil, false, nil, nil, false, StatisticsItem{}, err } + if (attributeFound == false){ + // This user did not respond to the X axis attribute + // We calculate the average for users who do not respond and put it in its own category + + numberOfUsersWithUnknownXAxisValue += 1 + usersWithUnknownXAxisValueYAxisValuesSum += userAttributeToGetAverageForValueFloat64 + continue + } + + responseCountsMap[userXAxisAttributeValue] += 1 + + responseSumsMap[userXAxisAttributeValue] += userAttributeToGetAverageForValueFloat64 + } + + _, _, formatYAxisValuesFunction, _, _, err := attributeDisplay.GetProfileAttributeDisplayInfo(attributeToGetAverageFor) + if (err != nil) { return 0, nil, nil, false, nil, nil, false, StatisticsItem{}, err } + + statisticsItemsList := make([]StatisticsItem, 0, len(responseCountsMap)) + + for attributeResponse, responsesCount := range responseCountsMap{ + + attributeResponseFormatted, err := formatXAxisValuesFunction(attributeResponse) + if (err != nil) { return 0, nil, nil, false, nil, nil, false, StatisticsItem{}, err } + + allResponsesSum, exists := responseSumsMap[attributeResponse] + if (exists == false){ + return 0, nil, nil, false, nil, nil, false, StatisticsItem{}, errors.New("Response sums map missing attribute value") + } + + averageValue := allResponsesSum/float64(responsesCount) + + averageValueString := helpers.ConvertFloat64ToStringRounded(averageValue, yAxisRoundingPrecision) + + averageValueFormatted, err := formatYAxisValuesFunction(averageValueString) + if (err != nil) { return 0, nil, nil, false, nil, nil, false, StatisticsItem{}, err } + + newStatisticsItem := StatisticsItem{ + Label: attributeResponse, + LabelFormatted: attributeResponseFormatted, + Value: averageValue, + ValueFormatted: averageValueFormatted, + } + + statisticsItemsList = append(statisticsItemsList, newStatisticsItem) + } + + // We use this function to format values after grouping, if grouping is needed + formatValuesFunction := func(input float64)(string, error){ + + inputString := helpers.ConvertFloat64ToStringRounded(input, yAxisRoundingPrecision) + + valueFormatted, err := formatYAxisValuesFunction(inputString) + if (err != nil) { return "", err } + + return valueFormatted, nil + } + + if (numberOfUsersWithUnknownXAxisValue == 0){ + return totalAnalyzedUsers, statisticsItemsList, responseCountsMap, true, responseSumsMap, formatValuesFunction, false, StatisticsItem{}, nil + } + + unknownResponsesAverage := usersWithUnknownXAxisValueYAxisValuesSum/float64(numberOfUsersWithUnknownXAxisValue) + + unknownResponsesValueFormatted, err := formatValuesFunction(unknownResponsesAverage) + if (err != nil) { return 0, nil, nil, false, nil, nil, false, StatisticsItem{}, err } + + // This item represents the average value for the yAxisAttribute for users who did not respond. + // For example, if the xAxisAttribute is Height, and the yAxisAttribute is AverageWealth, this value + // will represent the average wealth for users who did not provide Height on their profile. + unknownStatisticsItem := StatisticsItem{ + Label: xAxisUnknownLabel, + LabelFormatted: xAxisUnknownLabel, + Value: unknownResponsesAverage, + ValueFormatted: unknownResponsesValueFormatted, + } + + return totalAnalyzedUsers, statisticsItemsList, responseCountsMap, true, responseSumsMap, formatValuesFunction, true, unknownStatisticsItem, nil + } + + return 0, nil, nil, false, nil, nil, false, StatisticsItem{}, errors.New("Invalid y-axis attribute: " + yAxisAttribute) + } + + totalAnalyzedUsers, statisticsItemsList, responseCountsMap, yAxisIsAverage, responseSumsMap, formatValuesFunction, includeUnknownItem, unknownValueItem, err := getStatisticsItemsList() + if (err != nil) { return 0, nil, false, nil, nil, err } + + sortStatisticsItemsList(statisticsItemsList, xAxisIsNumerical) + + // We now see if we need to group the items in the list together + // We do this if there are more than 10 categories + + if (len(statisticsItemsList) <= 10){ + + // No grouping needed. We are done. + + if (includeUnknownItem == true){ + statisticsItemsList = append(statisticsItemsList, unknownValueItem) + } + + return totalAnalyzedUsers, statisticsItemsList, false, nil, formatValuesFunction, nil + } + + groupedStatisticsItemsList, err := getStatisticsItemsListGrouped(10, statisticsItemsList, xAxisIsNumerical, responseCountsMap, yAxisIsAverage, responseSumsMap, formatValuesFunction) + if (err != nil) { return 0, nil, false, nil, nil, err } + + if (includeUnknownItem == true){ + statisticsItemsList = append(statisticsItemsList, unknownValueItem) + groupedStatisticsItemsList = append(groupedStatisticsItemsList, unknownValueItem) + } + + return totalAnalyzedUsers, statisticsItemsList, true, groupedStatisticsItemsList, formatValuesFunction, nil +} + + +//Outputs: +// -int: Number of users analyzed in statistics +// -[]StatisticsItem: Statistics items list (sorted, not grouped) +// -bool: Grouping performed +// -[]StatisticsItem: Grouped items list +// -error +func GetUserStatisticsItemsLists_DonutChart(identityType string, + networkType byte, + attributeToAnalyze string, + attributeIsNumerical bool, + formatAttributeLabelsFunction func(string)(string, error), + unknownLabelTranslated string)(int, []StatisticsItem, bool, []StatisticsItem, error){ + + isValid := helpers.VerifyNetworkType(networkType) + if (isValid == false){ + networkTypeString := helpers.ConvertByteToString(networkType) + return 0, nil, false, nil, errors.New("GetUserStatisticsItemsLists_DonutChart called with invalid networkType: " + networkTypeString) + } + + totalAnalyzedUsers, statisticsItemsList, attributeCountsMap, numberOfUnknownResponders, err := getProfileAttributeCountStatisticsItemsList(identityType, networkType, attributeToAnalyze, formatAttributeLabelsFunction) + if (err != nil) { return 0, nil, false, nil, err } + + sortStatisticsItemsList(statisticsItemsList, attributeIsNumerical) + + getUnknownValueItem := func()StatisticsItem{ + + numberOfUnknownRespondersString := helpers.ConvertIntToString(numberOfUnknownResponders) + + unknownValueItem := StatisticsItem{ + Label: unknownLabelTranslated, + LabelFormatted: unknownLabelTranslated, + Value: float64(numberOfUnknownResponders), + ValueFormatted: numberOfUnknownRespondersString, + } + + return unknownValueItem + } + + if (len(statisticsItemsList) <= 8){ + + // No grouping needed. + + if (numberOfUnknownResponders != 0){ + + unknownValueItem := getUnknownValueItem() + + statisticsItemsList = append(statisticsItemsList, unknownValueItem) + } + + return totalAnalyzedUsers, statisticsItemsList, false, nil, nil + } + + formatValuesFunction := func(input float64)(string, error){ + + // input will always be the number of users who responded with the value + // Thus, input will always be an integer + + result := helpers.ConvertFloat64ToStringRounded(input, 0) + + return result, nil + } + + groupedStatisticsItemsList, err := getStatisticsItemsListGrouped(8, statisticsItemsList, attributeIsNumerical, attributeCountsMap, false, nil, formatValuesFunction) + if (err != nil) { return 0, nil, false, nil, err } + + if (numberOfUnknownResponders != 0){ + + unknownValueItem := getUnknownValueItem() + + statisticsItemsList = append(statisticsItemsList, unknownValueItem) + groupedStatisticsItemsList = append(groupedStatisticsItemsList, unknownValueItem) + } + + return totalAnalyzedUsers, statisticsItemsList, true, groupedStatisticsItemsList, nil +} + + +// This function will return a statistics items list of the following format: +// "Label": Attribute name (Example: "Male") +// "Value": The number of users who responded with the attribute (in this example: "Male") +// +// All users of provided identityType who are not disabled will be analyzed +// -int: Number of analyzed users +// -[]StatisticsItem: Statistics items list (not sorted or grouped) +// -map[string]int: Response counts map (Response -> Number of responders) +// -int: Number of No Response/Unknown value responders +// -error +func getProfileAttributeCountStatisticsItemsList(identityType string, + networkType byte, + attributeName string, + formatLabelsFunction func(string)(string, error))(int, []StatisticsItem, map[string]int, int, error){ + + userIdentityHashesToAnalyzeList, err := getUserIdentityHashesToAnalyzeList(identityType) + if (err != nil) { return 0, nil, nil, 0, err } + + totalAnalyzedUsers := 0 + + // Map structure: User Attribute response -> Number of users who responded with the response + responseCountsMap := make(map[string]int) + + // This stores the number of users for whom we do not know their value + numberOfUnknownValueUsers := 0 + + for _, userIdentityHash := range userIdentityHashesToAnalyzeList{ + + profileFound, getAnyUserAttributeValueFunction, err := getRetrieveAnyAttributeFromUserNewestProfileFunction(userIdentityHash, networkType) + if (err != nil) { return 0, nil, nil, 0, err } + if (profileFound == false){ + continue + } + userIsDisabled, _, _, err := getAnyUserAttributeValueFunction("Disabled") + if (err != nil) { return 0, nil, nil, 0, err } + if (userIsDisabled == true){ + continue + } + + totalAnalyzedUsers += 1 + + attributeFound, _, attributeValue, err := getAnyUserAttributeValueFunction(attributeName) + if (err != nil) { return 0, nil, nil, 0, err } + if (attributeFound == false){ + numberOfUnknownValueUsers += 1 + continue + } + + responseCountsMap[attributeValue] += 1 + } + + statisticsItemsList := make([]StatisticsItem, 0, len(responseCountsMap)) + + for attributeResponse, numberOfUsers := range responseCountsMap{ + + attributeResponseFormatted, err := formatLabelsFunction(attributeResponse) + if (err != nil) { return 0, nil, nil, 0, err } + + attributeNumberOfUsersString := helpers.ConvertIntToString(numberOfUsers) + + newStatisticsItem := StatisticsItem{ + Label: attributeResponse, + LabelFormatted: attributeResponseFormatted, + Value: float64(numberOfUsers), + ValueFormatted: attributeNumberOfUsersString, + } + + statisticsItemsList = append(statisticsItemsList, newStatisticsItem) + } + + return totalAnalyzedUsers, statisticsItemsList, responseCountsMap, numberOfUnknownValueUsers, nil +} + +func sortStatisticsItemsList(inputStatisticsItemsList []StatisticsItem, labelIsNumerical bool){ + + if (len(inputStatisticsItemsList) <= 1){ + return + } + + if (labelIsNumerical == true){ + + // We sort the items by label values in ascending order + // Example: Bar chart columns are ages, in order of youngest to oldest + + compareItemsFunction := func(itemA StatisticsItem, itemB StatisticsItem)int{ + + itemALabel := itemA.Label + itemBLabel := itemB.Label + + itemAFloat64, err := helpers.ConvertStringToFloat64(itemALabel) + if (err != nil) { + panic("Invalid statistics item: Item Label is not float: " + itemALabel) + } + + itemBFloat64, err := helpers.ConvertStringToFloat64(itemBLabel) + if (err != nil) { + panic("Invalid statistics item: Item Label is not float: " + itemBLabel) + } + + if (itemAFloat64 == itemBFloat64){ + return 0 + } + + if (itemAFloat64 < itemBFloat64){ + return -1 + } + + return 1 + } + + slices.SortFunc(inputStatisticsItemsList, compareItemsFunction) + + return + } + + // We sort the items by their values in descending order + + compareItemsFunction := func(itemA StatisticsItem, itemB StatisticsItem)int{ + + itemAValue := itemA.Value + itemBValue := itemB.Value + + if (itemAValue == itemBValue){ + return 0 + } + + if (itemAValue < itemBValue){ + return 1 + } + + return -1 + } + + slices.SortFunc(inputStatisticsItemsList, compareItemsFunction) +} + +// This function will group a statistics items list. +// It will group Labels and their values to fit into a specified number of groups +// Example: "1","2","3","4" -> "1-2", "3-4" +//Inputs: +// -int: Maximum groups to create +// -[]StatisticsItem: Statistics items list to group +// -bool: Label is numerical +// -If it is, we will group labels into groups of numbers. +// -Otherwise, we will group all categories after the first maximumGroupsToCreate into a group called Other +// -map[string]int +// -Response Counts map (Attribute response -> Number of responders with that response) +// -bool: Value is average +// -If value is average, we will combine the values of each group and find their average +// -Otherwise, we will find their sum +// -map[string]float64 +// -Response Sums map (X-axis attribute response -> all Y-axis responses summed) (If yAxisIsAverage == true) +// -func(float64)(string, error): This is the function we use to format the values +//Outputs: +// -[]StatisticsItem: Grouped statistics items list +// -error +func getStatisticsItemsListGrouped(maximumGroupsToCreate int, + inputStatisticsItemsList []StatisticsItem, + labelIsNumerical bool, + responseCountsMap map[string]int, + valueIsAverage bool, + responseSumsMap map[string]float64, + formatValuesFunction func(float64)(string, error))([]StatisticsItem, error){ + + if (len(inputStatisticsItemsList) <= maximumGroupsToCreate){ + return nil, errors.New("maximumGroupsToCreate is <= length of input statistics items list") + } + + // We deep copy the statistics items list to retain the sorted version + // We need to retain both versions because the user can view the raw or grouped data in the GUI + + statisticsItemsList := slices.Clone(inputStatisticsItemsList) + + // We use this function to get the new value for a group of labels + getGroupValue := func(itemsToCombineList []StatisticsItem)(float64, error){ + + // This will count the total number of users who responded with the responses within this group + // Example: Labels are "Blue", "Green", this variable will store the number of users who responded with either Blue or Green + totalRespondersCount := float64(0) + + // This will store the sum for all response values within the group + // We only need to add to this sum if valueIsAverage == true + allReponsesSummed := float64(0) + + for _, statisticsItem := range itemsToCombineList{ + + itemLabel := statisticsItem.Label + + responderCount, exists := responseCountsMap[itemLabel] + if (exists == false){ + return 0, errors.New("responseCountsMap missing label: " + itemLabel) + } + + totalRespondersCount += float64(responderCount) + + if (valueIsAverage == true){ + + yAxisAttributeResponsesSum, exists := responseSumsMap[itemLabel] + if (exists == false){ + return 0, errors.New("responseSumsMap missing label: " + itemLabel) + } + allReponsesSummed += yAxisAttributeResponsesSum + } + } + + if (valueIsAverage == false){ + + return totalRespondersCount, nil + } + + // The value is an average + // We need to find the average for all of the user responses for the labels in the input list + // The Values in the inputStatisticsItemsList are averages + // We can't average out the averages, because that will not give us the true average + // We have to use the original sums for all group items and average them + + if (totalRespondersCount == 0){ + return 0, errors.New("totalRespondersCount is 0.") + } + + value := allReponsesSummed/float64(totalRespondersCount) + + return value, nil + } + + if (labelIsNumerical == true){ + + maximumItemsPerCategory := int(math.Ceil(float64(len(statisticsItemsList))/float64(maximumGroupsToCreate))) + + statisticsItemsListSublists, err := helpers.SplitListIntoSublists(statisticsItemsList, maximumItemsPerCategory) + if (err != nil) { return nil, err } + + groupedItemsList := make([]StatisticsItem, 0, len(statisticsItemsListSublists)) + + for _, groupItemsListSublist := range statisticsItemsListSublists{ + + if (len(groupItemsListSublist) == 1){ + // Sometimes, a group with 1 item will be created + // This happens if the groups cannot be evenly divided, so there is a remainder of 1. + // Example: 10->4 groups = 3, 3, 3, 1. + //TODO: Prevent this from happening so groups always have more than 1 subitem + + groupItem := groupItemsListSublist[0] + + groupedItemsList = append(groupedItemsList, groupItem) + continue + } + + finalIndex := len(groupItemsListSublist)-1 + + initialItem := groupItemsListSublist[0] + finalItem := groupItemsListSublist[finalIndex] + + initialLabel := initialItem.Label + initialLabelFormatted := initialItem.LabelFormatted + + finalLabel := finalItem.Label + finalLabelFormatted := finalItem.LabelFormatted + + groupValue, err := getGroupValue(groupItemsListSublist) + if (err != nil) { return nil, err } + + groupValueFormatted, err := formatValuesFunction(groupValue) + if (err != nil) { return nil, err } + + newGroupStatisticsItem := StatisticsItem{ + Label: initialLabel + "-" + finalLabel, + LabelFormatted: initialLabelFormatted + "-" + finalLabelFormatted, + Value: groupValue, + ValueFormatted: groupValueFormatted, + } + + groupedItemsList = append(groupedItemsList, newGroupStatisticsItem) + } + + return groupedItemsList, nil + } + + // Label is not numerical + // We combine all categories after the first maximumGroupsToCreate into a category called Other + + itemsToKeep := statisticsItemsList[:maximumGroupsToCreate] + + itemsToCombine := statisticsItemsList[maximumGroupsToCreate:] + + otherTranslated := translation.TranslateTextFromEnglishToMyLanguage("Other") + + otherGroupValue, err := getGroupValue(itemsToCombine) + if (err != nil) { return nil, err } + + otherGroupValueFormatted, err := formatValuesFunction(otherGroupValue) + if (err != nil) { return nil, err } + + otherGroupItem := StatisticsItem{ + Label: "Other", + LabelFormatted: otherTranslated, + Value: otherGroupValue, + ValueFormatted: otherGroupValueFormatted, + } + + groupedStatisticsItemsList := append(itemsToKeep, otherGroupItem) + + return groupedStatisticsItemsList, nil +} + + +func getUserIdentityHashesToAnalyzeList(identityType string)([][16]byte, error){ + + allUserIdentityHashesList, err := badgerDatabase.GetAllProfileIdentityHashes(identityType) + if (err != nil) { return nil, err } + + if (len(allUserIdentityHashesList) < 10000){ + return allUserIdentityHashesList, nil + } + + helpers.RandomizeListOrder(allUserIdentityHashesList) + + identityHashesToAnalyzeList := allUserIdentityHashesList[:10000] + + return identityHashesToAnalyzeList, nil +} + +//Outputs: +// -bool: Profile Exists +// -func(string)(bool, int, string, error) +// -error +func getRetrieveAnyAttributeFromUserNewestProfileFunction(identityHash [16]byte, networkType byte)(bool, func(string)(bool, int, string, error), error){ + + newestProfileExists, profileVersion, _, _, _, newestProfileMap, err := profileStorage.GetNewestUserProfile(identityHash, networkType) + if (err != nil) { return false, nil, err } + if (newestProfileExists == false){ + return false, nil, nil + } + + getAnyAttributeFromUserNewestProfileFunction, err := calculatedAttributes.GetRetrieveAnyProfileAttributeIncludingCalculatedFunction(profileVersion, newestProfileMap) + if (err != nil) { return false, nil, err } + + return true, getAnyAttributeFromUserNewestProfileFunction, nil +} + diff --git a/internal/profiles/viewableProfiles/viewableProfiles.go b/internal/profiles/viewableProfiles/viewableProfiles.go new file mode 100644 index 0000000..7b8c126 --- /dev/null +++ b/internal/profiles/viewableProfiles/viewableProfiles.go @@ -0,0 +1,300 @@ + +// viewableProfiles provides functions to retrieve viewable user profiles + +package viewableProfiles + +// A viewable profile is one that can be shown to non-moderators during normal use of Seekia + +// A viewable Mate profile is one that has been approved. +// A viewable Host/Moderator profile is one that has not been banned. + +// A viewable profile must also be created by a non-banned identity +// All viewable statuses are derived from the profile/identity sticky consensus statuses, not their realtime consensus statuses + +import "seekia/internal/badgerDatabase" +import "seekia/internal/encoding" +import "seekia/internal/helpers" +import "seekia/internal/identity" +import "seekia/internal/moderation/verifiedStickyStatus" +import "seekia/internal/moderation/trustedViewableStatus" +import "seekia/internal/profiles/calculatedAttributes" +import "seekia/internal/profiles/readProfiles" + +import messagepack "github.com/vmihailenco/msgpack/v5" + +import "errors" + +// This function gets any attribute from a user's newest viewable profile. It includes calculated attributes. +//Outputs: +// -bool: Profile exists +// -int: Profile version +// -bool: Attribute exists +// -string: Attribute value +// -error +func GetAnyAttributeFromNewestViewableUserProfile(identityHash [16]byte, networkType byte, attribute string, allowTrustedStatus bool, allowUnknownStatus bool, alwaysAllowDisabled bool)(bool, int, bool, string, error){ + + isValid := helpers.VerifyNetworkType(networkType) + if (isValid == false){ + networkTypeString := helpers.ConvertByteToString(networkType) + return false, 0, false, "", errors.New("GetAnyAttributeFromNewestViewableUserProfile called with invalid networkType: " + networkTypeString) + } + + profileExists, _, retrieveAnyAttributeFunction, err := GetRetrieveAnyNewestViewableUserProfileAttributeFunction(identityHash, networkType, allowTrustedStatus, allowUnknownStatus, alwaysAllowDisabled) + if (err != nil) { return false, 0, false, "", err } + if (profileExists == false){ + return false, 0, false, "", nil + } + + exists, profileVersion, retrievedAttribute, err := retrieveAnyAttributeFunction(attribute) + if (err != nil) { return false, 0, false, "", err } + if (exists == false){ + return true, 0, false, "", nil + } + return true, profileVersion, true, retrievedAttribute, nil +} + +// This function returns a function to retrieve any attribute from a user's newest viewable profile. +// This makes retrieving multiple attributes faster. +// The newest viewable profile only needs to be found and read into memory once. +//Outputs: +// -bool: Profile exists +// -[28]byte: Profile hash +// -func(string)(bool, int, string, error) - Retrieve any attribute function +// -error +func GetRetrieveAnyNewestViewableUserProfileAttributeFunction(identityHash [16]byte, networkType byte, allowTrustedStatus bool, allowUnknownStatus bool, alwaysAllowDisabled bool)(bool, [28]byte, func(string)(bool, int, string, error), error){ + + isValid := helpers.VerifyNetworkType(networkType) + if (isValid == false){ + networkTypeString := helpers.ConvertByteToString(networkType) + return false, [28]byte{}, nil, errors.New("GetRetrieveAnyNewestViewableUserProfileAttributeFunction called with invalid networkType: " + networkTypeString) + } + + profileExists, profileVersion, profileHash, _, _, rawProfileMap, err := GetNewestViewableUserProfile(identityHash, networkType, allowTrustedStatus, allowUnknownStatus, alwaysAllowDisabled) + if (err != nil) { return false, [28]byte{}, nil, err } + if (profileExists == false){ + + return false, [28]byte{}, nil, nil + } + + getAnyAttributeFunction, err := calculatedAttributes.GetRetrieveAnyProfileAttributeIncludingCalculatedFunction(profileVersion, rawProfileMap) + if (err != nil){ return false, [28]byte{}, nil, err } + + return true, profileHash, getAnyAttributeFunction, nil +} + +//Inputs: +// -[16]byte: Identity hash of user whose profile we are retrieving +// -byte: Network type to retrieve profile for +// -bool: Allow trusted viewable status to be retrieved. +// -If false, only verified viewable statuses will be allowed (this is used for hosts) +// -bool: Allow Unknown Status +// -This is only used to find hosts to query from. This must be false for hosts retrieving viewable host profiles to share. +// -bool: Always allow disabled profiles +// -If true, the function will return disabled profiles, even if the identity is banned. +// -If false, a disabled profile will be considered unviewable if the identity is banned. +//Outputs: +// -bool: Viewable Profile exists +// -int: Profile version +// -[28]byte: Viewable Profile hash +// -[]byte: Viewable Profile bytes +// -int64: Profile broadcast time +// -map[int]messagepack.RawMessage: Viewable Profile Map +// -error +func GetNewestViewableUserProfile(identityHash [16]byte, networkType byte, allowTrustedStatus bool, allowUnknownStatus bool, alwaysAllowDisabled bool)(bool, int, [28]byte, []byte, int64, map[int]messagepack.RawMessage, error){ + + identityType, err := identity.GetIdentityTypeFromIdentityHash(identityHash) + if (err != nil) { + identityHashHex := encoding.EncodeBytesToHexString(identityHash[:]) + return false, 0, [28]byte{}, nil, 0, nil, errors.New("GetNewestViewableUserProfile called with invalid identityHash: " + identityHashHex) + } + + isValid := helpers.VerifyNetworkType(networkType) + if (isValid == false){ + networkTypeString := helpers.ConvertByteToString(networkType) + return false, 0, [28]byte{}, nil, 0, nil, errors.New("GetNewestViewableUserProfile called with invalid networkType: " + networkTypeString) + } + + // We check if the identity is banned + + getIdentityIsViewableStatus := func()(bool, error){ + + downloadingRequiredReviews, parametersExist, stickyConsensusEstablished, identityIsViewableStatus, err := verifiedStickyStatus.GetVerifiedIdentityIsViewableStickyStatus(identityHash, networkType) + if (err != nil) { return false, err } + if (downloadingRequiredReviews == true && parametersExist == true && stickyConsensusEstablished == true){ + + return identityIsViewableStatus, nil + } + if (allowTrustedStatus == false){ + + // The trusted viewable status of the identity is unknown + // We are not downloading the required reviews + + if (allowUnknownStatus == true){ + return true, nil + } + + return false, nil + } + + statusIsKnown, identityIsViewable, _, err := trustedViewableStatus.GetTrustedIdentityIsViewableStatus(identityHash, networkType) + if (err != nil) { return false, err } + if (statusIsKnown == true){ + return identityIsViewable, nil + } + + if (allowUnknownStatus == true){ + return true, nil + } + return false, nil + } + + identityIsViewableStatus, err := getIdentityIsViewableStatus() + if (err != nil) { return false, 0, [28]byte{}, nil, 0, nil, err } + + if (identityIsViewableStatus == false && alwaysAllowDisabled == false){ + return false, 0, [28]byte{}, nil, 0, nil, nil + } + + exists, profileHashesList, err := badgerDatabase.GetIdentityProfileHashesList(identityHash) + if (err != nil) { return false, 0, [28]byte{}, nil, 0, nil, err } + if (exists == false){ + return false, 0, [28]byte{}, nil, 0, nil, nil + } + + newestViewableProfileFound := false + newestViewableProfileVersion := 0 + var newestViewableProfileHash [28]byte + newestViewableProfileIsDisabled := false + newestViewableProfileRawProfileMap := make(map[int]messagepack.RawMessage) + newestViewableProfileBytes := make([]byte, 0) + newestViewableProfileBroadcastTime := int64(0) + + for _, profileHash := range profileHashesList{ + + exists, profileBytes, err := badgerDatabase.GetUserProfile(identityType, profileHash) + if (err != nil) { return false, 0, [28]byte{}, nil, 0, nil, err } + if (exists == false){ + // Identity profile hashes list is outdated, it will be updated automatically in the background + continue + } + + ableToRead, profileVersion, profileNetworkType, profileIdentityHash, profileBroadcastTime, profileIsDisabled, rawProfileMap, err := readProfiles.ReadProfile(false, profileBytes) + if (err != nil) { return false, 0, [28]byte{}, nil, 0, nil, err } + if (ableToRead == false){ + return false, 0, [28]byte{}, nil, 0, nil, errors.New("Database corrupt: Contains invalid profile.") + } + if (profileIdentityHash != identityHash){ + return false, 0, [28]byte{}, nil, 0, nil, errors.New("Database corrupt: Identity Profiles list profile does not match identity hash.") + } + if (profileNetworkType != networkType){ + // The profile belongs to a different networkType + // We skip it. + continue + } + + // Disabled profiles are always viewable, unless the author identity is banned. + + if (profileIsDisabled == false){ + + profileIsDisabled, profileMetadataIsKnown, profileNetworkType, profileIdentityHash, downloadingRequiredReviews, networkParametersExist, stickyConsensusEstablished, profileIsViewableStatus, err := verifiedStickyStatus.GetVerifiedProfileIsViewableStickyStatus(profileHash) + if (err != nil) { return false, 0, [28]byte{}, nil, 0, nil, err } + if (profileIsDisabled == true){ + return false, 0, [28]byte{}, nil, 0, nil, errors.New("GetVerifiedProfileIsViewableStickyStatus returning mismatched profileIsDisabled status") + } + if (profileMetadataIsKnown == false){ + // The metadata must have been deleted after we retrieved profiles from database. Skip profile + continue + } + if (profileNetworkType != networkType){ + return false, 0, [28]byte{}, nil, 0, nil, errors.New("GetVerifiedProfileIsViewableStickyStatus returning mismatched profileNetworkType.") + } + if (profileIdentityHash != identityHash){ + return false, 0, [28]byte{}, nil, 0, nil, errors.New("GetVerifiedProfileIsViewableStickyStatus returning invalid profileIdentityHash") + } + + // Outputs: + // -bool: Profile is viewable + // -error + getProfileIsViewableStatus := func()(bool, error){ + + if (downloadingRequiredReviews == true && networkParametersExist == true && stickyConsensusEstablished == true){ + + return profileIsViewableStatus, nil + } + + // Verified status is unknown. We must attempt to get trusted status + + if (allowTrustedStatus == false){ + + // The trusted viewable status of the profile is unknown + // We are not downloading the required reviews, or the profile is not downloaded + + if (allowUnknownStatus == true){ + return true, nil + } + // The profile cannot be verified as Viewable + return false, nil + } + + statusIsKnown, isViewable, _, err := trustedViewableStatus.GetTrustedProfileIsViewableStatus(profileHash) + if (err != nil) { return false, err } + if (statusIsKnown == true){ + + return isViewable, nil + } + + // We do not have a trusted or verified verdict. Profile's viewable status is unknown. + + if (allowUnknownStatus == true){ + return true, nil + } + + return false, nil + } + + profileIsViewable, err := getProfileIsViewableStatus() + if (err != nil) { return false, 0, [28]byte{}, nil, 0, nil, err } + if (profileIsViewable == false){ + // Profile is not viewable, so we skip it + + if (newestViewableProfileFound == true && newestViewableProfileIsDisabled == true && newestViewableProfileBroadcastTime < profileBroadcastTime){ + + // This user broadcasted a non-disabled profile after broadcasting their disabled profile + // We reset the newestViewableProfileFound status so that we do not return the disabled profile + // We only allow the disabled profile to be returned if it is the newest profile the user has broadcasted + // Otherwise, users could see a user as being disabled, when in reality their newest profile has not been approved yet + + newestViewableProfileFound = false + } + + continue + } + } + + if (newestViewableProfileFound == false || newestViewableProfileBroadcastTime < profileBroadcastTime){ + newestViewableProfileFound = true + newestViewableProfileVersion = profileVersion + newestViewableProfileHash = profileHash + newestViewableProfileIsDisabled = profileIsDisabled + newestViewableProfileBytes = profileBytes + newestViewableProfileRawProfileMap = rawProfileMap + newestViewableProfileBroadcastTime = profileBroadcastTime + } + } + + if (newestViewableProfileFound == false){ + return false, 0, [28]byte{}, nil, 0, nil, nil + } + + if (newestViewableProfileIsDisabled == true && alwaysAllowDisabled == true){ + return true, newestViewableProfileVersion, newestViewableProfileHash, newestViewableProfileBytes, newestViewableProfileBroadcastTime, newestViewableProfileRawProfileMap, nil + } + + if (identityIsViewableStatus == false){ + return false, 0, [28]byte{}, nil, 0, nil, nil + } + + return true, newestViewableProfileVersion, newestViewableProfileHash, newestViewableProfileBytes, newestViewableProfileBroadcastTime, newestViewableProfileRawProfileMap, nil +} + + diff --git a/internal/readContent/readContent.go b/internal/readContent/readContent.go new file mode 100644 index 0000000..6613db7 --- /dev/null +++ b/internal/readContent/readContent.go @@ -0,0 +1,78 @@ + +// readContent provides a function to verify a piece of content +// "Content" refers to a Profile, Message, Review, Report, or Parameters + +package readContent + +import "seekia/internal/profiles/readProfiles" +import "seekia/internal/messaging/readMessages" +import "seekia/internal/moderation/readReviews" +import "seekia/internal/moderation/readReports" +import "seekia/internal/parameters/readParameters" + +import "errors" + +//Outputs: +// -bool: Able to read content +// -[]byte: Content Hash +// -error +func GetContentHashFromContentBytes(verifyContent bool, contentType string, contentBytes []byte)(bool, []byte, error){ + + if (contentType == "Profile"){ + + ableToRead, profileHash, _, _, _, _, _, _, err := readProfiles.ReadProfileAndHash(verifyContent, contentBytes) + if (err != nil) { return false, nil, err } + if (ableToRead == false){ + return false, nil, nil + } + return true, profileHash[:], nil + } + if (contentType == "Message"){ + + ableToRead, messageHash, _, _, _, _, _, _, _, _, _, err := readMessages.ReadChatMessagePublicDataAndHash(verifyContent, contentBytes) + if (err != nil){ return false, nil, err } + if (ableToRead == false){ + return false, nil, nil + } + + return true, messageHash[:], nil + } + if (contentType == "Review"){ + + ableToRead, reviewHash, _, _, _, _, _, _, _, _, err := readReviews.ReadReviewAndHash(verifyContent, contentBytes) + if (err != nil) { return false, nil, err } + if (ableToRead == false){ + return false, nil, nil + } + + return true, reviewHash[:], nil + } + if (contentType == "Report"){ + + ableToRead, reportHash, _, _, _, _, _, _, err := readReports.ReadReportAndHash(verifyContent, contentBytes) + if (err != nil){ return false, nil, err } + if (ableToRead == false){ + return false, nil, nil + } + + return true, reportHash[:], nil + } + + if (contentType == "Parameters"){ + + ableToRead, parametersHash, _, _, _, _, _, _, err := readParameters.ReadParametersAndHash(verifyContent, contentBytes) + if (err != nil){ return false, nil, err } + if (ableToRead == false){ + return false, nil, nil + } + + return true, parametersHash[:], nil + } + + return false, nil, errors.New("GetContentHashFromContentBytes called with invalid contentType: " + contentType) +} + + + + + diff --git a/internal/seedPhrase/seedPhrase.go b/internal/seedPhrase/seedPhrase.go new file mode 100644 index 0000000..b00ad4f --- /dev/null +++ b/internal/seedPhrase/seedPhrase.go @@ -0,0 +1,146 @@ + +// seedPhrase provides functions to read/create seed phrases, and derive seed phrase hashes + +package seedPhrase + +// Seed Phrase is 15 words, separated by " " +// A seed phrase hash is a 32 bytes long blake3 hash of the seed phrase unicode bytes + +import "seekia/resources/wordLists" + +import "seekia/internal/cryptography/blake3" + +import "crypto/rand" +import "math/big" +import "errors" +import "strings" + + +func VerifySeedPhrase(inputSeedPhrase string)bool{ + + seedPhraseCharacterCount := len(inputSeedPhrase) + + if (seedPhraseCharacterCount < 44){ + // There are 15 words, each word is at least 2 characters long + // 15 words * 2 characters each = 30 characters + // Each word is separated by a space + // 30 + 14 == 44 characters + return false + } + + numberOfSpaces := 0 + + currentNumberOfCharacters := 0 + + finalIndex := seedPhraseCharacterCount - 1 + + for index, character := range inputSeedPhrase{ + + if (character == ' '){ + + if (currentNumberOfCharacters < 2){ + // The seed phrase contains a word with less than 2 characters + return false + } + + currentNumberOfCharacters = 0 + numberOfSpaces += 1 + + } else { + currentNumberOfCharacters += 1 + } + + if (index == finalIndex && currentNumberOfCharacters < 2){ + // The seed phrase ends with a word containing less than 2 characters + return false + } + } + + if (numberOfSpaces != 14){ + return false + } + + return true +} + +func ConvertSeedPhraseToSeedPhraseHash(inputSeedPhrase string)([32]byte, error){ + + isValid := VerifySeedPhrase(inputSeedPhrase) + if (isValid == false){ + return [32]byte{}, errors.New("ConvertSeedPhraseToSeedPhraseHash called with invalid seed phrase.") + } + + seedPhraseBytes := []byte(inputSeedPhrase) + + seedPhraseHash, err := blake3.Get32ByteBlake3Hash(seedPhraseBytes) + if (err != nil) { return [32]byte{}, err } + + return seedPhraseHash, nil +} + + +// This function is slower, only use it for generating a few seed phrases +// For many generations, use GetNewSeedPhraseFromWordList, which is faster +//Outputs: +// -string: New seed phrase +// -[32]byte: New seed phrase hash +// -error +func GetNewRandomSeedPhrase(languageName string)(string, [32]byte, error){ + + wordList, err := wordLists.GetWordListFromLanguage(languageName) + if (err != nil) { return "", [32]byte{}, err } + + newSeedPhrase, newSeedPhraseHash, err := GetNewSeedPhraseFromWordList(wordList) + if (err != nil) { return "", [32]byte{}, err } + + return newSeedPhrase, newSeedPhraseHash, nil +} + +// wordList must be retrieved from resources/wordLists/wordLists.go +//Outputs: +// -string: Seed phrase +// -[32]byte: Seed phrase hash +// -error +func GetNewSeedPhraseFromWordList(wordList []string)(string, [32]byte, error){ + + lengthOfWordList := len(wordList) + + if (lengthOfWordList < 2048) { + return "", [32]byte{}, errors.New("GetNewSeedPhraseFromWordList called with word list that is too short.") + } + + upperLimitInt64 := int64(lengthOfWordList-1) + upperLimit := big.NewInt(upperLimitInt64) + + // We use this to build the seed phrase + var seedPhraseBuilder strings.Builder + + for i := 0; i < 15; i++ { + + randomNumber, err := rand.Int(rand.Reader, upperLimit) + if (err != nil) { return "", [32]byte{}, err } + + wordIndex := int(randomNumber.Int64()) + randomWord := wordList[wordIndex] + + _, err = seedPhraseBuilder.WriteString(randomWord) + if (err != nil) { return "", [32]byte{}, err } + + if (i < 14){ + // There is a space between every word, and no trailing space + _, err := seedPhraseBuilder.WriteString(" ") + if (err != nil) { return "", [32]byte{}, err } + } + } + + newSeedPhrase := seedPhraseBuilder.String() + + newSeedPhraseBytes := []byte(newSeedPhrase) + + newSeedPhraseHash, err := blake3.Get32ByteBlake3Hash(newSeedPhraseBytes) + if (err != nil) { return "", [32]byte{}, err } + + return newSeedPhrase, newSeedPhraseHash, nil +} + + diff --git a/internal/seedPhrase/seedPhrase_test.go b/internal/seedPhrase/seedPhrase_test.go new file mode 100644 index 0000000..507aa01 --- /dev/null +++ b/internal/seedPhrase/seedPhrase_test.go @@ -0,0 +1,35 @@ +package seedPhrase_test + +import "seekia/internal/seedPhrase" + +import "testing" + + +func TestSeedPhraseFunctions(t *testing.T){ + + for i:=0; i < 1000; i++{ + + randomSeedPhrase, _, err := seedPhrase.GetNewRandomSeedPhrase("English") + if (err != nil){ + t.Fatalf("GetNewRandomSeedPhrase failed: " + err.Error()) + } + + isValid := seedPhrase.VerifySeedPhrase(randomSeedPhrase) + if (isValid == false){ + t.Fatalf("GetNewRandomSeedPhrase returning invalid seed phrase: " + randomSeedPhrase) + } + } + + testPhrase := "mention kitten rival rice minute follow problem sense december vicious unit silk limit odor quarter" + + seedPhraseHash, err := seedPhrase.ConvertSeedPhraseToSeedPhraseHash(testPhrase) + if (err != nil){ + t.Fatalf("ConvertSeedPhraseToSeedPhraseHash failed: " + err.Error()) + } + + expectedSeedPhraseHash := [32]byte{71, 29, 32, 59, 153, 84, 251, 93, 180, 116, 220, 228, 127, 229, 228, 132, 158, 62, 177, 9, 117, 139, 119, 216, 221, 204, 26, 233, 113, 71, 151, 134} + + if (seedPhraseHash != expectedSeedPhraseHash){ + t.Fatalf("ConvertSeedPhraseToSeedPhraseHash returning unexpected seed phrase hash.") + } +} diff --git a/internal/translation/translation.go b/internal/translation/translation.go new file mode 100644 index 0000000..136bd96 --- /dev/null +++ b/internal/translation/translation.go @@ -0,0 +1,22 @@ + +// translation provides functions to translate text into a user's chosen language + +package translation + +//TODO: Build this package + +// Returns user's current selected language +func GetMyLanguage()string{ + + //TODO + return "English" +} + + +// If the text is unknown, it will return the input text +func TranslateTextFromEnglishToMyLanguage(inputText string)string{ + + //TODO + + return inputText +} \ No newline at end of file diff --git a/internal/unixTime/unixTime.go b/internal/unixTime/unixTime.go new file mode 100644 index 0000000..480e938 --- /dev/null +++ b/internal/unixTime/unixTime.go @@ -0,0 +1,30 @@ + +// unixTime provides functions to get unix time values + +package unixTime + +func GetMinuteUnix()int64{ + return 60 +} + +func GetHourUnix()int64{ + return 3600 +} + +func GetDayUnix()int64{ + return 86400 +} + +func GetWeekUnix()int64{ + return 604800 +} + +func GetMonthUnix()int64{ + return 2629743 +} + +func GetYearUnix()int64{ + return 31556926 +} + + diff --git a/licenses/BadgerDB License.md b/licenses/BadgerDB License.md new file mode 100644 index 0000000..d9a10c0 --- /dev/null +++ b/licenses/BadgerDB License.md @@ -0,0 +1,176 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS diff --git a/licenses/CIRCL License.md b/licenses/CIRCL License.md new file mode 100644 index 0000000..daa303d --- /dev/null +++ b/licenses/CIRCL License.md @@ -0,0 +1,31 @@ + +BSD 3-Clause License + + +Copyright (c) 2019 Cloudflare. All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright +notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above +copyright notice, this list of conditions and the following disclaimer +in the documentation and/or other materials provided with the +distribution. + * Neither the name of Cloudflare nor the names of its +contributors may be used to endorse or promote products derived from +this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/licenses/Countries States Cities Database License.md b/licenses/Countries States Cities Database License.md new file mode 100644 index 0000000..a68f763 --- /dev/null +++ b/licenses/Countries States Cities Database License.md @@ -0,0 +1,543 @@ +# ODC Open Database License (ODbL) + +### Preamble + +The Open Database License (ODbL) is a license agreement intended to +allow users to freely share, modify, and use this Database while +maintaining this same freedom for others. Many databases are covered by +copyright, and therefore this document licenses these rights. Some +jurisdictions, mainly in the European Union, have specific rights that +cover databases, and so the ODbL addresses these rights, too. Finally, +the ODbL is also an agreement in contract for users of this Database to +act in certain ways in return for accessing this Database. + +Databases can contain a wide variety of types of content (images, +audiovisual material, and sounds all in the same database, for example), +and so the ODbL only governs the rights over the Database, and not the +contents of the Database individually. Licensors should use the ODbL +together with another license for the contents, if the contents have a +single set of rights that uniformly covers all of the contents. If the +contents have multiple sets of different rights, Licensors should +describe what rights govern what contents together in the individual +record or in some other way that clarifies what rights apply. + +Sometimes the contents of a database, or the database itself, can be +covered by other rights not addressed here (such as private contracts, +trade mark over the name, or privacy rights / data protection rights +over information in the contents), and so you are advised that you may +have to consult other documents or clear other rights before doing +activities not covered by this License. + +------ + +The Licensor (as defined below) + +and + +You (as defined below) + +agree as follows: + +### 1.0 Definitions of Capitalised Words + +"Collective Database" - Means this Database in unmodified form as part +of a collection of independent databases in themselves that together are +assembled into a collective whole. A work that constitutes a Collective +Database will not be considered a Derivative Database. + +"Convey" - As a verb, means Using the Database, a Derivative Database, +or the Database as part of a Collective Database in any way that enables +a Person to make or receive copies of the Database or a Derivative +Database. Conveying does not include interaction with a user through a +computer network, or creating and Using a Produced Work, where no +transfer of a copy of the Database or a Derivative Database occurs. +"Contents" - The contents of this Database, which includes the +information, independent works, or other material collected into the +Database. For example, the contents of the Database could be factual +data or works such as images, audiovisual material, text, or sounds. + +"Database" - A collection of material (the Contents) arranged in a +systematic or methodical way and individually accessible by electronic +or other means offered under the terms of this License. + +"Database Directive" - Means Directive 96/9/EC of the European +Parliament and of the Council of 11 March 1996 on the legal protection +of databases, as amended or succeeded. + +"Database Right" - Means rights resulting from the Chapter III ("sui +generis") rights in the Database Directive (as amended and as transposed +by member states), which includes the Extraction and Re-utilisation of +the whole or a Substantial part of the Contents, as well as any similar +rights available in the relevant jurisdiction under Section 10.4. + +"Derivative Database" - Means a database based upon the Database, and +includes any translation, adaptation, arrangement, modification, or any +other alteration of the Database or of a Substantial part of the +Contents. This includes, but is not limited to, Extracting or +Re-utilising the whole or a Substantial part of the Contents in a new +Database. + +"Extraction" - Means the permanent or temporary transfer of all or a +Substantial part of the Contents to another medium by any means or in +any form. + +"License" - Means this license agreement and is both a license of rights +such as copyright and Database Rights and an agreement in contract. + +"Licensor" - Means the Person that offers the Database under the terms +of this License. + +"Person" - Means a natural or legal person or a body of persons +corporate or incorporate. + +"Produced Work" - a work (such as an image, audiovisual material, text, +or sounds) resulting from using the whole or a Substantial part of the +Contents (via a search or other query) from this Database, a Derivative +Database, or this Database as part of a Collective Database. + +"Publicly" - means to Persons other than You or under Your control by +either more than 50% ownership or by the power to direct their +activities (such as contracting with an independent consultant). + +"Re-utilisation" - means any form of making available to the public all +or a Substantial part of the Contents by the distribution of copies, by +renting, by online or other forms of transmission. + +"Substantial" - Means substantial in terms of quantity or quality or a +combination of both. The repeated and systematic Extraction or +Re-utilisation of insubstantial parts of the Contents may amount to the +Extraction or Re-utilisation of a Substantial part of the Contents. + +"Use" - As a verb, means doing any act that is restricted by copyright +or Database Rights whether in the original medium or any other; and +includes without limitation distributing, copying, publicly performing, +publicly displaying, and preparing derivative works of the Database, as +well as modifying the Database as may be technically necessary to use it +in a different mode or format. + +"You" - Means a Person exercising rights under this License who has not +previously violated the terms of this License with respect to the +Database, or who has received express permission from the Licensor to +exercise rights under this License despite a previous violation. + +Words in the singular include the plural and vice versa. + +### 2.0 What this License covers + +2.1. Legal effect of this document. This License is: + + a. A license of applicable copyright and neighbouring rights; + + b. A license of the Database Right; and + + c. An agreement in contract between You and the Licensor. + +2.2 Legal rights covered. This License covers the legal rights in the +Database, including: + + a. Copyright. Any copyright or neighbouring rights in the Database. + The copyright licensed includes any individual elements of the + Database, but does not cover the copyright over the Contents + independent of this Database. See Section 2.4 for details. Copyright + law varies between jurisdictions, but is likely to cover: the Database + model or schema, which is the structure, arrangement, and organisation + of the Database, and can also include the Database tables and table + indexes; the data entry and output sheets; and the Field names of + Contents stored in the Database; + + b. Database Rights. Database Rights only extend to the Extraction and + Re-utilisation of the whole or a Substantial part of the Contents. + Database Rights can apply even when there is no copyright over the + Database. Database Rights can also apply when the Contents are removed + from the Database and are selected and arranged in a way that would + not infringe any applicable copyright; and + + c. Contract. This is an agreement between You and the Licensor for + access to the Database. In return you agree to certain conditions of + use on this access as outlined in this License. + +2.3 Rights not covered. + + a. This License does not apply to computer programs used in the making + or operation of the Database; + + b. This License does not cover any patents over the Contents or the + Database; and + + c. This License does not cover any trademarks associated with the + Database. + +2.4 Relationship to Contents in the Database. The individual items of +the Contents contained in this Database may be covered by other rights, +including copyright, patent, data protection, privacy, or personality +rights, and this License does not cover any rights (other than Database +Rights or in contract) in individual Contents contained in the Database. +For example, if used on a Database of images (the Contents), this +License would not apply to copyright over individual images, which could +have their own separate licenses, or one single license covering all of +the rights over the images. + +### 3.0 Rights granted + +3.1 Subject to the terms and conditions of this License, the Licensor +grants to You a worldwide, royalty-free, non-exclusive, terminable (but +only under Section 9) license to Use the Database for the duration of +any applicable copyright and Database Rights. These rights explicitly +include commercial use, and do not exclude any field of endeavour. To +the extent possible in the relevant jurisdiction, these rights may be +exercised in all media and formats whether now known or created in the +future. + +The rights granted cover, for example: + + a. Extraction and Re-utilisation of the whole or a Substantial part of + the Contents; + + b. Creation of Derivative Databases; + + c. Creation of Collective Databases; + + d. Creation of temporary or permanent reproductions by any means and + in any form, in whole or in part, including of any Derivative + Databases or as a part of Collective Databases; and + + e. Distribution, communication, display, lending, making available, or + performance to the public by any means and in any form, in whole or in + part, including of any Derivative Database or as a part of Collective + Databases. + +3.2 Compulsory license schemes. For the avoidance of doubt: + + a. Non-waivable compulsory license schemes. In those jurisdictions in + which the right to collect royalties through any statutory or + compulsory licensing scheme cannot be waived, the Licensor reserves + the exclusive right to collect such royalties for any exercise by You + of the rights granted under this License; + + b. Waivable compulsory license schemes. In those jurisdictions in + which the right to collect royalties through any statutory or + compulsory licensing scheme can be waived, the Licensor waives the + exclusive right to collect such royalties for any exercise by You of + the rights granted under this License; and, + + c. Voluntary license schemes. The Licensor waives the right to collect + royalties, whether individually or, in the event that the Licensor is + a member of a collecting society that administers voluntary licensing + schemes, via that society, from any exercise by You of the rights + granted under this License. + +3.3 The right to release the Database under different terms, or to stop +distributing or making available the Database, is reserved. Note that +this Database may be multiple-licensed, and so You may have the choice +of using alternative licenses for this Database. Subject to Section +10.4, all other rights not expressly granted by Licensor are reserved. + +### 4.0 Conditions of Use + +4.1 The rights granted in Section 3 above are expressly made subject to +Your complying with the following conditions of use. These are important +conditions of this License, and if You fail to follow them, You will be +in material breach of its terms. + +4.2 Notices. If You Publicly Convey this Database, any Derivative +Database, or the Database as part of a Collective Database, then You +must: + + a. Do so only under the terms of this License or another license + permitted under Section 4.4; + + b. Include a copy of this License (or, as applicable, a license + permitted under Section 4.4) or its Uniform Resource Identifier (URI) + with the Database or Derivative Database, including both in the + Database or Derivative Database and in any relevant documentation; and + + c. Keep intact any copyright or Database Right notices and notices + that refer to this License. + + d. If it is not possible to put the required notices in a particular + file due to its structure, then You must include the notices in a + location (such as a relevant directory) where users would be likely to + look for it. + +4.3 Notice for using output (Contents). Creating and Using a Produced +Work does not require the notice in Section 4.2. However, if you +Publicly Use a Produced Work, You must include a notice associated with +the Produced Work reasonably calculated to make any Person that uses, +views, accesses, interacts with, or is otherwise exposed to the Produced +Work aware that Content was obtained from the Database, Derivative +Database, or the Database as part of a Collective Database, and that it +is available under this License. + + a. Example notice. The following text will satisfy notice under + Section 4.3: + + Contains information from DATABASE NAME, which is made available + here under the Open Database License (ODbL). + +DATABASE NAME should be replaced with the name of the Database and a +hyperlink to the URI of the Database. "Open Database License" should +contain a hyperlink to the URI of the text of this License. If +hyperlinks are not possible, You should include the plain text of the +required URI's with the above notice. + +4.4 Share alike. + + a. Any Derivative Database that You Publicly Use must be only under + the terms of: + + i. This License; + + ii. A later version of this License similar in spirit to this + License; or + + iii. A compatible license. + + If You license the Derivative Database under one of the licenses + mentioned in (iii), You must comply with the terms of that license. + + b. For the avoidance of doubt, Extraction or Re-utilisation of the + whole or a Substantial part of the Contents into a new database is a + Derivative Database and must comply with Section 4.4. + + c. Derivative Databases and Produced Works. A Derivative Database is + Publicly Used and so must comply with Section 4.4. if a Produced Work + created from the Derivative Database is Publicly Used. + + d. Share Alike and additional Contents. For the avoidance of doubt, + You must not add Contents to Derivative Databases under Section 4.4 a + that are incompatible with the rights granted under this License. + + e. Compatible licenses. Licensors may authorise a proxy to determine + compatible licenses under Section 4.4 a iii. If they do so, the + authorised proxy's public statement of acceptance of a compatible + license grants You permission to use the compatible license. + + +4.5 Limits of Share Alike. The requirements of Section 4.4 do not apply +in the following: + + a. For the avoidance of doubt, You are not required to license + Collective Databases under this License if You incorporate this + Database or a Derivative Database in the collection, but this License + still applies to this Database or a Derivative Database as a part of + the Collective Database; + + b. Using this Database, a Derivative Database, or this Database as + part of a Collective Database to create a Produced Work does not + create a Derivative Database for purposes of Section 4.4; and + + c. Use of a Derivative Database internally within an organisation is + not to the public and therefore does not fall under the requirements + of Section 4.4. + +4.6 Access to Derivative Databases. If You Publicly Use a Derivative +Database or a Produced Work from a Derivative Database, You must also +offer to recipients of the Derivative Database or Produced Work a copy +in a machine readable form of: + + a. The entire Derivative Database; or + + b. A file containing all of the alterations made to the Database or + the method of making the alterations to the Database (such as an + algorithm), including any additional Contents, that make up all the + differences between the Database and the Derivative Database. + +The Derivative Database (under a.) or alteration file (under b.) must be +available at no more than a reasonable production cost for physical +distributions and free of charge if distributed over the internet. + +4.7 Technological measures and additional terms + + a. This License does not allow You to impose (except subject to + Section 4.7 b.) any terms or any technological measures on the + Database, a Derivative Database, or the whole or a Substantial part of + the Contents that alter or restrict the terms of this License, or any + rights granted under it, or have the effect or intent of restricting + the ability of any person to exercise those rights. + + b. Parallel distribution. You may impose terms or technological + measures on the Database, a Derivative Database, or the whole or a + Substantial part of the Contents (a "Restricted Database") in + contravention of Section 4.74 a. only if You also make a copy of the + Database or a Derivative Database available to the recipient of the + Restricted Database: + + i. That is available without additional fee; + + ii. That is available in a medium that does not alter or restrict + the terms of this License, or any rights granted under it, or have + the effect or intent of restricting the ability of any person to + exercise those rights (an "Unrestricted Database"); and + + iii. The Unrestricted Database is at least as accessible to the + recipient as a practical matter as the Restricted Database. + + c. For the avoidance of doubt, You may place this Database or a + Derivative Database in an authenticated environment, behind a + password, or within a similar access control scheme provided that You + do not alter or restrict the terms of this License or any rights + granted under it or have the effect or intent of restricting the + ability of any person to exercise those rights. + +4.8 Licensing of others. You may not sublicense the Database. Each time +You communicate the Database, the whole or Substantial part of the +Contents, or any Derivative Database to anyone else in any way, the +Licensor offers to the recipient a license to the Database on the same +terms and conditions as this License. You are not responsible for +enforcing compliance by third parties with this License, but You may +enforce any rights that You have over a Derivative Database. You are +solely responsible for any modifications of a Derivative Database made +by You or another Person at Your direction. You may not impose any +further restrictions on the exercise of the rights granted or affirmed +under this License. + +### 5.0 Moral rights + +5.1 Moral rights. This section covers moral rights, including any rights +to be identified as the author of the Database or to object to treatment +that would otherwise prejudice the author's honour and reputation, or +any other derogatory treatment: + + a. For jurisdictions allowing waiver of moral rights, Licensor waives + all moral rights that Licensor may have in the Database to the fullest + extent possible by the law of the relevant jurisdiction under Section + 10.4; + + b. If waiver of moral rights under Section 5.1 a in the relevant + jurisdiction is not possible, Licensor agrees not to assert any moral + rights over the Database and waives all claims in moral rights to the + fullest extent possible by the law of the relevant jurisdiction under + Section 10.4; and + + c. For jurisdictions not allowing waiver or an agreement not to assert + moral rights under Section 5.1 a and b, the author may retain their + moral rights over certain aspects of the Database. + +Please note that some jurisdictions do not allow for the waiver of moral +rights, and so moral rights may still subsist over the Database in some +jurisdictions. + +### 6.0 Fair dealing, Database exceptions, and other rights not affected + +6.1 This License does not affect any rights that You or anyone else may +independently have under any applicable law to make any use of this +Database, including without limitation: + + a. Exceptions to the Database Right including: Extraction of Contents + from non-electronic Databases for private purposes, Extraction for + purposes of illustration for teaching or scientific research, and + Extraction or Re-utilisation for public security or an administrative + or judicial procedure. + + b. Fair dealing, fair use, or any other legally recognised limitation + or exception to infringement of copyright or other applicable laws. + +6.2 This License does not affect any rights of lawful users to Extract +and Re-utilise insubstantial parts of the Contents, evaluated +quantitatively or qualitatively, for any purposes whatsoever, including +creating a Derivative Database (subject to other rights over the +Contents, see Section 2.4). The repeated and systematic Extraction or +Re-utilisation of insubstantial parts of the Contents may however amount +to the Extraction or Re-utilisation of a Substantial part of the +Contents. + +### 7.0 Warranties and Disclaimer + +7.1 The Database is licensed by the Licensor "as is" and without any +warranty of any kind, either express, implied, or arising by statute, +custom, course of dealing, or trade usage. Licensor specifically +disclaims any and all implied warranties or conditions of title, +non-infringement, accuracy or completeness, the presence or absence of +errors, fitness for a particular purpose, merchantability, or otherwise. +Some jurisdictions do not allow the exclusion of implied warranties, so +this exclusion may not apply to You. + +### 8.0 Limitation of liability + +8.1 Subject to any liability that may not be excluded or limited by law, +the Licensor is not liable for, and expressly excludes, all liability +for loss or damage however and whenever caused to anyone by any use +under this License, whether by You or by anyone else, and whether caused +by any fault on the part of the Licensor or not. This exclusion of +liability includes, but is not limited to, any special, incidental, +consequential, punitive, or exemplary damages such as loss of revenue, +data, anticipated profits, and lost business. This exclusion applies +even if the Licensor has been advised of the possibility of such +damages. + +8.2 If liability may not be excluded by law, it is limited to actual and +direct financial loss to the extent it is caused by proved negligence on +the part of the Licensor. + +### 9.0 Termination of Your rights under this License + +9.1 Any breach by You of the terms and conditions of this License +automatically terminates this License with immediate effect and without +notice to You. For the avoidance of doubt, Persons who have received the +Database, the whole or a Substantial part of the Contents, Derivative +Databases, or the Database as part of a Collective Database from You +under this License will not have their licenses terminated provided +their use is in full compliance with this License or a license granted +under Section 4.8 of this License. Sections 1, 2, 7, 8, 9 and 10 will +survive any termination of this License. + +9.2 If You are not in breach of the terms of this License, the Licensor +will not terminate Your rights under it. + +9.3 Unless terminated under Section 9.1, this License is granted to You +for the duration of applicable rights in the Database. + +9.4 Reinstatement of rights. If you cease any breach of the terms and +conditions of this License, then your full rights under this License +will be reinstated: + + a. Provisionally and subject to permanent termination until the 60th + day after cessation of breach; + + b. Permanently on the 60th day after cessation of breach unless + otherwise reasonably notified by the Licensor; or + + c. Permanently if reasonably notified by the Licensor of the + violation, this is the first time You have received notice of + violation of this License from the Licensor, and You cure the + violation prior to 30 days after your receipt of the notice. + +Persons subject to permanent termination of rights are not eligible to +be a recipient and receive a license under Section 4.8. + +9.5 Notwithstanding the above, Licensor reserves the right to release +the Database under different license terms or to stop distributing or +making available the Database. Releasing the Database under different +license terms or stopping the distribution of the Database will not +withdraw this License (or any other license that has been, or is +required to be, granted under the terms of this License), and this +License will continue in full force and effect unless terminated as +stated above. + +### 10.0 General + +10.1 If any provision of this License is held to be invalid or +unenforceable, that must not affect the validity or enforceability of +the remainder of the terms and conditions of this License and each +remaining provision of this License shall be valid and enforced to the +fullest extent permitted by law. + +10.2 This License is the entire agreement between the parties with +respect to the rights granted here over the Database. It replaces any +earlier understandings, agreements or representations with respect to +the Database. + +10.3 If You are in breach of the terms of this License, You will not be +entitled to rely on the terms of this License or to complain of any +breach by the Licensor. + +10.4 Choice of law. This License takes effect in and will be governed by +the laws of the relevant jurisdiction in which the License terms are +sought to be enforced. If the standard suite of rights granted under +applicable copyright law and Database Rights in the relevant +jurisdiction includes additional rights not granted under this License, +these additional rights are granted in this License in order to meet the +terms of this License. + + +For more information, please refer to diff --git a/licenses/Fyne License.md b/licenses/Fyne License.md new file mode 100644 index 0000000..3b16f62 --- /dev/null +++ b/licenses/Fyne License.md @@ -0,0 +1,29 @@ + +BSD 3-Clause License + +Copyright (C) 2018 Fyne.io developers + +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + * Neither the name of Fyne.io nor the names of its contributors may be + used to endorse or promote products derived from this software without + specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER BE LIABLE FOR ANY +DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND +ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + diff --git a/licenses/Geodist License.md b/licenses/Geodist License.md new file mode 100644 index 0000000..d045fbd --- /dev/null +++ b/licenses/Geodist License.md @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2021 John Taylor + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/licenses/Gift License.md b/licenses/Gift License.md new file mode 100644 index 0000000..d9b9c2b --- /dev/null +++ b/licenses/Gift License.md @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2014-2018 Grigory Dryapak + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/licenses/Golang License.md b/licenses/Golang License.md new file mode 100644 index 0000000..5db5d1b --- /dev/null +++ b/licenses/Golang License.md @@ -0,0 +1,29 @@ +BSD 3-Clause License + +Copyright (c) 2009 The Go Authors. All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright +notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above +copyright notice, this list of conditions and the following disclaimer +in the documentation and/or other materials provided with the +distribution. + * Neither the name of Google Inc. nor the names of its +contributors may be used to endorse or promote products derived from +this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/licenses/Gorgonia License.md b/licenses/Gorgonia License.md new file mode 100644 index 0000000..c7a1c7b --- /dev/null +++ b/licenses/Gorgonia License.md @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright 2019 Gorgonia Authors + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/licenses/Msgpack License.md b/licenses/Msgpack License.md new file mode 100644 index 0000000..b749d07 --- /dev/null +++ b/licenses/Msgpack License.md @@ -0,0 +1,25 @@ +Copyright (c) 2013 The github.com/vmihailenco/msgpack Authors. +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright +notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above +copyright notice, this list of conditions and the following disclaimer +in the documentation and/or other materials provided with the +distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/licenses/Openmoji License.md b/licenses/Openmoji License.md new file mode 100644 index 0000000..cc3e245 --- /dev/null +++ b/licenses/Openmoji License.md @@ -0,0 +1,427 @@ +Attribution-ShareAlike 4.0 International + +======================================================================= + +Creative Commons Corporation ("Creative Commons") is not a law firm and +does not provide legal services or legal advice. Distribution of +Creative Commons public licenses does not create a lawyer-client or +other relationship. Creative Commons makes its licenses and related +information available on an "as-is" basis. Creative Commons gives no +warranties regarding its licenses, any material licensed under their +terms and conditions, or any related information. Creative Commons +disclaims all liability for damages resulting from their use to the +fullest extent possible. + +Using Creative Commons Public Licenses + +Creative Commons public licenses provide a standard set of terms and +conditions that creators and other rights holders may use to share +original works of authorship and other material subject to copyright +and certain other rights specified in the public license below. The +following considerations are for informational purposes only, are not +exhaustive, and do not form part of our licenses. + + Considerations for licensors: Our public licenses are + intended for use by those authorized to give the public + permission to use material in ways otherwise restricted by + copyright and certain other rights. Our licenses are + irrevocable. Licensors should read and understand the terms + and conditions of the license they choose before applying it. + Licensors should also secure all rights necessary before + applying our licenses so that the public can reuse the + material as expected. Licensors should clearly mark any + material not subject to the license. This includes other CC- + licensed material, or material used under an exception or + limitation to copyright. More considerations for licensors: + wiki.creativecommons.org/Considerations_for_licensors + + Considerations for the public: By using one of our public + licenses, a licensor grants the public permission to use the + licensed material under specified terms and conditions. If + the licensor's permission is not necessary for any reason--for + example, because of any applicable exception or limitation to + copyright--then that use is not regulated by the license. Our + licenses grant only permissions under copyright and certain + other rights that a licensor has authority to grant. Use of + the licensed material may still be restricted for other + reasons, including because others have copyright or other + rights in the material. A licensor may make special requests, + such as asking that all changes be marked or described. + Although not required by our licenses, you are encouraged to + respect those requests where reasonable. More considerations + for the public: + wiki.creativecommons.org/Considerations_for_licensees + +======================================================================= + +Creative Commons Attribution-ShareAlike 4.0 International Public +License + +By exercising the Licensed Rights (defined below), You accept and agree +to be bound by the terms and conditions of this Creative Commons +Attribution-ShareAlike 4.0 International Public License ("Public +License"). To the extent this Public License may be interpreted as a +contract, You are granted the Licensed Rights in consideration of Your +acceptance of these terms and conditions, and the Licensor grants You +such rights in consideration of benefits the Licensor receives from +making the Licensed Material available under these terms and +conditions. + + +Section 1 -- Definitions. + + a. Adapted Material means material subject to Copyright and Similar + Rights that is derived from or based upon the Licensed Material + and in which the Licensed Material is translated, altered, + arranged, transformed, or otherwise modified in a manner requiring + permission under the Copyright and Similar Rights held by the + Licensor. For purposes of this Public License, where the Licensed + Material is a musical work, performance, or sound recording, + Adapted Material is always produced where the Licensed Material is + synched in timed relation with a moving image. + + b. Adapter's License means the license You apply to Your Copyright + and Similar Rights in Your contributions to Adapted Material in + accordance with the terms and conditions of this Public License. + + c. BY-SA Compatible License means a license listed at + creativecommons.org/compatiblelicenses, approved by Creative + Commons as essentially the equivalent of this Public License. + + d. Copyright and Similar Rights means copyright and/or similar rights + closely related to copyright including, without limitation, + performance, broadcast, sound recording, and Sui Generis Database + Rights, without regard to how the rights are labeled or + categorized. For purposes of this Public License, the rights + specified in Section 2(b)(1)-(2) are not Copyright and Similar + Rights. + + e. Effective Technological Measures means those measures that, in the + absence of proper authority, may not be circumvented under laws + fulfilling obligations under Article 11 of the WIPO Copyright + Treaty adopted on December 20, 1996, and/or similar international + agreements. + + f. Exceptions and Limitations means fair use, fair dealing, and/or + any other exception or limitation to Copyright and Similar Rights + that applies to Your use of the Licensed Material. + + g. License Elements means the license attributes listed in the name + of a Creative Commons Public License. The License Elements of this + Public License are Attribution and ShareAlike. + + h. Licensed Material means the artistic or literary work, database, + or other material to which the Licensor applied this Public + License. + + i. Licensed Rights means the rights granted to You subject to the + terms and conditions of this Public License, which are limited to + all Copyright and Similar Rights that apply to Your use of the + Licensed Material and that the Licensor has authority to license. + + j. Licensor means the individual(s) or entity(ies) granting rights + under this Public License. + + k. Share means to provide material to the public by any means or + process that requires permission under the Licensed Rights, such + as reproduction, public display, public performance, distribution, + dissemination, communication, or importation, and to make material + available to the public including in ways that members of the + public may access the material from a place and at a time + individually chosen by them. + + l. Sui Generis Database Rights means rights other than copyright + resulting from Directive 96/9/EC of the European Parliament and of + the Council of 11 March 1996 on the legal protection of databases, + as amended and/or succeeded, as well as other essentially + equivalent rights anywhere in the world. + + m. You means the individual or entity exercising the Licensed Rights + under this Public License. Your has a corresponding meaning. + + +Section 2 -- Scope. + + a. License grant. + + 1. Subject to the terms and conditions of this Public License, + the Licensor hereby grants You a worldwide, royalty-free, + non-sublicensable, non-exclusive, irrevocable license to + exercise the Licensed Rights in the Licensed Material to: + + a. reproduce and Share the Licensed Material, in whole or + in part; and + + b. produce, reproduce, and Share Adapted Material. + + 2. Exceptions and Limitations. For the avoidance of doubt, where + Exceptions and Limitations apply to Your use, this Public + License does not apply, and You do not need to comply with + its terms and conditions. + + 3. Term. The term of this Public License is specified in Section + 6(a). + + 4. Media and formats; technical modifications allowed. The + Licensor authorizes You to exercise the Licensed Rights in + all media and formats whether now known or hereafter created, + and to make technical modifications necessary to do so. The + Licensor waives and/or agrees not to assert any right or + authority to forbid You from making technical modifications + necessary to exercise the Licensed Rights, including + technical modifications necessary to circumvent Effective + Technological Measures. For purposes of this Public License, + simply making modifications authorized by this Section 2(a) + (4) never produces Adapted Material. + + 5. Downstream recipients. + + a. Offer from the Licensor -- Licensed Material. Every + recipient of the Licensed Material automatically + receives an offer from the Licensor to exercise the + Licensed Rights under the terms and conditions of this + Public License. + + b. Additional offer from the Licensor -- Adapted Material. + Every recipient of Adapted Material from You + automatically receives an offer from the Licensor to + exercise the Licensed Rights in the Adapted Material + under the conditions of the Adapter's License You apply. + + c. No downstream restrictions. You may not offer or impose + any additional or different terms or conditions on, or + apply any Effective Technological Measures to, the + Licensed Material if doing so restricts exercise of the + Licensed Rights by any recipient of the Licensed + Material. + + 6. No endorsement. Nothing in this Public License constitutes or + may be construed as permission to assert or imply that You + are, or that Your use of the Licensed Material is, connected + with, or sponsored, endorsed, or granted official status by, + the Licensor or others designated to receive attribution as + provided in Section 3(a)(1)(A)(i). + + b. Other rights. + + 1. Moral rights, such as the right of integrity, are not + licensed under this Public License, nor are publicity, + privacy, and/or other similar personality rights; however, to + the extent possible, the Licensor waives and/or agrees not to + assert any such rights held by the Licensor to the limited + extent necessary to allow You to exercise the Licensed + Rights, but not otherwise. + + 2. Patent and trademark rights are not licensed under this + Public License. + + 3. To the extent possible, the Licensor waives any right to + collect royalties from You for the exercise of the Licensed + Rights, whether directly or through a collecting society + under any voluntary or waivable statutory or compulsory + licensing scheme. In all other cases the Licensor expressly + reserves any right to collect such royalties. + + +Section 3 -- License Conditions. + +Your exercise of the Licensed Rights is expressly made subject to the +following conditions. + + a. Attribution. + + 1. If You Share the Licensed Material (including in modified + form), You must: + + a. retain the following if it is supplied by the Licensor + with the Licensed Material: + + i. identification of the creator(s) of the Licensed + Material and any others designated to receive + attribution, in any reasonable manner requested by + the Licensor (including by pseudonym if + designated); + + ii. a copyright notice; + + iii. a notice that refers to this Public License; + + iv. a notice that refers to the disclaimer of + warranties; + + v. a URI or hyperlink to the Licensed Material to the + extent reasonably practicable; + + b. indicate if You modified the Licensed Material and + retain an indication of any previous modifications; and + + c. indicate the Licensed Material is licensed under this + Public License, and include the text of, or the URI or + hyperlink to, this Public License. + + 2. You may satisfy the conditions in Section 3(a)(1) in any + reasonable manner based on the medium, means, and context in + which You Share the Licensed Material. For example, it may be + reasonable to satisfy the conditions by providing a URI or + hyperlink to a resource that includes the required + information. + + 3. If requested by the Licensor, You must remove any of the + information required by Section 3(a)(1)(A) to the extent + reasonably practicable. + + b. ShareAlike. + + In addition to the conditions in Section 3(a), if You Share + Adapted Material You produce, the following conditions also apply. + + 1. The Adapter's License You apply must be a Creative Commons + license with the same License Elements, this version or + later, or a BY-SA Compatible License. + + 2. You must include the text of, or the URI or hyperlink to, the + Adapter's License You apply. You may satisfy this condition + in any reasonable manner based on the medium, means, and + context in which You Share Adapted Material. + + 3. You may not offer or impose any additional or different terms + or conditions on, or apply any Effective Technological + Measures to, Adapted Material that restrict exercise of the + rights granted under the Adapter's License You apply. + + +Section 4 -- Sui Generis Database Rights. + +Where the Licensed Rights include Sui Generis Database Rights that +apply to Your use of the Licensed Material: + + a. for the avoidance of doubt, Section 2(a)(1) grants You the right + to extract, reuse, reproduce, and Share all or a substantial + portion of the contents of the database; + + b. if You include all or a substantial portion of the database + contents in a database in which You have Sui Generis Database + Rights, then the database in which You have Sui Generis Database + Rights (but not its individual contents) is Adapted Material, + + including for purposes of Section 3(b); and + c. You must comply with the conditions in Section 3(a) if You Share + all or a substantial portion of the contents of the database. + +For the avoidance of doubt, this Section 4 supplements and does not +replace Your obligations under this Public License where the Licensed +Rights include other Copyright and Similar Rights. + + +Section 5 -- Disclaimer of Warranties and Limitation of Liability. + + a. UNLESS OTHERWISE SEPARATELY UNDERTAKEN BY THE LICENSOR, TO THE + EXTENT POSSIBLE, THE LICENSOR OFFERS THE LICENSED MATERIAL AS-IS + AND AS-AVAILABLE, AND MAKES NO REPRESENTATIONS OR WARRANTIES OF + ANY KIND CONCERNING THE LICENSED MATERIAL, WHETHER EXPRESS, + IMPLIED, STATUTORY, OR OTHER. THIS INCLUDES, WITHOUT LIMITATION, + WARRANTIES OF TITLE, MERCHANTABILITY, FITNESS FOR A PARTICULAR + PURPOSE, NON-INFRINGEMENT, ABSENCE OF LATENT OR OTHER DEFECTS, + ACCURACY, OR THE PRESENCE OR ABSENCE OF ERRORS, WHETHER OR NOT + KNOWN OR DISCOVERABLE. WHERE DISCLAIMERS OF WARRANTIES ARE NOT + ALLOWED IN FULL OR IN PART, THIS DISCLAIMER MAY NOT APPLY TO YOU. + + b. TO THE EXTENT POSSIBLE, IN NO EVENT WILL THE LICENSOR BE LIABLE + TO YOU ON ANY LEGAL THEORY (INCLUDING, WITHOUT LIMITATION, + NEGLIGENCE) OR OTHERWISE FOR ANY DIRECT, SPECIAL, INDIRECT, + INCIDENTAL, CONSEQUENTIAL, PUNITIVE, EXEMPLARY, OR OTHER LOSSES, + COSTS, EXPENSES, OR DAMAGES ARISING OUT OF THIS PUBLIC LICENSE OR + USE OF THE LICENSED MATERIAL, EVEN IF THE LICENSOR HAS BEEN + ADVISED OF THE POSSIBILITY OF SUCH LOSSES, COSTS, EXPENSES, OR + DAMAGES. WHERE A LIMITATION OF LIABILITY IS NOT ALLOWED IN FULL OR + IN PART, THIS LIMITATION MAY NOT APPLY TO YOU. + + c. The disclaimer of warranties and limitation of liability provided + above shall be interpreted in a manner that, to the extent + possible, most closely approximates an absolute disclaimer and + waiver of all liability. + + +Section 6 -- Term and Termination. + + a. This Public License applies for the term of the Copyright and + Similar Rights licensed here. However, if You fail to comply with + this Public License, then Your rights under this Public License + terminate automatically. + + b. Where Your right to use the Licensed Material has terminated under + Section 6(a), it reinstates: + + 1. automatically as of the date the violation is cured, provided + it is cured within 30 days of Your discovery of the + violation; or + + 2. upon express reinstatement by the Licensor. + + For the avoidance of doubt, this Section 6(b) does not affect any + right the Licensor may have to seek remedies for Your violations + of this Public License. + + c. For the avoidance of doubt, the Licensor may also offer the + Licensed Material under separate terms or conditions or stop + distributing the Licensed Material at any time; however, doing so + will not terminate this Public License. + + d. Sections 1, 5, 6, 7, and 8 survive termination of this Public + License. + + +Section 7 -- Other Terms and Conditions. + + a. The Licensor shall not be bound by any additional or different + terms or conditions communicated by You unless expressly agreed. + + b. Any arrangements, understandings, or agreements regarding the + Licensed Material not stated herein are separate from and + independent of the terms and conditions of this Public License. + + +Section 8 -- Interpretation. + + a. For the avoidance of doubt, this Public License does not, and + shall not be interpreted to, reduce, limit, restrict, or impose + conditions on any use of the Licensed Material that could lawfully + be made without permission under this Public License. + + b. To the extent possible, if any provision of this Public License is + deemed unenforceable, it shall be automatically reformed to the + minimum extent necessary to make it enforceable. If the provision + cannot be reformed, it shall be severed from this Public License + without affecting the enforceability of the remaining terms and + conditions. + + c. No term or condition of this Public License will be waived and no + failure to comply consented to unless expressly agreed to by the + Licensor. + + d. Nothing in this Public License constitutes or may be interpreted + as a limitation upon, or waiver of, any privileges and immunities + that apply to the Licensor or You, including from the legal + processes of any jurisdiction or authority. + + +======================================================================= + +Creative Commons is not a party to its public +licenses. Notwithstanding, Creative Commons may elect to apply one of +its public licenses to material it publishes and in those instances +will be considered the “Licensor.” The text of the Creative Commons +public licenses is dedicated to the public domain under the CC0 Public +Domain Dedication. Except for the limited purpose of indicating that +material is shared under a Creative Commons public license or as +otherwise permitted by the Creative Commons policies published at +creativecommons.org/policies, Creative Commons does not authorize the +use of the trademark "Creative Commons" or any other trademark or logo +of Creative Commons without its prior written consent including, +without limitation, in connection with any unauthorized modifications +to any of its public licenses or any other arrangements, +understandings, or agreements concerning use of licensed material. For +the avoidance of doubt, this paragraph does not form part of the +public licenses. + +Creative Commons may be contacted at creativecommons.org. diff --git a/licenses/Unlicense.md b/licenses/Unlicense.md new file mode 100644 index 0000000..60e72c5 --- /dev/null +++ b/licenses/Unlicense.md @@ -0,0 +1,13 @@ + +# Unlicense + +This is free and unencumbered software released into the public domain. + +Anyone is free to copy, modify, publish, use, compile, sell, or distribute this software, either in source code form or as a compiled binary, for any purpose, commercial or non-commercial, and by any means. + +In jurisdictions that recognize copyright laws, the author or authors of this software dedicate any and all copyright interest in the software to the public domain. We make this dedication for the benefit of the public at large and to the detriment of our heirs and successors. We intend this dedication to be an overt act of relinquishment in perpetuity of all present and future rights to this software under copyright law. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +For more information, please refer to [unlicense.org](http://unlicense.org) + diff --git a/licenses/Zeebo Blake3 License.md b/licenses/Zeebo Blake3 License.md new file mode 100644 index 0000000..0e259d4 --- /dev/null +++ b/licenses/Zeebo Blake3 License.md @@ -0,0 +1,121 @@ +Creative Commons Legal Code + +CC0 1.0 Universal + + CREATIVE COMMONS CORPORATION IS NOT A LAW FIRM AND DOES NOT PROVIDE + LEGAL SERVICES. DISTRIBUTION OF THIS DOCUMENT DOES NOT CREATE AN + ATTORNEY-CLIENT RELATIONSHIP. CREATIVE COMMONS PROVIDES THIS + INFORMATION ON AN "AS-IS" BASIS. CREATIVE COMMONS MAKES NO WARRANTIES + REGARDING THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS + PROVIDED HEREUNDER, AND DISCLAIMS LIABILITY FOR DAMAGES RESULTING FROM + THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS PROVIDED + HEREUNDER. + +Statement of Purpose + +The laws of most jurisdictions throughout the world automatically confer +exclusive Copyright and Related Rights (defined below) upon the creator +and subsequent owner(s) (each and all, an "owner") of an original work of +authorship and/or a database (each, a "Work"). + +Certain owners wish to permanently relinquish those rights to a Work for +the purpose of contributing to a commons of creative, cultural and +scientific works ("Commons") that the public can reliably and without fear +of later claims of infringement build upon, modify, incorporate in other +works, reuse and redistribute as freely as possible in any form whatsoever +and for any purposes, including without limitation commercial purposes. +These owners may contribute to the Commons to promote the ideal of a free +culture and the further production of creative, cultural and scientific +works, or to gain reputation or greater distribution for their Work in +part through the use and efforts of others. + +For these and/or other purposes and motivations, and without any +expectation of additional consideration or compensation, the person +associating CC0 with a Work (the "Affirmer"), to the extent that he or she +is an owner of Copyright and Related Rights in the Work, voluntarily +elects to apply CC0 to the Work and publicly distribute the Work under its +terms, with knowledge of his or her Copyright and Related Rights in the +Work and the meaning and intended legal effect of CC0 on those rights. + +1. Copyright and Related Rights. A Work made available under CC0 may be +protected by copyright and related or neighboring rights ("Copyright and +Related Rights"). Copyright and Related Rights include, but are not +limited to, the following: + + i. the right to reproduce, adapt, distribute, perform, display, + communicate, and translate a Work; + ii. moral rights retained by the original author(s) and/or performer(s); +iii. publicity and privacy rights pertaining to a person's image or + likeness depicted in a Work; + iv. rights protecting against unfair competition in regards to a Work, + subject to the limitations in paragraph 4(a), below; + v. rights protecting the extraction, dissemination, use and reuse of data + in a Work; + vi. database rights (such as those arising under Directive 96/9/EC of the + European Parliament and of the Council of 11 March 1996 on the legal + protection of databases, and under any national implementation + thereof, including any amended or successor version of such + directive); and +vii. other similar, equivalent or corresponding rights throughout the + world based on applicable law or treaty, and any national + implementations thereof. + +2. Waiver. To the greatest extent permitted by, but not in contravention +of, applicable law, Affirmer hereby overtly, fully, permanently, +irrevocably and unconditionally waives, abandons, and surrenders all of +Affirmer's Copyright and Related Rights and associated claims and causes +of action, whether now known or unknown (including existing as well as +future claims and causes of action), in the Work (i) in all territories +worldwide, (ii) for the maximum duration provided by applicable law or +treaty (including future time extensions), (iii) in any current or future +medium and for any number of copies, and (iv) for any purpose whatsoever, +including without limitation commercial, advertising or promotional +purposes (the "Waiver"). Affirmer makes the Waiver for the benefit of each +member of the public at large and to the detriment of Affirmer's heirs and +successors, fully intending that such Waiver shall not be subject to +revocation, rescission, cancellation, termination, or any other legal or +equitable action to disrupt the quiet enjoyment of the Work by the public +as contemplated by Affirmer's express Statement of Purpose. + +3. Public License Fallback. Should any part of the Waiver for any reason +be judged legally invalid or ineffective under applicable law, then the +Waiver shall be preserved to the maximum extent permitted taking into +account Affirmer's express Statement of Purpose. In addition, to the +extent the Waiver is so judged Affirmer hereby grants to each affected +person a royalty-free, non transferable, non sublicensable, non exclusive, +irrevocable and unconditional license to exercise Affirmer's Copyright and +Related Rights in the Work (i) in all territories worldwide, (ii) for the +maximum duration provided by applicable law or treaty (including future +time extensions), (iii) in any current or future medium and for any number +of copies, and (iv) for any purpose whatsoever, including without +limitation commercial, advertising or promotional purposes (the +"License"). The License shall be deemed effective as of the date CC0 was +applied by Affirmer to the Work. Should any part of the License for any +reason be judged legally invalid or ineffective under applicable law, such +partial invalidity or ineffectiveness shall not invalidate the remainder +of the License, and in such case Affirmer hereby affirms that he or she +will not (i) exercise any of his or her remaining Copyright and Related +Rights in the Work or (ii) assert any associated claims and causes of +action with respect to the Work, in either case contrary to Affirmer's +express Statement of Purpose. + +4. Limitations and Disclaimers. + + a. No trademark or patent rights held by Affirmer are waived, abandoned, + surrendered, licensed or otherwise affected by this document. + b. Affirmer offers the Work as-is and makes no representations or + warranties of any kind concerning the Work, express, implied, + statutory or otherwise, including without limitation warranties of + title, merchantability, fitness for a particular purpose, non + infringement, or the absence of latent or other defects, accuracy, or + the present or absence of errors, whether or not discoverable, all to + the greatest extent permissible under applicable law. + c. Affirmer disclaims responsibility for clearing rights of other persons + that may apply to the Work or any use thereof, including without + limitation any person's Copyright and Related Rights in the Work. + Further, Affirmer disclaims responsibility for obtaining any necessary + consents, permissions or other rights required for any use of the + Work. + d. Affirmer understands and acknowledges that Creative Commons is not a + party to this document and has no duty or obligation with respect to + this CC0 or use of the Work. diff --git a/licenses/btcd License.md b/licenses/btcd License.md new file mode 100644 index 0000000..46dcd39 --- /dev/null +++ b/licenses/btcd License.md @@ -0,0 +1,16 @@ +ISC License + +Copyright (c) 2013-2023 The btcsuite developers +Copyright (c) 2015-2016 The Decred developers + +Permission to use, copy, modify, and distribute this software for any +purpose with or without fee is hereby granted, provided that the above +copyright notice and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. diff --git a/licenses/chai2010 webp License.md b/licenses/chai2010 webp License.md new file mode 100644 index 0000000..e6fa36d --- /dev/null +++ b/licenses/chai2010 webp License.md @@ -0,0 +1,28 @@ +Copyright (c) 2014 chaishushan{AT}gmail.com. +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright +notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above +copyright notice, this list of conditions and the following disclaimer +in the documentation and/or other materials provided with the +distribution. + * Neither the name of {organization}. nor the names of its +contributors may be used to endorse or promote products derived from +this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. \ No newline at end of file diff --git a/licenses/go-charts License.md b/licenses/go-charts License.md new file mode 100644 index 0000000..7fd6465 --- /dev/null +++ b/licenses/go-charts License.md @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2016 William Charczuk. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/licenses/go-effects License.md b/licenses/go-effects License.md new file mode 100644 index 0000000..1e30dfd --- /dev/null +++ b/licenses/go-effects License.md @@ -0,0 +1,20 @@ +The MIT License (MIT) + +Copyright (c) 2019 Amangeldy Kadyl + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/licenses/go-ethereum License.md b/licenses/go-ethereum License.md new file mode 100644 index 0000000..f288702 --- /dev/null +++ b/licenses/go-ethereum License.md @@ -0,0 +1,674 @@ + GNU GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU General Public License is a free, copyleft license for +software and other kinds of works. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +the GNU General Public License is intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. We, the Free Software Foundation, use the +GNU General Public License for most of our software; it applies also to +any other work released this way by its authors. You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + To protect your rights, we need to prevent others from denying you +these rights or asking you to surrender the rights. Therefore, you have +certain responsibilities if you distribute copies of the software, or if +you modify it: responsibilities to respect the freedom of others. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must pass on to the recipients the same +freedoms that you received. You must make sure that they, too, receive +or can get the source code. And you must show them these terms so they +know their rights. + + Developers that use the GNU GPL protect your rights with two steps: +(1) assert copyright on the software, and (2) offer you this License +giving you legal permission to copy, distribute and/or modify it. + + For the developers' and authors' protection, the GPL clearly explains +that there is no warranty for this free software. For both users' and +authors' sake, the GPL requires that modified versions be marked as +changed, so that their problems will not be attributed erroneously to +authors of previous versions. + + Some devices are designed to deny users access to install or run +modified versions of the software inside them, although the manufacturer +can do so. This is fundamentally incompatible with the aim of +protecting users' freedom to change the software. The systematic +pattern of such abuse occurs in the area of products for individuals to +use, which is precisely where it is most unacceptable. Therefore, we +have designed this version of the GPL to prohibit the practice for those +products. If such problems arise substantially in other domains, we +stand ready to extend this provision to those domains in future versions +of the GPL, as needed to protect the freedom of users. + + Finally, every program is threatened constantly by software patents. +States should not allow patents to restrict development and use of +software on general-purpose computers, but in those that do, we wish to +avoid the special danger that patents applied to a free program could +make it effectively proprietary. To prevent this, the GPL assures that +patents cannot be used to render the program non-free. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Use with the GNU Affero General Public License. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU Affero General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the special requirements of the GNU Affero General Public License, +section 13, concerning interaction through a network will apply to the +combination as such. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If the program does terminal interaction, make it output a short +notice like this when it starts in an interactive mode: + + Copyright (C) + This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, your program's commands +might be different; for a GUI interface, you would use an "about box". + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU GPL, see +. + + The GNU General Public License does not permit incorporating your program +into proprietary programs. If your program is a subroutine library, you +may consider it more useful to permit linking proprietary applications with +the library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. But first, please read +. diff --git a/licenses/oksvg License.md b/licenses/oksvg License.md new file mode 100644 index 0000000..ab2ed68 --- /dev/null +++ b/licenses/oksvg License.md @@ -0,0 +1,29 @@ +BSD 3-Clause License + +Copyright (c) 2018, Steven R Wiley +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +* Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +* Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +* Neither the name of the copyright holder nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/main.go b/main.go new file mode 100644 index 0000000..717aa07 --- /dev/null +++ b/main.go @@ -0,0 +1,15 @@ + +// Seekia: A race-aware mate discovery network. +// Cure racial loneliness. Beautify the human species. Be race aware. +// Released into the public domain. (see Unlicense.md) + +package main + +import "seekia/gui" + +func main(){ + + gui.StartGui() +} + + diff --git a/resources/currencies/currencies.go b/resources/currencies/currencies.go new file mode 100644 index 0000000..ca78624 --- /dev/null +++ b/resources/currencies/currencies.go @@ -0,0 +1,710 @@ + +// currencies provides a list of currencies, and a way to retrieve information about them + +package currencies + +import "errors" + +type CurrencyObject struct{ + + // The name of the currency + Name string + + // The three letter ISO code that is used to refer to the currency + Code string + + // The symbol or abbreviation prefix for displaying the amount. + // Example: $5, €6 + Symbol string +} + +func VerifyCurrencyCode(currencyCode string)(bool, error){ + + currencyObjectsMap, err := GetCurrencyObjectsMap() + if (err != nil){ return false, err } + + _, exists := currencyObjectsMap[currencyCode] + if (exists == true){ + return true, nil + } + + return false, nil +} + + +var currencyObjectsMap map[string]CurrencyObject +var currencyObjectsList []CurrencyObject + +func init(){ + + initializeCurrencyObjectsList() + + // We initialize the currencyObjectsMap + + currencyObjectsMap = make(map[string]CurrencyObject) + + for _, currencyObject := range currencyObjectsList{ + + currencyCode := currencyObject.Code + + currencyObjectsMap[currencyCode] = currencyObject + } +} + +//Outputs: +// -string: Currency name +// -string: Currency symbol +// -error +func GetCurrencyInfoFromCurrencyCode(currencyCode string)(string, string, error){ + + currencyObjectsMap, err := GetCurrencyObjectsMap() + if (err != nil) { return "", "", err } + + currencyObject, exists := currencyObjectsMap[currencyCode] + if (exists == false){ + return "", "", errors.New("GetCurrencyInfoFromCurrencyCode called with invalid currency code: " + currencyCode) + } + + currencyName := currencyObject.Name + currencySymbol := currencyObject.Symbol + + return currencyName, currencySymbol, nil +} + +//Output: +// -map[string]CurrencyObject: Currency Code -> Currency Object +// -error +func GetCurrencyObjectsMap()(map[string]CurrencyObject, error){ + + if (currencyObjectsMap == nil){ + return nil, errors.New("GetCurrencyObjectsMap called when currencyObjectsMap is not initialized.") + } + + return currencyObjectsMap, nil +} + +func GetCurrencyObjectsList()([]CurrencyObject, error){ + + if (currencyObjectsList == nil){ + return nil, errors.New("GetCurrencyObjectsList called when currencyObjectsList is not initialized.") + } + + return currencyObjectsList, nil +} + + +func initializeCurrencyObjectsList(){ + + currencyObject_1 := CurrencyObject{ + Name: "US Dollar", + Code: "USD", + Symbol: "$", + } + currencyObject_2 := CurrencyObject{ + Name: "Canadian Dollar", + Code: "CAD", + Symbol: "CA$", + } + currencyObject_3 := CurrencyObject{ + Name: "Euro", + Code: "EUR", + Symbol: "€", + } + currencyObject_4 := CurrencyObject{ + Name: "United Arab Emirates Dirham", + Code: "AED", + Symbol: "AED", + } + currencyObject_5 := CurrencyObject{ + Name: "Afghan Afghani", + Code: "AFN", + Symbol: "Af", + } + currencyObject_6 := CurrencyObject{ + Name: "Albanian Lek", + Code: "ALL", + Symbol: "ALL", + } + currencyObject_7 := CurrencyObject{ + Name: "Armenian Dram", + Code: "AMD", + Symbol: "AMD", + } + currencyObject_8 := CurrencyObject{ + Name: "Argentine Peso", + Code: "ARS", + Symbol: "AR$", + } + currencyObject_9 := CurrencyObject{ + Name: "Australian Dollar", + Code: "AUD", + Symbol: "AU$", + } + currencyObject_10 := CurrencyObject{ + Name: "Azerbaijani Manat", + Code: "AZN", + Symbol: "₼", + } + currencyObject_11 := CurrencyObject{ + Name: "Bosnia and Herzegovina Convertible Mark", + Code: "BAM", + Symbol: "KM", + } + currencyObject_12 := CurrencyObject{ + Name: "Bangladeshi Taka", + Code: "BDT", + Symbol: "Tk", + } + currencyObject_13 := CurrencyObject{ + Name: "Bulgarian Lev", + Code: "BGN", + Symbol: "BGN", + } + currencyObject_14 := CurrencyObject{ + Name: "Bahraini Dinar", + Code: "BHD", + Symbol: "BD", + } + currencyObject_15 := CurrencyObject{ + Name: "Burundian Franc", + Code: "BIF", + Symbol: "FBu", + } + currencyObject_16 := CurrencyObject{ + Name: "Brunei Dollar", + Code: "BND", + Symbol: "BN$", + } + currencyObject_17 := CurrencyObject{ + Name: "Bolivian Boliviano", + Code: "BOB", + Symbol: "Bs", + } + currencyObject_18 := CurrencyObject{ + Name: "Brazilian Real", + Code: "BRL", + Symbol: "R$", + } + currencyObject_19 := CurrencyObject{ + Name: "Botswanan Pula", + Code: "BWP", + Symbol: "BWP", + } + currencyObject_20 := CurrencyObject{ + Name: "Belarusian Ruble", + Code: "BYN", + Symbol: "Br", + } + currencyObject_21 := CurrencyObject{ + Name: "Belize Dollar", + Code: "BZD", + Symbol: "BZ$", + } + currencyObject_22 := CurrencyObject{ + Name: "Congolese Franc", + Code: "CDF", + Symbol: "CDF", + } + currencyObject_23 := CurrencyObject{ + Name: "Swiss Franc", + Code: "CHF", + Symbol: "CHF", + } + currencyObject_24 := CurrencyObject{ + Name: "Chilean Peso", + Code: "CLP", + Symbol: "CL$", + } + currencyObject_25 := CurrencyObject{ + Name: "Chinese Yuan", + Code: "CNY", + Symbol: "CN¥", + } + currencyObject_26 := CurrencyObject{ + Name: "Colombian Peso", + Code: "COP", + Symbol: "CO$", + } + currencyObject_27 := CurrencyObject{ + Name: "Costa Rican Colón", + Code: "CRC", + Symbol: "₡", + } + currencyObject_28 := CurrencyObject{ + Name: "Cape Verdean Escudo", + Code: "CVE", + Symbol: "CV$", + } + currencyObject_29 := CurrencyObject{ + Name: "Czech Republic Koruna", + Code: "CZK", + Symbol: "Kč", + } + currencyObject_30 := CurrencyObject{ + Name: "Djiboutian Franc", + Code: "DJF", + Symbol: "Fr", + } + currencyObject_31 := CurrencyObject{ + Name: "Danish Krone", + Code: "DKK", + Symbol: "Dkr", + } + currencyObject_32 := CurrencyObject{ + Name: "Dominican Peso", + Code: "DOP", + Symbol: "RD$", + } + currencyObject_33 := CurrencyObject{ + Name: "Algerian Dinar", + Code: "DZD", + Symbol: "DA", + } + currencyObject_34 := CurrencyObject{ + Name: "Estonian Kroon", + Code: "EEK", + Symbol: "Ekr", + } + currencyObject_35 := CurrencyObject{ + Name: "Egyptian Pound", + Code: "EGP", + Symbol: "EGP", + } + currencyObject_36 := CurrencyObject{ + Name: "Eritrean Nakfa", + Code: "ERN", + Symbol: "Nfk", + } + currencyObject_37 := CurrencyObject{ + Name: "Ethiopian Birr", + Code: "ETB", + Symbol: "Br", + } + currencyObject_38 := CurrencyObject{ + Name: "British Pound Sterling", + Code: "GBP", + Symbol: "£", + } + currencyObject_39 := CurrencyObject{ + Name: "Georgian Lari", + Code: "GEL", + Symbol: "GEL", + } + currencyObject_40 := CurrencyObject{ + Name: "Ghanaian Cedi", + Code: "GHS", + Symbol: "GH₵", + } + currencyObject_41 := CurrencyObject{ + Name: "Guinean Franc", + Code: "GNF", + Symbol: "FG", + } + currencyObject_42 := CurrencyObject{ + Name: "Guatemalan Quetzal", + Code: "GTQ", + Symbol: "GTQ", + } + currencyObject_43 := CurrencyObject{ + Name: "Hong Kong Dollar", + Code: "HKD", + Symbol: "HK$", + } + currencyObject_44 := CurrencyObject{ + Name: "Honduran Lempira", + Code: "HNL", + Symbol: "HNL", + } + currencyObject_45 := CurrencyObject{ + Name: "Croatian Kuna", + Code: "HRK", + Symbol: "kn", + } + currencyObject_46 := CurrencyObject{ + Name: "Hungarian Forint", + Code: "HUF", + Symbol: "Ft", + } + currencyObject_47 := CurrencyObject{ + Name: "Indonesian Rupiah", + Code: "IDR", + Symbol: "Rp", + } + currencyObject_48 := CurrencyObject{ + Name: "Israeli New Sheqel", + Code: "ILS", + Symbol: "₪", + } + currencyObject_49 := CurrencyObject{ + Name: "Indian Rupee", + Code: "INR", + Symbol: "Rs", + } + currencyObject_50 := CurrencyObject{ + Name: "Iraqi Dinar", + Code: "IQD", + Symbol: "IQD", + } + currencyObject_51 := CurrencyObject{ + Name: "Iranian Rial", + Code: "IRR", + Symbol: "IRR", + } + currencyObject_52 := CurrencyObject{ + Name: "Icelandic Króna", + Code: "ISK", + Symbol: "Ikr", + } + currencyObject_53 := CurrencyObject{ + Name: "Jamaican Dollar", + Code: "JMD", + Symbol: "J$", + } + currencyObject_54 := CurrencyObject{ + Name: "Jordanian Dinar", + Code: "JOD", + Symbol: "JD", + } + currencyObject_55 := CurrencyObject{ + Name: "Japanese Yen", + Code: "JPY", + Symbol: "¥", + } + currencyObject_56 := CurrencyObject{ + Name: "Kenyan Shilling", + Code: "KES", + Symbol: "Ksh", + } + currencyObject_57 := CurrencyObject{ + Name: "Cambodian Riel", + Code: "KHR", + Symbol: "KHR", + } + currencyObject_58 := CurrencyObject{ + Name: "Comorian Franc", + Code: "KMF", + Symbol: "CF", + } + currencyObject_59 := CurrencyObject{ + Name: "South Korean Won", + Code: "KRW", + Symbol: "₩", + } + currencyObject_60 := CurrencyObject{ + Name: "Kuwaiti Dinar", + Code: "KWD", + Symbol: "KD", + } + currencyObject_61 := CurrencyObject{ + Name: "Kazakhstani Tenge", + Code: "KZT", + Symbol: "KZT", + } + currencyObject_62 := CurrencyObject{ + Name: "Lebanese Pound", + Code: "LBP", + Symbol: "LB£", + } + currencyObject_63 := CurrencyObject{ + Name: "Sri Lankan Rupee", + Code: "LKR", + Symbol: "SLRs", + } + currencyObject_64 := CurrencyObject{ + Name: "Lithuanian Litas", + Code: "LTL", + Symbol: "Lt", + } + currencyObject_65 := CurrencyObject{ + Name: "Latvian Lats", + Code: "LVL", + Symbol: "Ls", + } + currencyObject_66 := CurrencyObject{ + Name: "Libyan Dinar", + Code: "LYD", + Symbol: "LD", + } + currencyObject_67 := CurrencyObject{ + Name: "Moroccan Dirham", + Code: "MAD", + Symbol: "MAD", + } + currencyObject_68 := CurrencyObject{ + Name: "Moldovan Leu", + Code: "MDL", + Symbol: "MDL", + } + currencyObject_69 := CurrencyObject{ + Name: "Malagasy Ariary", + Code: "MGA", + Symbol: "MGA", + } + currencyObject_70 := CurrencyObject{ + Name: "Macedonian Denar", + Code: "MKD", + Symbol: "MKD", + } + currencyObject_71 := CurrencyObject{ + Name: "Myanma Kyat", + Code: "MMK", + Symbol: "MMK", + } + currencyObject_72 := CurrencyObject{ + Name: "Macanese Pataca", + Code: "MOP", + Symbol: "MOP$", + } + currencyObject_73 := CurrencyObject{ + Name: "Mauritian Rupee", + Code: "MUR", + Symbol: "MURs", + } + currencyObject_74 := CurrencyObject{ + Name: "Mexican Peso", + Code: "MXN", + Symbol: "MX$", + } + currencyObject_75 := CurrencyObject{ + Name: "Malaysian Ringgit", + Code: "MYR", + Symbol: "RM", + } + currencyObject_76 := CurrencyObject{ + Name: "Mozambican Metical", + Code: "MZN", + Symbol: "MTn", + } + currencyObject_77 := CurrencyObject{ + Name: "Namibian Dollar", + Code: "NAD", + Symbol: "N$", + } + currencyObject_78 := CurrencyObject{ + Name: "Nigerian Naira", + Code: "NGN", + Symbol: "₦", + } + currencyObject_79 := CurrencyObject{ + Name: "Nicaraguan Córdoba", + Code: "NIO", + Symbol: "C$", + } + currencyObject_80 := CurrencyObject{ + Name: "Norwegian Krone", + Code: "NOK", + Symbol: "Nkr", + } + currencyObject_81 := CurrencyObject{ + Name: "Nepalese Rupee", + Code: "NPR", + Symbol: "NPRs", + } + currencyObject_82 := CurrencyObject{ + Name: "New Zealand Dollar", + Code: "NZD", + Symbol: "NZ$", + } + currencyObject_83 := CurrencyObject{ + Name: "Omani Rial", + Code: "OMR", + Symbol: "OMR", + } + currencyObject_84 := CurrencyObject{ + Name: "Panamanian Balboa", + Code: "PAB", + Symbol: "B/", + } + currencyObject_85 := CurrencyObject{ + Name: "Peruvian Nuevo Sol", + Code: "PEN", + Symbol: "S/", + } + currencyObject_86 := CurrencyObject{ + Name: "Philippine Peso", + Code: "PHP", + Symbol: "₱", + } + currencyObject_87 := CurrencyObject{ + Name: "Pakistani Rupee", + Code: "PKR", + Symbol: "PKRs", + } + currencyObject_88 := CurrencyObject{ + Name: "Polish Zloty", + Code: "PLN", + Symbol: "zł", + } + currencyObject_89 := CurrencyObject{ + Name: "Paraguayan Guarani", + Code: "PYG", + Symbol: "₲", + } + currencyObject_90 := CurrencyObject{ + Name: "Qatari Rial", + Code: "QAR", + Symbol: "QR", + } + currencyObject_91 := CurrencyObject{ + Name: "Romanian Leu", + Code: "RON", + Symbol: "RON", + } + currencyObject_92 := CurrencyObject{ + Name: "Serbian Dinar", + Code: "RSD", + Symbol: "din", + } + currencyObject_93 := CurrencyObject{ + Name: "Russian Ruble", + Code: "RUB", + Symbol: "RUB", + } + currencyObject_94 := CurrencyObject{ + Name: "Rwandan Franc", + Code: "RWF", + Symbol: "RWF", + } + currencyObject_95 := CurrencyObject{ + Name: "Saudi Riyal", + Code: "SAR", + Symbol: "SR", + } + currencyObject_96 := CurrencyObject{ + Name: "Sudanese Pound", + Code: "SDG", + Symbol: "SDG", + } + currencyObject_97 := CurrencyObject{ + Name: "Swedish Krona", + Code: "SEK", + Symbol: "Skr", + } + currencyObject_98 := CurrencyObject{ + Name: "Singapore Dollar", + Code: "SGD", + Symbol: "S$", + } + currencyObject_99 := CurrencyObject{ + Name: "Somali Shilling", + Code: "SOS", + Symbol: "Ssh", + } + currencyObject_100 := CurrencyObject{ + Name: "Syrian Pound", + Code: "SYP", + Symbol: "SY£", + } + currencyObject_101 := CurrencyObject{ + Name: "Thai Baht", + Code: "THB", + Symbol: "฿", + } + currencyObject_102 := CurrencyObject{ + Name: "Tunisian Dinar", + Code: "TND", + Symbol: "DT", + } + currencyObject_103 := CurrencyObject{ + Name: "Tongan Paʻanga", + Code: "TOP", + Symbol: "T$", + } + currencyObject_104 := CurrencyObject{ + Name: "Turkish Lira", + Code: "TRY", + Symbol: "TL", + } + currencyObject_105 := CurrencyObject{ + Name: "Trinidad and Tobago Dollar", + Code: "TTD", + Symbol: "TT$", + } + currencyObject_106 := CurrencyObject{ + Name: "New Taiwan Dollar", + Code: "TWD", + Symbol: "NT$", + } + currencyObject_107 := CurrencyObject{ + Name: "Tanzanian Shilling", + Code: "TZS", + Symbol: "TSh", + } + currencyObject_108 := CurrencyObject{ + Name: "Ukrainian Hryvnia", + Code: "UAH", + Symbol: "₴", + } + currencyObject_109 := CurrencyObject{ + Name: "Ugandan Shilling", + Code: "UGX", + Symbol: "USh", + } + currencyObject_110 := CurrencyObject{ + Name: "Uruguayan Peso", + Code: "UYU", + Symbol: "$U", + } + currencyObject_111 := CurrencyObject{ + Name: "Uzbekistan Som", + Code: "UZS", + Symbol: "UZS", + } + currencyObject_112 := CurrencyObject{ + Name: "Venezuelan Bolívar", + Code: "VES", + Symbol: "Bs.S.", + } + currencyObject_113 := CurrencyObject{ + Name: "Vietnamese Dong", + Code: "VND", + Symbol: "₫", + } + currencyObject_114 := CurrencyObject{ + Name: "Central African CFA Franc", + Code: "XAF", + Symbol: "Fr", + } + currencyObject_115 := CurrencyObject{ + Name: "West African CFA Franc", + Code: "XOF", + Symbol: "Fr", + } + currencyObject_116 := CurrencyObject{ + Name: "Yemeni Rial", + Code: "YER", + Symbol: "YR", + } + currencyObject_117 := CurrencyObject{ + Name: "South African Rand", + Code: "ZAR", + Symbol: "R", + } + currencyObject_118 := CurrencyObject{ + Name: "Zambian Kwacha", + Code: "ZMK", + Symbol: "ZK", + } + currencyObject_119 := CurrencyObject{ + Name: "Zimbabwean Dollar", + Code: "ZWL", + Symbol: "ZWL$", + } + + currencyObject_120 := CurrencyObject{ + Name: "Ethereum", + Code: "ETH", + Symbol: "ETH", + } + + currencyObject_121 := CurrencyObject{ + Name: "Cardano", + Code: "ADA", + Symbol: "ADA", + } + + currencyObjectsList = []CurrencyObject{currencyObject_1, currencyObject_2, currencyObject_3, currencyObject_4, currencyObject_5, currencyObject_6, currencyObject_7, currencyObject_8, currencyObject_9, currencyObject_10, currencyObject_11, currencyObject_12, currencyObject_13, currencyObject_14, currencyObject_15, currencyObject_16, currencyObject_17, currencyObject_18, currencyObject_19, currencyObject_20, currencyObject_21, currencyObject_22, currencyObject_23, currencyObject_24, currencyObject_25, currencyObject_26, currencyObject_27, currencyObject_28, currencyObject_29, currencyObject_30, currencyObject_31, currencyObject_32, currencyObject_33, currencyObject_34, currencyObject_35, currencyObject_36, currencyObject_37, currencyObject_38, currencyObject_39, currencyObject_40, currencyObject_41, currencyObject_42, currencyObject_43, currencyObject_44, currencyObject_45, currencyObject_46, currencyObject_47, currencyObject_48, currencyObject_49, currencyObject_50, currencyObject_51, currencyObject_52, currencyObject_53, currencyObject_54, currencyObject_55, currencyObject_56, currencyObject_57, currencyObject_58, currencyObject_59, currencyObject_60, currencyObject_61, currencyObject_62, currencyObject_63, currencyObject_64, currencyObject_65, currencyObject_66, currencyObject_67, currencyObject_68, currencyObject_69, currencyObject_70, currencyObject_71, currencyObject_72, currencyObject_73, currencyObject_74, currencyObject_75, currencyObject_76, currencyObject_77, currencyObject_78, currencyObject_79, currencyObject_80, currencyObject_81, currencyObject_82, currencyObject_83, currencyObject_84, currencyObject_85, currencyObject_86, currencyObject_87, currencyObject_88, currencyObject_89, currencyObject_90, currencyObject_91, currencyObject_92, currencyObject_93, currencyObject_94, currencyObject_95, currencyObject_96, currencyObject_97, currencyObject_98, currencyObject_99, currencyObject_100, currencyObject_101, currencyObject_102, currencyObject_103, currencyObject_104, currencyObject_105, currencyObject_106, currencyObject_107, currencyObject_108, currencyObject_109, currencyObject_110, currencyObject_111, currencyObject_112, currencyObject_113, currencyObject_114, currencyObject_115, currencyObject_116, currencyObject_117, currencyObject_118, currencyObject_119, currencyObject_120, currencyObject_121} + +} + + diff --git a/resources/currencies/currencies_test.go b/resources/currencies/currencies_test.go new file mode 100644 index 0000000..4ec8a36 --- /dev/null +++ b/resources/currencies/currencies_test.go @@ -0,0 +1,47 @@ + +package currencies_test + +import "seekia/resources/currencies" + +import "testing" + + +func TestCurrencies(t *testing.T){ + + currencyObjectsList, err := currencies.GetCurrencyObjectsList() + if (err != nil){ + t.Fatalf("GetCurrencyObjectsList failed: " + err.Error()) + } + + // We make sure there are no collisions of currency names or codes + + currencyNamesMap := make(map[string]struct{}) + currencyCodesMap := make(map[string]struct{}) + + for _, currencyObject := range currencyObjectsList{ + + currencyName := currencyObject.Name + currencyCode := currencyObject.Code + + if (currencyName == ""){ + t.Fatalf("Empty currency name exists.") + } + if (currencyCode == ""){ + t.Fatalf("Empty currency code exists.") + } + + _, exists := currencyNamesMap[currencyName] + if (exists == true){ + t.Fatalf("Currency name collision exists: " + currencyName) + } + _, exists = currencyCodesMap[currencyCode] + if (exists == true){ + t.Fatalf("Currency code collision exists: " + currencyCode) + } + + currencyNamesMap[currencyName] = struct{}{} + currencyCodesMap[currencyCode] = struct{}{} + } +} + + diff --git a/resources/geneticReferences/geneticReferences_test.go b/resources/geneticReferences/geneticReferences_test.go new file mode 100644 index 0000000..3f99b23 --- /dev/null +++ b/resources/geneticReferences/geneticReferences_test.go @@ -0,0 +1,639 @@ + +// verifyGeneticReferences provides functions to run a check to make sure the genetic resources are valid and have no conflicts + +package verifyGeneticReferences + +// We check to make sure: +// 1. No identifier collisions exist +// 2. No disease/trait name collisions exist +// 4. Verifies the minimum and maximum risk weights for each polygenic disease locus +// 5. Each identifier is the correct format (3 bytes encoded hex) + +// Identifiers are 3 bytes/24 bits long, so there is at least a 1 in 16 million chance that two will collide when generating them randomly + +import "seekia/resources/geneticReferences/locusMetadata" +import "seekia/resources/geneticReferences/monogenicDiseases" +import "seekia/resources/geneticReferences/polygenicDiseases" +import "seekia/resources/geneticReferences/traits" + +import "seekia/internal/helpers" +import "seekia/internal/encoding" + +import "testing" +import "strings" +import "slices" + +func TestGeneticReferences(t *testing.T){ + + verifyIdentifier := func(inputIdentifier string)bool{ + + decodedBytes, err := encoding.DecodeHexStringToBytes(inputIdentifier) + if (err != nil) { + return false + } + + if (len(decodedBytes) != 3){ + return false + } + + return true + } + + verifyBase := func(inputBase string)bool{ + + if (inputBase != "A" && inputBase != "G" && inputBase != "C" && inputBase != "T" && inputBase != "I" && inputBase != "D"){ + return false + } + return true + } + + verifyBasePair := func(inputBasePair string)bool{ + + baseA, baseB, delimiterFound := strings.Cut(inputBasePair, ";") + if (delimiterFound == false){ + return false + } + + baseIsValid := verifyBase(baseA) + if (baseIsValid == false){ + return false + } + + baseIsValid = verifyBase(baseB) + if (baseIsValid == false){ + return false + } + return true + } + + verifyReferencesMap := func(inputReferencesMap map[string]string)bool{ + + if (len(inputReferencesMap) == 0){ + return true + } + + for referenceName, referenceLink := range inputReferencesMap{ + + if (referenceName == ""){ + return false + } + if (referenceLink == ""){ + return false + } + } + + return true + } + + monogenicDiseases.InitializeMonogenicDiseaseVariables() + + monogenicDiseasesObjectsList, err := monogenicDiseases.GetMonogenicDiseaseObjectsList() + if (err != nil){ + t.Fatalf("Failed to get monogenic disease objects list: " + err.Error()) + } + + // We use this map to make sure all RSIDs have metadata in locusMetadata + allRSIDsMap := make(map[int64]struct{}) + + allIdentifiersMap := make(map[string]struct{}) + + monogenicDiseaseNamesMap := make(map[string]struct{}) + + for _, diseaseObject := range monogenicDiseasesObjectsList{ + + diseaseName := diseaseObject.DiseaseName + diseaseGeneName := diseaseObject.GeneName + dominantOrRecessive := diseaseObject.DominantOrRecessive + variantsList := diseaseObject.VariantsList + diseaseReferencesMap := diseaseObject.References + + if (diseaseName == ""){ + t.Fatalf("Monogenic Disease name is empty.") + } + _, exists := monogenicDiseaseNamesMap[diseaseName] + if (exists == true){ + t.Fatalf("Monogenic Disease name collision found: " + diseaseName) + } + monogenicDiseaseNamesMap[diseaseName] = struct{}{} + + // Monogenic disease names cannot contain underscores + // This is because when we encode monogenic disease names in user profiles, we replace the whitespace with underscores + // We have to be able to reliably undo this + containsUnderscore := strings.Contains(diseaseName, "_") + if (containsUnderscore == true){ + t.Fatalf("Monogenic Disease name contains underscore: " + diseaseName) + } + + if (diseaseGeneName == ""){ + t.Fatalf("Monogenic Disease gene name is empty: " + diseaseName) + } + if (dominantOrRecessive != "Dominant" && dominantOrRecessive != "Recessive"){ + t.Fatalf("Monogenic Disease dominantOrRecessive is invalid: " + diseaseName) + } + + referencesAreValid := verifyReferencesMap(diseaseReferencesMap) + if (referencesAreValid == false){ + t.Fatalf("Monogenic Disease references are invalid: " + diseaseName) + } + + if (len(variantsList) == 0){ + t.Fatalf("Monogenic Disease contains no variants: " + diseaseName) + } + + for _, variantObject := range variantsList{ + + variantIdentifier := variantObject.VariantIdentifier + variantRSID := variantObject.VariantRSID + variantNamesList := variantObject.VariantNames + variantHealthyBase := variantObject.HealthyBase + variantDefectiveBase := variantObject.DefectiveBase + variantReferences := variantObject.References + + allRSIDsMap[variantRSID] = struct{}{} + + identifierIsValid := verifyIdentifier(variantIdentifier) + if (identifierIsValid == false){ + t.Fatalf(diseaseName + " Invalid variant identifier found: " + variantIdentifier) + } + + _, exists := allIdentifiersMap[variantIdentifier] + if (exists == true){ + t.Fatalf(diseaseName + " Duplicate variant identifier found: " + variantIdentifier) + } + allIdentifiersMap[variantIdentifier] = struct{}{} + + if (len(variantNamesList) == 0){ + t.Fatalf("Variant names list is empty: " + variantIdentifier) + } + for _, variantName := range variantNamesList{ + if (variantName == ""){ + t.Fatalf("Variant name is empty: " + variantIdentifier) + } + } + + healthyBaseIsValid := verifyBase(variantHealthyBase) + defectiveBaseIsValid := verifyBase(variantDefectiveBase) + + if (healthyBaseIsValid == false || defectiveBaseIsValid == false){ + t.Fatalf(diseaseName + " Invalid healthy/defective base found: " + variantIdentifier) + } + + if (variantHealthyBase == variantDefectiveBase){ + t.Fatalf(diseaseName + " Identical healthy/defective bases found: " + variantIdentifier) + } + + referencesAreValid := verifyReferencesMap(variantReferences) + if (referencesAreValid == false){ + t.Fatalf("Disease variant references map is invalid: " + variantIdentifier) + } + } + } + + polygenicDiseases.InitializePolygenicDiseaseVariables() + + polygenicDiseaseObjectsList, err := polygenicDiseases.GetPolygenicDiseaseObjectsList() + if (err != nil) { + t.Fatalf("Failed to get polygenicDisease objects list: " + err.Error()) + } + + polygenicDiseaseNamesMap := make(map[string]struct{}) + + for _, diseaseObject := range polygenicDiseaseObjectsList{ + + diseaseName := diseaseObject.DiseaseName + diseaseDescription := diseaseObject.DiseaseDescription + diseaseEffectedSex := diseaseObject.EffectedSex + diseaseLociList := diseaseObject.LociList + diseaseReferencesMap := diseaseObject.References + + if (diseaseName == ""){ + t.Fatalf("PolygenicDisease name is empty.") + } + _, exists := polygenicDiseaseNamesMap[diseaseName] + if (exists == true){ + t.Fatalf("PolygenicDisease name collision found: " + diseaseName) + } + polygenicDiseaseNamesMap[diseaseName] = struct{}{} + + if (diseaseDescription == ""){ + t.Fatalf("PolygenicDisease description is empty for disease: " + diseaseName) + } + if (diseaseEffectedSex != "Male" && diseaseEffectedSex != "Female" && diseaseEffectedSex != "Both"){ + t.Fatalf("PolygenicDisease effected sex is invalid: " + diseaseEffectedSex) + } + + referencesAreValid := verifyReferencesMap(diseaseReferencesMap) + if (referencesAreValid == false){ + t.Fatalf("PolygenicDisease references map is invalid for disease: " + diseaseName) + } + + // We use this map to make sure each disease locus references a unique rsid + allPolygenicDiseaseRSIDsMap := make(map[int64]struct{}) + + for _, locusObject := range diseaseLociList{ + + locusIdentifier := locusObject.LocusIdentifier + locusRSID := locusObject.LocusRSID + riskWeightsMap := locusObject.RiskWeightsMap + oddsRatiosMap := locusObject.OddsRatiosMap + minimumWeight := locusObject.MinimumRiskWeight + maximumWeight := locusObject.MaximumRiskWeight + + allRSIDsMap[locusRSID] = struct{}{} + + identifierIsValid := verifyIdentifier(locusIdentifier) + if (identifierIsValid == false){ + t.Fatalf(diseaseName + " Invalid locus identifier found: " + locusIdentifier) + } + + _, exists := allIdentifiersMap[locusIdentifier] + if (exists == true){ + t.Fatalf(diseaseName + " Duplicate locus identifier found: " + locusIdentifier) + } + allIdentifiersMap[locusIdentifier] = struct{}{} + + _, exists = allPolygenicDiseaseRSIDsMap[locusRSID] + if (exists == true){ + rsidString := helpers.ConvertInt64ToString(locusRSID) + t.Fatalf(diseaseName + " RSID Collision found: " + rsidString) + } + + allPolygenicDiseaseRSIDsMap[locusRSID] = struct{}{} + + if (len(riskWeightsMap) == 0){ + t.Fatalf("Empty base weights map found: " + locusIdentifier) + } + + trueMinimumWeight := 100000 + trueMaximumWeight := -100000 + + for basePair, basePairWeight := range riskWeightsMap{ + + isValid := verifyBasePair(basePair) + if (isValid == false){ + t.Fatalf("Base pair weights map contains invalid base pair: " + locusIdentifier) + } + + if (basePairWeight < trueMinimumWeight){ + trueMinimumWeight = basePairWeight + } + if (basePairWeight > trueMaximumWeight){ + trueMaximumWeight = basePairWeight + } + } + + if (trueMinimumWeight != minimumWeight){ + t.Fatalf(diseaseName + ": Invalid minimum base pair weight found: " + locusIdentifier) + } + if (trueMaximumWeight != maximumWeight){ + t.Fatalf(diseaseName + ": Invalid maximum base pair weight found: " + locusIdentifier) + } + + for basePair, _ := range oddsRatiosMap{ + isValid := verifyBasePair(basePair) + if (isValid == false){ + t.Fatalf("Odds ratio weights map contains invalid base pair: " + locusIdentifier) + } + } + + //TODO: Make sure that duplicate base pairs have same weight, odds ratios and probabilities + } + } + + traits.InitializeTraitVariables() + + traitObjectsList, err := traits.GetTraitObjectsList() + if (err != nil){ + t.Fatalf("Failed to get trait objects list: " + err.Error()) + } + + traitNamesMap := make(map[string]struct{}) + + for _, traitObject := range traitObjectsList{ + + traitName := traitObject.TraitName + traitDescription := traitObject.TraitDescription + traitLociList := traitObject.LociList + traitRulesList := traitObject.RulesList + traitOutcomesList := traitObject.OutcomesList + traitReferencesMap := traitObject.References + + if (traitName == ""){ + t.Fatalf("Empty trait name exists.") + } + _, exists := traitNamesMap[traitName] + if (exists == true){ + t.Fatalf("Duplicate trait name exists: " + traitName) + } + traitNamesMap[traitName] = struct{}{} + + if (traitDescription == ""){ + t.Fatalf("Empty trait description exists for trait: " + traitName) + } + if (len(traitOutcomesList) != 0){ + + if (len(traitOutcomesList) < 2){ + t.Fatalf("Not enough trait outcomes for trait: " + traitName) + } + for _, traitOutcome := range traitOutcomesList{ + if (traitOutcome == ""){ + t.Fatalf("Empty trait outcome exists for trait: " + traitName) + } + } + } else { + + // If there are no outcomes, then no rules can exist + if (len(traitRulesList) != 0){ + t.Fatalf("Trait outcomes list is empty, trait rules list is not.") + } + } + + referencesAreValid := verifyReferencesMap(traitReferencesMap) + if (referencesAreValid == false){ + t.Fatalf("Invalid references exist for trait: " + traitName) + } + + if (len(traitLociList) == 0){ + t.Fatalf("No trait loci exist for trait: " + traitName) + } + + for _, locusRSID := range traitLociList{ + allRSIDsMap[locusRSID] = struct{}{} + } + + containsDuplicates, duplicateLocus := helpers.CheckIfListContainsDuplicates(traitLociList) + if (containsDuplicates == true){ + duplicateLocusString := helpers.ConvertInt64ToString(duplicateLocus) + t.Fatalf("traitLociList contains duplicates for trait: " + traitName + ". RSID: " + duplicateLocusString) + } + + if (len(traitRulesList) == 0){ + // No rules exist. + continue + } + + for _, ruleObject := range traitRulesList{ + + ruleIdentifier := ruleObject.RuleIdentifier + ruleLociList := ruleObject.LociList + ruleOutcomePointsMap := ruleObject.OutcomePointsMap + ruleReferences := ruleObject.References + + identifierIsValid := verifyIdentifier(ruleIdentifier) + if (identifierIsValid == false){ + t.Fatalf("Invalid identifier exists: " + ruleIdentifier) + } + _, exists := allIdentifiersMap[ruleIdentifier] + if (exists == true){ + t.Fatalf("Duplicate identifier exists: " + ruleIdentifier) + } + allIdentifiersMap[ruleIdentifier] = struct{}{} + + if (len(ruleOutcomePointsMap) == 0){ + t.Fatalf("Rule contains empty rule outcome points map: " + ruleIdentifier) + } + + for outcomeName, _ := range ruleOutcomePointsMap{ + isValid := slices.Contains(traitOutcomesList, outcomeName) + if (isValid == false){ + t.Fatalf("Rule outcome points map contains invalid outcome: " + outcomeName) + } + } + + if (len(ruleLociList) == 0){ + t.Fatalf("Rule contains empty rule loci list: " + ruleIdentifier) + } + + for _, locusObject := range ruleLociList{ + + locusIdentifier := locusObject.LocusIdentifier + locusRSID := locusObject.LocusRSID + locusBasePairsList := locusObject.BasePairsList + + allRSIDsMap[locusRSID] = struct{}{} + + isValid := verifyIdentifier(locusIdentifier) + if (isValid == false){ + t.Fatalf("Trait rule Locus identifier is invalid: " + locusIdentifier) + } + + listContainsItem := slices.Contains(traitLociList, locusRSID) + if (listContainsItem == false){ + t.Fatalf("Rule locus contains rsid which is not contained within traitLociList.") + } + + if (len(locusBasePairsList) == 0){ + t.Fatalf("Trait rule locus base pairs list is empty: " + locusIdentifier) + } + for _, locusBasePair := range locusBasePairsList{ + + basePairIsValid := verifyBasePair(locusBasePair) + if (basePairIsValid == false){ + t.Fatalf("Rule Locus base pairs list contains invalid base pair: " + locusBasePair) + } + } + } + + referencesAreValid := verifyReferencesMap(ruleReferences) + if (referencesAreValid == false){ + t.Fatalf("Invalid references map for trait rule locus: " + ruleIdentifier) + } + } + } + + err = locusMetadata.InitializeLocusMetadataVariables() + if (err != nil){ + t.Fatalf("Failed to initialize locus metadata variables: " + err.Error()) + } + + locusMetadataObjectsList, err := locusMetadata.GetLocusMetadataObjectsList() + if (err != nil){ + t.Fatalf("GetLocusMetadataObjectsList failed: " + err.Error()) + } + + // We use the locusPositionsMap to make sure there are no locations that refer to the same position on the same chromosome + + type locusPositionStruct struct{ + chromosome int + position int + } + + locusPositionsMap := make(map[locusPositionStruct]struct{}) + + // We use the companyAliasesMap to make sure there are no company alias collisions. + // + // We only care about alias collisions within each company. + // Multiple companies can refer to the same location with the same alias. + // + + type companyAliasStruct struct{ + + geneticsCompany locusMetadata.GeneticsCompany + + locusAlias string + } + + companyAliasesMap := make(map[companyAliasStruct]struct{}) + + // We use this map to make sure that locus metadata rsIDs do not collide. + // We don't want any duplicate rsIDs within any of the loci. + locusMetadataRSIDsMap := make(map[int64]struct{}) + + for _, locusMetadataObject := range locusMetadataObjectsList{ + + rsidsList := locusMetadataObject.RSIDsList + locusChromosome := locusMetadataObject.Chromosome + locusPosition := locusMetadataObject.Position + geneNamesList := locusMetadataObject.GeneNamesList + locusCompanyAliasesMap := locusMetadataObject.CompanyAliases + referencesMap := locusMetadataObject.References + + if (len(rsidsList) == 0){ + t.Fatalf("locusMetadataObjectsList contains locus with empty RSIDs list.") + } + + // The primary RSID is the only rsID which should appear in the genetic references + // The primary RSID is the first rsID in the locus rsIDs list + primaryRSID := rsidsList[0] + + _, exists := allRSIDsMap[primaryRSID] + if (exists == false){ + t.Fatalf("locusMetadataObjectsList contains unnecessary locus: No matching rsids exist.") + } + + for index, rsID := range rsidsList{ + + _, exists := locusMetadataRSIDsMap[rsID] + if (exists == true){ + + RSIDString := helpers.ConvertInt64ToString(rsID) + t.Fatalf("locusMetadataObjectsList contains duplicate RSID: " + RSIDString) + } + + locusMetadataRSIDsMap[rsID] = struct{}{} + + if (index != 0){ + + // This is not a primary rsID + _, exists = allRSIDsMap[rsID] + if (exists == true){ + rsIDString := helpers.ConvertInt64ToString(rsID) + t.Fatalf("allRSIDsMap contains non-primary rsID: " + rsIDString) + } + } + } + + if (locusChromosome == 0){ + // 0 is uninitialized. + t.Fatalf("locusMetadataObjectsList contains locus with 0 chromosome.") + } + + if (locusPosition == 0){ + // 0 is uninitialized. + t.Fatalf("locusMetadataObjectsList contains locus with 0 position.") + } + + locusPositionObject := locusPositionStruct{ + chromosome: locusChromosome, + position: locusPosition, + } + + _, exists = locusPositionsMap[locusPositionObject] + if (exists == true){ + t.Fatalf("locusMetadataObjectsList contains locus position collision.") + } + + locusPositionsMap[locusPositionObject] = struct{}{} + + if (len(geneNamesList) != 0){ + for _, geneName := range geneNamesList{ + if (geneName == ""){ + t.Fatalf("locusMetadataObjectsList contains locus with empty geneName in geneNamesList.") + } + } + } + + for companyObject, companyAliasesList := range locusCompanyAliasesMap{ + + for _, locusCompanyAlias := range companyAliasesList{ + + companyAliasObject := companyAliasStruct{ + + geneticsCompany: companyObject, + locusAlias: locusCompanyAlias, + } + + _, exists := companyAliasesMap[companyAliasObject] + if (exists == true){ + t.Fatalf("locusMetadataObjectsList contains companyAlias collision: " + locusCompanyAlias) + } + + companyAliasesMap[companyAliasObject] = struct{}{} + } + } + + isValid := verifyReferencesMap(referencesMap) + if (isValid == false){ + t.Fatalf("locusMetadataObjectsList contains invalid references map.") + } + } + + missingLociList := make([]int64, 0) + + for rsID, _ := range allRSIDsMap{ + + _, exists := locusMetadataRSIDsMap[rsID] + if (exists == false){ + missingLociList = append(missingLociList, rsID) + } + } + + if (len(missingLociList) != 0){ + + missingLociStringsList := make([]string, 0, len(missingLociList)) + + for _, rsID := range missingLociList{ + + rsIDString := helpers.ConvertInt64ToString(rsID) + + missingLociStringsList = append(missingLociStringsList, rsIDString) + } + + missingLociListFormatted := strings.Join(missingLociStringsList, ", ") + + t.Fatalf("locusMetadata is missing loci: " + missingLociListFormatted) + } +} + + +/* +// We use this to determine the greatest possible number of variants tested +// This needs to be updated in profileFormat whenever a new monogenic disease is added which exceeds this value +func TestGetHighestPossibleMonogenicDiseaseVariantCount(t *testing.T){ + + monogenicDiseases.InitializeMonogenicDiseaseVariables() + + monogenicDiseasesObjectsList, err := monogenicDiseases.GetMonogenicDiseaseObjectsList() + if (err != nil){ + t.Fatalf("Failed to get monogenic disease objects list: " + err.Error()) + } + + highestCount := 0 + + for _, diseaseObject := range monogenicDiseasesObjectsList{ + + diseaseVariantsList := diseaseObject.VariantsList + + diseaseNumberOfVariants := len(diseaseVariantsList) + + if (diseaseNumberOfVariants > highestCount){ + highestCount = diseaseNumberOfVariants + } + } + + highestVariantCountString := helpers.ConvertIntToString(highestCount) + + log.Println("Most monogenic disease variants: " + highestVariantCountString) +} + +*/ diff --git a/resources/geneticReferences/locusMetadata/LocusMetadata_Chromosome1.json b/resources/geneticReferences/locusMetadata/LocusMetadata_Chromosome1.json new file mode 100644 index 0000000..ee00ffc --- /dev/null +++ b/resources/geneticReferences/locusMetadata/LocusMetadata_Chromosome1.json @@ -0,0 +1,182 @@ +[ + { + "RSIDsList": [ + 17646946 + ], + "Chromosome": 1, + "Position": 152090291, + "GeneNamesList": [ + "TCHHL1" + ], + "CompanyAliases": {}, + "References": { + "SNPedia.com - rs17646946": "https://www.snpedia.com/index.php/Rs17646946" + } + }, + { + "RSIDsList": [ + 11803731 + ], + "Chromosome": 1, + "Position": 152110849, + "GeneNamesList": [ + "TCHH" + ], + "CompanyAliases": {}, + "References": { + "SNPedia.com - rs11803731": "https://www.snpedia.com/index.php/Rs11803731" + } + }, + { + "RSIDsList": [ + 4648379 + ], + "Chromosome": 1, + "Position": 3261516, + "GeneNamesList": [ + "MISSING" + ], + "CompanyAliases": {}, + "References": { + "SNPedia.com - Appearance": "https://www.snpedia.com/index.php/Appearance" + } + }, + { + "RSIDsList": [ + 1999527 + ], + "Chromosome": 1, + "Position": 3256108, + "GeneNamesList": [ + "MISSING" + ], + "CompanyAliases": {}, + "References": {} + }, + { + "RSIDsList": [ + 7516150 + ], + "Chromosome": 1, + "Position": 3253889, + "GeneNamesList": [ + "MISSING" + ], + "CompanyAliases": {}, + "References": {} + }, + { + "RSIDsList": [ + 7552331 + ], + "Chromosome": 1, + "Position": 3253941, + "GeneNamesList": [ + "MISSING" + ], + "CompanyAliases": {}, + "References": {} + }, + { + "RSIDsList": [ + 9782955 + ], + "Chromosome": 1, + "Position": 236039877, + "GeneNamesList": [ + "MISSING" + ], + "CompanyAliases": {}, + "References": {} + }, + { + "RSIDsList": [ + 3768056 + ], + "Chromosome": 1, + "Position": 235907825, + "GeneNamesList": [ + "MISSING" + ], + "CompanyAliases": {}, + "References": {} + }, + { + "RSIDsList": [ + 351385 + ], + "Chromosome": 1, + "Position": 212421629, + "GeneNamesList": [ + "MISSING" + ], + "CompanyAliases": {}, + "References": {} + }, + { + "RSIDsList": [ + 1572037 + ], + "Chromosome": 1, + "Position": 3254369, + "GeneNamesList": [ + "MISSING" + ], + "CompanyAliases": {}, + "References": {} + }, + { + "RSIDsList": [ + 6693258, + 56426910 + ], + "Chromosome": 1, + "Position": 9106285, + "GeneNamesList": [ + "GPR157" + ], + "CompanyAliases": {}, + "References": {} + }, + { + "RSIDsList": [ + 4648477 + ], + "Chromosome": 1, + "Position": 3335411, + "GeneNamesList": [ + "PRDM16" + ], + "CompanyAliases": {}, + "References": {} + }, + { + "RSIDsList": [ + 4648478, + 56579652, + 58636362 + ], + "Chromosome": 1, + "Position": 3335443, + "GeneNamesList": [ + "PRDM16" + ], + "CompanyAliases": {}, + "References": {} + }, + { + "RSIDsList": [ + 2385028, + 35558782, + 4660119, + 4428879 + ], + "Chromosome": 1, + "Position": 235872505, + "GeneNamesList": [ + "LYST" + ], + "CompanyAliases": {}, + "References": {} + } +] \ No newline at end of file diff --git a/resources/geneticReferences/locusMetadata/LocusMetadata_Chromosome10.json b/resources/geneticReferences/locusMetadata/LocusMetadata_Chromosome10.json new file mode 100644 index 0000000..d6c562e --- /dev/null +++ b/resources/geneticReferences/locusMetadata/LocusMetadata_Chromosome10.json @@ -0,0 +1,97 @@ +[ + { + "RSIDsList": [ + 2274107 + ], + "Chromosome": 10, + "Position": 105838703, + "GeneNamesList": [ + "MISSING" + ], + "CompanyAliases": {}, + "References": {} + }, + { + "RSIDsList": [ + 1747677 + ], + "Chromosome": 10, + "Position": 105815241, + "GeneNamesList": [ + "MISSING" + ], + "CompanyAliases": {}, + "References": {} + }, + { + "RSIDsList": [ + 805722 + ], + "Chromosome": 10, + "Position": 105810400, + "GeneNamesList": [ + "MISSING" + ], + "CompanyAliases": {}, + "References": {} + }, + { + "RSIDsList": [ + 805693 + ], + "Chromosome": 10, + "Position": 105815324, + "GeneNamesList": [ + "MISSING" + ], + "CompanyAliases": {}, + "References": {} + }, + { + "RSIDsList": [ + 12358982 + ], + "Chromosome": 10, + "Position": 104094571, + "GeneNamesList": [], + "CompanyAliases": {}, + "References": {} + }, + { + "RSIDsList": [ + 805694 + ], + "Chromosome": 10, + "Position": 104055696, + "GeneNamesList": [ + "COL17A1" + ], + "CompanyAliases": {}, + "References": {} + }, + { + "RSIDsList": [ + 11191909 + ], + "Chromosome": 10, + "Position": 104053243, + "GeneNamesList": [ + "COL17A1" + ], + "CompanyAliases": {}, + "References": {} + }, + { + "RSIDsList": [ + 9971100, + 10883964 + ], + "Chromosome": 10, + "Position": 104066661, + "GeneNamesList": [ + "COL17A1" + ], + "CompanyAliases": {}, + "References": {} + } +] \ No newline at end of file diff --git a/resources/geneticReferences/locusMetadata/LocusMetadata_Chromosome11.json b/resources/geneticReferences/locusMetadata/LocusMetadata_Chromosome11.json new file mode 100644 index 0000000..7151477 --- /dev/null +++ b/resources/geneticReferences/locusMetadata/LocusMetadata_Chromosome11.json @@ -0,0 +1,280 @@ +[ + { + "RSIDsList": [ + 4987945, + 2227924 + ], + "Chromosome": 11, + "Position": 108251865, + "GeneNamesList": [ + "ATM" + ], + "CompanyAliases": {}, + "References": { + "SNPedia.com - rs4987945": "https://www.snpedia.com/index.php/Rs4987945" + } + }, + { + "RSIDsList": [ + 3218695 + ], + "Chromosome": 11, + "Position": 108259051, + "GeneNamesList": [ + "ATM" + ], + "CompanyAliases": {}, + "References": { + "SNPedia.com - rs3218695": "https://www.snpedia.com/index.php/Rs3218695" + } + }, + { + "RSIDsList": [ + 3218707 + ], + "Chromosome": 11, + "Position": 108244000, + "GeneNamesList": [ + "ATM" + ], + "CompanyAliases": {}, + "References": { + "SNPedia.com - rs3218707": "https://www.snpedia.com/index.php/Rs3218707" + } + }, + { + "RSIDsList": [ + 334, + 77121243 + ], + "Chromosome": 11, + "Position": 5227002, + "GeneNamesList": [ + "HBB" + ], + "CompanyAliases": { + "1": [ + "i3003137" + ] + }, + "References": { + "SNPedia.com - rs334": "https://www.snpedia.com/index.php/Rs334" + } + }, + { + "RSIDsList": [ + 1801673 + ], + "Chromosome": 11, + "Position": 108304736, + "GeneNamesList": [ + "ATM" + ], + "CompanyAliases": {}, + "References": { + "SNPedia.com - rs1801673": "https://www.snpedia.com/index.php/Rs1801673" + } + }, + { + "RSIDsList": [ + 1800056 + ], + "Chromosome": 11, + "Position": 108267276, + "GeneNamesList": [ + "ATM" + ], + "CompanyAliases": {}, + "References": { + "SNPedia.com - rs1800056": "https://www.snpedia.com/index.php/Rs1800056" + } + }, + { + "RSIDsList": [ + 1800057 + ], + "Chromosome": 11, + "Position": 108272729, + "GeneNamesList": [ + "ATM" + ], + "CompanyAliases": {}, + "References": { + "SNPedia.com - rs1800057": "https://www.snpedia.com/index.php/Rs1800057" + } + }, + { + "RSIDsList": [ + 4986761 + ], + "Chromosome": 11, + "Position": 108254034, + "GeneNamesList": [ + "ATM" + ], + "CompanyAliases": {}, + "References": { + "SNPedia.com - rs4986761": "https://www.snpedia.com/index.php/Rs4986761" + } + }, + { + "RSIDsList": [ + 3092856 + ], + "Chromosome": 11, + "Position": 108289005, + "GeneNamesList": [ + "ATM" + ], + "CompanyAliases": {}, + "References": { + "SNPedia.com - rs3092856": "https://www.snpedia.com/index.php/Rs3092856" + } + }, + { + "RSIDsList": [ + 1800058 + ], + "Chromosome": 11, + "Position": 108289623, + "GeneNamesList": [ + "ATM" + ], + "CompanyAliases": {}, + "References": { + "SNPedia.com - rs1800058": "https://www.snpedia.com/index.php/Rs1800058" + } + }, + { + "RSIDsList": [ + 11237982 + ], + "Chromosome": 11, + "Position": 79441694, + "GeneNamesList": [ + "MISSING" + ], + "CompanyAliases": {}, + "References": {} + }, + { + "RSIDsList": [ + 1939707 + ], + "Chromosome": 11, + "Position": 100102098, + "GeneNamesList": [ + "MISSING" + ], + "CompanyAliases": {}, + "References": {} + }, + { + "RSIDsList": [ + 1042602 + ], + "Chromosome": 11, + "Position": 88911696, + "GeneNamesList": [ + "MISSING" + ], + "CompanyAliases": {}, + "References": {} + }, + { + "RSIDsList": [ + 1393350 + ], + "Chromosome": 11, + "Position": 89011046, + "GeneNamesList": [ + "MISSING" + ], + "CompanyAliases": {}, + "References": {} + }, + { + "RSIDsList": [ + 1126809 + ], + "Chromosome": 11, + "Position": 89017961, + "GeneNamesList": [ + "MISSING" + ], + "CompanyAliases": {}, + "References": {} + }, + { + "RSIDsList": [ + 11604811 + ], + "Chromosome": 11, + "Position": 72389984, + "GeneNamesList": [ + "MISSING" + ], + "CompanyAliases": {}, + "References": {} + }, + { + "RSIDsList": [ + 3829241 + ], + "Chromosome": 11, + "Position": 68855363, + "GeneNamesList": [ + "MISSING" + ], + "CompanyAliases": {}, + "References": {} + }, + { + "RSIDsList": [ + 35264875 + ], + "Chromosome": 11, + "Position": 68846399, + "GeneNamesList": [ + "MISSING" + ], + "CompanyAliases": {}, + "References": {} + }, + { + "RSIDsList": [ + 1939697 + ], + "Chromosome": 11, + "Position": 100091693, + "GeneNamesList": [ + "MISSING" + ], + "CompanyAliases": {}, + "References": {} + }, + { + "RSIDsList": [ + 1800422 + ], + "Chromosome": 11, + "Position": 89284793, + "GeneNamesList": [ + "TYR" + ], + "CompanyAliases": {}, + "References": {} + }, + { + "RSIDsList": [ + 72928978 + ], + "Chromosome": 11, + "Position": 69063896, + "GeneNamesList": [ + "TPCN2" + ], + "CompanyAliases": {}, + "References": {} + } +] \ No newline at end of file diff --git a/resources/geneticReferences/locusMetadata/LocusMetadata_Chromosome12.json b/resources/geneticReferences/locusMetadata/LocusMetadata_Chromosome12.json new file mode 100644 index 0000000..aa18a64 --- /dev/null +++ b/resources/geneticReferences/locusMetadata/LocusMetadata_Chromosome12.json @@ -0,0 +1,137 @@ +[ + { + "RSIDsList": [ + 34330 + ], + "Chromosome": 12, + "Position": 12717761, + "GeneNamesList": [ + "CDKN1B", + "GPR19" + ], + "CompanyAliases": {}, + "References": { + "SNPedia - rs34330": "https://www.snpedia.com/index.php/Rs34330" + } + }, + { + "RSIDsList": [ + 17252053 + ], + "Chromosome": 12, + "Position": 85727948, + "GeneNamesList": [ + "MISSING" + ], + "CompanyAliases": {}, + "References": {} + }, + { + "RSIDsList": [ + 1887276 + ], + "Chromosome": 12, + "Position": 100797485, + "GeneNamesList": [ + "MISSING" + ], + "CompanyAliases": {}, + "References": {} + }, + { + "RSIDsList": [ + 4433629 + ], + "Chromosome": 12, + "Position": 90341455, + "GeneNamesList": [ + "MISSING" + ], + "CompanyAliases": {}, + "References": {} + }, + { + "RSIDsList": [ + 10843104 + ], + "Chromosome": 12, + "Position": 28276626, + "GeneNamesList": [ + "MISSING" + ], + "CompanyAliases": {}, + "References": {} + }, + { + "RSIDsList": [ + 12821256 + ], + "Chromosome": 12, + "Position": 89328335, + "GeneNamesList": [ + "MISSING" + ], + "CompanyAliases": {}, + "References": {} + }, + { + "RSIDsList": [ + 7965082 + ], + "Chromosome": 12, + "Position": 100800193, + "GeneNamesList": [ + "MISSING" + ], + "CompanyAliases": {}, + "References": {} + }, + { + "RSIDsList": [ + 9971729 + ], + "Chromosome": 12, + "Position": 23979791, + "GeneNamesList": [ + "MISSING" + ], + "CompanyAliases": {}, + "References": {} + }, + { + "RSIDsList": [ + 642742 + ], + "Chromosome": 12, + "Position": 89299746, + "GeneNamesList": [ + "MISSING" + ], + "CompanyAliases": {}, + "References": {} + }, + { + "RSIDsList": [ + 7966317 + ], + "Chromosome": 12, + "Position": 100795311, + "GeneNamesList": [ + "MISSING" + ], + "CompanyAliases": {}, + "References": {} + }, + { + "RSIDsList": [ + 790464 + ], + "Chromosome": 12, + "Position": 92174057, + "GeneNamesList": [ + "BTG1-DT" + ], + "CompanyAliases": {}, + "References": {} + } +] \ No newline at end of file diff --git a/resources/geneticReferences/locusMetadata/LocusMetadata_Chromosome13.json b/resources/geneticReferences/locusMetadata/LocusMetadata_Chromosome13.json new file mode 100644 index 0000000..8656f57 --- /dev/null +++ b/resources/geneticReferences/locusMetadata/LocusMetadata_Chromosome13.json @@ -0,0 +1,162 @@ +[ + { + "RSIDsList": [ + 11571746 + ], + "Chromosome": 13, + "Position": 32370971, + "GeneNamesList": [ + "BRCA2" + ], + "CompanyAliases": { + "1": [ + "i5009299" + ] + }, + "References": { + "SNPedia.com - rs11571746": "https://www.snpedia.com/index.php/Rs11571746" + } + }, + { + "RSIDsList": [ + 11571747 + ], + "Chromosome": 13, + "Position": 32371035, + "GeneNamesList": [ + "BRCA2" + ], + "CompanyAliases": {}, + "References": { + "SNPedia.com - rs11571747": "https://www.snpedia.com/index.php/Rs11571747" + } + }, + { + "RSIDsList": [ + 766173 + ], + "Chromosome": 13, + "Position": 32332343, + "GeneNamesList": [ + "BRCA2" + ], + "CompanyAliases": {}, + "References": { + "SNPedia.com - rs766173": "https://www.snpedia.com/index.php/Rs766173" + } + }, + { + "RSIDsList": [ + 1801426 + ], + "Chromosome": 13, + "Position": 32398747, + "GeneNamesList": [ + "BRCA2" + ], + "CompanyAliases": { + "1": [ + "i5009256" + ] + }, + "References": { + "SNPedia.com - rs1801426": "https://www.snpedia.com/index.php/Rs1801426" + } + }, + { + "RSIDsList": [ + 4987117 + ], + "Chromosome": 13, + "Position": 32340099, + "GeneNamesList": [ + "BRCA2" + ], + "CompanyAliases": {}, + "References": { + "SNPedia.com - rs4987117": "https://www.snpedia.com/index.php/Rs4987117" + } + }, + { + "RSIDsList": [ + 1799954 + ], + "Chromosome": 13, + "Position": 32340455, + "GeneNamesList": [ + "BRCA2" + ], + "CompanyAliases": {}, + "References": { + "SNPedia.com - rs1799954": "https://www.snpedia.com/index.php/Rs1799954" + } + }, + { + "RSIDsList": [ + 144848 + ], + "Chromosome": 13, + "Position": 32332592, + "GeneNamesList": [ + "BRCA2" + ], + "CompanyAliases": {}, + "References": { + "SNPedia.com - rs144848": "https://www.snpedia.com/index.php/Rs144848" + } + }, + { + "RSIDsList": [ + 4987047 + ], + "Chromosome": 13, + "Position": 32379392, + "GeneNamesList": [ + "BRCA2" + ], + "CompanyAliases": {}, + "References": { + "SNPedia.com - rs4987047": "https://www.snpedia.com/index.php/Rs4987047" + } + }, + { + "RSIDsList": [ + 11571833 + ], + "Chromosome": 13, + "Position": 32398489, + "GeneNamesList": [ + "BRCA2" + ], + "CompanyAliases": {}, + "References": { + "SNPedia.com - rs11571833": "https://www.snpedia.com/index.php/Rs11571833" + } + }, + { + "RSIDsList": [ + 2095645 + ], + "Chromosome": 13, + "Position": 74178399, + "GeneNamesList": [ + "MISSING" + ], + "CompanyAliases": {}, + "References": {} + }, + { + "RSIDsList": [ + 9301973, + 61272261, + 17254025 + ], + "Chromosome": 13, + "Position": 94537147, + "GeneNamesList": [ + "DCT" + ], + "CompanyAliases": {}, + "References": {} + } +] \ No newline at end of file diff --git a/resources/geneticReferences/locusMetadata/LocusMetadata_Chromosome14.json b/resources/geneticReferences/locusMetadata/LocusMetadata_Chromosome14.json new file mode 100644 index 0000000..d6784c6 --- /dev/null +++ b/resources/geneticReferences/locusMetadata/LocusMetadata_Chromosome14.json @@ -0,0 +1,36 @@ +[ + { + "RSIDsList": [ + 12896399 + ], + "Chromosome": 14, + "Position": 92773663, + "GeneNamesList": [ + "MISSING" + ], + "CompanyAliases": {}, + "References": {} + }, + { + "RSIDsList": [ + 17184180 + ], + "Chromosome": 14, + "Position": 92780387, + "GeneNamesList": [ + "MISSING" + ], + "CompanyAliases": {}, + "References": {} + }, + { + "RSIDsList": [ + 138777265 + ], + "Chromosome": 14, + "Position": 68769419, + "GeneNamesList": [], + "CompanyAliases": {}, + "References": {} + } +] \ No newline at end of file diff --git a/resources/geneticReferences/locusMetadata/LocusMetadata_Chromosome15.json b/resources/geneticReferences/locusMetadata/LocusMetadata_Chromosome15.json new file mode 100644 index 0000000..2099587 --- /dev/null +++ b/resources/geneticReferences/locusMetadata/LocusMetadata_Chromosome15.json @@ -0,0 +1,447 @@ +[ + { + "RSIDsList": [ + 7183877 + ], + "Chromosome": 15, + "Position": 28365733, + "GeneNamesList": [ + "MISSING" + ], + "CompanyAliases": {}, + "References": {} + }, + { + "RSIDsList": [ + 1800407 + ], + "Chromosome": 15, + "Position": 28230318, + "GeneNamesList": [ + "MISSING" + ], + "CompanyAliases": {}, + "References": {} + }, + { + "RSIDsList": [ + 1129038 + ], + "Chromosome": 15, + "Position": 28356859, + "GeneNamesList": [ + "MISSING" + ], + "CompanyAliases": {}, + "References": {} + }, + { + "RSIDsList": [ + 7495174 + ], + "Chromosome": 15, + "Position": 28344238, + "GeneNamesList": [ + "MISSING" + ], + "CompanyAliases": {}, + "References": {} + }, + { + "RSIDsList": [ + 7174027 + ], + "Chromosome": 15, + "Position": 28328765, + "GeneNamesList": [ + "MISSING" + ], + "CompanyAliases": {}, + "References": {} + }, + { + "RSIDsList": [ + 1800414 + ], + "Chromosome": 15, + "Position": 28197037, + "GeneNamesList": [ + "MISSING" + ], + "CompanyAliases": {}, + "References": {} + }, + { + "RSIDsList": [ + 2240203 + ], + "Chromosome": 15, + "Position": 28494202, + "GeneNamesList": [ + "MISSING" + ], + "CompanyAliases": {}, + "References": {} + }, + { + "RSIDsList": [ + 4778218 + ], + "Chromosome": 15, + "Position": 28211758, + "GeneNamesList": [ + "MISSING" + ], + "CompanyAliases": {}, + "References": {} + }, + { + "RSIDsList": [ + 4778211 + ], + "Chromosome": 15, + "Position": 28199305, + "GeneNamesList": [ + "MISSING" + ], + "CompanyAliases": {}, + "References": {} + }, + { + "RSIDsList": [ + 728405 + ], + "Chromosome": 15, + "Position": 28199853, + "GeneNamesList": [ + "MISSING" + ], + "CompanyAliases": {}, + "References": {} + }, + { + "RSIDsList": [ + 8028689 + ], + "Chromosome": 15, + "Position": 28488888, + "GeneNamesList": [ + "MISSING" + ], + "CompanyAliases": {}, + "References": {} + }, + { + "RSIDsList": [ + 12906280 + ], + "Chromosome": 15, + "Position": 30265887, + "GeneNamesList": [ + "MISSING" + ], + "CompanyAliases": {}, + "References": {} + }, + { + "RSIDsList": [ + 3935591 + ], + "Chromosome": 15, + "Position": 28374012, + "GeneNamesList": [ + "MISSING" + ], + "CompanyAliases": {}, + "References": {} + }, + { + "RSIDsList": [ + 1667394 + ], + "Chromosome": 15, + "Position": 28530182, + "GeneNamesList": [ + "MISSING" + ], + "CompanyAliases": {}, + "References": {} + }, + { + "RSIDsList": [ + 1800401 + ], + "Chromosome": 15, + "Position": 28260053, + "GeneNamesList": [ + "MISSING" + ], + "CompanyAliases": {}, + "References": {} + }, + { + "RSIDsList": [ + 12913823 + ], + "Chromosome": 15, + "Position": 50509591, + "GeneNamesList": [ + "MISSING" + ], + "CompanyAliases": {}, + "References": {} + }, + { + "RSIDsList": [ + 1426654 + ], + "Chromosome": 15, + "Position": 48426484, + "GeneNamesList": [ + "MISSING" + ], + "CompanyAliases": {}, + "References": {} + }, + { + "RSIDsList": [ + 12913832 + ], + "Chromosome": 15, + "Position": 28365618, + "GeneNamesList": [ + "MISSING" + ], + "CompanyAliases": {}, + "References": {} + }, + { + "RSIDsList": [ + 12593929 + ], + "Chromosome": 15, + "Position": 28359258, + "GeneNamesList": [ + "MISSING" + ], + "CompanyAliases": {}, + "References": {} + }, + { + "RSIDsList": [ + 916977 + ], + "Chromosome": 15, + "Position": 28513364, + "GeneNamesList": [ + "MISSING" + ], + "CompanyAliases": {}, + "References": {} + }, + { + "RSIDsList": [ + 11636232 + ], + "Chromosome": 15, + "Position": 28386626, + "GeneNamesList": [ + "MISSING" + ], + "CompanyAliases": {}, + "References": {} + }, + { + "RSIDsList": [ + 4778241 + ], + "Chromosome": 15, + "Position": 28338713, + "GeneNamesList": [ + "MISSING" + ], + "CompanyAliases": {}, + "References": {} + }, + { + "RSIDsList": [ + 8039195 + ], + "Chromosome": 15, + "Position": 28516084, + "GeneNamesList": [ + "MISSING" + ], + "CompanyAliases": {}, + "References": {} + }, + { + "RSIDsList": [ + 3794604 + ], + "Chromosome": 15, + "Position": 28272065, + "GeneNamesList": [ + "MISSING" + ], + "CompanyAliases": {}, + "References": {} + }, + { + "RSIDsList": [ + 989869 + ], + "Chromosome": 15, + "Position": 28006306, + "GeneNamesList": [ + "MISSING" + ], + "CompanyAliases": {}, + "References": {} + }, + { + "RSIDsList": [ + 1834640 + ], + "Chromosome": 15, + "Position": 48392165, + "GeneNamesList": [ + "MISSING" + ], + "CompanyAliases": {}, + "References": {} + }, + { + "RSIDsList": [ + 7170852 + ], + "Chromosome": 15, + "Position": 28427986, + "GeneNamesList": [ + "MISSING" + ], + "CompanyAliases": {}, + "References": {} + }, + { + "RSIDsList": [ + 4778138 + ], + "Chromosome": 15, + "Position": 28335820, + "GeneNamesList": [ + "MISSING" + ], + "CompanyAliases": {}, + "References": {} + }, + { + "RSIDsList": [ + 784416 + ], + "Chromosome": 15, + "Position": 49012925, + "GeneNamesList": [ + "MISSING" + ], + "CompanyAliases": {}, + "References": {} + }, + { + "RSIDsList": [ + 7176696 + ], + "Chromosome": 15, + "Position": 49073903, + "GeneNamesList": [ + "MISSING" + ], + "CompanyAliases": {}, + "References": {} + }, + { + "RSIDsList": [ + 3940272 + ], + "Chromosome": 15, + "Position": 28468723, + "GeneNamesList": [ + "MISSING" + ], + "CompanyAliases": {}, + "References": {} + }, + { + "RSIDsList": [ + 2238289 + ], + "Chromosome": 15, + "Position": 28453215, + "GeneNamesList": [ + "MISSING" + ], + "CompanyAliases": {}, + "References": {} + }, + { + "RSIDsList": [ + 937171 + ], + "Chromosome": 15, + "Position": 50194749, + "GeneNamesList": [ + "MISSING" + ], + "CompanyAliases": {}, + "References": {} + }, + { + "RSIDsList": [ + 11631797 + ], + "Chromosome": 15, + "Position": 28502279, + "GeneNamesList": [ + "MISSING" + ], + "CompanyAliases": {}, + "References": {} + }, + { + "RSIDsList": [ + 12437560 + ], + "Chromosome": 15, + "Position": 61832507, + "GeneNamesList": [], + "CompanyAliases": {}, + "References": {} + }, + { + "RSIDsList": [ + 11855019, + 59065625 + ], + "Chromosome": 15, + "Position": 28090674, + "GeneNamesList": [ + "OCA2" + ], + "CompanyAliases": {}, + "References": {} + }, + { + "RSIDsList": [ + 7182710, + 17466298, + 61298156 + ], + "Chromosome": 15, + "Position": 48812737, + "GeneNamesList": [ + "CEP152" + ], + "CompanyAliases": {}, + "References": {} + } +] \ No newline at end of file diff --git a/resources/geneticReferences/locusMetadata/LocusMetadata_Chromosome16.json b/resources/geneticReferences/locusMetadata/LocusMetadata_Chromosome16.json new file mode 100644 index 0000000..e1e1dcd --- /dev/null +++ b/resources/geneticReferences/locusMetadata/LocusMetadata_Chromosome16.json @@ -0,0 +1,50 @@ +[ + { + "RSIDsList": [ + 1805007 + ], + "Chromosome": 16, + "Position": 89986117, + "GeneNamesList": [ + "MISSING" + ], + "CompanyAliases": {}, + "References": {} + }, + { + "RSIDsList": [ + 1805008 + ], + "Chromosome": 16, + "Position": 89986144, + "GeneNamesList": [ + "MISSING" + ], + "CompanyAliases": {}, + "References": {} + }, + { + "RSIDsList": [ + 3212369 + ], + "Chromosome": 16, + "Position": 89986760, + "GeneNamesList": [ + "MISSING" + ], + "CompanyAliases": {}, + "References": {} + }, + { + "RSIDsList": [ + 3212368 + ], + "Chromosome": 16, + "Position": 89920224, + "GeneNamesList": [ + "MC1R" + ], + "CompanyAliases": {}, + "References": {} + } +] \ No newline at end of file diff --git a/resources/geneticReferences/locusMetadata/LocusMetadata_Chromosome17.json b/resources/geneticReferences/locusMetadata/LocusMetadata_Chromosome17.json new file mode 100644 index 0000000..7fb5a32 --- /dev/null +++ b/resources/geneticReferences/locusMetadata/LocusMetadata_Chromosome17.json @@ -0,0 +1,214 @@ +[ + { + "RSIDsList": [ + 1799966 + ], + "Chromosome": 17, + "Position": 43071077, + "GeneNamesList": [ + "BRCA1" + ], + "CompanyAliases": {}, + "References": { + "SNPedia.com - rs1799966": "https://www.snpedia.com/index.php/Rs1799966" + } + }, + { + "RSIDsList": [ + 1799950 + ], + "Chromosome": 17, + "Position": 43094464, + "GeneNamesList": [ + "BRCA1" + ], + "CompanyAliases": {}, + "References": { + "SNPedia.com - rs1799950": "https://www.snpedia.com/index.php/Rs1799950" + } + }, + { + "RSIDsList": [ + 2227945 + ], + "Chromosome": 17, + "Position": 43092113, + "GeneNamesList": [ + "BRCA1" + ], + "CompanyAliases": {}, + "References": { + "SNPedia.com - rs2227945": "https://www.snpedia.com/index.php/Rs2227945" + } + }, + { + "RSIDsList": [ + 16942 + ], + "Chromosome": 17, + "Position": 43091983, + "GeneNamesList": [ + "BRCA1" + ], + "CompanyAliases": {}, + "References": { + "SNPedia.com - rs16942": "https://www.snpedia.com/index.php/Rs16942" + } + }, + { + "RSIDsList": [ + 4986850 + ], + "Chromosome": 17, + "Position": 43093454, + "GeneNamesList": [ + "BRCA1" + ], + "CompanyAliases": {}, + "References": { + "SNPedia.com - rs4986850": "https://www.snpedia.com/index.php/Rs4986850" + } + }, + { + "RSIDsList": [ + 9894429 + ], + "Chromosome": 17, + "Position": 79596811, + "GeneNamesList": [ + "MISSING" + ], + "CompanyAliases": {}, + "References": {} + }, + { + "RSIDsList": [ + 12452184 + ], + "Chromosome": 17, + "Position": 79664426, + "GeneNamesList": [ + "MISSING" + ], + "CompanyAliases": {}, + "References": {} + }, + { + "RSIDsList": [ + 16977009 + ], + "Chromosome": 17, + "Position": 69916524, + "GeneNamesList": [ + "MISSING" + ], + "CompanyAliases": {}, + "References": {} + }, + { + "RSIDsList": [ + 7219915 + ], + "Chromosome": 17, + "Position": 79591813, + "GeneNamesList": [ + "MISSING" + ], + "CompanyAliases": {}, + "References": {} + }, + { + "RSIDsList": [ + 8079498 + ], + "Chromosome": 17, + "Position": 69919452, + "GeneNamesList": [ + "MISSING" + ], + "CompanyAliases": {}, + "References": {} + }, + { + "RSIDsList": [ + 3809761 + ], + "Chromosome": 17, + "Position": 67497367, + "GeneNamesList": [ + "MISSING" + ], + "CompanyAliases": {}, + "References": {} + }, + { + "RSIDsList": [ + 16977008 + ], + "Chromosome": 17, + "Position": 69916480, + "GeneNamesList": [ + "MISSING" + ], + "CompanyAliases": {}, + "References": {} + }, + { + "RSIDsList": [ + 16977002 + ], + "Chromosome": 17, + "Position": 71919192, + "GeneNamesList": [], + "CompanyAliases": {}, + "References": {} + }, + { + "RSIDsList": [ + 6420484, + 59590586, + 17859003, + 17846019 + ], + "Chromosome": 17, + "Position": 81645371, + "GeneNamesList": [ + "TSPAN10" + ], + "CompanyAliases": {}, + "References": {} + }, + { + "RSIDsList": [ + 4793389 + ], + "Chromosome": 17, + "Position": 71921776, + "GeneNamesList": [], + "CompanyAliases": {}, + "References": {} + }, + { + "RSIDsList": [ + 4790309, + 58087488 + ], + "Chromosome": 17, + "Position": 2063595, + "GeneNamesList": [ + "HIC1" + ], + "CompanyAliases": {}, + "References": {} + }, + { + "RSIDsList": [ + 7214306 + ], + "Chromosome": 17, + "Position": 71925130, + "GeneNamesList": [], + "CompanyAliases": {}, + "References": {} + } +] \ No newline at end of file diff --git a/resources/geneticReferences/locusMetadata/LocusMetadata_Chromosome19.json b/resources/geneticReferences/locusMetadata/LocusMetadata_Chromosome19.json new file mode 100644 index 0000000..54ab3b5 --- /dev/null +++ b/resources/geneticReferences/locusMetadata/LocusMetadata_Chromosome19.json @@ -0,0 +1,62 @@ +[ + { + "RSIDsList": [ + 1008591 + ], + "Chromosome": 19, + "Position": 46730614, + "GeneNamesList": [ + "MISSING" + ], + "CompanyAliases": {}, + "References": {} + }, + { + "RSIDsList": [ + 1019212, + 58273978, + 17660257 + ], + "Chromosome": 19, + "Position": 46225962, + "GeneNamesList": [], + "CompanyAliases": {}, + "References": {} + }, + { + "RSIDsList": [ + 73488486 + ], + "Chromosome": 19, + "Position": 7516739, + "GeneNamesList": [ + "ZNF358" + ], + "CompanyAliases": {}, + "References": {} + }, + { + "RSIDsList": [ + 10424065 + ], + "Chromosome": 19, + "Position": 3545024, + "GeneNamesList": [ + "MFSD12" + ], + "CompanyAliases": {}, + "References": {} + }, + { + "RSIDsList": [ + 142317543 + ], + "Chromosome": 19, + "Position": 3547687, + "GeneNamesList": [ + "MFSD12" + ], + "CompanyAliases": {}, + "References": {} + } +] \ No newline at end of file diff --git a/resources/geneticReferences/locusMetadata/LocusMetadata_Chromosome2.json b/resources/geneticReferences/locusMetadata/LocusMetadata_Chromosome2.json new file mode 100644 index 0000000..780499d --- /dev/null +++ b/resources/geneticReferences/locusMetadata/LocusMetadata_Chromosome2.json @@ -0,0 +1,276 @@ +[ + { + "RSIDsList": [ + 182549 + ], + "Chromosome": 2, + "Position": 135859184, + "GeneNamesList": [ + "MCM6" + ], + "CompanyAliases": {}, + "References": { + "SNPedia.com - rs182549": "https://www.snpedia.com/index.php/Rs182549" + } + }, + { + "RSIDsList": [ + 1045485 + ], + "Chromosome": 2, + "Position": 201284866, + "GeneNamesList": [ + "CASP8" + ], + "CompanyAliases": {}, + "References": { + "SNPedia.com - rs1045485": "https://www.snpedia.com/index.php/Rs1045485" + } + }, + { + "RSIDsList": [ + 4988235 + ], + "Chromosome": 2, + "Position": 135851076, + "GeneNamesList": [ + "MCM6" + ], + "CompanyAliases": {}, + "References": { + "SNPedia.com - rs4988235": "https://www.snpedia.com/index.php/Rs4988235" + } + }, + { + "RSIDsList": [ + 7349332 + ], + "Chromosome": 2, + "Position": 218891661, + "GeneNamesList": [ + "WNT10A" + ], + "CompanyAliases": {}, + "References": { + "SNPedia.com - rs7349332": "https://www.snpedia.com/index.php/Rs7349332" + } + }, + { + "RSIDsList": [ + 2422241 + ], + "Chromosome": 2, + "Position": 119043036, + "GeneNamesList": [ + "MISSING" + ], + "CompanyAliases": {}, + "References": {} + }, + { + "RSIDsList": [ + 16863422 + ], + "Chromosome": 2, + "Position": 222990015, + "GeneNamesList": [ + "MISSING" + ], + "CompanyAliases": {}, + "References": {} + }, + { + "RSIDsList": [ + 12694574 + ], + "Chromosome": 2, + "Position": 222993733, + "GeneNamesList": [ + "MISSING" + ], + "CompanyAliases": {}, + "References": {} + }, + { + "RSIDsList": [ + 1105879 + ], + "Chromosome": 2, + "Position": 234602202, + "GeneNamesList": [ + "MISSING" + ], + "CompanyAliases": {}, + "References": {} + }, + { + "RSIDsList": [ + 974448 + ], + "Chromosome": 2, + "Position": 223005314, + "GeneNamesList": [ + "MISSING" + ], + "CompanyAliases": {}, + "References": {} + }, + { + "RSIDsList": [ + 1005999 + ], + "Chromosome": 2, + "Position": 105523791, + "GeneNamesList": [ + "MISSING" + ], + "CompanyAliases": {}, + "References": {} + }, + { + "RSIDsList": [ + 2070959 + ], + "Chromosome": 2, + "Position": 234602191, + "GeneNamesList": [ + "MISSING" + ], + "CompanyAliases": {}, + "References": {} + }, + { + "RSIDsList": [ + 1978859 + ], + "Chromosome": 2, + "Position": 223082331, + "GeneNamesList": [ + "MISSING" + ], + "CompanyAliases": {}, + "References": {} + }, + { + "RSIDsList": [ + 2894450 + ], + "Chromosome": 2, + "Position": 222997104, + "GeneNamesList": [ + "MISSING" + ], + "CompanyAliases": {}, + "References": {} + }, + { + "RSIDsList": [ + 2422239 + ], + "Chromosome": 2, + "Position": 119029079, + "GeneNamesList": [ + "MISSING" + ], + "CompanyAliases": {}, + "References": {} + }, + { + "RSIDsList": [ + 892839 + ], + "Chromosome": 2, + "Position": 239406446, + "GeneNamesList": [ + "MISSING" + ], + "CompanyAliases": {}, + "References": {} + }, + { + "RSIDsList": [ + 10209564 + ], + "Chromosome": 2, + "Position": 239459603, + "GeneNamesList": [ + "MISSING" + ], + "CompanyAliases": {}, + "References": {} + }, + { + "RSIDsList": [ + 121908120 + ], + "Chromosome": 2, + "Position": 219755011, + "GeneNamesList": [ + "MISSING" + ], + "CompanyAliases": {}, + "References": {} + }, + { + "RSIDsList": [ + 12614022 + ], + "Chromosome": 2, + "Position": 222618951, + "GeneNamesList": [ + "FARSB" + ], + "CompanyAliases": {}, + "References": {} + }, + { + "RSIDsList": [ + 6749293 + ], + "Chromosome": 2, + "Position": 172302075, + "GeneNamesList": [ + "LOC107985960" + ], + "CompanyAliases": {}, + "References": {} + }, + { + "RSIDsList": [ + 112747614 + ], + "Chromosome": 2, + "Position": 206085512, + "GeneNamesList": [ + "INO80D" + ], + "CompanyAliases": {}, + "References": {} + }, + { + "RSIDsList": [ + 74409360 + ], + "Chromosome": 2, + "Position": 238367637, + "GeneNamesList": [ + "TRAF3IP1" + ], + "CompanyAliases": {}, + "References": {} + }, + { + "RSIDsList": [ + 13016869, + 56853446, + 56528773 + ], + "Chromosome": 2, + "Position": 46006242, + "GeneNamesList": [ + "PRKCE" + ], + "CompanyAliases": {}, + "References": {} + } +] \ No newline at end of file diff --git a/resources/geneticReferences/locusMetadata/LocusMetadata_Chromosome20.json b/resources/geneticReferences/locusMetadata/LocusMetadata_Chromosome20.json new file mode 100644 index 0000000..ec5d9b9 --- /dev/null +++ b/resources/geneticReferences/locusMetadata/LocusMetadata_Chromosome20.json @@ -0,0 +1,234 @@ +[ + { + "RSIDsList": [ + 4053148 + ], + "Chromosome": 20, + "Position": 8772544, + "GeneNamesList": [ + "MISSING" + ], + "CompanyAliases": {}, + "References": {} + }, + { + "RSIDsList": [ + 4911414 + ], + "Chromosome": 20, + "Position": 32729444, + "GeneNamesList": [ + "MISSING" + ], + "CompanyAliases": {}, + "References": {} + }, + { + "RSIDsList": [ + 4911442 + ], + "Chromosome": 20, + "Position": 33355046, + "GeneNamesList": [ + "MISSING" + ], + "CompanyAliases": {}, + "References": {} + }, + { + "RSIDsList": [ + 2748901 + ], + "Chromosome": 20, + "Position": 4948248, + "GeneNamesList": [ + "MISSING" + ], + "CompanyAliases": {}, + "References": {} + }, + { + "RSIDsList": [ + 1015092 + ], + "Chromosome": 20, + "Position": 8750062, + "GeneNamesList": [ + "MISSING" + ], + "CompanyAliases": {}, + "References": {} + }, + { + "RSIDsList": [ + 911020 + ], + "Chromosome": 20, + "Position": 49671946, + "GeneNamesList": [ + "MISSING" + ], + "CompanyAliases": {}, + "References": {} + }, + { + "RSIDsList": [ + 6058017 + ], + "Chromosome": 20, + "Position": 32856998, + "GeneNamesList": [ + "MISSING" + ], + "CompanyAliases": {}, + "References": {} + }, + { + "RSIDsList": [ + 2327089 + ], + "Chromosome": 20, + "Position": 8769180, + "GeneNamesList": [ + "MISSING" + ], + "CompanyAliases": {}, + "References": {} + }, + { + "RSIDsList": [ + 6020957 + ], + "Chromosome": 20, + "Position": 49687635, + "GeneNamesList": [ + "MISSING" + ], + "CompanyAliases": {}, + "References": {} + }, + { + "RSIDsList": [ + 2327101 + ], + "Chromosome": 20, + "Position": 8734263, + "GeneNamesList": [ + "MISSING" + ], + "CompanyAliases": {}, + "References": {} + }, + { + "RSIDsList": [ + 6039266 + ], + "Chromosome": 20, + "Position": 8766071, + "GeneNamesList": [ + "MISSING" + ], + "CompanyAliases": {}, + "References": {} + }, + { + "RSIDsList": [ + 6056066 + ], + "Chromosome": 20, + "Position": 8738169, + "GeneNamesList": [ + "MISSING" + ], + "CompanyAliases": {}, + "References": {} + }, + { + "RSIDsList": [ + 975633 + ], + "Chromosome": 20, + "Position": 8765289, + "GeneNamesList": [ + "MISSING" + ], + "CompanyAliases": {}, + "References": {} + }, + { + "RSIDsList": [ + 4633993, + 111186477 + ], + "Chromosome": 20, + "Position": 8789461, + "GeneNamesList": [ + "PLCB1" + ], + "CompanyAliases": {}, + "References": {} + }, + { + "RSIDsList": [ + 911015, + 60505384 + ], + "Chromosome": 20, + "Position": 51073634, + "GeneNamesList": [], + "CompanyAliases": {}, + "References": {} + }, + { + "RSIDsList": [ + 6020940, + 7271570 + ], + "Chromosome": 20, + "Position": 51058312, + "GeneNamesList": [], + "CompanyAliases": {}, + "References": {} + }, + { + "RSIDsList": [ + 6056119, + 58122852, + 6516401 + ], + "Chromosome": 20, + "Position": 8792648, + "GeneNamesList": [ + "PLCB1" + ], + "CompanyAliases": {}, + "References": {} + }, + { + "RSIDsList": [ + 6056126, + 59241198, + 7260663 + ], + "Chromosome": 20, + "Position": 8795023, + "GeneNamesList": [ + "PLCB1" + ], + "CompanyAliases": {}, + "References": {} + }, + { + "RSIDsList": [ + 6039272, + 7269212 + ], + "Chromosome": 20, + "Position": 8792227, + "GeneNamesList": [ + "PLCB1" + ], + "CompanyAliases": {}, + "References": {} + } +] \ No newline at end of file diff --git a/resources/geneticReferences/locusMetadata/LocusMetadata_Chromosome21.json b/resources/geneticReferences/locusMetadata/LocusMetadata_Chromosome21.json new file mode 100644 index 0000000..2cac74d --- /dev/null +++ b/resources/geneticReferences/locusMetadata/LocusMetadata_Chromosome21.json @@ -0,0 +1,98 @@ +[ + { + "RSIDsList": [ + 2252893 + ], + "Chromosome": 21, + "Position": 38507572, + "GeneNamesList": [ + "MISSING" + ], + "CompanyAliases": {}, + "References": {} + }, + { + "RSIDsList": [ + 2835630 + ], + "Chromosome": 21, + "Position": 38521842, + "GeneNamesList": [ + "MISSING" + ], + "CompanyAliases": {}, + "References": {} + }, + { + "RSIDsList": [ + 1003719 + ], + "Chromosome": 21, + "Position": 38491095, + "GeneNamesList": [ + "MISSING" + ], + "CompanyAliases": {}, + "References": {} + }, + { + "RSIDsList": [ + 2835621 + ], + "Chromosome": 21, + "Position": 38510616, + "GeneNamesList": [ + "MISSING" + ], + "CompanyAliases": {}, + "References": {} + }, + { + "RSIDsList": [ + 2832438 + ], + "Chromosome": 21, + "Position": 31137937, + "GeneNamesList": [ + "MISSING" + ], + "CompanyAliases": {}, + "References": {} + }, + { + "RSIDsList": [ + 7277820 + ], + "Chromosome": 21, + "Position": 38580309, + "GeneNamesList": [ + "MISSING" + ], + "CompanyAliases": {}, + "References": {} + }, + { + "RSIDsList": [ + 2835660 + ], + "Chromosome": 21, + "Position": 37196581, + "GeneNamesList": [ + "TTC3" + ], + "CompanyAliases": {}, + "References": {} + }, + { + "RSIDsList": [ + 622330 + ], + "Chromosome": 21, + "Position": 43363407, + "GeneNamesList": [ + "LINC01679" + ], + "CompanyAliases": {}, + "References": {} + } +] \ No newline at end of file diff --git a/resources/geneticReferences/locusMetadata/LocusMetadata_Chromosome22.json b/resources/geneticReferences/locusMetadata/LocusMetadata_Chromosome22.json new file mode 100644 index 0000000..ff6c23c --- /dev/null +++ b/resources/geneticReferences/locusMetadata/LocusMetadata_Chromosome22.json @@ -0,0 +1,41 @@ +[ + { + "RSIDsList": [ + 17879961 + ], + "Chromosome": 22, + "Position": 28725099, + "GeneNamesList": [ + "CHEK2" + ], + "CompanyAliases": {}, + "References": { + "SNPedia.com - rs17879961": "https://www.snpedia.com/index.php/Rs17879961" + } + }, + { + "RSIDsList": [ + 397723 + ], + "Chromosome": 22, + "Position": 48112790, + "GeneNamesList": [ + "MISSING" + ], + "CompanyAliases": {}, + "References": {} + }, + { + "RSIDsList": [ + 35051352, + 62226058 + ], + "Chromosome": 22, + "Position": 45973777, + "GeneNamesList": [ + "WNT7B" + ], + "CompanyAliases": {}, + "References": {} + } +] \ No newline at end of file diff --git a/resources/geneticReferences/locusMetadata/LocusMetadata_Chromosome3.json b/resources/geneticReferences/locusMetadata/LocusMetadata_Chromosome3.json new file mode 100644 index 0000000..51c878c --- /dev/null +++ b/resources/geneticReferences/locusMetadata/LocusMetadata_Chromosome3.json @@ -0,0 +1,231 @@ +[ + { + "RSIDsList": [ + 4552364 + ], + "Chromosome": 3, + "Position": 88974863, + "GeneNamesList": [ + "MISSING" + ], + "CompanyAliases": {}, + "References": {} + }, + { + "RSIDsList": [ + 717463 + ], + "Chromosome": 3, + "Position": 59372700, + "GeneNamesList": [ + "MISSING" + ], + "CompanyAliases": {}, + "References": {} + }, + { + "RSIDsList": [ + 116359091 + ], + "Chromosome": 3, + "Position": 69980177, + "GeneNamesList": [ + "MISSING" + ], + "CompanyAliases": {}, + "References": {} + }, + { + "RSIDsList": [ + 6795519 + ], + "Chromosome": 3, + "Position": 59388206, + "GeneNamesList": [ + "MISSING" + ], + "CompanyAliases": {}, + "References": {} + }, + { + "RSIDsList": [ + 9858909 + ], + "Chromosome": 3, + "Position": 88378348, + "GeneNamesList": [ + "MISSING" + ], + "CompanyAliases": {}, + "References": {} + }, + { + "RSIDsList": [ + 13097965 + ], + "Chromosome": 3, + "Position": 184339757, + "GeneNamesList": [ + "MISSING" + ], + "CompanyAliases": {}, + "References": {} + }, + { + "RSIDsList": [ + 17447439 + ], + "Chromosome": 3, + "Position": 189549423, + "GeneNamesList": [ + "MISSING" + ], + "CompanyAliases": {}, + "References": {} + }, + { + "RSIDsList": [ + 4353811 + ], + "Chromosome": 3, + "Position": 88981207, + "GeneNamesList": [ + "MISSING" + ], + "CompanyAliases": {}, + "References": {} + }, + { + "RSIDsList": [ + 7628370 + ], + "Chromosome": 3, + "Position": 59370600, + "GeneNamesList": [ + "MISSING" + ], + "CompanyAliases": {}, + "References": {} + }, + { + "RSIDsList": [ + 2034127 + ], + "Chromosome": 3, + "Position": 59368074, + "GeneNamesList": [ + "MISSING" + ], + "CompanyAliases": {}, + "References": {} + }, + { + "RSIDsList": [ + 2168809 + ], + "Chromosome": 3, + "Position": 88377746, + "GeneNamesList": [ + "MISSING" + ], + "CompanyAliases": {}, + "References": {} + }, + { + "RSIDsList": [ + 2034129 + ], + "Chromosome": 3, + "Position": 59368293, + "GeneNamesList": [ + "MISSING" + ], + "CompanyAliases": {}, + "References": {} + }, + { + "RSIDsList": [ + 2034128 + ], + "Chromosome": 3, + "Position": 59368259, + "GeneNamesList": [ + "MISSING" + ], + "CompanyAliases": {}, + "References": {} + }, + { + "RSIDsList": [ + 894883 + ], + "Chromosome": 3, + "Position": 59373255, + "GeneNamesList": [ + "MISSING" + ], + "CompanyAliases": {}, + "References": {} + }, + { + "RSIDsList": [ + 3912104 + ], + "Chromosome": 3, + "Position": 42720996, + "GeneNamesList": [ + "CCDC13" + ], + "CompanyAliases": {}, + "References": {} + }, + { + "RSIDsList": [ + 7617069 + ], + "Chromosome": 3, + "Position": 59384969, + "GeneNamesList": [ + "CFAP20DC-DT" + ], + "CompanyAliases": {}, + "References": {} + }, + { + "RSIDsList": [ + 13098099, + 60851446 + ], + "Chromosome": 3, + "Position": 184621879, + "GeneNamesList": [], + "CompanyAliases": {}, + "References": {} + }, + { + "RSIDsList": [ + 7640340, + 61716056 + ], + "Chromosome": 3, + "Position": 59394285, + "GeneNamesList": [ + "CFAP20DC-DT" + ], + "CompanyAliases": {}, + "References": {} + }, + { + "RSIDsList": [ + 875143, + 61193087 + ], + "Chromosome": 3, + "Position": 59394645, + "GeneNamesList": [ + "CFAP20DC-DT" + ], + "CompanyAliases": {}, + "References": {} + } +] \ No newline at end of file diff --git a/resources/geneticReferences/locusMetadata/LocusMetadata_Chromosome4.json b/resources/geneticReferences/locusMetadata/LocusMetadata_Chromosome4.json new file mode 100644 index 0000000..9ca69ed --- /dev/null +++ b/resources/geneticReferences/locusMetadata/LocusMetadata_Chromosome4.json @@ -0,0 +1,37 @@ +[ + { + "RSIDsList": [ + 6828137 + ], + "Chromosome": 4, + "Position": 90059434, + "GeneNamesList": [ + "MISSING" + ], + "CompanyAliases": {}, + "References": {} + }, + { + "RSIDsList": [ + 141318671 + ], + "Chromosome": 4, + "Position": 58493393, + "GeneNamesList": [], + "CompanyAliases": {}, + "References": {} + }, + { + "RSIDsList": [ + 4521336, + 58489362 + ], + "Chromosome": 4, + "Position": 23937776, + "GeneNamesList": [ + "PPARGC1A" + ], + "CompanyAliases": {}, + "References": {} + } +] \ No newline at end of file diff --git a/resources/geneticReferences/locusMetadata/LocusMetadata_Chromosome5.json b/resources/geneticReferences/locusMetadata/LocusMetadata_Chromosome5.json new file mode 100644 index 0000000..a885a70 --- /dev/null +++ b/resources/geneticReferences/locusMetadata/LocusMetadata_Chromosome5.json @@ -0,0 +1,96 @@ +[ + { + "RSIDsList": [ + 11957757 + ], + "Chromosome": 5, + "Position": 148216187, + "GeneNamesList": [ + "MISSING" + ], + "CompanyAliases": {}, + "References": {} + }, + { + "RSIDsList": [ + 16891982 + ], + "Chromosome": 5, + "Position": 33951693, + "GeneNamesList": [ + "MISSING" + ], + "CompanyAliases": {}, + "References": {} + }, + { + "RSIDsList": [ + 348613 + ], + "Chromosome": 5, + "Position": 40273518, + "GeneNamesList": [ + "MISSING" + ], + "CompanyAliases": {}, + "References": {} + }, + { + "RSIDsList": [ + 6555969 + ], + "Chromosome": 5, + "Position": 171128464, + "GeneNamesList": [ + "MISSING" + ], + "CompanyAliases": {}, + "References": {} + }, + { + "RSIDsList": [ + 26722 + ], + "Chromosome": 5, + "Position": 33963870, + "GeneNamesList": [ + "MISSING" + ], + "CompanyAliases": {}, + "References": {} + }, + { + "RSIDsList": [ + 28777 + ], + "Chromosome": 5, + "Position": 33958959, + "GeneNamesList": [ + "MISSING" + ], + "CompanyAliases": {}, + "References": {} + }, + { + "RSIDsList": [ + 72777200 + ], + "Chromosome": 5, + "Position": 124561295, + "GeneNamesList": [], + "CompanyAliases": {}, + "References": {} + }, + { + "RSIDsList": [ + 62330021 + ], + "Chromosome": 5, + "Position": 311787, + "GeneNamesList": [ + "PDCD6" + ], + "CompanyAliases": {}, + "References": {} + } +] \ No newline at end of file diff --git a/resources/geneticReferences/locusMetadata/LocusMetadata_Chromosome6.json b/resources/geneticReferences/locusMetadata/LocusMetadata_Chromosome6.json new file mode 100644 index 0000000..a305d91 --- /dev/null +++ b/resources/geneticReferences/locusMetadata/LocusMetadata_Chromosome6.json @@ -0,0 +1,66 @@ +[ + { + "RSIDsList": [ + 6918152 + ], + "Chromosome": 6, + "Position": 542159, + "GeneNamesList": [ + "MISSING" + ], + "CompanyAliases": {}, + "References": {} + }, + { + "RSIDsList": [ + 1540771 + ], + "Chromosome": 6, + "Position": 466033, + "GeneNamesList": [ + "MISSING" + ], + "CompanyAliases": {}, + "References": {} + }, + { + "RSIDsList": [ + 12203592 + ], + "Chromosome": 6, + "Position": 396321, + "GeneNamesList": [ + "MISSING" + ], + "CompanyAliases": {}, + "References": {} + }, + { + "RSIDsList": [ + 6910861, + 111318576, + 63129962, + 58859209 + ], + "Chromosome": 6, + "Position": 10537950, + "GeneNamesList": [ + "GCNT2" + ], + "CompanyAliases": {}, + "References": {} + }, + { + "RSIDsList": [ + 341147, + 614213 + ], + "Chromosome": 6, + "Position": 158420693, + "GeneNamesList": [ + "TULP4" + ], + "CompanyAliases": {}, + "References": {} + } +] \ No newline at end of file diff --git a/resources/geneticReferences/locusMetadata/LocusMetadata_Chromosome7.json b/resources/geneticReferences/locusMetadata/LocusMetadata_Chromosome7.json new file mode 100644 index 0000000..0a085a1 --- /dev/null +++ b/resources/geneticReferences/locusMetadata/LocusMetadata_Chromosome7.json @@ -0,0 +1,808 @@ +[ + { + "RSIDsList": [ + 80034486 + ], + "Chromosome": 7, + "Position": 117652877, + "GeneNamesList": [ + "CFTR" + ], + "CompanyAliases": { + "1": [ + "i5012079", + "i4000311" + ] + }, + "References": { + "SNPedia.com - rs80034486": "https://www.snpedia.com/index.php/Rs80034486" + } + }, + { + "RSIDsList": [ + 121908745 + ], + "Chromosome": 7, + "Position": 117559590, + "GeneNamesList": [ + "CFTR" + ], + "CompanyAliases": {}, + "References": { + "SNPedia.com - rs121908745": "https://www.snpedia.com/index.php/Rs121908745" + } + }, + { + "RSIDsList": [ + 74551128 + ], + "Chromosome": 7, + "Position": 117548795, + "GeneNamesList": [ + "CFTR" + ], + "CompanyAliases": { + "1": [ + "i4000291", + "i5006050", + "i5011205" + ] + }, + "References": { + "SNPedia.com - rs74551128": "https://www.snpedia.com/index.php/Rs74551128" + } + }, + { + "RSIDsList": [ + 75096551 + ], + "Chromosome": 7, + "Position": 117606754, + "GeneNamesList": [ + "CFTR" + ], + "CompanyAliases": { + "1": [ + "i5011728", + "i6056297", + "i4000321" + ] + }, + "References": { + "SNPedia.com - rs75096551": "https://www.snpedia.com/index.php/Rs75096551" + } + }, + { + "RSIDsList": [ + 76713772 + ], + "Chromosome": 7, + "Position": 117587738, + "GeneNamesList": [ + "CFTR" + ], + "CompanyAliases": { + "1": [ + "i4000317", + "i5011301", + "i6056292" + ], + "2": [ + "VG07S45090" + ], + "3": [ + "VG07S45090" + ] + }, + "References": { + "SNPedia.com - rs76713772": "https://www.snpedia.com/index.php/Rs76713772" + } + }, + { + "RSIDsList": [ + 121909011 + ], + "Chromosome": 7, + "Position": 117540230, + "GeneNamesList": [ + "CFTR" + ], + "CompanyAliases": { + "1": [ + "i4000296", + "i5006070", + "i5011077" + ] + }, + "References": { + "SNPedia.com - rs121909011": "https://www.snpedia.com/index.php/Rs121909011" + } + }, + { + "RSIDsList": [ + 75961395 + ], + "Chromosome": 7, + "Position": 117509123, + "GeneNamesList": [ + "CFTR" + ], + "CompanyAliases": { + "1": [ + "i4000294" + ], + "2": [ + "VG07S29458" + ], + "3": [ + "VG07S29458" + ] + }, + "References": { + "SNPedia.com - rs75961395": "https://www.snpedia.com/index.php/Rs75961395" + } + }, + { + "RSIDsList": [ + 78655421 + ], + "Chromosome": 7, + "Position": 117530975, + "GeneNamesList": [ + "CFTR" + ], + "CompanyAliases": { + "1": [ + "i5010839", + "i5006049", + "i4000295", + "i5010838", + "i5010837" + ], + "2": [ + "VG07S29628" + ], + "3": [ + "VG07S29628" + ] + }, + "References": { + "SNPedia.com - rs78655421": "https://www.snpedia.com/index.php/Rs78655421" + } + }, + { + "RSIDsList": [ + 75039782 + ], + "Chromosome": 7, + "Position": 117639961, + "GeneNamesList": [ + "CFTR" + ], + "CompanyAliases": { + "1": [ + "i5011981", + "i4000325" + ], + "2": [ + "VG07S52449" + ], + "3": [ + "VG07S52449" + ] + }, + "References": { + "SNPedia.com - rs75039782": "https://www.snpedia.com/index.php/Rs75039782" + } + }, + { + "RSIDsList": [ + 80224560 + ], + "Chromosome": 7, + "Position": 117602868, + "GeneNamesList": [ + "CFTR" + ], + "CompanyAliases": { + "1": [ + "i4000320", + "i5011620" + ] + }, + "References": { + "SNPedia.com - i4000320": "https://www.snpedia.com/index.php/I4000320", + "SNPedia.com - rs80224560": "https://www.snpedia.com/index.php/Rs80224560" + } + }, + { + "RSIDsList": [ + 77188391 + ], + "Chromosome": 7, + "Position": 117534366, + "GeneNamesList": [ + "CFTR" + ], + "CompanyAliases": { + "1": [ + "i4000315", + "i5010951" + ], + "2": [ + "VG07S44986" + ], + "3": [ + "VG07S44986" + ] + }, + "References": { + "SNPedia.com - rs77188391": "https://www.snpedia.com/index.php/Rs77188391" + } + }, + { + "RSIDsList": [ + 74597325 + ], + "Chromosome": 7, + "Position": 117587811, + "GeneNamesList": [ + "CFTR" + ], + "CompanyAliases": { + "1": [ + "i4000306", + "i5006055", + "i5011335", + "i6056294" + ], + "2": [ + "VG07S29297" + ], + "3": [ + "VG07S29297" + ] + }, + "References": { + "SNPedia.com - rs74597325": "https://www.snpedia.com/index.php/Rs74597325" + } + }, + { + "RSIDsList": [ + 121908747 + ], + "Chromosome": 7, + "Position": 117627581, + "GeneNamesList": [ + "CFTR" + ], + "CompanyAliases": { + "1": [ + "i4000322" + ] + }, + "References": { + "SNPedia.com - rs121908747": "https://www.snpedia.com/index.php/Rs121908747" + } + }, + { + "RSIDsList": [ + 113993960, + 199826652 + ], + "Chromosome": 7, + "Position": 117559592, + "GeneNamesList": [ + "CFTR" + ], + "CompanyAliases": { + "1": [ + "i3000001", + "i5011261" + ] + }, + "References": { + "SNPedia.com - rs113993960": "https://www.snpedia.com/index.php/Rs113993960" + } + }, + { + "RSIDsList": [ + 77932196 + ], + "Chromosome": 7, + "Position": 117540270, + "GeneNamesList": [ + "CFTR" + ], + "CompanyAliases": { + "1": [ + "i4000297", + "i5011094", + "i5011095" + ] + }, + "References": { + "SNPedia.com - rs77932196": "https://www.snpedia.com/index.php/Rs77932196" + } + }, + { + "RSIDsList": [ + 121908748 + ], + "Chromosome": 7, + "Position": 117590440, + "GeneNamesList": [ + "CFTR" + ], + "CompanyAliases": { + "1": [ + "i4000318", + "i5006139", + "i5011416", + "i5011417", + "i5011418" + ] + }, + "References": { + "SNPedia.com - rs121908748": "https://www.snpedia.com/index.php/Rs121908748" + } + }, + { + "RSIDsList": [ + 113993959 + ], + "Chromosome": 7, + "Position": 117587778, + "GeneNamesList": [ + "CFTR" + ], + "CompanyAliases": { + "1": [ + "i4000300", + "i5006109", + "i5011314" + ] + }, + "References": { + "SNPedia.com - rs113993959": "https://www.snpedia.com/index.php/Rs113993959" + } + }, + { + "RSIDsList": [ + 74767530 + ], + "Chromosome": 7, + "Position": 117627537, + "GeneNamesList": [ + "CFTR" + ], + "CompanyAliases": { + "1": [ + "i5011932", + "i4000308", + "i6056298" + ], + "2": [ + "VG07S29424" + ], + "3": [ + "VG07S29424" + ] + }, + "References": { + "SNPedia.com - rs74767530": "https://www.snpedia.com/index.php/Rs74767530" + } + }, + { + "RSIDsList": [ + 77010898 + ], + "Chromosome": 7, + "Position": 117642566, + "GeneNamesList": [ + "CFTR" + ], + "CompanyAliases": { + "1": [ + "i4000309", + "i5012037", + "i6056299" + ], + "2": [ + "VG07S29451" + ], + "3": [ + "VG07S29451" + ] + }, + "References": { + "SNPedia.com - rs77010898": "https://www.snpedia.com/index.php/Rs77010898" + } + }, + { + "RSIDsList": [ + 121908746 + ], + "Chromosome": 7, + "Position": 117592219, + "GeneNamesList": [ + "CFTR" + ], + "CompanyAliases": {}, + "References": { + "SNPedia.com - rs121908746": "https://www.snpedia.com/index.php/Rs121908746" + } + }, + { + "RSIDsList": [ + 75527207 + ], + "Chromosome": 7, + "Position": 117587806, + "GeneNamesList": [ + "CFTR" + ], + "CompanyAliases": { + "1": [ + "i4000305", + "i5006054", + "i5011331" + ], + "2": [ + "VG07S29293" + ], + "3": [ + "VG07S29293" + ] + }, + "References": { + "SNPedia.com - rs75527207": "https://www.snpedia.com/index.php/Rs75527207" + } + }, + { + "RSIDsList": [ + 78756941 + ], + "Chromosome": 7, + "Position": 117531115, + "GeneNamesList": [ + "CFTR" + ], + "CompanyAliases": { + "1": [ + "i4000314", + "i5010909", + "i6056291" + ], + "2": [ + "VG07S44961" + ], + "3": [ + "VG07S44961" + ] + }, + "References": { + "SNPedia.com - rs78756941": "https://www.snpedia.com/index.php/Rs78756941" + } + }, + { + "RSIDsList": [ + 80055610 + ], + "Chromosome": 7, + "Position": 117587833, + "GeneNamesList": [ + "CFTR" + ], + "CompanyAliases": { + "1": [ + "i4000307", + "i5011358", + "i5011359" + ] + }, + "References": { + "SNPedia.com - rs80055610": "https://www.snpedia.com/index.php/Rs80055610" + } + }, + { + "RSIDsList": [ + 6944702 + ], + "Chromosome": 7, + "Position": 83653553, + "GeneNamesList": [ + "MISSING" + ], + "CompanyAliases": {}, + "References": {} + }, + { + "RSIDsList": [ + 6462562 + ], + "Chromosome": 7, + "Position": 4088555, + "GeneNamesList": [ + "MISSING" + ], + "CompanyAliases": {}, + "References": {} + }, + { + "RSIDsList": [ + 2854746 + ], + "Chromosome": 7, + "Position": 45960645, + "GeneNamesList": [ + "MISSING" + ], + "CompanyAliases": {}, + "References": {} + }, + { + "RSIDsList": [ + 6462544 + ], + "Chromosome": 7, + "Position": 4077620, + "GeneNamesList": [ + "MISSING" + ], + "CompanyAliases": {}, + "References": {} + }, + { + "RSIDsList": [ + 10237838 + ], + "Chromosome": 7, + "Position": 4073998, + "GeneNamesList": [ + "MISSING" + ], + "CompanyAliases": {}, + "References": {} + }, + { + "RSIDsList": [ + 12155314 + ], + "Chromosome": 7, + "Position": 4081194, + "GeneNamesList": [ + "MISSING" + ], + "CompanyAliases": {}, + "References": {} + }, + { + "RSIDsList": [ + 10266101 + ], + "Chromosome": 7, + "Position": 4073819, + "GeneNamesList": [ + "MISSING" + ], + "CompanyAliases": {}, + "References": {} + }, + { + "RSIDsList": [ + 2108166 + ], + "Chromosome": 7, + "Position": 42125871, + "GeneNamesList": [ + "MISSING" + ], + "CompanyAliases": {}, + "References": {} + }, + { + "RSIDsList": [ + 10485860 + ], + "Chromosome": 7, + "Position": 4090283, + "GeneNamesList": [ + "MISSING" + ], + "CompanyAliases": {}, + "References": {} + }, + { + "RSIDsList": [ + 9692219 + ], + "Chromosome": 7, + "Position": 4043701, + "GeneNamesList": [ + "SDK1" + ], + "CompanyAliases": {}, + "References": {} + }, + { + "RSIDsList": [ + 7803030 + ], + "Chromosome": 7, + "Position": 4038558, + "GeneNamesList": [ + "SDK1" + ], + "CompanyAliases": {}, + "References": {} + }, + { + "RSIDsList": [ + 2342494 + ], + "Chromosome": 7, + "Position": 4032591, + "GeneNamesList": [ + "SDK1" + ], + "CompanyAliases": {}, + "References": {} + }, + { + "RSIDsList": [ + 6950754 + ], + "Chromosome": 7, + "Position": 4037491, + "GeneNamesList": [ + "SDK1" + ], + "CompanyAliases": {}, + "References": {} + }, + { + "RSIDsList": [ + 7807181 + ], + "Chromosome": 7, + "Position": 4046812, + "GeneNamesList": [ + "SDK1" + ], + "CompanyAliases": {}, + "References": {} + }, + { + "RSIDsList": [ + 10278187, + 57744561 + ], + "Chromosome": 7, + "Position": 4034741, + "GeneNamesList": [ + "SDK1" + ], + "CompanyAliases": {}, + "References": {} + }, + { + "RSIDsList": [ + 1562005, + 58720272 + ], + "Chromosome": 7, + "Position": 4044191, + "GeneNamesList": [ + "SDK1" + ], + "CompanyAliases": {}, + "References": {} + }, + { + "RSIDsList": [ + 7781059, + 57876852, + 10351382 + ], + "Chromosome": 7, + "Position": 4046687, + "GeneNamesList": [ + "SDK1" + ], + "CompanyAliases": {}, + "References": {} + }, + { + "RSIDsList": [ + 10237319 + ], + "Chromosome": 7, + "Position": 4033969, + "GeneNamesList": [ + "SDK1" + ], + "CompanyAliases": {}, + "References": {} + }, + { + "RSIDsList": [ + 10237488, + 59841339 + ], + "Chromosome": 7, + "Position": 4034710, + "GeneNamesList": [ + "SDK1" + ], + "CompanyAliases": {}, + "References": {} + }, + { + "RSIDsList": [ + 10234405, + 58770991 + ], + "Chromosome": 7, + "Position": 4034827, + "GeneNamesList": [ + "SDK1" + ], + "CompanyAliases": {}, + "References": {} + }, + { + "RSIDsList": [ + 1562006, + 59417113 + ], + "Chromosome": 7, + "Position": 4043872, + "GeneNamesList": [ + "SDK1" + ], + "CompanyAliases": {}, + "References": {} + }, + { + "RSIDsList": [ + 7779616, + 56955120, + 17293919, + 10377747 + ], + "Chromosome": 7, + "Position": 4046408, + "GeneNamesList": [ + "SDK1" + ], + "CompanyAliases": {}, + "References": {} + }, + { + "RSIDsList": [ + 10265937 + ], + "Chromosome": 7, + "Position": 4034017, + "GeneNamesList": [ + "SDK1" + ], + "CompanyAliases": {}, + "References": {} + }, + { + "RSIDsList": [ + 7799331, + 58503650, + 10365277 + ], + "Chromosome": 7, + "Position": 4046491, + "GeneNamesList": [ + "SDK1" + ], + "CompanyAliases": {}, + "References": {} + } +] \ No newline at end of file diff --git a/resources/geneticReferences/locusMetadata/LocusMetadata_Chromosome8.json b/resources/geneticReferences/locusMetadata/LocusMetadata_Chromosome8.json new file mode 100644 index 0000000..acf51d5 --- /dev/null +++ b/resources/geneticReferences/locusMetadata/LocusMetadata_Chromosome8.json @@ -0,0 +1,36 @@ +[ + { + "RSIDsList": [ + 147068120 + ], + "Chromosome": 8, + "Position": 81350433, + "GeneNamesList": [ + "MISSING" + ], + "CompanyAliases": {}, + "References": {} + }, + { + "RSIDsList": [ + 12543326 + ], + "Chromosome": 8, + "Position": 42003663, + "GeneNamesList": [ + "MISSING" + ], + "CompanyAliases": {}, + "References": {} + }, + { + "RSIDsList": [ + 6997494 + ], + "Chromosome": 8, + "Position": 12833488, + "GeneNamesList": [], + "CompanyAliases": {}, + "References": {} + } +] \ No newline at end of file diff --git a/resources/geneticReferences/locusMetadata/LocusMetadata_Chromosome9.json b/resources/geneticReferences/locusMetadata/LocusMetadata_Chromosome9.json new file mode 100644 index 0000000..48fee30 --- /dev/null +++ b/resources/geneticReferences/locusMetadata/LocusMetadata_Chromosome9.json @@ -0,0 +1,108 @@ +[ + { + "RSIDsList": [ + 12552712 + ], + "Chromosome": 9, + "Position": 27366436, + "GeneNamesList": [ + "MISSING" + ], + "CompanyAliases": {}, + "References": {} + }, + { + "RSIDsList": [ + 6478394 + ], + "Chromosome": 9, + "Position": 121836674, + "GeneNamesList": [ + "MISSING" + ], + "CompanyAliases": {}, + "References": {} + }, + { + "RSIDsList": [ + 1158810 + ], + "Chromosome": 9, + "Position": 121809519, + "GeneNamesList": [ + "MISSING" + ], + "CompanyAliases": {}, + "References": {} + }, + { + "RSIDsList": [ + 13297008 + ], + "Chromosome": 9, + "Position": 12677471, + "GeneNamesList": [ + "MISSING" + ], + "CompanyAliases": {}, + "References": {} + }, + { + "RSIDsList": [ + 2762462 + ], + "Chromosome": 9, + "Position": 12699776, + "GeneNamesList": [ + "MISSING" + ], + "CompanyAliases": {}, + "References": {} + }, + { + "RSIDsList": [ + 1408799 + ], + "Chromosome": 9, + "Position": 12672097, + "GeneNamesList": [ + "MISSING" + ], + "CompanyAliases": {}, + "References": {} + }, + { + "RSIDsList": [ + 1325127 + ], + "Chromosome": 9, + "Position": 12668328, + "GeneNamesList": [ + "MISSING" + ], + "CompanyAliases": {}, + "References": {} + }, + { + "RSIDsList": [ + 2733832 + ], + "Chromosome": 9, + "Position": 12704725, + "GeneNamesList": [ + "MISSING" + ], + "CompanyAliases": {}, + "References": {} + }, + { + "RSIDsList": [ + 12335410 + ], + "Chromosome": 9, + "Position": 129238777, + "GeneNamesList": [], + "CompanyAliases": {}, + "References": {} + } +] \ No newline at end of file diff --git a/resources/geneticReferences/locusMetadata/locusMetadata.go b/resources/geneticReferences/locusMetadata/locusMetadata.go new file mode 100644 index 0000000..cee9900 --- /dev/null +++ b/resources/geneticReferences/locusMetadata/locusMetadata.go @@ -0,0 +1,408 @@ + +// locusMetadata provides information about gene locations. + +package locusMetadata + +// Locus position information should correspond to Human genome reference build 38. + +import "seekia/internal/helpers" + +import _ "embed" + +import "encoding/json" +import "errors" + + +//go:embed LocusMetadata_Chromosome1.json +var LocusMetadataFile_Chromosome1 []byte + +//go:embed LocusMetadata_Chromosome2.json +var LocusMetadataFile_Chromosome2 []byte + +//go:embed LocusMetadata_Chromosome3.json +var LocusMetadataFile_Chromosome3 []byte + +//go:embed LocusMetadata_Chromosome4.json +var LocusMetadataFile_Chromosome4 []byte + +//go:embed LocusMetadata_Chromosome5.json +var LocusMetadataFile_Chromosome5 []byte + +//go:embed LocusMetadata_Chromosome6.json +var LocusMetadataFile_Chromosome6 []byte + +//go:embed LocusMetadata_Chromosome7.json +var LocusMetadataFile_Chromosome7 []byte + +//go:embed LocusMetadata_Chromosome8.json +var LocusMetadataFile_Chromosome8 []byte + +//go:embed LocusMetadata_Chromosome9.json +var LocusMetadataFile_Chromosome9 []byte + +//go:embed LocusMetadata_Chromosome10.json +var LocusMetadataFile_Chromosome10 []byte + +//go:embed LocusMetadata_Chromosome11.json +var LocusMetadataFile_Chromosome11 []byte + +//go:embed LocusMetadata_Chromosome12.json +var LocusMetadataFile_Chromosome12 []byte + +//go:embed LocusMetadata_Chromosome13.json +var LocusMetadataFile_Chromosome13 []byte + +//go:embed LocusMetadata_Chromosome14.json +var LocusMetadataFile_Chromosome14 []byte + +//go:embed LocusMetadata_Chromosome15.json +var LocusMetadataFile_Chromosome15 []byte + +//go:embed LocusMetadata_Chromosome16.json +var LocusMetadataFile_Chromosome16 []byte + +//go:embed LocusMetadata_Chromosome17.json +var LocusMetadataFile_Chromosome17 []byte + +//go:embed LocusMetadata_Chromosome19.json +var LocusMetadataFile_Chromosome19 []byte + +//go:embed LocusMetadata_Chromosome20.json +var LocusMetadataFile_Chromosome20 []byte + +//go:embed LocusMetadata_Chromosome21.json +var LocusMetadataFile_Chromosome21 []byte + +//go:embed LocusMetadata_Chromosome22.json +var LocusMetadataFile_Chromosome22 []byte + + +type LocusMetadata struct{ + + // A list of RSIDs that refer to this location + // Each RSID is equivalent and refers to the same location + // rsID stands for Reference SNP cluster ID. + // Each rsID is an "rs" followed by a number. + // We store the number after the rs as an int64. + RSIDsList []int64 + + // The chromosome which this location exists on + Chromosome int + + // The position of this locus + // This is a number describing its location on the chromosome it exists on. + Position int + + // A list of gene names which refer to the gene which this locus belongs to. + // Each gene name refers to the same gene. + // Will be a list containing "MISSING" if the gene name has not been added yet + // Will be an empty list if no gene exists + GeneNamesList []string + + // A list of alternate names for the rsid used by companies + // These are the names that the raw genome files exported from companies sometimes use instead of rsIDs + // Example: TwentyThreeAndMe -> []string{"i5010839", "i5006049", "i4000295", "i5010838", "i5010837"} + CompanyAliases map[GeneticsCompany][]string + + // Reference name -> Reference link + References map[string]string +} + +// We use this data structure to save space, rather than using String +type GeneticsCompany byte + +const TwentyThreeAndMe GeneticsCompany = 1 +const FamilyTreeDNA GeneticsCompany = 2 +const MyHeritage GeneticsCompany = 3 + +// Map Structure: RSID -> LocusMetadata object +var lociMetadataMap map[int64]LocusMetadata + +// This map stores a list of aliases for rsids which have aliases +// An alias is a different rsid which represents the same locus +var rsidAliasesMap map[int64][]int64 + +// We use these maps to store the locus aliases for rsIDs used by companies +// Map structure: Alias -> Primary rsID (there may be aliases) +// Example: "i5010839" -> 78655421 +var companyAliasesMap_23andMe map[string]int64 +var companyAliasesMap_FamilyTreeDNA map[string]int64 +var companyAliasesMap_MyHeritage map[string]int64 + + +func InitializeLocusMetadataVariables()error{ + + lociMetadataMap = make(map[int64]LocusMetadata) + rsidAliasesMap = make(map[int64][]int64) + + companyAliasesMap_23andMe = make(map[string]int64) + companyAliasesMap_FamilyTreeDNA = make(map[string]int64) + companyAliasesMap_MyHeritage = make(map[string]int64) + + locusObjectsList, err := GetLocusMetadataObjectsList() + if (err != nil) { return err } + + for _, locusObject := range locusObjectsList{ + + rsidsList := locusObject.RSIDsList + + for _, rsid := range rsidsList{ + + _, exists := lociMetadataMap[rsid] + if (exists == true){ + return errors.New("lociMetadataMap contains duplicate rsid.") + } + + lociMetadataMap[rsid] = locusObject + } + + if (len(rsidsList) > 1){ + + // We add rsid aliases to map + + for _, rsid := range rsidsList{ + + rsidAliasesList := make([]int64, 0) + + for _, rsidInner := range rsidsList{ + + if (rsid != rsidInner){ + rsidAliasesList = append(rsidAliasesList, rsidInner) + } + } + + rsidAliasesMap[rsid] = rsidAliasesList + } + } + + companyAliasesMap := locusObject.CompanyAliases + + if (len(companyAliasesMap) > 0){ + + // Now we add company aliases to maps + + primaryRSID := rsidsList[0] + + for companyObject, companyAliasesList := range companyAliasesMap{ + + if (companyObject == TwentyThreeAndMe){ + + for _, locusAlias := range companyAliasesList{ + companyAliasesMap_23andMe[locusAlias] = primaryRSID + } + + } else if (companyObject == FamilyTreeDNA){ + + for _, locusAlias := range companyAliasesList{ + companyAliasesMap_FamilyTreeDNA[locusAlias] = primaryRSID + } + + } else if (companyObject == MyHeritage){ + + for _, locusAlias := range companyAliasesList{ + companyAliasesMap_MyHeritage[locusAlias] = primaryRSID + } + + } else { + companyByteString := helpers.ConvertIntToString(int(companyObject)) + return errors.New("Locus Object company aliases map contains invalid company object: " + companyByteString) + } + } + } + } + + return nil +} + +//Outputs: +// -bool: Locus metadata exists +// -LocusMetadata +// -error +func GetLocusMetadata(inputRSID int64)(bool, LocusMetadata, error){ + + if (lociMetadataMap == nil){ + return false, LocusMetadata{}, errors.New("GetLocusMetadata called when lociMetadataMap is not initialized.") + } + + locusMetadataObject, exists := lociMetadataMap[inputRSID] + if (exists == false){ + return false, LocusMetadata{}, nil + } + + return true, locusMetadataObject, nil +} + +// This function will return a list of RSIDs which refer to the same location as the input RSID +// -bool: Any Aliases exist +// -[]int64: List of alias RSIDs +// -error (if RSID is unknown) +func GetRSIDAliases(inputRSID int64)(bool, []int64, error){ + + if (rsidAliasesMap == nil){ + return false, nil, errors.New("rsidAliasesMap called when rsidAliasesMap is not initialized.") + } + + aliasesList, exists := rsidAliasesMap[inputRSID] + if (exists == false){ + return false, nil, nil + } + + return true, aliasesList, nil +} + + +//Outputs: +// -bool: Alias found +// -int64: Primary rsID alias to use to represent this locus +// -error +func GetCompanyAliasRSID(companyName string, locusAlias string)(bool, int64, error){ + + if (companyName == "23andMe"){ + + locusRSID, exists := companyAliasesMap_23andMe[locusAlias] + if (exists == false){ + return false, 0, nil + } + + return true, locusRSID, nil + + } else if (companyName == "FamilyTreeDNA"){ + + locusRSID, exists := companyAliasesMap_FamilyTreeDNA[locusAlias] + if (exists == false){ + return false, 0, nil + } + + return true, locusRSID, nil + + } else if (companyName == "MyHeritage"){ + + locusRSID, exists := companyAliasesMap_MyHeritage[locusAlias] + if (exists == false){ + return false, 0, nil + } + + return true, locusRSID, nil + } + + return false, 0, errors.New("GetCompanyAliasRSID called with invalid companyName: " + companyName) +} + + +// This function is only public for use in testing +func GetLocusMetadataObjectsList()([]LocusMetadata, error){ + + chromosomesList := []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22} + + locusMetadataObjectsList := make([]LocusMetadata, 0, len(chromosomesList)) + + for _, chromosomesInt := range chromosomesList{ + + chromosomeLocusMetadataObjectsList, err := GetLocusMetadataObjectsListByChromosome(chromosomesInt) + if (err != nil){ return nil, err } + + locusMetadataObjectsList = append(locusMetadataObjectsList, chromosomeLocusMetadataObjectsList...) + } + + return locusMetadataObjectsList, nil +} + + +func GetLocusMetadataObjectsListByChromosome(chromosome int)([]LocusMetadata, error){ + + if (chromosome < 1 || chromosome > 22){ + chromosomeString := helpers.ConvertIntToString(chromosome) + return nil, errors.New("GetLocusMetadataObjectsListByChromosome called with invalid chromosome: " + chromosomeString) + } + + // Outputs: + // -bool: File exists + // -[]byte: File bytes + getFileBytes := func()(bool, []byte){ + + if (chromosome == 1){ + return true, LocusMetadataFile_Chromosome1 + } + if (chromosome == 2){ + return true, LocusMetadataFile_Chromosome2 + } + if (chromosome == 3){ + return true, LocusMetadataFile_Chromosome3 + } + if (chromosome == 4){ + return true, LocusMetadataFile_Chromosome4 + } + if (chromosome == 5){ + return true, LocusMetadataFile_Chromosome5 + } + if (chromosome == 6){ + return true, LocusMetadataFile_Chromosome6 + } + if (chromosome == 7){ + return true, LocusMetadataFile_Chromosome7 + } + if (chromosome == 8){ + return true, LocusMetadataFile_Chromosome8 + } + if (chromosome == 9){ + return true, LocusMetadataFile_Chromosome9 + } + if (chromosome == 10){ + return true, LocusMetadataFile_Chromosome10 + } + if (chromosome == 11){ + return true, LocusMetadataFile_Chromosome11 + } + if (chromosome == 12){ + return true, LocusMetadataFile_Chromosome12 + } + if (chromosome == 13){ + return true, LocusMetadataFile_Chromosome13 + } + if (chromosome == 14){ + return true, LocusMetadataFile_Chromosome14 + } + if (chromosome == 15){ + return true, LocusMetadataFile_Chromosome15 + } + if (chromosome == 16){ + return true, LocusMetadataFile_Chromosome16 + } + if (chromosome == 17){ + return true, LocusMetadataFile_Chromosome17 + } + //if (chromosome == 18){ + // return true, LocusMetadataFile_Chromosome18 + //} + if (chromosome == 19){ + return true, LocusMetadataFile_Chromosome19 + } + if (chromosome == 20){ + return true, LocusMetadataFile_Chromosome20 + } + if (chromosome == 21){ + return true, LocusMetadataFile_Chromosome21 + } + if (chromosome == 22){ + return true, LocusMetadataFile_Chromosome22 + } + return false, nil + } + + fileExists, fileBytes := getFileBytes() + if (fileExists == false){ + // No loci exist for this chromosome + emptyList := make([]LocusMetadata, 0) + return emptyList, nil + } + + var locusMetadataObjectsList []LocusMetadata + + err := json.Unmarshal(fileBytes, &locusMetadataObjectsList) + if (err != nil) { return nil, err } + + return locusMetadataObjectsList, nil +} + + + diff --git a/resources/geneticReferences/monogenicDiseases/cysticFibrosis.go b/resources/geneticReferences/monogenicDiseases/cysticFibrosis.go new file mode 100644 index 0000000..5b955d9 --- /dev/null +++ b/resources/geneticReferences/monogenicDiseases/cysticFibrosis.go @@ -0,0 +1,522 @@ + +package monogenicDiseases + +func getCysticFibrosisDiseaseObject()MonogenicDisease{ + + variant1_ReferencesMap := make(map[string]string) + variant1_ReferencesMap["SNPedia.com - rs113993960"] = "https://www.snpedia.com/index.php/Rs113993960" + + variant1_Object := DiseaseVariant{ + + VariantIdentifier: "36965d", + VariantNames: []string{"deltaF508", "F508del"}, + NucleotideChange: "c.1521_1523delCTT", + AminoAcidChange: "p.Phe508del", + VariantRSID: 113993960, + HealthyBase: "I", //CTT + DefectiveBase: "D", + EffectIsMild: false, + References: variant1_ReferencesMap, + } + + variant2_ReferencesMap := make(map[string]string) + variant2_ReferencesMap["SNPedia.com - rs113993959"] = "https://www.snpedia.com/index.php/Rs113993959" + + variant2_Object := DiseaseVariant{ + + VariantIdentifier: "5706b0", + VariantNames: []string{"G542X"}, + NucleotideChange: "c.1624G>T", + AminoAcidChange: "p.Gly542Ter", + VariantRSID: 113993959, + HealthyBase: "G", + DefectiveBase: "T", + EffectIsMild: false, + References: variant2_ReferencesMap, + } + + variant3_ReferencesMap := make(map[string]string) + variant3_ReferencesMap["SNPedia.com - rs77010898"] = "https://www.snpedia.com/index.php/Rs77010898" + + variant3_Object := DiseaseVariant{ + + VariantIdentifier: "01a2a0", + VariantNames: []string{"W1282X"}, + NucleotideChange: "c.3846G>A", + AminoAcidChange: "p.Trp1282Ter", + VariantRSID: 77010898, + HealthyBase: "G", + DefectiveBase: "A", + EffectIsMild: false, + References: variant3_ReferencesMap, + } + + variant4_ReferencesMap := make(map[string]string) + variant4_ReferencesMap["SNPedia.com - rs75527207"] = "https://www.snpedia.com/index.php/Rs75527207" + + variant4_Object := DiseaseVariant{ + + VariantIdentifier: "bd4106", + VariantNames: []string{"G551D"}, + NucleotideChange: "c.1652G>A", + AminoAcidChange: "p.Gly551Asp", + VariantRSID: 75527207, + HealthyBase: "G", + DefectiveBase: "A", + EffectIsMild: false, + References: variant4_ReferencesMap, + } + + variant5_ReferencesMap := make(map[string]string) + variant5_ReferencesMap["SNPedia.com - rs78756941"] = "https://www.snpedia.com/index.php/Rs78756941" + + variant5_Object := DiseaseVariant{ + + VariantIdentifier: "4d6f38", + VariantNames: []string{"621+1G>T"}, + NucleotideChange: "c.489+1G>T", + AminoAcidChange: "", + VariantRSID: 78756941, + HealthyBase: "G", + DefectiveBase: "T", + EffectIsMild: false, + References: variant5_ReferencesMap, + } + + variant6_ReferencesMap := make(map[string]string) + variant6_ReferencesMap["SNPedia.com - rs80034486"] = "https://www.snpedia.com/index.php/Rs80034486" + + variant6_Object := DiseaseVariant{ + + VariantIdentifier: "c6135a", + VariantNames: []string{"N1303K"}, + NucleotideChange: "c.3909C>G", + AminoAcidChange: "p.Asn1303Lys", + VariantRSID: 80034486, + HealthyBase: "C", + DefectiveBase: "G", + EffectIsMild: false, + References: variant6_ReferencesMap, + } + + variant7_ReferencesMap := make(map[string]string) + variant7_ReferencesMap["SNPedia.com - rs74597325"] = "https://www.snpedia.com/index.php/Rs74597325" + + variant7_Object := DiseaseVariant{ + + VariantIdentifier: "88a7f4", + VariantNames: []string{"R553X"}, + NucleotideChange: "c.1657C>T", + AminoAcidChange: "p.Arg553Ter", + VariantRSID: 74597325, + HealthyBase: "C", + DefectiveBase: "T", + EffectIsMild: false, + References: variant7_ReferencesMap, + } + + variant8_ReferencesMap := make(map[string]string) + variant8_ReferencesMap["SNPedia.com - rs121908745"] = "https://www.snpedia.com/index.php/Rs121908745" + + variant8_Object := DiseaseVariant{ + + VariantIdentifier: "058a4d", + VariantNames: []string{"Delta I507", "I507del"}, + NucleotideChange: "c.1519_1521delATC", + AminoAcidChange: "p.Ile507del", + VariantRSID: 121908745, + HealthyBase: "I", //ATC + DefectiveBase: "D", + EffectIsMild: false, + References: variant8_ReferencesMap, + } + + variant9_ReferencesMap := make(map[string]string) + variant9_ReferencesMap["SNPedia.com - rs75039782"] = "https://www.snpedia.com/index.php/Rs75039782" + + variant9_Object := DiseaseVariant{ + + VariantIdentifier: "2a4ddf", + VariantNames: []string{"3849+10kbC>T"}, + NucleotideChange: "c.3718-2477C>T", + AminoAcidChange: "", + VariantRSID: 75039782, + HealthyBase: "C", + DefectiveBase: "T", + EffectIsMild: false, + References: variant9_ReferencesMap, + } + + variant10_ReferencesMap := make(map[string]string) + variant10_ReferencesMap["SNPedia.com - rs75096551"] = "https://www.snpedia.com/index.php/Rs75096551" + + variant10_Object := DiseaseVariant{ + + VariantIdentifier: "e1bcfb", + VariantNames: []string{"3120+1G>A"}, + NucleotideChange: "c.2988+1G>A", + AminoAcidChange: "", + VariantRSID: 75096551, + HealthyBase: "G", + DefectiveBase: "A", + EffectIsMild: false, + References: variant10_ReferencesMap, + } + + variant11_ReferencesMap := make(map[string]string) + variant11_ReferencesMap["SNPedia.com - rs75096551"] = "https://www.snpedia.com/index.php/Rs75096551" + + variant11_Object := DiseaseVariant{ + + VariantIdentifier: "c28795", + VariantNames: []string{"3120+1G>T"}, + NucleotideChange: "c.2988+1G>T", + AminoAcidChange: "", + VariantRSID: 75096551, + HealthyBase: "G", + DefectiveBase: "T", + EffectIsMild: false, + References: variant11_ReferencesMap, + } + + variant12_ReferencesMap := make(map[string]string) + variant12_ReferencesMap["SNPedia.com - rs78655421"] = "https://www.snpedia.com/index.php/Rs78655421" + + variant12_Object := DiseaseVariant{ + + VariantIdentifier: "f1965c", + VariantNames: []string{"R117H"}, + NucleotideChange: "c.350G>A", + AminoAcidChange: "p.Arg117His", + VariantRSID: 78655421, + HealthyBase: "G", + DefectiveBase: "A", + EffectIsMild: true, + References: variant12_ReferencesMap, + } + + variant13_ReferencesMap := make(map[string]string) + variant13_ReferencesMap["SNPedia.com - rs76713772"] = "https://www.snpedia.com/index.php/Rs76713772" + + variant13_Object := DiseaseVariant{ + + VariantIdentifier: "3420c1", + VariantNames: []string{"1717-1G>A"}, + NucleotideChange: "c.1585-1G>A", + AminoAcidChange: "", + VariantRSID: 76713772, + HealthyBase: "G", + DefectiveBase: "A", + EffectIsMild: false, + References: variant13_ReferencesMap, + } + + variant14_ReferencesMap := make(map[string]string) + variant14_ReferencesMap["SNPedia.com - rs76713772"] = "https://www.snpedia.com/index.php/Rs76713772" + + variant14_Object := DiseaseVariant{ + + VariantIdentifier: "25d0b4", + VariantNames: []string{"1717-1G>T"}, + NucleotideChange: "c.1585-1G>T", + AminoAcidChange: "", + VariantRSID: 76713772, + HealthyBase: "G", + DefectiveBase: "T", + EffectIsMild: false, + References: variant14_ReferencesMap, + } + + variant15_ReferencesMap := make(map[string]string) + variant15_ReferencesMap["SNPedia.com - rs80224560"] = "https://www.snpedia.com/index.php/Rs80224560" + + variant15_Object := DiseaseVariant{ + + VariantIdentifier: "139ab2", + VariantNames: []string{"2789+5G>A"}, + NucleotideChange: "c.2657+5G>A", + AminoAcidChange: "", + VariantRSID: 80224560, + HealthyBase: "G", + DefectiveBase: "A", + EffectIsMild: true, + References: variant15_ReferencesMap, + } + + variant16_ReferencesMap := make(map[string]string) + variant16_ReferencesMap["SNPedia.com - rs77932196"] = "https://www.snpedia.com/index.php/Rs77932196" + + variant16_Object := DiseaseVariant{ + + VariantIdentifier: "f7a12e", + VariantNames: []string{"R347P"}, + NucleotideChange: "c.1040G>T", + AminoAcidChange: "p.Arg347Pro", + VariantRSID: 77932196, + HealthyBase: "G", + DefectiveBase: "T", + EffectIsMild: false, + References: variant16_ReferencesMap, + } + + variant17_ReferencesMap := make(map[string]string) + variant17_ReferencesMap["SNPedia.com - rs77932196"] = "https://www.snpedia.com/index.php/Rs77932196" + + variant17_Object := DiseaseVariant{ + + VariantIdentifier: "deb2e2", + VariantNames: []string{"R347"}, + NucleotideChange: "c.1040G>C", + AminoAcidChange: "", + VariantRSID: 77932196, + HealthyBase: "G", + DefectiveBase: "C", + EffectIsMild: false, + References: variant17_ReferencesMap, + } + + variant18_ReferencesMap := make(map[string]string) + variant18_ReferencesMap["SNPedia.com - rs77932196"] = "https://www.snpedia.com/index.php/Rs77932196" + + variant18_Object := DiseaseVariant{ + + VariantIdentifier: "884cf0", + VariantNames: []string{"R347H"}, + NucleotideChange: "c.1040G>A", + AminoAcidChange: "p.Arg347His", + VariantRSID: 77932196, + HealthyBase: "G", + DefectiveBase: "A", + EffectIsMild: false, + References: variant18_ReferencesMap, + } + + variant19_ReferencesMap := make(map[string]string) + variant19_ReferencesMap["SNPedia.com - rs77932196"] = "https://www.snpedia.com/index.php/Rs77932196" + + variant19_Object := DiseaseVariant{ + + VariantIdentifier: "b9fad1", + VariantNames: []string{"R347P"}, + NucleotideChange: "c.1040G>T", + AminoAcidChange: "p.Arg347Pro", + VariantRSID: 77932196, + HealthyBase: "G", + DefectiveBase: "T", + EffectIsMild: false, + References: variant19_ReferencesMap, + } + + variant20_ReferencesMap := make(map[string]string) + variant20_ReferencesMap["SNPedia.com - rs77188391"] = "https://www.snpedia.com/index.php/Rs77188391" + + variant20_Object := DiseaseVariant{ + + VariantIdentifier: "f2448d", + VariantNames: []string{"711+1G>T"}, + NucleotideChange: "c.579+1G>T", + AminoAcidChange: "", + VariantRSID: 77188391, + HealthyBase: "G", + DefectiveBase: "T", + EffectIsMild: false, + References: variant20_ReferencesMap, + } + + variant21_ReferencesMap := make(map[string]string) + variant21_ReferencesMap["SNPedia.com - rs121909011"] = "https://www.snpedia.com/index.php/Rs121909011" + + variant21_Object := DiseaseVariant{ + + VariantIdentifier: "09b96f", + VariantNames: []string{"R334W"}, + NucleotideChange: "c.1000C>T", + AminoAcidChange: "p.Arg334Trp", + VariantRSID: 121909011, + HealthyBase: "C", + DefectiveBase: "T", + EffectIsMild: false, + References: variant21_ReferencesMap, + } + + variant22_ReferencesMap := make(map[string]string) + variant22_ReferencesMap["SNPedia.com - rs80055610"] = "https://www.snpedia.com/index.php/Rs80055610" + + variant22_Object := DiseaseVariant{ + + VariantIdentifier: "2f8651", + VariantNames: []string{"R560T"}, + NucleotideChange: "c.1679G>C", + AminoAcidChange: "p.Arg560Thr", + VariantRSID: 80055610, + HealthyBase: "G", + DefectiveBase: "C", + EffectIsMild: false, + References: variant22_ReferencesMap, + } + + variant23_ReferencesMap := make(map[string]string) + variant23_ReferencesMap["SNPedia.com - rs80055610"] = "https://www.snpedia.com/index.php/Rs80055610" + + variant23_Object := DiseaseVariant{ + + VariantIdentifier: "46efe1", + VariantNames: []string{"R560K"}, + NucleotideChange: "c.1679G>A", + AminoAcidChange: "p.Arg560Lys", + VariantRSID: 80055610, + HealthyBase: "G", + DefectiveBase: "A", + EffectIsMild: false, + References: variant23_ReferencesMap, + } + + variant24_ReferencesMap := make(map[string]string) + variant24_ReferencesMap["SNPedia.com - rs74767530"] = "https://www.snpedia.com/index.php/Rs74767530" + + variant24_Object := DiseaseVariant{ + + VariantIdentifier: "528406", + VariantNames: []string{"R1162X"}, + NucleotideChange: "c.3484C>T", + AminoAcidChange: "p.Arg1162Ter", + VariantRSID: 74767530, + HealthyBase: "C", + DefectiveBase: "T", + EffectIsMild: false, + References: variant24_ReferencesMap, + } + + variant25_ReferencesMap := make(map[string]string) + variant25_ReferencesMap["SNPedia.com - rs121908747"] = "https://www.snpedia.com/index.php/Rs121908747" + + variant25_Object := DiseaseVariant{ + + VariantIdentifier: "e8e8fc", + VariantNames: []string{"3659delC"}, + NucleotideChange: "c.3528delC", + AminoAcidChange: "p.Lys1177Serfs", + VariantRSID: 121908747, + HealthyBase: "C", + DefectiveBase: "D", + EffectIsMild: false, + References: variant25_ReferencesMap, + } + + variant26_ReferencesMap := make(map[string]string) + variant26_ReferencesMap["SNPedia.com - rs74551128"] = "https://www.snpedia.com/index.php/Rs74551128" + + variant26_Object := DiseaseVariant{ + + VariantIdentifier: "e60633", + VariantNames: []string{"A455E"}, + NucleotideChange: "c.1364C>A", + AminoAcidChange: "p.Ala455Glu", + VariantRSID: 74551128, + HealthyBase: "C", + DefectiveBase: "A", + EffectIsMild: false, + References: variant26_ReferencesMap, + } + + variant27_ReferencesMap := make(map[string]string) + variant27_ReferencesMap["SNPedia.com - rs75961395"] = "https://www.snpedia.com/index.php/Rs75961395" + + variant27_Object := DiseaseVariant{ + + VariantIdentifier: "d72d30", + VariantNames: []string{"G85E"}, + NucleotideChange: "c.254G>A", + AminoAcidChange: "p.Gly85Glu", + VariantRSID: 75961395, + HealthyBase: "G", + DefectiveBase: "A", + EffectIsMild: false, + References: variant27_ReferencesMap, + } + + variant28_ReferencesMap := make(map[string]string) + variant28_ReferencesMap["SNPedia.com - rs121908746"] = "https://www.snpedia.com/index.php/Rs121908746" + + variant28_Object := DiseaseVariant{ + + VariantIdentifier: "a3b068", + VariantNames: []string{"2184delA"}, + NucleotideChange: "c.2052delA", + AminoAcidChange: "p.Lys684Asnfs", + VariantRSID: 121908746, + HealthyBase: "I", //CA + DefectiveBase: "D", + EffectIsMild: false, + References: variant28_ReferencesMap, + } + + variant29_ReferencesMap := make(map[string]string) + variant29_ReferencesMap["SNPedia.com - rs121908748"] = "https://www.snpedia.com/index.php/Rs121908748" + + variant29_Object := DiseaseVariant{ + + VariantIdentifier: "770086", + VariantNames: []string{"1898+1G>A"}, + NucleotideChange: "c.1766+1G>A", + AminoAcidChange: "", + VariantRSID: 121908748, + HealthyBase: "G", + DefectiveBase: "A", + EffectIsMild: false, + References: variant29_ReferencesMap, + } + + variant30_ReferencesMap := make(map[string]string) + variant30_ReferencesMap["SNPedia.com - rs121908748"] = "https://www.snpedia.com/index.php/Rs121908748" + + variant30_Object := DiseaseVariant{ + + VariantIdentifier: "a00f11", + VariantNames: []string{"1898+1G>C"}, + NucleotideChange: "c.1766+1G>C", + AminoAcidChange: "", + VariantRSID: 121908748, + HealthyBase: "G", + DefectiveBase: "C", + EffectIsMild: false, + References: variant30_ReferencesMap, + } + + variant31_ReferencesMap := make(map[string]string) + variant31_ReferencesMap["SNPedia.com - rs121908748"] = "https://www.snpedia.com/index.php/Rs121908748" + + variant31_Object := DiseaseVariant{ + + VariantIdentifier: "79d73b", + VariantNames: []string{"1898+1G>T"}, + NucleotideChange: "c.1766+1G>T", + AminoAcidChange: "", + VariantRSID: 121908748, + HealthyBase: "G", + DefectiveBase: "T", + EffectIsMild: false, + References: variant31_ReferencesMap, + } + + cysticFibrosisVariantsList := []DiseaseVariant{variant1_Object, variant2_Object, variant3_Object, variant4_Object, variant5_Object, variant6_Object, variant7_Object, variant8_Object, variant9_Object, variant10_Object, variant11_Object, variant12_Object, variant13_Object, variant14_Object, variant15_Object, variant16_Object, variant17_Object, variant18_Object, variant19_Object, variant20_Object, variant21_Object, variant22_Object, variant23_Object, variant24_Object, variant25_Object, variant26_Object, variant27_Object, variant28_Object, variant29_Object, variant30_Object, variant31_Object} + + referencesMap := make(map[string]string) + referencesMap["Cystic fibrosis population carrier screening: 2004 revision of American College of Medical Genetics mutation panel"] = "https://www.ncbi.nlm.nih.gov/pmc/articles/PMC3110945/" + referencesMap["SNPedia.com - Cystic Fibrosis"] = "https://www.snpedia.com/index.php/Cystic_Fibrosis" + + cysticFibrosisObject := MonogenicDisease{ + + DiseaseName: "Cystic Fibrosis", + GeneName: "CFTR", + DominantOrRecessive: "Recessive", + DiseaseDescription: "A genetic condition that causes severe damage to the body's respiratory and digestive systems. The body creates a thick, sticky mucus that clogs all areas of the human body, especially the lungs and pancreas.", + VariantsList: cysticFibrosisVariantsList, + References: referencesMap, + } + + return cysticFibrosisObject +} + + + diff --git a/resources/geneticReferences/monogenicDiseases/monogenicDiseases.go b/resources/geneticReferences/monogenicDiseases/monogenicDiseases.go new file mode 100644 index 0000000..bb920ff --- /dev/null +++ b/resources/geneticReferences/monogenicDiseases/monogenicDiseases.go @@ -0,0 +1,154 @@ + +// monogenicDiseases provides information about monogenic genetic diseases + +package monogenicDiseases + +import "errors" + +// D == Deletion, I == Insertion + +// All bases reference the DNA plus strand + +// A disease variant is specific change on a specific location on the genome that causes the disease if mutated +type DiseaseVariant struct{ + + // 3 byte hex identifier. We must ensure no variants have the same identifier. + VariantIdentifier string + + VariantNames []string + + // Example: c.1657C>T + NucleotideChange string + + // Example: Gly542Ter + AminoAcidChange string + + // RSID that represents the location of this variant + // If multiple RSIDs represent the same variant location, use the first rsID for the locus in the locusMetadata package + VariantRSID int64 + + // Base which indicate allele does not have the disease (wild-type, reference, normal, functional) + HealthyBase string + + // Base which mean allele tests positive for the disease (mutated) + DefectiveBase string + + // Is true if the variant is known to have a mild effect + EffectIsMild bool + + // Reference name -> Reference link + References map[string]string +} + +type MonogenicDisease struct{ + + DiseaseName string + + // Name of gene that if mutated causes disease + GeneName string + + DominantOrRecessive string + + DiseaseDescription string + + VariantsList []DiseaseVariant + + // Reference name -> Reference link + References map[string]string +} + +var monogenicDiseaseNamesList []string +var monogenicDiseaseObjectsList []MonogenicDisease + +// This must be called once during application startup +func InitializeMonogenicDiseaseVariables(){ + + cysticFibrosisObject := getCysticFibrosisDiseaseObject() + sickleCellAnemiaObject := getSickleCellAnemiaDiseaseObject() + + monogenicDiseaseObjectsList = []MonogenicDisease{cysticFibrosisObject, sickleCellAnemiaObject} + + monogenicDiseaseNamesList = make([]string, 0, len(monogenicDiseaseObjectsList)) + + for _, diseaseObject := range monogenicDiseaseObjectsList{ + + diseaseName := diseaseObject.DiseaseName + monogenicDiseaseNamesList = append(monogenicDiseaseNamesList, diseaseName) + } +} + +// Be aware that all of these functions are returning original objects/slices, not copies +// Thus, we cannot edit the objects/slices that are returned. We must copy the fields first if we want to edit them. + +func GetMonogenicDiseaseNamesList()([]string, error){ + + if (monogenicDiseaseNamesList == nil){ + return nil, errors.New("GetMonogenicDiseaseNamesList called when list is not initialized.") + } + + return monogenicDiseaseNamesList, nil +} + +func GetMonogenicDiseaseObjectsList()([]MonogenicDisease, error){ + + if (monogenicDiseaseObjectsList == nil){ + return nil, errors.New("GetMonogenicDiseaseObjectsList called when list is not initialized.") + } + + return monogenicDiseaseObjectsList, nil +} + +func GetMonogenicDiseaseObject(diseaseName string)(MonogenicDisease, error){ + + monogenicDiseasesList, err := GetMonogenicDiseaseObjectsList() + if (err != nil) { return MonogenicDisease{}, err } + + for _, diseaseObject := range monogenicDiseasesList{ + + currentDiseaseName := diseaseObject.DiseaseName + if (currentDiseaseName == diseaseName){ + return diseaseObject, nil + } + } + + return MonogenicDisease{}, errors.New("GetMonogenicDiseaseObject called with unknown disease: " + diseaseName) +} + +//Outputs: +// -map[string]DiseaseVariant: Variant Identifier -> Disease Variant +// -error +func GetMonogenicDiseaseVariantsMap(diseaseName string)(map[string]DiseaseVariant, error){ + + diseaseObject, err := GetMonogenicDiseaseObject(diseaseName) + if (err != nil){ + return nil, errors.New("GetMonogenicDiseaseVariantsMap failed: " + err.Error()) + } + + diseaseVariantsMap := make(map[string]DiseaseVariant) + + diseaseVariantsList := diseaseObject.VariantsList + + for _, variantObject := range diseaseVariantsList{ + + variantIdentifier := variantObject.VariantIdentifier + + diseaseVariantsMap[variantIdentifier] = variantObject + } + + return diseaseVariantsMap, nil +} + +func GetMonogenicDiseaseVariantObject(diseaseName string, variantIdentifier string)(DiseaseVariant, error){ + + diseaseVariantsMap, err := GetMonogenicDiseaseVariantsMap(diseaseName) + if (err != nil) { return DiseaseVariant{}, err } + + variantObject, exists := diseaseVariantsMap[variantIdentifier] + if (exists == false) { + return DiseaseVariant{}, errors.New("GetMonogenicDiseaseVariantObject called with unknown variant. Disease: " + diseaseName + ", Variant: " + variantIdentifier) + } + + return variantObject, nil +} + + diff --git a/resources/geneticReferences/monogenicDiseases/sickleCellAnemia.go b/resources/geneticReferences/monogenicDiseases/sickleCellAnemia.go new file mode 100644 index 0000000..00b0505 --- /dev/null +++ b/resources/geneticReferences/monogenicDiseases/sickleCellAnemia.go @@ -0,0 +1,40 @@ + +package monogenicDiseases + +func getSickleCellAnemiaDiseaseObject()MonogenicDisease{ + + variant1_ReferencesMap := make(map[string]string) + variant1_ReferencesMap["SNPedia.com - rs334"] = "https://www.snpedia.com/index.php/Rs334" + + variant1_Object := DiseaseVariant{ + + VariantIdentifier: "50e857", + VariantNames: []string{"rs334"}, + NucleotideChange: "", + AminoAcidChange: "", + VariantRSID: 334, + HealthyBase: "T", + DefectiveBase: "A", + EffectIsMild: false, + References: variant1_ReferencesMap, + } + + cysticFibrosisVariantsList := []DiseaseVariant{variant1_Object} + + referencesMap := make(map[string]string) + referencesMap["SNPedia.com - Sickle Cell Anemia"] = "https://www.snpedia.com/index.php/Sickle_Cell_Anemia" + + sickleCellAnemiaObject := MonogenicDisease{ + + DiseaseName: "Sickle Cell Anemia", + GeneName: "HBB", + DominantOrRecessive: "Recessive", + DiseaseDescription: "A blood disorder that causes a constant shortage of red blood cells. Symptoms include organ damage, chronic pain, infection and stroke.", + VariantsList: cysticFibrosisVariantsList, + References: referencesMap, + } + + return sickleCellAnemiaObject +} + + diff --git a/resources/geneticReferences/polygenicDiseases/breastCancer.go b/resources/geneticReferences/polygenicDiseases/breastCancer.go new file mode 100644 index 0000000..79936ed --- /dev/null +++ b/resources/geneticReferences/polygenicDiseases/breastCancer.go @@ -0,0 +1,960 @@ +package polygenicDiseases + +import "errors" + + +func getBreastCancerDiseaseObject()PolygenicDisease{ + + locus1_ReferencesMap := make(map[string]string) + locus1_ReferencesMap["SNPedia.com - rs16942"] = "https://www.snpedia.com/index.php/Rs16942" + + locus1_RiskWeightsMap := make(map[string]int) + locus1_RiskWeightsMap["T;T"] = 0 + locus1_RiskWeightsMap["T;C"] = 1 + locus1_RiskWeightsMap["C;T"] = 1 + locus1_RiskWeightsMap["C;C"] = 1 + + locus1_OddsRatiosMap := make(map[string]float64) + locus1_OddsRatiosMap["T;T"] = 1 + locus1_OddsRatiosMap["T;C"] = 1.14 + locus1_OddsRatiosMap["C;T"] = 1.14 + locus1_OddsRatiosMap["C;C"] = 1.28 + + locus1_BasePairProbabilitiesMap := make(map[string]float64) + locus1_BasePairProbabilitiesMap["T;T"] = .45 + locus1_BasePairProbabilitiesMap["T;C"] = .45 + locus1_BasePairProbabilitiesMap["C;T"] = .45 + locus1_BasePairProbabilitiesMap["C;C"] = .10 + + locus1_Object := DiseaseLocus{ + + LocusIdentifier: "d7891c", + LocusRSID: 16942, + RiskWeightsMap: locus1_RiskWeightsMap, + MinimumRiskWeight: 0, + MaximumRiskWeight: 1, + OddsRatiosMap: locus1_OddsRatiosMap, + BasePairProbabilitiesMap: locus1_BasePairProbabilitiesMap, + References: locus1_ReferencesMap, + } + + locus2_ReferencesMap := make(map[string]string) + locus2_ReferencesMap["SNPedia.com - rs1045485"] = "https://www.snpedia.com/index.php/Rs1045485" + + locus2_RiskWeightsMap := make(map[string]int) + locus2_RiskWeightsMap["C;C"] = -2 + locus2_RiskWeightsMap["C;G"] = -1 + locus2_RiskWeightsMap["G;C"] = -1 + locus2_RiskWeightsMap["G;G"] = 0 + + locus2_OddsRatiosMap := make(map[string]float64) + + locus2_OddsRatiosMap["C;C"] = 0.74 + locus2_OddsRatiosMap["C;G"] = 0.89 + locus2_OddsRatiosMap["G;C"] = 0.89 + locus2_OddsRatiosMap["G;G"] = 1 + + locus2_BasePairProbabilitiesMap := make(map[string]float64) + locus2_BasePairProbabilitiesMap["C;C"] = .01 + locus2_BasePairProbabilitiesMap["C;G"] = .04 + locus2_BasePairProbabilitiesMap["G;C"] = .04 + locus2_BasePairProbabilitiesMap["G;G"] = .95 + + locus2_Object := DiseaseLocus{ + + LocusIdentifier: "41c164", + LocusRSID: 1045485, + RiskWeightsMap: locus2_RiskWeightsMap, + MinimumRiskWeight: -2, + MaximumRiskWeight: 0, + OddsRatiosMap: locus2_OddsRatiosMap, + BasePairProbabilitiesMap: locus2_BasePairProbabilitiesMap, + References: locus2_ReferencesMap, + } + + locus3_ReferencesMap := make(map[string]string) + locus3_ReferencesMap["SNPedia.com - rs34330"] = "https://www.snpedia.com/index.php/Rs34330" + + locus3_RiskWeightsMap := make(map[string]int) + locus3_RiskWeightsMap["C;C"] = 0 + locus3_RiskWeightsMap["T;T"] = 1 + + locus3_OddsRatiosMap := make(map[string]float64) + locus3_OddsRatiosMap["C;C"] = 0 + locus3_OddsRatiosMap["T;T"] = 1.22 + + locus3_BasePairProbabilitiesMap := make(map[string]float64) + locus3_BasePairProbabilitiesMap["C;C"] = .40 + locus3_BasePairProbabilitiesMap["C;T"] = .45 + locus3_BasePairProbabilitiesMap["T;C"] = .45 + locus3_BasePairProbabilitiesMap["T;T"] = .15 + + locus3_Object := DiseaseLocus{ + + LocusIdentifier: "f3a097", + LocusRSID: 34330, + RiskWeightsMap: locus3_RiskWeightsMap, + MinimumRiskWeight: 0, + MaximumRiskWeight: 1, + OddsRatiosMap: locus3_OddsRatiosMap, + BasePairProbabilitiesMap: locus3_BasePairProbabilitiesMap, + References: locus3_ReferencesMap, + } + + locus4_ReferencesMap := make(map[string]string) + locus4_ReferencesMap["SNPedia.com - rs144848"] = "https://www.snpedia.com/index.php/Rs144848" + + locus4_RiskWeightsMap := make(map[string]int) + locus4_RiskWeightsMap["A;A"] = 0 + locus4_RiskWeightsMap["C;A"] = 1 + locus4_RiskWeightsMap["A;C"] = 1 + locus4_RiskWeightsMap["C;C"] = 2 + + locus4_OddsRatiosMap := make(map[string]float64) + locus4_OddsRatiosMap["A;A"] = 1 + locus4_OddsRatiosMap["A;C"] = 1.14 + locus4_OddsRatiosMap["C;A"] = 1.14 + locus4_OddsRatiosMap["C;C"] = 1.31 + + locus4_BasePairProbabilitiesMap := make(map[string]float64) + locus4_BasePairProbabilitiesMap["A;A"] = .55 + locus4_BasePairProbabilitiesMap["C;A"] = .36 + locus4_BasePairProbabilitiesMap["A;C"] = .36 + locus4_BasePairProbabilitiesMap["C;C"] = .09 + + locus4_Object := DiseaseLocus{ + + LocusIdentifier: "d4626f", + LocusRSID: 144848, + RiskWeightsMap: locus4_RiskWeightsMap, + MinimumRiskWeight: 0, + MaximumRiskWeight: 2, + OddsRatiosMap: locus4_OddsRatiosMap, + BasePairProbabilitiesMap: locus4_BasePairProbabilitiesMap, + References: locus4_ReferencesMap, + } + + locus5_ReferencesMap := make(map[string]string) + locus5_ReferencesMap["SNPedia.com - rs766173"] = "https://www.snpedia.com/index.php/Rs766173" + + locus5_RiskWeightsMap := make(map[string]int) + locus5_RiskWeightsMap["A;A"] = 0 + locus5_RiskWeightsMap["A;C"] = 1 + locus5_RiskWeightsMap["C;A"] = 1 + locus5_RiskWeightsMap["C;C"] = 2 + + locus5_OddsRatiosMap := make(map[string]float64) + locus5_OddsRatiosMap["A;A"] = 1 + locus5_OddsRatiosMap["A;C"] = 1.14 + locus5_OddsRatiosMap["C;A"] = 1.14 + locus5_OddsRatiosMap["C;C"] = 1.28 + + locus5_BasePairProbabilitiesMap := make(map[string]float64) + locus5_BasePairProbabilitiesMap["A;A"] = .85 + locus5_BasePairProbabilitiesMap["A;C"] = .13 + locus5_BasePairProbabilitiesMap["C;A"] = .13 + locus5_BasePairProbabilitiesMap["C;C"] = .02 + + locus5_Object := DiseaseLocus{ + + LocusIdentifier: "84aaa4", + LocusRSID: 766173, + RiskWeightsMap: locus5_RiskWeightsMap, + MinimumRiskWeight: 0, + MaximumRiskWeight: 2, + OddsRatiosMap: locus5_OddsRatiosMap, + BasePairProbabilitiesMap: locus5_BasePairProbabilitiesMap, + References: locus5_ReferencesMap, + } + + locus6_ReferencesMap := make(map[string]string) + locus6_ReferencesMap["SNPedia.com - rs1799950"] = "https://www.snpedia.com/index.php/Rs1799950" + + locus6_RiskWeightsMap := make(map[string]int) + locus6_RiskWeightsMap["T;T"] = 0 + locus6_RiskWeightsMap["T;C"] = 1 + locus6_RiskWeightsMap["C;T"] = 1 + locus6_RiskWeightsMap["C;C"] = 2 + + locus6_OddsRatiosMap := make(map[string]float64) + locus6_OddsRatiosMap["T;T"] = 1 + locus6_OddsRatiosMap["T;C"] = 1.5 + locus6_OddsRatiosMap["C;T"] = 1.5 + locus6_OddsRatiosMap["C;C"] = 1.72 + + locus6_BasePairProbabilitiesMap := make(map[string]float64) + locus6_BasePairProbabilitiesMap["T;T"] = .98 + locus6_BasePairProbabilitiesMap["T;C"] = .02 + locus6_BasePairProbabilitiesMap["C;T"] = .02 + locus6_BasePairProbabilitiesMap["C;C"] = .001 + + locus6_Object := DiseaseLocus{ + + LocusIdentifier: "c8de7a", + LocusRSID: 1799950, + RiskWeightsMap: locus6_RiskWeightsMap, + MinimumRiskWeight: 0, + MaximumRiskWeight: 2, + OddsRatiosMap: locus6_OddsRatiosMap, + BasePairProbabilitiesMap: locus6_BasePairProbabilitiesMap, + References: locus6_ReferencesMap, + } + + locus7_ReferencesMap := make(map[string]string) + locus7_ReferencesMap["SNPedia.com - rs4986850"] = "https://www.snpedia.com/index.php/Rs4986850" + + locus7_RiskWeightsMap := make(map[string]int) + locus7_RiskWeightsMap["C;C"] = 0 + locus7_RiskWeightsMap["T;C"] = 1 + locus7_RiskWeightsMap["C;T"] = 1 + locus7_RiskWeightsMap["T;T"] = 2 + + locus7_OddsRatiosMap := make(map[string]float64) + locus7_OddsRatiosMap["C;C"] = 1 + locus7_OddsRatiosMap["T;C"] = 1.14 + locus7_OddsRatiosMap["C;T"] = 1.14 + locus7_OddsRatiosMap["T;T"] = 1.28 + + locus7_BasePairProbabilitiesMap := make(map[string]float64) + locus7_BasePairProbabilitiesMap["C;C"] = .92 + locus7_BasePairProbabilitiesMap["T;C"] = .07 + locus7_BasePairProbabilitiesMap["C;T"] = .07 + locus7_BasePairProbabilitiesMap["T;T"] = .01 + + locus7_Object := DiseaseLocus{ + + LocusIdentifier: "d30087", + LocusRSID: 4986850, + RiskWeightsMap: locus7_RiskWeightsMap, + MinimumRiskWeight: 0, + MaximumRiskWeight: 2, + OddsRatiosMap: locus7_OddsRatiosMap, + BasePairProbabilitiesMap: locus7_BasePairProbabilitiesMap, + References: locus7_ReferencesMap, + } + + locus8_ReferencesMap := make(map[string]string) + locus8_ReferencesMap["SNPedia.com - rs2227945"] = "https://www.snpedia.com/index.php/Rs2227945" + + locus8_RiskWeightsMap := make(map[string]int) + locus8_RiskWeightsMap["T;T"] = 0 + locus8_RiskWeightsMap["T;C"] = 1 + locus8_RiskWeightsMap["C;T"] = 1 + locus8_RiskWeightsMap["C;C"] = 2 + + locus8_OddsRatiosMap := make(map[string]float64) + locus8_OddsRatiosMap["T;T"] = 1 + locus8_OddsRatiosMap["T;C"] = 1.14 + locus8_OddsRatiosMap["C;T"] = 1.14 + locus8_OddsRatiosMap["C;C"] = 1.28 + + locus8_BasePairProbabilitiesMap := make(map[string]float64) + locus8_BasePairProbabilitiesMap["T;T"] = .97 + locus8_BasePairProbabilitiesMap["T;C"] = .03 + locus8_BasePairProbabilitiesMap["C;T"] = .03 + locus8_BasePairProbabilitiesMap["C;C"] = .001 + + locus8_Object := DiseaseLocus{ + + LocusIdentifier: "cafa72", + LocusRSID: 2227945, + RiskWeightsMap: locus8_RiskWeightsMap, + MinimumRiskWeight: 0, + MaximumRiskWeight: 2, + OddsRatiosMap: locus8_OddsRatiosMap, + BasePairProbabilitiesMap: locus8_BasePairProbabilitiesMap, + References: locus8_ReferencesMap, + } + + locus9_ReferencesMap := make(map[string]string) + locus9_ReferencesMap["SNPedia.com - rs1799966"] = "https://www.snpedia.com/index.php/Rs1799966" + + locus9_RiskWeightsMap := make(map[string]int) + locus9_RiskWeightsMap["T;T"] = 0 + locus9_RiskWeightsMap["T;C"] = 1 + locus9_RiskWeightsMap["C;T"] = 1 + locus9_RiskWeightsMap["C;C"] = 2 + + locus9_OddsRatiosMap := make(map[string]float64) + locus9_OddsRatiosMap["T;T"] = 1 + locus9_OddsRatiosMap["T;C"] = 1.14 + locus9_OddsRatiosMap["C;T"] = 1.14 + locus9_OddsRatiosMap["C;C"] = 1.28 + + locus9_BasePairProbabilitiesMap := make(map[string]float64) + locus9_BasePairProbabilitiesMap["T;T"] = .45 + locus9_BasePairProbabilitiesMap["T;C"] = .45 + locus9_BasePairProbabilitiesMap["C;T"] = .45 + locus9_BasePairProbabilitiesMap["C;C"] = .10 + + locus9_Object := DiseaseLocus{ + + LocusIdentifier: "8f671c", + LocusRSID: 1799966, + RiskWeightsMap: locus9_RiskWeightsMap, + MinimumRiskWeight: 0, + MaximumRiskWeight: 2, + OddsRatiosMap: locus9_OddsRatiosMap, + BasePairProbabilitiesMap: locus9_BasePairProbabilitiesMap, + References: locus9_ReferencesMap, + } + + locus10_ReferencesMap := make(map[string]string) + locus10_ReferencesMap["SNPedia.com - rs4987117"] = "https://www.snpedia.com/index.php/Rs4987117" + + locus10_RiskWeightsMap := make(map[string]int) + locus10_RiskWeightsMap["C;C"] = 0 + locus10_RiskWeightsMap["T;C"] = 1 + locus10_RiskWeightsMap["C;T"] = 1 + locus10_RiskWeightsMap["T;T"] = 2 + + locus10_OddsRatiosMap := make(map[string]float64) + locus10_OddsRatiosMap["C;C"] = 1 + locus10_OddsRatiosMap["T;C"] = 1.14 + locus10_OddsRatiosMap["C;T"] = 1.14 + locus10_OddsRatiosMap["T;T"] = 1.28 + + locus10_BasePairProbabilitiesMap := make(map[string]float64) + locus10_BasePairProbabilitiesMap["C;C"] = .98 + locus10_BasePairProbabilitiesMap["T;C"] = .02 + locus10_BasePairProbabilitiesMap["C;T"] = .02 + locus10_BasePairProbabilitiesMap["T;T"] = .001 + + locus10_Object := DiseaseLocus{ + + LocusIdentifier: "b3e49a", + LocusRSID: 4987117, + RiskWeightsMap: locus10_RiskWeightsMap, + MinimumRiskWeight: 0, + MaximumRiskWeight: 2, + OddsRatiosMap: locus10_OddsRatiosMap, + BasePairProbabilitiesMap: locus10_BasePairProbabilitiesMap, + References: locus10_ReferencesMap, + } + + locus11_ReferencesMap := make(map[string]string) + locus11_ReferencesMap["SNPedia.com - rs1799954"] = "https://www.snpedia.com/index.php/Rs1799954" + + locus11_RiskWeightsMap := make(map[string]int) + locus11_RiskWeightsMap["C;C"] = 0 + locus11_RiskWeightsMap["T;C"] = 1 + locus11_RiskWeightsMap["C;T"] = 1 + locus11_RiskWeightsMap["T;T"] = 2 + + locus11_OddsRatiosMap := make(map[string]float64) + locus11_OddsRatiosMap["C;C"] = 1 + locus11_OddsRatiosMap["T;C"] = 1.14 + locus11_OddsRatiosMap["C;T"] = 1.14 + locus11_OddsRatiosMap["T;T"] = 1.28 + + locus11_BasePairProbabilitiesMap := make(map[string]float64) + locus11_BasePairProbabilitiesMap["C;C"] = .97 + locus11_BasePairProbabilitiesMap["T;C"] = .03 + locus11_BasePairProbabilitiesMap["C;T"] = .03 + locus11_BasePairProbabilitiesMap["T;T"] = .001 + + locus11_Object := DiseaseLocus{ + + LocusIdentifier: "8b0b02", + LocusRSID: 1799954, + RiskWeightsMap: locus11_RiskWeightsMap, + MinimumRiskWeight: 0, + MaximumRiskWeight: 2, + OddsRatiosMap: locus11_OddsRatiosMap, + BasePairProbabilitiesMap: locus11_BasePairProbabilitiesMap, + References: locus11_ReferencesMap, + } + + locus12_ReferencesMap := make(map[string]string) + locus12_ReferencesMap["SNPedia.com - rs11571746"] = "https://www.snpedia.com/index.php/Rs11571746" + + locus12_RiskWeightsMap := make(map[string]int) + locus12_RiskWeightsMap["T;T"] = 0 + locus12_RiskWeightsMap["T;C"] = 1 + locus12_RiskWeightsMap["C;T"] = 1 + locus12_RiskWeightsMap["C;C"] = 2 + + locus12_OddsRatiosMap := make(map[string]float64) + locus12_OddsRatiosMap["T;T"] = 1 + locus12_OddsRatiosMap["T;C"] = 1.14 + locus12_OddsRatiosMap["C;T"] = 1.14 + locus12_OddsRatiosMap["C;C"] = 1.28 + + locus12_BasePairProbabilitiesMap := make(map[string]float64) + locus12_BasePairProbabilitiesMap["T;T"] = .98 + locus12_BasePairProbabilitiesMap["T;C"] = .02 + locus12_BasePairProbabilitiesMap["C;T"] = .02 + locus12_BasePairProbabilitiesMap["C;C"] = .001 + + locus12_Object := DiseaseLocus{ + + LocusIdentifier: "25cafc", + LocusRSID: 11571746, + RiskWeightsMap: locus12_RiskWeightsMap, + MinimumRiskWeight: 0, + MaximumRiskWeight: 2, + OddsRatiosMap: locus12_OddsRatiosMap, + BasePairProbabilitiesMap: locus12_BasePairProbabilitiesMap, + References: locus12_ReferencesMap, + } + + locus13_ReferencesMap := make(map[string]string) + locus13_ReferencesMap["SNPedia.com - rs11571747"] = "https://www.snpedia.com/index.php/Rs11571747" + + locus13_RiskWeightsMap := make(map[string]int) + locus13_RiskWeightsMap["A;A"] = 0 + locus13_RiskWeightsMap["A;C"] = 1 + locus13_RiskWeightsMap["C;A"] = 1 + locus13_RiskWeightsMap["C;C"] = 2 + + locus13_OddsRatiosMap := make(map[string]float64) + locus13_OddsRatiosMap["A;A"] = 1 + locus13_OddsRatiosMap["A;C"] = 1.14 + locus13_OddsRatiosMap["C;A"] = 1.14 + locus13_OddsRatiosMap["C;C"] = 1.28 + + locus13_BasePairProbabilitiesMap := make(map[string]float64) + locus13_BasePairProbabilitiesMap["A;A"] = .99 + locus13_BasePairProbabilitiesMap["A;C"] = .001 + locus13_BasePairProbabilitiesMap["C;A"] = .001 + locus13_BasePairProbabilitiesMap["C;C"] = .001 + + locus13_Object := DiseaseLocus{ + + LocusIdentifier: "34c7e5", + LocusRSID: 11571747, + RiskWeightsMap: locus13_RiskWeightsMap, + MinimumRiskWeight: 0, + MaximumRiskWeight: 2, + OddsRatiosMap: locus13_OddsRatiosMap, + BasePairProbabilitiesMap: locus13_BasePairProbabilitiesMap, + References: locus13_ReferencesMap, + } + + locus14_ReferencesMap := make(map[string]string) + locus14_ReferencesMap["SNPedia.com - rs4987047"] = "https://www.snpedia.com/index.php/Rs4987047" + + locus14_RiskWeightsMap := make(map[string]int) + locus14_RiskWeightsMap["A;A"] = 0 + locus14_RiskWeightsMap["A;T"] = 1 + locus14_RiskWeightsMap["T;A"] = 1 + locus14_RiskWeightsMap["T;T"] = 2 + + locus14_OddsRatiosMap := make(map[string]float64) + locus14_OddsRatiosMap["A;A"] = 1 + locus14_OddsRatiosMap["A;T"] = 1.14 + locus14_OddsRatiosMap["T;A"] = 1.14 + locus14_OddsRatiosMap["T;T"] = 1.28 + + locus14_BasePairProbabilitiesMap := make(map[string]float64) + locus14_BasePairProbabilitiesMap["A;A"] = .94 + locus14_BasePairProbabilitiesMap["A;T"] = .94 + locus14_BasePairProbabilitiesMap["T;A"] = .05 + locus14_BasePairProbabilitiesMap["T;T"] = .01 + + locus14_Object := DiseaseLocus{ + + LocusIdentifier: "60ce27", + LocusRSID: 4987047, + RiskWeightsMap: locus14_RiskWeightsMap, + MinimumRiskWeight: 0, + MaximumRiskWeight: 2, + OddsRatiosMap: locus14_OddsRatiosMap, + BasePairProbabilitiesMap: locus14_BasePairProbabilitiesMap, + References: locus14_ReferencesMap, + } + + locus15_ReferencesMap := make(map[string]string) + locus15_ReferencesMap["SNPedia.com - rs11571833"] = "https://www.snpedia.com/index.php/Rs11571833" + + locus15_RiskWeightsMap := make(map[string]int) + locus15_RiskWeightsMap["A;A"] = 0 + locus15_RiskWeightsMap["T;A"] = 1 + locus15_RiskWeightsMap["A;T"] = 1 + locus15_RiskWeightsMap["T;T"] = 2 + + locus15_OddsRatiosMap := make(map[string]float64) + locus15_OddsRatiosMap["A;A"] = 1 + locus15_OddsRatiosMap["T;A"] = 1.14 + locus15_OddsRatiosMap["A;T"] = 1.14 + locus15_OddsRatiosMap["T;T"] = 1.28 + + locus15_BasePairProbabilitiesMap := make(map[string]float64) + locus15_BasePairProbabilitiesMap["A;A"] = .99 + locus15_BasePairProbabilitiesMap["T;A"] = .01 + locus15_BasePairProbabilitiesMap["A;T"] = .01 + locus15_BasePairProbabilitiesMap["T;T"] = .001 + + locus15_Object := DiseaseLocus{ + + LocusIdentifier: "328cdf", + LocusRSID: 11571833, + RiskWeightsMap: locus15_RiskWeightsMap, + MinimumRiskWeight: 0, + MaximumRiskWeight: 2, + OddsRatiosMap: locus15_OddsRatiosMap, + BasePairProbabilitiesMap: locus15_BasePairProbabilitiesMap, + References: locus15_ReferencesMap, + } + + locus16_ReferencesMap := make(map[string]string) + locus16_ReferencesMap["SNPedia.com - rs1801426"] = "https://www.snpedia.com/index.php/Rs1801426" + + locus16_RiskWeightsMap := make(map[string]int) + locus16_RiskWeightsMap["A;A"] = 0 + locus16_RiskWeightsMap["G;A"] = 1 + locus16_RiskWeightsMap["A;G"] = 1 + locus16_RiskWeightsMap["G;G"] = 2 + + locus16_OddsRatiosMap := make(map[string]float64) + locus16_OddsRatiosMap["A;A"] = 1 + locus16_OddsRatiosMap["G;A"] = 1.14 + locus16_OddsRatiosMap["A;G"] = 1.14 + locus16_OddsRatiosMap["G;G"] = 1.28 + + locus16_BasePairProbabilitiesMap := make(map[string]float64) + locus16_BasePairProbabilitiesMap["A;A"] = .90 + locus16_BasePairProbabilitiesMap["G;A"] = .09 + locus16_BasePairProbabilitiesMap["A;G"] = .09 + locus16_BasePairProbabilitiesMap["G;G"] = .01 + + locus16_Object := DiseaseLocus{ + + LocusIdentifier: "849bc7", + LocusRSID: 1801426, + RiskWeightsMap: locus16_RiskWeightsMap, + MinimumRiskWeight: 0, + MaximumRiskWeight: 2, + OddsRatiosMap: locus16_OddsRatiosMap, + BasePairProbabilitiesMap: locus16_BasePairProbabilitiesMap, + References: locus16_ReferencesMap, + } + + locus17_ReferencesMap := make(map[string]string) + locus17_ReferencesMap["SNPedia.com - rs3218707"] = "https://www.snpedia.com/index.php/Rs3218707" + + locus17_RiskWeightsMap := make(map[string]int) + locus17_RiskWeightsMap["G;G"] = 0 + locus17_RiskWeightsMap["G;C"] = 1 + locus17_RiskWeightsMap["C;G"] = 1 + locus17_RiskWeightsMap["C;C"] = 2 + + locus17_OddsRatiosMap := make(map[string]float64) + locus17_OddsRatiosMap["G;G"] = 1 + locus17_OddsRatiosMap["G;C"] = 1.14 + locus17_OddsRatiosMap["C;G"] = 1.14 + locus17_OddsRatiosMap["C;C"] = 1.28 + + locus17_BasePairProbabilitiesMap := make(map[string]float64) + locus17_BasePairProbabilitiesMap["G;G"] = .96 + locus17_BasePairProbabilitiesMap["G;C"] = .04 + locus17_BasePairProbabilitiesMap["C;G"] = .04 + locus17_BasePairProbabilitiesMap["C;C"] = .001 + + locus17_Object := DiseaseLocus{ + + LocusIdentifier: "5af5e3", + LocusRSID: 3218707, + RiskWeightsMap: locus17_RiskWeightsMap, + MinimumRiskWeight: 0, + MaximumRiskWeight: 2, + OddsRatiosMap: locus17_OddsRatiosMap, + BasePairProbabilitiesMap: locus17_BasePairProbabilitiesMap, + References: locus17_ReferencesMap, + } + + locus18_ReferencesMap := make(map[string]string) + locus18_ReferencesMap["SNPedia.com - rs4987945"] = "https://www.snpedia.com/index.php/Rs4987945" + + locus18_RiskWeightsMap := make(map[string]int) + locus18_RiskWeightsMap["C;C"] = 0 + locus18_RiskWeightsMap["C;G"] = 1 + locus18_RiskWeightsMap["G;C"] = 1 + locus18_RiskWeightsMap["G;G"] = 2 + + locus18_OddsRatiosMap := make(map[string]float64) + locus18_OddsRatiosMap["C;C"] = 1 + locus18_OddsRatiosMap["C;G"] = 1.14 + locus18_OddsRatiosMap["G;C"] = 1.14 + locus18_OddsRatiosMap["G;G"] = 1.28 + + locus18_BasePairProbabilitiesMap := make(map[string]float64) + locus18_BasePairProbabilitiesMap["C;C"] = .95 + locus18_BasePairProbabilitiesMap["C;G"] = .04 + locus18_BasePairProbabilitiesMap["G;C"] = .04 + locus18_BasePairProbabilitiesMap["G;G"] = .01 + + locus18_Object := DiseaseLocus{ + + LocusIdentifier: "c354fa", + LocusRSID: 4987945, + RiskWeightsMap: locus18_RiskWeightsMap, + MinimumRiskWeight: 0, + MaximumRiskWeight: 2, + OddsRatiosMap: locus18_OddsRatiosMap, + BasePairProbabilitiesMap: locus18_BasePairProbabilitiesMap, + References: locus18_ReferencesMap, + } + + locus19_ReferencesMap := make(map[string]string) + locus19_ReferencesMap["SNPedia.com - rs4986761"] = "https://www.snpedia.com/index.php/Rs4986761" + + locus19_RiskWeightsMap := make(map[string]int) + locus19_RiskWeightsMap["T;T"] = 0 + locus19_RiskWeightsMap["C;T"] = 1 + locus19_RiskWeightsMap["T;C"] = 1 + locus19_RiskWeightsMap["C;C"] = 2 + + locus19_OddsRatiosMap := make(map[string]float64) + locus19_OddsRatiosMap["T;T"] = 1 + locus19_OddsRatiosMap["C;T"] = 1.05 + locus19_OddsRatiosMap["T;C"] = 1.05 + locus19_OddsRatiosMap["C;C"] = 1.51 + + locus19_BasePairProbabilitiesMap := make(map[string]float64) + locus19_BasePairProbabilitiesMap["T;T"] = .99 + locus19_BasePairProbabilitiesMap["C;T"] = .01 + locus19_BasePairProbabilitiesMap["T;C"] = .01 + locus19_BasePairProbabilitiesMap["C;C"] = .001 + + locus19_Object := DiseaseLocus{ + + LocusIdentifier: "eedc23", + LocusRSID: 4986761, + RiskWeightsMap: locus19_RiskWeightsMap, + MinimumRiskWeight: 0, + MaximumRiskWeight: 2, + OddsRatiosMap: locus19_OddsRatiosMap, + BasePairProbabilitiesMap: locus19_BasePairProbabilitiesMap, + References: locus19_ReferencesMap, + } + + locus20_ReferencesMap := make(map[string]string) + locus20_ReferencesMap["SNPedia.com - rs3218695"] = "https://www.snpedia.com/index.php/Rs3218695" + + locus20_RiskWeightsMap := make(map[string]int) + locus20_RiskWeightsMap["C;C"] = 0 + locus20_RiskWeightsMap["C;A"] = 1 + locus20_RiskWeightsMap["A;C"] = 1 + locus20_RiskWeightsMap["A;A"] = 2 + + locus20_OddsRatiosMap := make(map[string]float64) + locus20_OddsRatiosMap["C;C"] = 1 + locus20_OddsRatiosMap["C;A"] = 1.14 + locus20_OddsRatiosMap["A;C"] = 1.14 + locus20_OddsRatiosMap["A;A"] = 1.28 + + locus20_BasePairProbabilitiesMap := make(map[string]float64) + locus20_BasePairProbabilitiesMap["C;C"] = .98 + locus20_BasePairProbabilitiesMap["C;A"] = .02 + locus20_BasePairProbabilitiesMap["A;C"] = .02 + locus20_BasePairProbabilitiesMap["A;A"] = .001 + + locus20_Object := DiseaseLocus{ + + LocusIdentifier: "2ee027", + LocusRSID: 3218695, + RiskWeightsMap: locus20_RiskWeightsMap, + MinimumRiskWeight: 0, + MaximumRiskWeight: 2, + OddsRatiosMap: locus20_OddsRatiosMap, + BasePairProbabilitiesMap: locus20_BasePairProbabilitiesMap, + References: locus20_ReferencesMap, + } + + locus21_ReferencesMap := make(map[string]string) + locus21_ReferencesMap["SNPedia.com - rs1800056"] = "https://www.snpedia.com/index.php/Rs1800056" + + locus21_RiskWeightsMap := make(map[string]int) + locus21_RiskWeightsMap["T;T"] = 0 + locus21_RiskWeightsMap["C;T"] = 1 + locus21_RiskWeightsMap["T;C"] = 1 + locus21_RiskWeightsMap["C;C"] = 2 + + locus21_OddsRatiosMap := make(map[string]float64) + locus21_OddsRatiosMap["T;T"] = 1 + locus21_OddsRatiosMap["C;T"] = 1.05 + locus21_OddsRatiosMap["T;C"] = 1.05 + locus21_OddsRatiosMap["C;C"] = 1.51 + + locus21_BasePairProbabilitiesMap := make(map[string]float64) + locus21_BasePairProbabilitiesMap["T;T"] = .97 + locus21_BasePairProbabilitiesMap["C;T"] = .03 + locus21_BasePairProbabilitiesMap["T;C"] = .03 + locus21_BasePairProbabilitiesMap["C;C"] = .001 + + locus21_Object := DiseaseLocus{ + + LocusIdentifier: "fc4bab", + LocusRSID: 1800056, + RiskWeightsMap: locus21_RiskWeightsMap, + MinimumRiskWeight: 0, + MaximumRiskWeight: 2, + OddsRatiosMap: locus21_OddsRatiosMap, + BasePairProbabilitiesMap: locus21_BasePairProbabilitiesMap, + References: locus21_ReferencesMap, + } + + locus22_ReferencesMap := make(map[string]string) + locus22_ReferencesMap["SNPedia.com - rs1800057"] = "https://www.snpedia.com/index.php/Rs1800057" + + locus22_RiskWeightsMap := make(map[string]int) + locus22_RiskWeightsMap["C;C"] = 0 + locus22_RiskWeightsMap["C;G"] = 1 + locus22_RiskWeightsMap["G;C"] = 1 + locus22_RiskWeightsMap["G;G"] = 2 + + locus22_OddsRatiosMap := make(map[string]float64) + locus22_OddsRatiosMap["C;C"] = 1 + locus22_OddsRatiosMap["C;G"] = 1.05 + locus22_OddsRatiosMap["G;C"] = 1.05 + locus22_OddsRatiosMap["G;G"] = 1.51 + + locus22_BasePairProbabilitiesMap := make(map[string]float64) + locus22_BasePairProbabilitiesMap["C;C"] = .97 + locus22_BasePairProbabilitiesMap["C;G"] = .03 + locus22_BasePairProbabilitiesMap["G;C"] = .03 + locus22_BasePairProbabilitiesMap["G;G"] = .001 + + locus22_Object := DiseaseLocus{ + + LocusIdentifier: "f8b225", + LocusRSID: 1800057, + RiskWeightsMap: locus22_RiskWeightsMap, + MinimumRiskWeight: 0, + MaximumRiskWeight: 2, + OddsRatiosMap: locus22_OddsRatiosMap, + BasePairProbabilitiesMap: locus22_BasePairProbabilitiesMap, + References: locus22_ReferencesMap, + } + + locus23_ReferencesMap := make(map[string]string) + locus23_ReferencesMap["SNPedia.com - rs3092856"] = "https://www.snpedia.com/index.php/Rs3092856" + + locus23_RiskWeightsMap := make(map[string]int) + locus23_RiskWeightsMap["C;C"] = 0 + locus23_RiskWeightsMap["C;T"] = 1 + locus23_RiskWeightsMap["T;C"] = 1 + locus23_RiskWeightsMap["T;T"] = 2 + + locus23_OddsRatiosMap := make(map[string]float64) + locus23_OddsRatiosMap["C;C"] = 1 + locus23_OddsRatiosMap["C;T"] = 1.14 + locus23_OddsRatiosMap["T;C"] = 1.14 + locus23_OddsRatiosMap["T;T"] = 1.28 + + locus23_BasePairProbabilitiesMap := make(map[string]float64) + locus23_BasePairProbabilitiesMap["C;C"] = .95 + locus23_BasePairProbabilitiesMap["C;T"] = .05 + locus23_BasePairProbabilitiesMap["T;C"] = .05 + locus23_BasePairProbabilitiesMap["T;T"] = .001 + + locus23_Object := DiseaseLocus{ + + LocusIdentifier: "4a072c", + LocusRSID: 3092856, + RiskWeightsMap: locus23_RiskWeightsMap, + MinimumRiskWeight: 0, + MaximumRiskWeight: 2, + OddsRatiosMap: locus23_OddsRatiosMap, + BasePairProbabilitiesMap: locus23_BasePairProbabilitiesMap, + References: locus23_ReferencesMap, + } + + locus24_ReferencesMap := make(map[string]string) + locus24_ReferencesMap["SNPedia.com - rs1800058"] = "https://www.snpedia.com/index.php/Rs1800058" + + locus24_RiskWeightsMap := make(map[string]int) + locus24_RiskWeightsMap["C;C"] = 0 + locus24_RiskWeightsMap["C;T"] = 1 + locus24_RiskWeightsMap["T;C"] = 1 + locus24_RiskWeightsMap["T;T"] = 2 + + locus24_OddsRatiosMap := make(map[string]float64) + locus24_OddsRatiosMap["C;C"] = 1 + locus24_OddsRatiosMap["C;T"] = 1.05 + locus24_OddsRatiosMap["T;C"] = 1.05 + locus24_OddsRatiosMap["T;T"] = 1.51 + + locus24_BasePairProbabilitiesMap := make(map[string]float64) + locus24_BasePairProbabilitiesMap["C;C"] = .95 + locus24_BasePairProbabilitiesMap["C;T"] = .04 + locus24_BasePairProbabilitiesMap["T;C"] = .04 + locus24_BasePairProbabilitiesMap["T;T"] = .01 + + locus24_Object := DiseaseLocus{ + + LocusIdentifier: "070f24", + LocusRSID: 1800058, + RiskWeightsMap: locus24_RiskWeightsMap, + MinimumRiskWeight: 0, + MaximumRiskWeight: 2, + OddsRatiosMap: locus24_OddsRatiosMap, + BasePairProbabilitiesMap: locus24_BasePairProbabilitiesMap, + References: locus24_ReferencesMap, + } + + locus25_ReferencesMap := make(map[string]string) + locus25_ReferencesMap["SNPedia.com - rs1801673"] = "https://www.snpedia.com/index.php/Rs1801673" + + locus25_RiskWeightsMap := make(map[string]int) + locus25_RiskWeightsMap["A;A"] = 0 + locus25_RiskWeightsMap["A;T"] = 1 + locus25_RiskWeightsMap["T;A"] = 1 + locus25_RiskWeightsMap["T;T"] = 2 + + locus25_OddsRatiosMap := make(map[string]float64) + locus25_OddsRatiosMap["A;A"] = 1 + locus25_OddsRatiosMap["A;T"] = 1.14 + locus25_OddsRatiosMap["T;A"] = 1.14 + locus25_OddsRatiosMap["T;T"] = 1.28 + + locus25_BasePairProbabilitiesMap := make(map[string]float64) + locus25_BasePairProbabilitiesMap["A;A"] = .99 + locus25_BasePairProbabilitiesMap["A;T"] = .01 + locus25_BasePairProbabilitiesMap["T;A"] = .01 + locus25_BasePairProbabilitiesMap["T;T"] = .001 + + locus25_Object := DiseaseLocus{ + + LocusIdentifier: "d08516", + LocusRSID: 1801673, + RiskWeightsMap: locus25_RiskWeightsMap, + MinimumRiskWeight: 0, + MaximumRiskWeight: 2, + OddsRatiosMap: locus25_OddsRatiosMap, + BasePairProbabilitiesMap: locus25_BasePairProbabilitiesMap, + References: locus25_ReferencesMap, + } + + locus26_ReferencesMap := make(map[string]string) + locus26_ReferencesMap["SNPedia.com - rs17879961"] = "https://www.snpedia.com/index.php/Rs17879961" + + locus26_RiskWeightsMap := make(map[string]int) + locus26_RiskWeightsMap["A;A"] = 0 + locus26_RiskWeightsMap["A;G"] = 1 + locus26_RiskWeightsMap["G;A"] = 1 + locus26_RiskWeightsMap["G;G"] = 2 + + locus26_OddsRatiosMap := make(map[string]float64) + locus26_OddsRatiosMap["A;A"] = 1 + locus26_OddsRatiosMap["A;G"] = 1.14 + locus26_OddsRatiosMap["G;A"] = 1.14 + locus26_OddsRatiosMap["G;G"] = 1.28 + + locus26_BasePairProbabilitiesMap := make(map[string]float64) + locus26_BasePairProbabilitiesMap["A;A"] = .98 + locus26_BasePairProbabilitiesMap["A;G"] = .02 + locus26_BasePairProbabilitiesMap["G;A"] = .02 + locus26_BasePairProbabilitiesMap["G;G"] = .001 + + locus26_Object := DiseaseLocus{ + + LocusIdentifier: "047b84", + LocusRSID: 17879961, + RiskWeightsMap: locus26_RiskWeightsMap, + MinimumRiskWeight: 0, + MaximumRiskWeight: 2, + OddsRatiosMap: locus26_OddsRatiosMap, + BasePairProbabilitiesMap: locus26_BasePairProbabilitiesMap, + References: locus26_ReferencesMap, + } + + // TODO: + //-https://www.snpedia.com/index.php/Rs1042522 + //-https://www.snpedia.com/index.php/Rs889312 + //-https://www.snpedia.com/index.php/Rs997669 + //-https://www.snpedia.com/index.php/Rs1042638 + //-https://www.snpedia.com/index.php/Rs1219648 + //-https://www.snpedia.com/index.php/Rs13281615 + //-https://www.snpedia.com/index.php/Rs3817198 + //-https://www.snpedia.com/index.php/Rs13387042 + //-https://www.snpedia.com/index.php/Rs4415084 + //-https://www.snpedia.com/index.php/Rs3803662 + //-https://www.snpedia.com/index.php/Rs2056116 + //-https://www.snpedia.com/index.php/Rs2268578 + //-https://www.snpedia.com/index.php/Rs2854344 + //-https://www.snpedia.com/index.php/Rs2981578 + //-https://www.snpedia.com/index.php/Rs2981582 + //-https://www.snpedia.com/index.php/Rs3176336 + //-https://www.snpedia.com/index.php/Rs3218005 + //-https://www.snpedia.com/index.php/Rs3218536 + //-https://www.snpedia.com/index.php/Rs3731239 + //-https://www.snpedia.com/index.php/Rs7895676 + //-https://www.snpedia.com/index.php/Rs140068132 + + breastCancerLociList := []DiseaseLocus{locus1_Object, locus2_Object, locus3_Object, locus4_Object, locus5_Object, locus6_Object, locus7_Object, locus8_Object, locus9_Object, locus10_Object, locus11_Object, locus12_Object, locus13_Object, locus14_Object, locus15_Object, locus16_Object, locus17_Object, locus18_Object, locus19_Object, locus20_Object, locus21_Object, locus22_Object, locus23_Object, locus24_Object, locus25_Object, locus26_Object} + + referencesMap := make(map[string]string) + referencesMap["SNPedia.com - Breast Cancer"] = "https://www.snpedia.com/index.php/Breast_cancer" + referencesMap["SNPedia.com - Breast Cancer Lifetime Risk"] = "https://www.snpedia.com/index.php/Breast_cancer_lifetime_risk" + + getAverageRiskProbabilitiesFunction := func(maleOrFemale string, inputAge int)(float64, error){ + + if (maleOrFemale == "Male"){ + //TODO: Men can get breast cancer too. Add risks here. + return 0, nil + } + + if (maleOrFemale != "Female"){ + return 0, errors.New("Trying to get breast cancer risk probability for invalid maleOrFemale: " + maleOrFemale) + } + + if (inputAge <= 19){ + return 0, nil + } + if (inputAge <= 24){ + return 0, nil + } + if (inputAge <= 29){ + return 0.1, nil + } + if (inputAge <= 34){ + return 0.2, nil + } + if (inputAge <= 39){ + return 0.6, nil + } + if (inputAge <= 44){ + return 1.4, nil + } + if (inputAge <= 49){ + return 2.5, nil + } + if (inputAge <= 54){ + return 4, nil + } + if (inputAge <= 59){ + return 5.6, nil + } + if (inputAge <= 64){ + return 7.6, nil + } + if (inputAge <= 69){ + return 9.7, nil + } + if (inputAge <= 74){ + return 11.7, nil + } + if (inputAge <= 79){ + return 13.4, nil + } + + return 15.8, nil + } + + breastCancerObject := PolygenicDisease{ + + DiseaseName: "Breast Cancer", + EffectedSex: "Both", + DiseaseDescription: "Cancer growth in the tissue of a person's breast.", + LociList: breastCancerLociList, + GetAverageRiskProbabilitiesFunction: getAverageRiskProbabilitiesFunction, + References: referencesMap, + } + + return breastCancerObject +} + + + diff --git a/resources/geneticReferences/polygenicDiseases/polygenicDiseases.go b/resources/geneticReferences/polygenicDiseases/polygenicDiseases.go new file mode 100644 index 0000000..f229384 --- /dev/null +++ b/resources/geneticReferences/polygenicDiseases/polygenicDiseases.go @@ -0,0 +1,167 @@ + +// polygenicDiseases provides information about polygenic diseases and the SNP base changes that effect a person's risk of becoming victim to them. + +package polygenicDiseases + +// Polygenic diseases are different from monogenicDiseases +// Monogenic diseases are well understood to be caused by mutations in a single gene, and thus have more accurate risk probabilities. +// Polygenic disease probabilities are less accurate, because individual base pair changes only cause comparatively small changes in the disease risk. +// Polygenic diseases are also more influenced by environmental factors, further decreasing risk accuracy. + +//TODO: Eventually we want to use neural networks for both polygenic disease and trait prediction. +// This package is currently a temporary, less accurate solution until we get access to the necessary training data. +// It may still be worth keeping this method in place for polygenic diseases and using neural networks in tandem. + +import "errors" + +// DiseaseLocus is a location on a human genome that has an effect on the disease +type DiseaseLocus struct{ + + // 3 byte identifier, encoded in Hex. + LocusIdentifier string + + // RSID that represents this locus + // If multiple RSIDs represent the same locus, use the first rsID for the locus in the locusMetadata package + LocusRSID int64 + + // Map Structure: Base pair -> Effect weight (Positive number = increased risk, negative number = decreased risk) + // The number only indicates a general effect of the base, for bases for which we do not have risk probability statistics + // 0 indicates that the base has no effect + RiskWeightsMap map[string]int + + // Minimum and maximum values in above map + MinimumRiskWeight int + MaximumRiskWeight int + + // Map Structure: Base Pair -> Odds ratio of BasePair/Normal (common) Base pair + // Number is greater than 1 = Increases risk, Number is less than 1 = decreases risk + // 1 indicates that the base has no impact + OddsRatiosMap map[string]float64 + + // Map Structure: Base Pair -> Probability that a person will have that base pair for the general population + BasePairProbabilitiesMap map[string]float64 + + // Map Structure: Reference name -> Reference link + References map[string]string +} + + +type PolygenicDisease struct{ + + DiseaseName string + + DiseaseDescription string + + // Is either "Mate"/"Female"/"Both" + EffectedSex string + + LociList []DiseaseLocus + + // Inputs: + // -string: "Mate"/"Female" + // -int: Age + // Output: + // -float64: Average probability of having had the disease at that point in person's life + // -error + GetAverageRiskProbabilitiesFunction func(string, int)(float64, error) + + // Map Structure: Reference name -> Reference link + References map[string]string +} + +var polygenicDiseaseNamesList []string +var polygenicDiseaseObjectsList []PolygenicDisease + +// This must be called once during application startup +func InitializePolygenicDiseaseVariables(){ + + breastCancerObject := getBreastCancerDiseaseObject() + + polygenicDiseaseObjectsList = []PolygenicDisease{breastCancerObject} + + polygenicDiseaseNamesList = make([]string, 0, len(polygenicDiseaseObjectsList)) + + for _, diseaseObject := range polygenicDiseaseObjectsList{ + + diseaseName := diseaseObject.DiseaseName + + polygenicDiseaseNamesList = append(polygenicDiseaseNamesList, diseaseName) + } +} + +// Be aware that all of these functions are returning original objects/slices, not copies +// Thus, we cannot edit the objects/slices that are returned. We must copy the fields first if we want to edit them. + +func GetPolygenicDiseaseNamesList()([]string, error){ + + if (polygenicDiseaseNamesList == nil){ + return nil, errors.New("GetDiseaseNamesList called when list is not initialized.") + } + + return polygenicDiseaseNamesList, nil +} + +func GetPolygenicDiseaseObjectsList()([]PolygenicDisease, error){ + + if (polygenicDiseaseObjectsList == nil){ + return nil, errors.New("GetPolygenicDiseaseObjectsList called when list is not initialized.") + } + + return polygenicDiseaseObjectsList, nil +} + +func GetPolygenicDiseaseObject(diseaseName string)(PolygenicDisease, error){ + + polygenicDiseaseObjectsList, err := GetPolygenicDiseaseObjectsList() + if (err != nil) { return PolygenicDisease{}, err } + + for _, diseaseObject := range polygenicDiseaseObjectsList{ + + currentDiseaseName := diseaseObject.DiseaseName + if (currentDiseaseName != diseaseName){ + continue + } + + return diseaseObject, nil + } + + return PolygenicDisease{}, errors.New("GetPolygenicDiseaseObject called with unknown disease name: " + diseaseName) +} + + +//Outputs: +// -map[string]DiseaseLocus: Map of LocusIdentifier -> LocusObject +// -error (will return err if diseaseName is not found) +func GetPolygenicDiseaseLociMap(diseaseName string)(map[string]DiseaseLocus, error){ + + diseaseObject, err := GetPolygenicDiseaseObject(diseaseName) + if (err != nil) { return nil, err } + + diseaseLociList := diseaseObject.LociList + + diseaseLociMap := make(map[string]DiseaseLocus) + + for _, locusObject := range diseaseLociList{ + + locusIdentifier := locusObject.LocusIdentifier + diseaseLociMap[locusIdentifier] = locusObject + } + + return diseaseLociMap, nil +} + +func GetPolygenicDiseaseLocusObject(diseaseName string, locusIdentifier string)(DiseaseLocus, error){ + + diseaseLociMap, err := GetPolygenicDiseaseLociMap(diseaseName) + if (err != nil){ return DiseaseLocus{}, err } + + locusObject, exists := diseaseLociMap[locusIdentifier] + if (exists == false){ + return DiseaseLocus{}, errors.New("GetDiseaseLocusObject called with unknown locus identifier: " + locusIdentifier) + } + + return locusObject, nil +} + + + diff --git a/resources/geneticReferences/traits/eyeColor.go b/resources/geneticReferences/traits/eyeColor.go new file mode 100644 index 0000000..46cd713 --- /dev/null +++ b/resources/geneticReferences/traits/eyeColor.go @@ -0,0 +1,134 @@ +package traits + + +func getEyeColorTraitObject()Trait{ + + eyeColorLociList := []int64{ + + //TODO: Add more SNPs. + + // These SNPs are taken from https://www.snpedia.com/index.php/Eye_color + + 2733832, + 1800401, + 1800407, + 1800414, + 12913823, + 4911442, + 6058017, + 4911414, + 4778241, + 12593929, + 7183877, + 3935591, + 7170852, + 2238289, + 3940272, + 8028689, + 2240203, + 11631797, + 916977, + 3768056, + 728405, + 2835621, + 892839, + 9782955, + 12452184, + 10209564, + 1325127, + 7277820, + 1105879, + 7219915, + 12913832, + 2070959, + 2835630, + 9894429, + 1393350, + 2252893, + 1003719, + 3794604, + 7174027, + 989869, + 4778138, + 12906280, + + // These SNPs are taken from https://pubmed.ncbi.nlm.nih.gov/20546537/ + 12203592, + 1408799, + 1126809, + 12896399, + 7495174, + 1667394, + + // These SNPs are taken from https://pubmed.ncbi.nlm.nih.gov/33692100/ + 6693258, + 351385, + 2385028, + 13016869, + 112747614, + 121908120, + 12614022, + 74409360, + 3912104, + 116359091, + 4521336, + 141318671, + 6828137, + 62330021, + 16891982, + 348613, + 72777200, + 11957757, + //12203592, (is a duplicate, was also found in the other study) + 6910861, + 341147, + 2854746, + 6944702, + 6997494, + 12543326, + 147068120, + 13297008, + 12552712, + 12335410, + 72928978, + //1126809, (is a duplicate, was also found in the other study) + 9971729, + 790464, + 2095645, + 9301973, + 138777265, + 17184180, + 4778218, + 1129038, + 1426654, + 4790309, + 3809761, + 6420484, + 73488486, + 2748901, + 2835660, + 622330, + 35051352, + + // TODO: Add these loci once we can handle X Chromosome loci. + // 78542430, + // 5957354, + } + + referencesMap := make(map[string]string) + referencesMap["SNPedia.com - Eye Color"] = "https://www.snpedia.com/index.php/Eye_color" + referencesMap["Genome-wide association studies of pigmentation and skin cancer: a review and meta-analysis"] = "https://pubmed.ncbi.nlm.nih.gov/20546537/" + referencesMap["Genome-wide association study in almost 195,000 individuals identifies 50 previously unidentified genetic loci for eye color."] = "https://pubmed.ncbi.nlm.nih.gov/33692100/" + + eyeColorObject := Trait{ + TraitName: "Eye Color", + TraitDescription: "The color of a person's eyes.", + LociList: eyeColorLociList, + RulesList: []TraitRule{}, + OutcomesList: []string{}, + References: referencesMap, + } + + return eyeColorObject +} + diff --git a/resources/geneticReferences/traits/facialStructure.go b/resources/geneticReferences/traits/facialStructure.go new file mode 100644 index 0000000..51cb0fe --- /dev/null +++ b/resources/geneticReferences/traits/facialStructure.go @@ -0,0 +1,131 @@ +package traits + + +func getFacialStructureTraitObject()Trait{ + + facialStructureLociList := []int64{ + + //TODO: Add more SNPs. + + // These SNPs are from https://www.snpedia.com/index.php/Appearance + 4648477, + 4648478, + 7516150, + 7552331, + 1572037, + 1999527, + 4648379, + 1005999, + 2422239, + 2422241, + 6749293, + 16863422, + 12694574, + 2894450, + 974448, + 1978859, + 2034127, + 2034128, + 2034129, + 7628370, + 7617069, + 717463, + 894883, + 7640340, + 875143, + 6795519, + 2168809, + 9858909, + 4552364, + 4353811, + 13098099, + 13097965, + 17447439, + 6555969, + 2342494, + 10237319, + 10265937, + 10266101, + 10237838, + 10237488, + 10278187, + 10234405, + 6950754, + 6462544, + 7803030, + 12155314, + 9692219, + 1562006, + 1562005, + 7779616, + 7799331, + 7781059, + 7807181, + 6462562, + 10485860, + 2108166, + 1158810, + 6478394, + 805722, + 11191909, + 1747677, + 805693, + 805694, + 9971100, + 2274107, + 12358982, + 11604811, + 11237982, + 1939697, + 1939707, + 10843104, + 17252053, + 4433629, + 7966317, + 1887276, + 7965082, + 12437560, + 16977002, + 16977008, + 16977009, + 4793389, + 8079498, + 7214306, + 1019212, + 1008591, + 2327101, + 6056066, + 1015092, + 975633, + 6039266, + 2327089, + 4633993, + 4053148, + 6039272, + 6056119, + 6056126, + 911020, + 6020940, + 6020957, + 911015, + 2832438, + 397723, + } + + referencesMap := make(map[string]string) + referencesMap["SNPedia.com - Appearance"] = "https://www.snpedia.com/index.php/Appearance" + referencesMap["A Genome-Wide Association Study Identifies Five Loci Influencing Facial Morphology in Europeans"] = "https://journals.plos.org/plosgenetics/article?id=10.1371/journal.pgen.1002932" + + facialStructureObject := Trait{ + TraitName: "Facial Structure", + TraitDescription: "The structure of a person's face.", + LociList: facialStructureLociList, + RulesList: []TraitRule{}, + OutcomesList: []string{}, + References: referencesMap, + } + + return facialStructureObject +} + + diff --git a/resources/geneticReferences/traits/hairColor.go b/resources/geneticReferences/traits/hairColor.go new file mode 100644 index 0000000..3d6c02e --- /dev/null +++ b/resources/geneticReferences/traits/hairColor.go @@ -0,0 +1,51 @@ +package traits + +// Hair color is influenced by thousands of genes +// We only have a few listed here + + +func getHairColorTraitObject()Trait{ + + hairColorLociList := []int64{ + + //These loci were taken from https://pubmed.ncbi.nlm.nih.gov/20546537/ + + 28777, + 12203592, + 1540771, + 6918152, + 35264875, + 3829241, + 12821256, + 12896399, + 7495174, + 4778211, + 7174027, + 11855019, + 1667394, + 12913832, + 7183877, + 11636232, + 8028689, + 8039195, + 1805007, + 1805008, + } + + referencesMap := make(map[string]string) + referencesMap["SNPedia.com - Appearance"] = "https://www.snpedia.com/index.php/Appearance" + referencesMap["Genome-wide association studies of pigmentation and skin cancer: a review and meta-analysis"] = "https://pubmed.ncbi.nlm.nih.gov/20546537/" + + hairColorObject := Trait{ + TraitName: "Hair Color", + TraitDescription: "The color of a person's hair.", + LociList: hairColorLociList, + RulesList: []TraitRule{}, + OutcomesList: []string{}, + References: referencesMap, + } + + return hairColorObject +} + + diff --git a/resources/geneticReferences/traits/hairTexture.go b/resources/geneticReferences/traits/hairTexture.go new file mode 100644 index 0000000..b487c1f --- /dev/null +++ b/resources/geneticReferences/traits/hairTexture.go @@ -0,0 +1,220 @@ +package traits + + + +func getHairTextureTraitObject()Trait{ + + rule1_ReferencesMap := make(map[string]string) + rule1_ReferencesMap["SNPedia.com - rs7349332"] = "https://www.snpedia.com/index.php/Rs7349332" + + rule1_Locus1Object := RuleLocus{ + + LocusIdentifier: "0e06e2", + LocusRSID: 7349332, + BasePairsList: []string{"C;C"}, + } + rule1_LociList := []RuleLocus{rule1_Locus1Object} + + rule1_OutcomePointsMap := make(map[string]int) + rule1_OutcomePointsMap["Straight"] = 2 + + rule1_Object := TraitRule{ + RuleIdentifier: "fde405", + LociList: rule1_LociList, + OutcomePointsMap: rule1_OutcomePointsMap, + References: rule1_ReferencesMap, + } + + //TODO: Make sure this is true, that a heterozygote has a higher likelihood of curly hair + rule2_ReferencesMap := make(map[string]string) + rule2_ReferencesMap["SNPedia.com - rs7349332"] = "https://www.snpedia.com/index.php/Rs7349332" + + rule2_Locus1Object := RuleLocus{ + + LocusIdentifier: "2da1b7", + LocusRSID: 7349332, + BasePairsList: []string{"C;T", "T;C"}, + } + rule2_LociList := []RuleLocus{rule2_Locus1Object} + + rule2_OutcomePointsMap := make(map[string]int) + rule2_OutcomePointsMap["Curly"] = 1 + + rule2_Object := TraitRule{ + RuleIdentifier: "6bd1da", + LociList: rule2_LociList, + OutcomePointsMap: rule2_OutcomePointsMap, + References: rule2_ReferencesMap, + } + + rule3_ReferencesMap := make(map[string]string) + rule3_ReferencesMap["SNPedia.com - rs7349332"] = "https://www.snpedia.com/index.php/Rs7349332" + + rule3_Locus1Object := RuleLocus{ + + LocusIdentifier: "c6760e", + LocusRSID: 7349332, + BasePairsList: []string{"T;T"}, + } + rule3_LociList := []RuleLocus{rule3_Locus1Object} + + rule3_OutcomePointsMap := make(map[string]int) + rule3_OutcomePointsMap["Curly"] = 2 + + rule3_Object := TraitRule{ + RuleIdentifier: "32e377", + LociList: rule3_LociList, + OutcomePointsMap: rule3_OutcomePointsMap, + References: rule3_ReferencesMap, + } + + rule4_ReferencesMap := make(map[string]string) + rule4_ReferencesMap["SNPedia.com - rs11803731"] = "https://www.snpedia.com/index.php/Rs11803731" + + rule4_Locus1Object := RuleLocus{ + + LocusIdentifier: "9079c9", + LocusRSID: 11803731, + BasePairsList: []string{"A;A"}, + } + rule4_LociList := []RuleLocus{rule4_Locus1Object} + + rule4_OutcomePointsMap := make(map[string]int) + rule4_OutcomePointsMap["Straight"] = 2 + + rule4_Object := TraitRule{ + RuleIdentifier: "34e6d2", + LociList: rule4_LociList, + OutcomePointsMap: rule4_OutcomePointsMap, + References: rule4_ReferencesMap, + } + + //TODO: Make sure this is true, that a heterozygote has a higher likelihood of curly hair + rule5_ReferencesMap := make(map[string]string) + rule5_ReferencesMap["SNPedia.com - rs11803731"] = "https://www.snpedia.com/index.php/Rs11803731" + + rule5_Locus1Object := RuleLocus{ + + LocusIdentifier: "d0aad3", + LocusRSID: 11803731, + BasePairsList: []string{"A;T", "T;A"}, + } + rule5_LociList := []RuleLocus{rule5_Locus1Object} + + rule5_OutcomePointsMap := make(map[string]int) + rule5_OutcomePointsMap["Curly"] = 1 + + rule5_Object := TraitRule{ + RuleIdentifier: "cf6cb5", + LociList: rule5_LociList, + OutcomePointsMap: rule5_OutcomePointsMap, + References: rule5_ReferencesMap, + } + + rule6_ReferencesMap := make(map[string]string) + rule6_ReferencesMap["SNPedia.com - rs11803731"] = "https://www.snpedia.com/index.php/Rs11803731" + + rule6_Locus1Object := RuleLocus{ + + LocusIdentifier: "f554b5", + LocusRSID: 11803731, + BasePairsList: []string{"T;T"}, + } + rule6_LociList := []RuleLocus{rule6_Locus1Object} + + rule6_OutcomePointsMap := make(map[string]int) + rule6_OutcomePointsMap["Curly"] = 2 + + rule6_Object := TraitRule{ + RuleIdentifier: "2ba65b", + LociList: rule6_LociList, + OutcomePointsMap: rule6_OutcomePointsMap, + References: rule6_ReferencesMap, + } + + rule7_ReferencesMap := make(map[string]string) + rule7_ReferencesMap["SNPedia.com - rs17646946"] = "https://www.snpedia.com/index.php/Rs17646946" + + rule7_Locus1Object := RuleLocus{ + + LocusIdentifier: "f500c2", + LocusRSID: 17646946, + BasePairsList: []string{"G;G"}, + } + rule7_LociList := []RuleLocus{rule7_Locus1Object} + + rule7_OutcomePointsMap := make(map[string]int) + rule7_OutcomePointsMap["Straight"] = 2 + + rule7_Object := TraitRule{ + RuleIdentifier: "ae3274", + LociList: rule7_LociList, + OutcomePointsMap: rule7_OutcomePointsMap, + References: rule7_ReferencesMap, + } + + rule8_ReferencesMap := make(map[string]string) + rule8_ReferencesMap["SNPedia.com - rs17646946"] = "https://www.snpedia.com/index.php/Rs17646946" + + rule8_Locus1Object := RuleLocus{ + + LocusIdentifier: "f1144a", + LocusRSID: 17646946, + BasePairsList: []string{"A;G", "G;A"}, + } + rule8_LociList := []RuleLocus{rule8_Locus1Object} + + rule8_OutcomePointsMap := make(map[string]int) + rule8_OutcomePointsMap["Curly"] = 1 + + rule8_Object := TraitRule{ + RuleIdentifier: "a546bf", + LociList: rule8_LociList, + OutcomePointsMap: rule8_OutcomePointsMap, + References: rule8_ReferencesMap, + } + + rule9_ReferencesMap := make(map[string]string) + rule9_ReferencesMap["SNPedia.com - rs17646946"] = "https://www.snpedia.com/index.php/Rs17646946" + + rule9_Locus1Object := RuleLocus{ + + LocusIdentifier: "468bb3", + LocusRSID: 17646946, + BasePairsList: []string{"A;A"}, + } + rule9_LociList := []RuleLocus{rule9_Locus1Object} + + rule9_OutcomePointsMap := make(map[string]int) + rule9_OutcomePointsMap["Curly"] = 2 + + rule9_Object := TraitRule{ + RuleIdentifier: "b8dc0a", + LociList: rule9_LociList, + OutcomePointsMap: rule9_OutcomePointsMap, + References: rule9_ReferencesMap, + } + + hairTextureRulesList := []TraitRule{rule1_Object, rule2_Object, rule3_Object, rule4_Object, rule5_Object, rule6_Object, rule7_Object, rule8_Object, rule9_Object} + + hairTextureLociList := []int64{17646946, 11803731, 7349332} + + referencesMap := make(map[string]string) + referencesMap["SNPedia.com - Hair Curliness"] = "https://www.snpedia.com/index.php/Hair_curliness" + + outcomesList := []string{"Straight", "Curly"} + + hairTextureObject := Trait{ + + TraitName: "Hair Texture", + TraitDescription: "The texture of a person's head hair.", + LociList: hairTextureLociList, + RulesList: hairTextureRulesList, + OutcomesList: outcomesList, + References: referencesMap, + } + + return hairTextureObject +} + + diff --git a/resources/geneticReferences/traits/lactoseTolerance.go b/resources/geneticReferences/traits/lactoseTolerance.go new file mode 100644 index 0000000..ca36490 --- /dev/null +++ b/resources/geneticReferences/traits/lactoseTolerance.go @@ -0,0 +1,133 @@ +package traits + + + +func getLactoseToleranceTraitObject()Trait{ + + rule1_ReferencesMap := make(map[string]string) + rule1_ReferencesMap["SNPedia.com - rs182549"] = "https://www.snpedia.com/index.php/Rs182549" + + rule1_Locus1Object := RuleLocus{ + + LocusIdentifier: "43bf19", + LocusRSID: 182549, + BasePairsList: []string{"C;C"}, + } + rule1_LociList := []RuleLocus{rule1_Locus1Object} + + rule1_OutcomePointsMap := make(map[string]int) + rule1_OutcomePointsMap["Intolerant"] = 1 + + rule1_Object := TraitRule{ + RuleIdentifier: "f4e02c", + LociList: rule1_LociList, + OutcomePointsMap: rule1_OutcomePointsMap, + References: rule1_ReferencesMap, + } + + rule2_ReferencesMap := make(map[string]string) + rule2_ReferencesMap["SNPedia.com - rs182549"] = "https://www.snpedia.com/index.php/Rs182549" + + rule2_Locus1Object := RuleLocus{ + + LocusIdentifier: "a7feff", + LocusRSID: 182549, + BasePairsList: []string{"C;T", "T;C", "C;T", "T;T"}, + } + rule2_LociList := []RuleLocus{rule2_Locus1Object} + + rule2_OutcomePointsMap := make(map[string]int) + rule2_OutcomePointsMap["Tolerant"] = 1 + + rule2_Object := TraitRule{ + RuleIdentifier: "cc3df0", + LociList: rule2_LociList, + OutcomePointsMap: rule2_OutcomePointsMap, + References: rule2_ReferencesMap, + } + + rule3_ReferencesMap := make(map[string]string) + rule3_ReferencesMap["SNPedia.com - rs4988235"] = "https://www.snpedia.com/index.php/Rs4988235" + + rule3_Locus1Object := RuleLocus{ + + LocusIdentifier: "da6b04", + LocusRSID: 4988235, + BasePairsList: []string{"G;G"}, + } + rule3_LociList := []RuleLocus{rule3_Locus1Object} + + rule3_OutcomePointsMap := make(map[string]int) + rule3_OutcomePointsMap["Intolerant"] = 1 + + rule3_Object := TraitRule{ + RuleIdentifier: "8170ee", + LociList: rule3_LociList, + OutcomePointsMap: rule3_OutcomePointsMap, + References: rule3_ReferencesMap, + } + + rule4_ReferencesMap := make(map[string]string) + rule4_ReferencesMap["SNPedia.com - rs4988235"] = "https://www.snpedia.com/index.php/Rs4988235" + + rule4_Locus1Object := RuleLocus{ + + LocusIdentifier: "176dde", + LocusRSID: 4988235, + BasePairsList: []string{"G;A", "A;G"}, + } + rule4_LociList := []RuleLocus{rule4_Locus1Object} + + rule4_OutcomePointsMap := make(map[string]int) + rule4_OutcomePointsMap["Tolerant"] = 1 + + rule4_Object := TraitRule{ + RuleIdentifier: "52425f", + LociList: rule4_LociList, + OutcomePointsMap: rule4_OutcomePointsMap, + References: rule4_ReferencesMap, + } + + rule5_ReferencesMap := make(map[string]string) + rule5_ReferencesMap["SNPedia.com - rs4988235"] = "https://www.snpedia.com/index.php/Rs4988235" + + rule5_Locus1Object := RuleLocus{ + + LocusIdentifier: "164acb", + LocusRSID: 4988235, + BasePairsList: []string{"A;A"}, + } + rule5_LociList := []RuleLocus{rule5_Locus1Object} + + rule5_OutcomePointsMap := make(map[string]int) + rule5_OutcomePointsMap["Tolerant"] = 2 + + rule5_Object := TraitRule{ + RuleIdentifier: "4b5c35", + LociList: rule5_LociList, + OutcomePointsMap: rule5_OutcomePointsMap, + References: rule5_ReferencesMap, + } + + lactoseToleranceRulesList := []TraitRule{rule1_Object, rule2_Object, rule3_Object, rule4_Object, rule5_Object} + + lactoseToleranceLociList := []int64{4988235, 182549} + + referencesMap := make(map[string]string) + referencesMap["SNPedia.com - Lactose Intolerance"] = "https://www.snpedia.com/index.php/Lactose_intolerance" + + outcomesList := []string{"Tolerant", "Intolerant"} + + lactoseToleranceObject := Trait{ + TraitName: "Lactose Tolerance", + TraitDescription: "The ability to tolerate lactose.", + LociList: lactoseToleranceLociList, + RulesList: lactoseToleranceRulesList, + OutcomesList: outcomesList, + References: referencesMap, + } + + return lactoseToleranceObject +} + + diff --git a/resources/geneticReferences/traits/skinColor.go b/resources/geneticReferences/traits/skinColor.go new file mode 100644 index 0000000..79e7af8 --- /dev/null +++ b/resources/geneticReferences/traits/skinColor.go @@ -0,0 +1,52 @@ +package traits + + + +func getSkinColorTraitObject()Trait{ + + skinColorLociList := []int64{ + + //TODO: Add more SNPs. + + // These SNPs are from https://www.snpedia.com/index.php/Appearance + 1800422, + 1126809, + 26722, + 1426654, + 642742, + + // These SNPs are from https://pubmed.ncbi.nlm.nih.gov/20546537/ + 16891982, + 12203592, + 1042602, + 1834640, + + // These SNPs are from https://link.springer.com/article/10.1007/s00403-019-01891-3 + 7182710, + 784416, + 7176696, + 937171, + 2762462, + 10424065, + 142317543, + 3212369, + 3212368, + } + + referencesMap := make(map[string]string) + referencesMap["SNPedia.com - Appearance"] = "https://www.snpedia.com/index.php/Appearance" + referencesMap["Genome-wide association studies of pigmentation and skin cancer: a review and meta-analysis"] = "https://pubmed.ncbi.nlm.nih.gov/20546537/" + referencesMap["Meta-analysis and prioritization of human skin pigmentation-associated GWAS-SNPs using ENCODE data-based web-tools"] = "https://link.springer.com/article/10.1007/s00403-019-01891-3" + + skinColorObject := Trait{ + TraitName: "Skin Color", + TraitDescription: "The color of a person's skin.", + LociList: skinColorLociList, + RulesList: []TraitRule{}, + OutcomesList: []string{}, + References: referencesMap, + } + + return skinColorObject +} + diff --git a/resources/geneticReferences/traits/traits.go b/resources/geneticReferences/traits/traits.go new file mode 100644 index 0000000..3259188 --- /dev/null +++ b/resources/geneticReferences/traits/traits.go @@ -0,0 +1,209 @@ + +// traits provides information about traits and the SNPs that influence them + +package traits + +// TODO: We want to eventually use neural nets for both trait and polygenic disease analysis +// These will be trained on a set of genomes and will output a probability analysis for each trait +// This is only possible once we get access to the necessary training data +// +// See createGeneticAnalysis.go for an explanation of how offspring trait prediction could work with neural nets + +import "errors" + +type RuleLocus struct{ + + // 3 byte hex encoded string + LocusIdentifier string + + // RSID that represents this locus + // If multiple RSIDs represent the same locus, use the first rsid for the locus in the locusMetadata package + LocusRSID int64 + + // List of base pair values that this RSID must fulfill to pass the rule + // As long as the value matches any base pair value in the list, the genome has passed this rule locus + // The genome must pass every rule locus within a rule to pass the rule + BasePairsList []string +} + + +type TraitRule struct{ + + // 3 byte identifier encoded hex + RuleIdentifier string + + // A list of RuleLocus objects which comprise the rule + // The genome must have a required base pair for each locus in this list to pass the rule + LociList []RuleLocus + + // The outcome that this rule will effect + // The number of points to add to the outcome if the rule RSID values are fulfilled + // Do not use negative values + + // Map Structure: Outcome name -> Points to add to outcome if rule passes + OutcomePointsMap map[string]int + + // Map structure: Reference name -> Reference link + References map[string]string +} + + +type Trait struct{ + + TraitName string + + TraitDescription string + + // This is a list of rsIDs which are known to have an effect on this trait + // These loci may not have any associated rules + // We use these loci to calculate Racial Similarity. + // We will also use neural networks to predict trait outcome scores using these loci + LociList []int64 + + // This list can be empty if no rules exist + // An empty list means we are relying on LociList and neural networks for trait prediction. + RulesList []TraitRule + + // List of outcomes + // Example: "Lactose Intolerant", "Lactore Tolerant" + // This list can be empty if outcomes are not text descriptions (Example: Facial structure) + // If there are no outcomes, then no rules can exist + OutcomesList []string + + // Map structure: Reference name -> Reference link + References map[string]string +} + +var traitNamesList []string +var traitObjectsList []Trait + +// Map Structure: Rule locus identifier -> RSID representing this locus +var locusRSIDsMap map[string]int64 + +func InitializeTraitVariables(){ + + lactoseToleranceObject := getLactoseToleranceTraitObject() + hairTextureObject := getHairTextureTraitObject() + facialStructureObject := getFacialStructureTraitObject() + eyeColorObject := getEyeColorTraitObject() + hairColorObject := getHairColorTraitObject() + skinColorObject := getSkinColorTraitObject() + + traitObjectsList = []Trait{lactoseToleranceObject, hairTextureObject, facialStructureObject, eyeColorObject, hairColorObject, skinColorObject} + + traitNamesList = make([]string, 0, len(traitObjectsList)) + locusRSIDsMap = make(map[string]int64) + + for _, traitObject := range traitObjectsList{ + + traitName := traitObject.TraitName + + traitNamesList = append(traitNamesList, traitName) + + traitRulesList := traitObject.RulesList + + for _, traitRuleObject := range traitRulesList{ + + ruleLociList := traitRuleObject.LociList + + for _, ruleLocusObject := range ruleLociList{ + + locusIdentifier := ruleLocusObject.LocusIdentifier + locusRSID := ruleLocusObject.LocusRSID + + locusRSIDsMap[locusIdentifier] = locusRSID + } + } + } +} + +// Be aware that all of these functions are returning original objects/slices, not copies +// Thus, we cannot edit the objects/slices that are returned. We must copy the fields first if we want to edit them. + +func GetTraitNamesList()([]string, error){ + + if (traitNamesList == nil){ + return nil, errors.New("GetTraitNamesList called when list is not initialized.") + } + + return traitNamesList, nil +} + +func GetTraitObjectsList()([]Trait, error){ + + if (traitObjectsList == nil){ + return nil, errors.New("GetTraitObjectsList called when list is not initialized.") + } + + return traitObjectsList, nil +} + +func GetTraitObject(traitName string)(Trait, error){ + + traitObjectsList, err := GetTraitObjectsList() + if (err != nil) { return Trait{}, err } + + for _, traitObject := range traitObjectsList{ + + currentTraitName := traitObject.TraitName + if (currentTraitName != traitName){ + continue + } + + return traitObject, nil + } + + return Trait{}, errors.New("GetTraitObject called with unknown trait: " + traitName) +} + +//Outputs: +// -map[string]TraitRule: Map of RuleIdentifier -> Rule Object +// -error (will return err if traitName is not found) +func GetTraitRulesMap(traitName string)(map[string]TraitRule, error){ + + traitObject, err := GetTraitObject(traitName) + if (err != nil) { return nil, err } + + traitRulesList := traitObject.RulesList + + traitRulesMap := make(map[string]TraitRule) + + for _, ruleObject := range traitRulesList{ + + ruleIdentifier := ruleObject.RuleIdentifier + traitRulesMap[ruleIdentifier] = ruleObject + } + + return traitRulesMap, nil +} + +func GetTraitRuleObject(traitName string, ruleIdentifier string)(TraitRule, error){ + + traitRulesMap, err := GetTraitRulesMap(traitName) + if (err != nil){ return TraitRule{}, err } + + ruleObject, exists := traitRulesMap[ruleIdentifier] + if (exists == false){ + return TraitRule{}, errors.New("GetTraitRuleObject called with unknown ruleIdentifier: " + ruleIdentifier) + } + + return ruleObject, nil +} + +//Outputs: +// -int64: The rsID which represents this locus +// -error +func GetTraitRuleLocusRSID(locusIdentifier string)(int64, error){ + + if (locusRSIDsMap == nil){ + return 0, errors.New("GetTraitRuleLocusRSID called when locusRSIDsMap is not initialized.") + } + + locusRSID, exists := locusRSIDsMap[locusIdentifier] + if (exists == false){ + return 0, errors.New("GetTraitRuleLocusRSID called with unknown locusIdentifier: " + locusIdentifier) + } + + return locusRSID, nil +} + diff --git a/resources/imageFiles/emojiCategories.go b/resources/imageFiles/emojiCategories.go new file mode 100644 index 0000000..4d5a854 --- /dev/null +++ b/resources/imageFiles/emojiCategories.go @@ -0,0 +1,77 @@ + +package imageFiles + +// This file provides the categories that each emoji belongs to +// This file can be updated without requiring a new profile/message version + +import "errors" + +var categoryNamesList []string = []string{"People", "Circle Face", "Bodies", "Creatures", "Food", "Hands", "Flags", "Miscellaneous"} + +func GetEmojiCategoriesList()[]string{ + + return categoryNamesList +} + +func GetEmojisInCategoryList(categoryName string)([]int, error){ + + if (categoryName == "People"){ + + return emojiCategoryList_People, nil + } + + if (categoryName == "Circle Face"){ + + return emojiCategoryList_CircleFace, nil + } + if (categoryName == "Bodies"){ + + return emojiCategoryList_Bodies, nil + } + + if (categoryName == "Creatures"){ + + return emojiCategoryList_Creatures, nil + } + + if (categoryName == "Food"){ + + return emojiCategoryList_Food, nil + } + + if (categoryName == "Hands"){ + + return emojiCategoryList_Hands, nil + } + + if (categoryName == "Flags"){ + + return emojiCategoryList_Flags, nil + } + + if (categoryName == "Miscellaneous"){ + + return emojiCategoryList_Miscellaneous, nil + } + + return nil, errors.New("GetEmojisInCategoryList called with invalid category name: " + categoryName) +} + + +var emojiCategoryList_People []int = []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, 96, 97, 98, 99, 100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 123, 124, 125, 126, 127, 128, 129, 130, 131, 132, 133, 134, 135, 136, 137, 138, 139, 140, 141, 142, 143, 144, 145, 146, 147, 148, 149, 150, 151, 152, 153, 154, 155, 156, 157, 158, 159, 160, 161, 162, 163, 164, 165, 166, 167, 168, 169, 170, 171, 172, 173, 174, 175, 176, 177, 178, 179, 180, 181, 182, 183, 184, 185, 186, 187, 188, 189, 190, 191, 192, 193, 194, 195, 196, 197, 198, 199, 200, 201, 202, 203, 204, 205, 206, 207, 208, 209, 210, 211, 212, 213, 214, 215, 216, 217, 218, 219, 220, 221, 222, 223, 224, 225, 226, 227, 228, 229, 230, 231, 232, 233, 234, 235, 236, 237, 238, 239, 240, 241, 242, 243, 244, 245, 246, 247, 248, 249, 250, 251, 252, 253, 254, 255, 256, 257, 258, 259, 260, 261, 262, 263, 264, 265, 266, 267, 268, 269, 270, 271, 272, 273, 274, 275, 276, 277, 278, 279, 280, 281, 282, 283, 284, 285, 286, 287, 288, 289, 290, 291, 292, 293, 294, 295, 296, 297, 298, 299, 300, 301, 302, 303, 304, 305, 306, 307, 308, 309, 310, 311, 312, 313, 314, 315, 316, 317, 318, 319, 320, 321, 322, 323, 324, 325, 326, 327, 328, 329, 330, 331, 332, 333, 334, 335, 336, 337, 338, 339, 340, 341, 342, 343, 344, 345, 346, 347, 348, 349, 350, 351, 352, 353, 354, 355, 356, 357, 358, 359, 360, 361, 362, 363, 364, 365, 366, 367, 368, 369, 370, 371, 372, 373, 374, 375, 376, 377, 378, 379, 380, 381, 382, 383, 384, 385, 386, 387, 388, 389, 390, 391, 392, 393, 394, 395, 396, 397, 398, 399, 400, 401, 402, 403, 404, 405, 406, 407, 408, 409, 410, 411, 412, 413, 414, 415, 416, 417, 418, 419, 420, 421, 422, 423, 424, 425, 426, 427, 428, 429, 430, 431, 432, 433, 434, 435, 436, 437, 438, 439, 440, 441, 442, 443, 444, 445, 446, 447, 448, 449, 450, 451, 452, 453, 454, 455, 456, 457, 458, 459, 460, 461, 462, 463, 464, 465, 466, 467, 468, 469, 470, 471, 472, 473, 474, 475, 476, 477, 478, 479, 480, 481, 482, 483, 484, 485, 486, 487, 488, 489, 490, 491, 492, 493, 494, 495, 496, 497, 498, 499, 500, 501, 502, 503, 504, 505, 506, 507, 508, 509, 510, 511, 512, 513, 514, 515, 516, 517, 518, 519, 520, 521, 522, 523, 524, 525, 526, 527, 528, 529, 530, 531, 532, 533, 534, 535, 536, 537, 538, 539, 540, 541, 542, 543, 544, 545, 546, 547, 548, 549, 550, 551, 552, 553, 554, 555, 556, 557, 558, 559, 560, 561, 562, 563, 564, 565, 566, 567, 568, 569, 570, 571, 572, 573, 574, 575, 576, 577, 578, 579, 580, 581, 582, 583, 584, 585, 586, 587, 588, 589, 590, 591, 592, 593, 594, 595, 596, 597, 598, 599, 600, 601, 602, 603, 604, 605, 606, 607, 608, 609, 610, 611, 612, 613, 614, 615, 616, 617, 618, 619, 620, 621, 622, 623, 624, 625, 626, 627, 628, 629, 630, 631, 632, 633, 634, 635, 636, 637, 638, 639, 640, 641, 642, 643, 644, 645, 646, 647, 648, 649, 650, 651, 652, 653, 654, 655, 656, 657, 658, 659, 660, 661, 662, 663, 664, 665, 666, 667, 668, 669, 670, 671, 672, 673, 674, 675, 676, 677, 678, 679, 680, 681, 682, 683, 684, 685, 686, 687, 688, 689, 690, 691, 692, 693, 694, 695, 696, 697, 698, 699, 700, 701, 702, 703, 704, 705, 706, 707, 708, 709, 710, 711, 712, 713, 714, 715, 716, 717, 718, 719, 720, 721, 722, 723, 724, 725, 726, 727, 728, 729, 730, 731, 732, 733, 734, 735, 736, 737, 738, 739, 740, 741, 742, 743, 744, 745, 746, 747, 748, 749, 750, 751, 752, 753, 754, 755, 756, 757, 758, 759, 760, 761, 762, 763, 764, 765, 766, 767, 768, 769, 770, 771, 772, 773, 774, 775, 776, 777, 778, 779, 780, 781, 782, 783, 784, 785, 786, 787, 788, 789, 790, 791, 792, 793, 794, 795, 796, 797, 798, 799, 800, 801, 802, 803, 804, 805, 806, 807, 808, 809, 810, 811, 812, 813, 814, 815, 816, 817, 818, 819, 820, 821, 822, 823, 824, 825, 826, 827, 828, 829, 830, 831, 832, 833, 834, 835, 836, 837, 838, 839, 840, 841, 842, 843, 844, 845, 846, 847, 848, 849, 850, 851, 852, 853, 854, 855, 856, 857, 858, 859, 860, 861, 862, 863, 864, 865, 866, 867, 868, 869, 870, 871, 872, 873, 874, 875, 876, 877, 878, 879, 880, 881, 882, 883, 884, 885, 886, 887, 888, 889, 890, 891, 892, 893, 894, 895, 896, 897, 898, 899, 900, 901, 902, 903, 904, 905, 906, 907, 908, 909, 910, 911, 912, 913, 914, 915, 916, 917, 918, 919, 920, 921, 922, 923, 924, 925, 926, 927, 928, 929, 930, 931, 932, 933, 934, 935, 936, 937, 938, 939, 940, 941, 942, 943, 944, 945, 946, 947, 948, 949, 950, 951, 952, 953, 954, 955, 956, 957, 958, 959, 960, 961, 962, 963, 964, 965, 966, 967, 968, 969, 970, 971, 972, 973, 974, 975, 976, 977, 978, 979, 980, 981, 982, 983, 984, 985, 986, 987, 988, 989, 990, 991, 992, 993, 994, 995, 996, 997, 998, 999, 1000, 1001, 1002, 1003, 1004, 1005, 1006, 1007, 1008, 1009, 1010, 1011, 1012, 1013, 1014, 1015, 1016, 1017, 1018, 1019, 1020, 1021, 1022, 1023, 1024, 1025, 1026, 1027, 1028, 1029, 1030, 1031, 1032, 1033, 1034, 1035, 1036, 1037, 1038, 1039, 1040, 1041, 1042, 1043, 1044, 1045, 1046, 1047, 1048, 1049, 1050, 1051, 1052, 1053, 1054, 1055, 1056, 1057, 1058, 1059, 1060, 1061, 1062, 1063, 1064, 1065, 1066, 1067, 1068, 1069, 1070, 1071, 1072, 1073, 1074, 1075, 1076, 1077, 1078, 1079, 1080, 1081, 1082, 1083, 1084, 1085, 1086, 1087, 1088, 1089, 1090, 1091, 1092, 1093, 1094, 1095, 1096, 1097, 1098, 1099, 1100, 1101} + +var emojiCategoryList_CircleFace []int = []int{1102, 1103, 1104, 1105, 1106, 1107, 1108, 1109, 1110, 1111, 1112, 1113, 1114, 1115, 1116, 1117, 1118, 1119, 1120, 1121, 1122, 1123, 1124, 1125, 1126, 1127, 1128, 1129, 1130, 1131, 1132, 1133, 1134, 1135, 1136, 1137, 1138, 1139, 1140, 1141, 1142, 1143, 1144, 1145, 1146, 1147, 1148, 1149, 1150, 1151, 1152, 1153, 1154, 1155, 1156, 1157, 1158, 1159, 1160, 1161, 1162, 1163, 1164, 1165, 1166, 1167, 1168, 1169, 1170, 1171, 1172, 1173, 1174, 1175, 1176, 1177, 1178, 1179, 1180, 1181, 1182, 1183, 1184, 1185, 1186, 1187, 1188, 1189, 1190, 1191, 1192, 1193, 1194, 1195, 1196, 1197, 1198, 1199, 1200, 1201, 1202, 1203, 1204, 1205, 1206, 1207, 1208, 1209, 1210, 1211} + +var emojiCategoryList_Bodies []int = []int{1212, 1213, 1214, 1215, 1216, 1217, 1218, 1219, 1220, 1221, 1222, 1223, 1224, 1225, 1226, 1227, 1228, 1229, 1230, 1231, 1232, 1233, 1234, 1235, 1236, 1237, 1238, 1239, 1240, 1241, 1242, 1243, 1244, 1245, 1246, 1247, 1248, 1249, 1250, 1251, 1252, 1253, 1254, 1255, 1256, 1257, 1258, 1259, 1260, 1261, 1262, 1263, 1264, 1265, 1266, 1267, 1268, 1269, 1270, 1271, 1272, 1273, 1274, 1275, 1276, 1277, 1278, 1279, 1280, 1281, 1282, 1283, 1284, 1285, 1286, 1287, 1288, 1289, 1290, 1291, 1292, 1293, 1294, 1295, 1296, 1297, 1298, 1299, 1300, 1301, 1302, 1303, 1304, 1305, 1306, 1307, 1308, 1309, 1310, 1311, 1312, 1313, 1314, 1315, 1316, 1317, 1318, 1319, 1320, 1321, 1322, 1323, 1324, 1325, 1326, 1327, 1328, 1329, 1330, 1331, 1332, 1333, 1334, 1335, 1336, 1337, 1338, 1339, 1340, 1341, 1342, 1343, 1344, 1345, 1346, 1347, 1348, 1349, 1350, 1351, 1352, 1353, 1354, 1355, 1356, 1357, 1358, 1359, 1360, 1361, 1362, 1363, 1364, 1365, 1366, 1367, 1368, 1369, 1370, 1371, 1372, 1373, 1374, 1375, 1376, 1377, 1378, 1379, 1380, 1381, 1382, 1383, 1384, 1385, 1386, 1387, 1388, 1389, 1390, 1391, 1392, 1393, 1394, 1395, 1396, 1397, 1398, 1399, 1400, 1401, 1402, 1403, 1404, 1405, 1406, 1407, 1408, 1409, 1410, 1411, 1412, 1413, 1414, 1415, 1416, 1417, 1418, 1419, 1420, 1421, 1422, 1423, 1424, 1425, 1426, 1427, 1428, 1429, 1430, 1431, 1432, 1433, 1434, 1435, 1436, 1437, 1438, 1439, 1440, 1441, 1442, 1443, 1444, 1445, 1446, 1447, 1448, 1449, 1450, 1451, 1452, 1453, 1454, 1455, 1456, 1457, 1458, 1459, 1460, 1461, 1462, 1463, 1464, 1465, 1466, 1467, 1468, 1469, 1470, 1471, 1472, 1473, 1474, 1475, 1476, 1477, 1478, 1479, 1480, 1481, 1482, 1483, 1484, 1485, 1486, 1487, 1488, 1489, 1490, 1491, 1492, 1493, 1494, 1495, 1496, 1497, 1498, 1499, 1500, 1501, 1502, 1503, 1504, 1505, 1506, 1507, 1508, 1509, 1510, 1511, 1512, 1513, 1514, 1515, 1516, 1517, 1518, 1519, 1520, 1521, 1522, 1523, 1524, 1525, 1526, 1527, 1528, 1529, 1530, 1531, 1532, 1533, 1534, 1535, 1536, 1537, 1538, 1539, 1540, 1541, 1542, 1543, 1544, 1545, 1546, 1547, 1548, 1549, 1550, 1551, 1552, 1553, 1554, 1555, 1556, 1557, 1558, 1559, 1560, 1561, 1562, 1563, 1564, 1565, 1566, 1567, 1568, 1569, 1570, 1571, 1572, 1573, 1574, 1575, 1576, 1577, 1578, 1579, 1580, 1581, 1582, 1583, 1584, 1585, 1586, 1587, 1588, 1589, 1590, 1591, 1592, 1593, 1594, 1595, 1596, 1597, 1598, 1599, 1600, 1601, 1602, 1603, 1604, 1605, 1606, 1607, 1608, 1609, 1610, 1611, 1612, 1613, 1614, 1615, 1616, 1617, 1618, 1619, 1620, 1621, 1622, 1623, 1624, 1625, 1626, 1627, 1628, 1629, 1630, 1631, 1632, 1633, 1634, 1635, 1636, 1637, 1638, 1639, 1640, 1641, 1642, 1643, 1644, 1645, 1646, 1647, 1648, 1649, 1650, 1651, 1652, 1653, 1654, 1655, 1656, 1657, 1658, 1659, 1660, 1661, 1662, 1663, 1664} + +var emojiCategoryList_Creatures []int = []int{1665, 1666, 1667, 1668, 1669, 1670, 1671, 1672, 1673, 1674, 1675, 1676, 1677, 1678, 1679, 1680, 1681, 1682, 1683, 1684, 1685, 1686, 1687, 1688, 1689, 1690, 1691, 1692, 1693, 1694, 1695, 1696, 1697, 1698, 1699, 1700, 1701, 1702, 1703, 1704, 1705, 1706, 1707, 1708, 1709, 1710, 1711, 1712, 1713, 1714, 1715, 1716, 1717, 1718, 1719, 1720, 1721, 1722, 1723, 1724, 1725, 1726, 1727, 1728, 1729, 1730, 1731, 1732, 1733, 1734, 1735, 1736, 1737, 1738, 1739, 1740, 1741, 1742, 1743, 1744, 1745, 1746, 1747, 1748, 1749, 1750, 1751, 1752, 1753, 1754, 1755, 1756, 1757, 1758, 1759, 1760, 1761, 1762, 1763, 1764, 1765, 1766, 1767, 1768, 1769, 1770, 1771, 1772, 1773, 1774, 1775, 1776, 1777, 1778, 1779, 1780, 1781, 1782, 1783, 1784, 1785, 1786, 1787, 1788, 1789, 1790, 1791, 1792, 1793, 1794, 1795, 1796, 1797, 1798, 1799, 1800, 1801, 1802, 1803, 1804, 1805, 1806, 1807, 1808, 1809, 1810} + +var emojiCategoryList_Food []int = []int{1811, 1812, 1813, 1814, 1815, 1816, 1817, 1818, 1819, 1820, 1821, 1822, 1823, 1824, 1825, 1826, 1827, 1828, 1829, 1830, 1831, 1832, 1833, 1834, 1835, 1836, 1837, 1838, 1839, 1840, 1841, 1842, 1843, 1844, 1845, 1846, 1847, 1848, 1849, 1850, 1851, 1852, 1853, 1854, 1855, 1856, 1857, 1858, 1859, 1860, 1861, 1862, 1863, 1864, 1865, 1866, 1867, 1868, 1869, 1870, 1871, 1872, 1873, 1874, 1875, 1876, 1877, 1878, 1879, 1880, 1881, 1882, 1883, 1884, 1885, 1886, 1887, 1888, 1889, 1890, 1891, 1892, 1893, 1894, 1895, 1896, 1897, 1898, 1899, 1900, 1901, 1902, 1903, 1904, 1905, 1906, 1907, 1908, 1909, 1910, 1911, 1912, 1913, 1914, 1915, 1916, 1917, 1918, 1919, 1920, 1921, 1922, 1923, 1924, 1925, 1926, 1927, 1928, 1929, 1930, 1931, 1932, 1933, 1934, 1935, 1936, 1937, 1938, 1939, 1940, 1941, 1942, 1943, 1944, 1945, 1946} + +var emojiCategoryList_Hands []int = []int{1947, 1948, 1949, 1950, 1951, 1952, 1953, 1954, 1955, 1956, 1957, 1958, 1959, 1960, 1961, 1962, 1963, 1964, 1965, 1966, 1967, 1968, 1969, 1970, 1971, 1972, 1973, 1974, 1975, 1976, 1977, 1978, 1979, 1980, 1981, 1982, 1983, 1984, 1985, 1986, 1987, 1988, 1989, 1990, 1991, 1992, 1993, 1994, 1995, 1996, 1997, 1998, 1999, 2000, 2001, 2002, 2003, 2004, 2005, 2006, 2007, 2008, 2009, 2010, 2011, 2012, 2013, 2014, 2015, 2016, 2017, 2018, 2019, 2020, 2021, 2022, 2023, 2024, 2025, 2026, 2027, 2028, 2029, 2030, 2031, 2032, 2033, 2034, 2035, 2036, 2037, 2038, 2039, 2040, 2041, 2042, 2043, 2044, 2045, 2046, 2047, 2048, 2049, 2050, 2051, 2052, 2053, 2054, 2055, 2056, 2057, 2058, 2059, 2060, 2061, 2062, 2063, 2064, 2065, 2066, 2067, 2068, 2069, 2070, 2071, 2072, 2073, 2074, 2075, 2076, 2077, 2078, 2079, 2080, 2081, 2082, 2083, 2084, 2085, 2086, 2087, 2088, 2089, 2090, 2091, 2092, 2093, 2094, 2095, 2096, 2097, 2098, 2099, 2100, 2101, 2102, 2103, 2104, 2105, 2106, 2107, 2108, 2109, 2110, 2111, 2112, 2113, 2114, 2115, 2116, 2117, 2118, 2119, 2120, 2121, 2122, 2123, 2124, 2125, 2126, 2127, 2128, 2129, 2130, 2131, 2132, 2133, 2134, 2135, 2136, 2137, 2138, 2139, 2140, 2141, 2142, 2143, 2144, 2145, 2146, 2147, 2148, 2149, 2150, 2151, 2152, 2153, 2154, 2155, 2156, 2157, 2158, 2159, 2160, 2161, 2162, 2163, 2164, 2165, 2166, 2167, 2168, 2169, 2170, 2171, 2172, 2173, 2174, 2175, 2176, 2177, 2178, 2179, 2180, 2181} + +var emojiCategoryList_Flags []int = []int{2182, 2183, 2184, 2185, 2186, 2187, 2188, 2189, 2190, 2191, 2192, 2193, 2194, 2195, 2196, 2197, 2198, 2199, 2200, 2201, 2202, 2203, 2204, 2205, 2206, 2207, 2208, 2209, 2210, 2211, 2212, 2213, 2214, 2215, 2216, 2217, 2218, 2219, 2220, 2221, 2222, 2223, 2224, 2225, 2226, 2227, 2228, 2229, 2230, 2231, 2232, 2233, 2234, 2235, 2236, 2237, 2238, 2239, 2240, 2241, 2242, 2243, 2244, 2245, 2246, 2247, 2248, 2249, 2250, 2251, 2252, 2253, 2254, 2255, 2256, 2257, 2258, 2259, 2260, 2261, 2262, 2263, 2264, 2265, 2266, 2267, 2268, 2269, 2270, 2271, 2272, 2273, 2274, 2275, 2276, 2277, 2278, 2279, 2280, 2281, 2282, 2283, 2284, 2285, 2286, 2287, 2288, 2289, 2290, 2291, 2292, 2293, 2294, 2295, 2296, 2297, 2298, 2299, 2300, 2301, 2302, 2303, 2304, 2305, 2306, 2307, 2308, 2309, 2310, 2311, 2312, 2313, 2314, 2315, 2316, 2317, 2318, 2319, 2320, 2321, 2322, 2323, 2324, 2325, 2326, 2327, 2328, 2329, 2330, 2331, 2332, 2333, 2334, 2335, 2336, 2337, 2338, 2339, 2340, 2341, 2342, 2343, 2344, 2345, 2346, 2347, 2348, 2349, 2350, 2351, 2352, 2353, 2354, 2355, 2356, 2357, 2358, 2359, 2360, 2361, 2362, 2363, 2364, 2365, 2366, 2367, 2368, 2369, 2370, 2371, 2372, 2373, 2374, 2375, 2376, 2377, 2378, 2379, 2380, 2381, 2382, 2383, 2384, 2385, 2386, 2387, 2388, 2389, 2390, 2391, 2392, 2393, 2394, 2395, 2396, 2397, 2398, 2399, 2400, 2401, 2402, 2403, 2404, 2405, 2406, 2407, 2408, 2409, 2410, 2411, 2412, 2413, 2414, 2415, 2416, 2417, 2418, 2419, 2420, 2421, 2422, 2423, 2424, 2425, 2426, 2427, 2428, 2429, 2430, 2431, 2432, 2433, 2434, 2435, 2436, 2437, 2438, 2439, 2440, 2441, 2442, 2443, 2444, 2445, 2446, 2447, 2448, 2449, 2450, 2451, 2452, 2453, 2454} + +var emojiCategoryList_Miscellaneous []int = []int{2455, 2456, 2457, 2458, 2459, 2460, 2461, 2462, 2463, 2464, 2465, 2466, 2467, 2468, 2469, 2470, 2471, 2472, 2473, 2474, 2475, 2476, 2477, 2478, 2479, 2480, 2481, 2482, 2483, 2484, 2485, 2486, 2487, 2488, 2489, 2490, 2491, 2492, 2493, 2494, 2495, 2496, 2497, 2498, 2499, 2500, 2501, 2502, 2503, 2504, 2505, 2506, 2507, 2508, 2509, 2510, 2511, 2512, 2513, 2514, 2515, 2516, 2517, 2518, 2519, 2520, 2521, 2522, 2523, 2524, 2525, 2526, 2527, 2528, 2529, 2530, 2531, 2532, 2533, 2534, 2535, 2536, 2537, 2538, 2539, 2540, 2541, 2542, 2543, 2544, 2545, 2546, 2547, 2548, 2549, 2550, 2551, 2552, 2553, 2554, 2555, 2556, 2557, 2558, 2559, 2560, 2561, 2562, 2563, 2564, 2565, 2566, 2567, 2568, 2569, 2570, 2571, 2572, 2573, 2574, 2575, 2576, 2577, 2578, 2579, 2580, 2581, 2582, 2583, 2584, 2585, 2586, 2587, 2588, 2589, 2590, 2591, 2592, 2593, 2594, 2595, 2596, 2597, 2598, 2599, 2600, 2601, 2602, 2603, 2604, 2605, 2606, 2607, 2608, 2609, 2610, 2611, 2612, 2613, 2614, 2615, 2616, 2617, 2618, 2619, 2620, 2621, 2622, 2623, 2624, 2625, 2626, 2627, 2628, 2629, 2630, 2631, 2632, 2633, 2634, 2635, 2636, 2637, 2638, 2639, 2640, 2641, 2642, 2643, 2644, 2645, 2646, 2647, 2648, 2649, 2650, 2651, 2652, 2653, 2654, 2655, 2656, 2657, 2658, 2659, 2660, 2661, 2662, 2663, 2664, 2665, 2666, 2667, 2668, 2669, 2670, 2671, 2672, 2673, 2674, 2675, 2676, 2677, 2678, 2679, 2680, 2681, 2682, 2683, 2684, 2685, 2686, 2687, 2688, 2689, 2690, 2691, 2692, 2693, 2694, 2695, 2696, 2697, 2698, 2699, 2700, 2701, 2702, 2703, 2704, 2705, 2706, 2707, 2708, 2709, 2710, 2711, 2712, 2713, 2714, 2715, 2716, 2717, 2718, 2719, 2720, 2721, 2722, 2723, 2724, 2725, 2726, 2727, 2728, 2729, 2730, 2731, 2732, 2733, 2734, 2735, 2736, 2737, 2738, 2739, 2740, 2741, 2742, 2743, 2744, 2745, 2746, 2747, 2748, 2749, 2750, 2751, 2752, 2753, 2754, 2755, 2756, 2757, 2758, 2759, 2760, 2761, 2762, 2763, 2764, 2765, 2766, 2767, 2768, 2769, 2770, 2771, 2772, 2773, 2774, 2775, 2776, 2777, 2778, 2779, 2780, 2781, 2782, 2783, 2784, 2785, 2786, 2787, 2788, 2789, 2790, 2791, 2792, 2793, 2794, 2795, 2796, 2797, 2798, 2799, 2800, 2801, 2802, 2803, 2804, 2805, 2806, 2807, 2808, 2809, 2810, 2811, 2812, 2813, 2814, 2815, 2816, 2817, 2818, 2819, 2820, 2821, 2822, 2823, 2824, 2825, 2826, 2827, 2828, 2829, 2830, 2831, 2832, 2833, 2834, 2835, 2836, 2837, 2838, 2839, 2840, 2841, 2842, 2843, 2844, 2845, 2846, 2847, 2848, 2849, 2850, 2851, 2852, 2853, 2854, 2855, 2856, 2857, 2858, 2859, 2860, 2861, 2862, 2863, 2864, 2865, 2866, 2867, 2868, 2869, 2870, 2871, 2872, 2873, 2874, 2875, 2876, 2877, 2878, 2879, 2880, 2881, 2882, 2883, 2884, 2885, 2886, 2887, 2888, 2889, 2890, 2891, 2892, 2893, 2894, 2895, 2896, 2897, 2898, 2899, 2900, 2901, 2902, 2903, 2904, 2905, 2906, 2907, 2908, 2909, 2910, 2911, 2912, 2913, 2914, 2915, 2916, 2917, 2918, 2919, 2920, 2921, 2922, 2923, 2924, 2925, 2926, 2927, 2928, 2929, 2930, 2931, 2932, 2933, 2934, 2935, 2936, 2937, 2938, 2939, 2940, 2941, 2942, 2943, 2944, 2945, 2946, 2947, 2948, 2949, 2950, 2951, 2952, 2953, 2954, 2955, 2956, 2957, 2958, 2959, 2960, 2961, 2962, 2963, 2964, 2965, 2966, 2967, 2968, 2969, 2970, 2971, 2972, 2973, 2974, 2975, 2976, 2977, 2978, 2979, 2980, 2981, 2982, 2983, 2984, 2985, 2986, 2987, 2988, 2989, 2990, 2991, 2992, 2993, 2994, 2995, 2996, 2997, 2998, 2999, 3000, 3001, 3002, 3003, 3004, 3005, 3006, 3007, 3008, 3009, 3010, 3011, 3012, 3013, 3014, 3015, 3016, 3017, 3018, 3019, 3020, 3021, 3022, 3023, 3024, 3025, 3026, 3027, 3028, 3029, 3030, 3031, 3032, 3033, 3034, 3035, 3036, 3037, 3038, 3039, 3040, 3041, 3042, 3043, 3044, 3045, 3046, 3047, 3048, 3049, 3050, 3051, 3052, 3053, 3054, 3055, 3056, 3057, 3058, 3059, 3060, 3061, 3062, 3063, 3064, 3065, 3066, 3067, 3068, 3069, 3070, 3071, 3072, 3073, 3074, 3075, 3076, 3077, 3078, 3079, 3080, 3081, 3082, 3083, 3084, 3085, 3086, 3087, 3088, 3089, 3090, 3091, 3092, 3093, 3094, 3095, 3096, 3097, 3098, 3099, 3100, 3101, 3102, 3103, 3104, 3105, 3106, 3107, 3108, 3109, 3110, 3111, 3112, 3113, 3114, 3115, 3116, 3117, 3118, 3119, 3120, 3121, 3122, 3123, 3124, 3125, 3126, 3127, 3128, 3129, 3130, 3131, 3132, 3133, 3134, 3135, 3136, 3137, 3138, 3139, 3140, 3141, 3142, 3143, 3144, 3145, 3146, 3147, 3148, 3149, 3150, 3151, 3152, 3153, 3154, 3155, 3156, 3157, 3158, 3159, 3160, 3161, 3162, 3163, 3164, 3165, 3166, 3167, 3168, 3169, 3170, 3171, 3172, 3173, 3174, 3175, 3176, 3177, 3178, 3179, 3180, 3181, 3182, 3183, 3184, 3185, 3186, 3187, 3188, 3189, 3190, 3191, 3192, 3193, 3194, 3195, 3196, 3197, 3198, 3199, 3200, 3201, 3202, 3203, 3204, 3205, 3206, 3207, 3208, 3209, 3210, 3211, 3212, 3213, 3214, 3215, 3216, 3217, 3218, 3219, 3220, 3221, 3222, 3223, 3224, 3225, 3226, 3227, 3228, 3229, 3230, 3231, 3232, 3233, 3234, 3235, 3236, 3237, 3238, 3239, 3240, 3241, 3242, 3243, 3244, 3245, 3246, 3247, 3248, 3249, 3250, 3251, 3252, 3253, 3254, 3255, 3256, 3257, 3258, 3259, 3260, 3261, 3262, 3263, 3264, 3265, 3266, 3267, 3268, 3269, 3270, 3271, 3272, 3273, 3274, 3275, 3276, 3277, 3278, 3279, 3280, 3281, 3282, 3283, 3284, 3285, 3286, 3287, 3288, 3289, 3290, 3291, 3292, 3293, 3294, 3295, 3296, 3297, 3298, 3299, 3300, 3301, 3302, 3303, 3304, 3305, 3306, 3307, 3308, 3309, 3310, 3311, 3312, 3313, 3314, 3315, 3316, 3317, 3318, 3319, 3320, 3321, 3322, 3323, 3324, 3325, 3326, 3327, 3328, 3329, 3330, 3331, 3332, 3333, 3334, 3335, 3336, 3337, 3338, 3339, 3340, 3341, 3342, 3343, 3344, 3345, 3346, 3347, 3348, 3349, 3350, 3351, 3352, 3353, 3354, 3355, 3356, 3357, 3358, 3359, 3360, 3361, 3362, 3363, 3364, 3365, 3366, 3367, 3368, 3369, 3370, 3371, 3372, 3373, 3374, 3375, 3376, 3377, 3378, 3379, 3380, 3381, 3382, 3383, 3384, 3385, 3386, 3387, 3388, 3389, 3390, 3391, 3392, 3393, 3394, 3395, 3396, 3397, 3398, 3399, 3400, 3401, 3402, 3403, 3404, 3405, 3406, 3407, 3408, 3409, 3410, 3411, 3412, 3413, 3414, 3415, 3416, 3417, 3418, 3419, 3420, 3421, 3422, 3423, 3424, 3425, 3426, 3427, 3428, 3429, 3430, 3431, 3432, 3433, 3434, 3435, 3436, 3437, 3438, 3439, 3440, 3441, 3442, 3443, 3444, 3445, 3446, 3447, 3448, 3449, 3450, 3451, 3452, 3453, 3454, 3455, 3456, 3457, 3458, 3459, 3460, 3461, 3462, 3463, 3464, 3465, 3466, 3467, 3468, 3469, 3470, 3471, 3472, 3473, 3474, 3475, 3476, 3477, 3478, 3479, 3480, 3481, 3482, 3483, 3484, 3485, 3486, 3487, 3488, 3489, 3490, 3491, 3492, 3493, 3494, 3495, 3496, 3497, 3498, 3499, 3500, 3501, 3502, 3503, 3504, 3505, 3506, 3507, 3508, 3509, 3510, 3511, 3512, 3513, 3514, 3515, 3516, 3517, 3518, 3519, 3520, 3521, 3522, 3523, 3524, 3525, 3526, 3527, 3528, 3529, 3530, 3531, 3532, 3533, 3534, 3535, 3536, 3537, 3538, 3539, 3540, 3541, 3542, 3543, 3544, 3545, 3546, 3547, 3548, 3549, 3550, 3551, 3552, 3553, 3554, 3555, 3556, 3557, 3558, 3559, 3560, 3561, 3562, 3563, 3564, 3565, 3566, 3567, 3568, 3569, 3570, 3571, 3572, 3573, 3574, 3575, 3576, 3577, 3578, 3579, 3580, 3581, 3582, 3583, 3584, 3585, 3586, 3587, 3588, 3589, 3590, 3591, 3592, 3593, 3594, 3595, 3596, 3597, 3598, 3599, 3600, 3601, 3602, 3603, 3604, 3605, 3606, 3607, 3608, 3609, 3610, 3611, 3612, 3613, 3614, 3615, 3616, 3617, 3618, 3619, 3620, 3621, 3622, 3623, 3624, 3625, 3626, 3627, 3628, 3629, 3630, 3631} + + diff --git a/resources/imageFiles/emojis.go b/resources/imageFiles/emojis.go new file mode 100644 index 0000000..fff31b1 --- /dev/null +++ b/resources/imageFiles/emojis.go @@ -0,0 +1,21816 @@ +package imageFiles + +import "seekia/internal/helpers" + +import _ "embed" + +import "errors" + + +func VerifyEmojiIdentifier(inputIdentifier int)bool{ + + if (inputIdentifier < 1 || inputIdentifier > 3631){ + return false + } + + return true +} + +//go:embed emojis/0001.svg +var Emoji_0001 []byte + +//go:embed emojis/0002.svg +var Emoji_0002 []byte + +//go:embed emojis/0003.svg +var Emoji_0003 []byte + +//go:embed emojis/0004.svg +var Emoji_0004 []byte + +//go:embed emojis/0005.svg +var Emoji_0005 []byte + +//go:embed emojis/0006.svg +var Emoji_0006 []byte + +//go:embed emojis/0007.svg +var Emoji_0007 []byte + +//go:embed emojis/0008.svg +var Emoji_0008 []byte + +//go:embed emojis/0009.svg +var Emoji_0009 []byte + +//go:embed emojis/0010.svg +var Emoji_0010 []byte + +//go:embed emojis/0011.svg +var Emoji_0011 []byte + +//go:embed emojis/0012.svg +var Emoji_0012 []byte + +//go:embed emojis/0013.svg +var Emoji_0013 []byte + +//go:embed emojis/0014.svg +var Emoji_0014 []byte + +//go:embed emojis/0015.svg +var Emoji_0015 []byte + +//go:embed emojis/0016.svg +var Emoji_0016 []byte + +//go:embed emojis/0017.svg +var Emoji_0017 []byte + +//go:embed emojis/0018.svg +var Emoji_0018 []byte + +//go:embed emojis/0019.svg +var Emoji_0019 []byte + +//go:embed emojis/0020.svg +var Emoji_0020 []byte + +//go:embed emojis/0021.svg +var Emoji_0021 []byte + +//go:embed emojis/0022.svg +var Emoji_0022 []byte + +//go:embed emojis/0023.svg +var Emoji_0023 []byte + +//go:embed emojis/0024.svg +var Emoji_0024 []byte + +//go:embed emojis/0025.svg +var Emoji_0025 []byte + +//go:embed emojis/0026.svg +var Emoji_0026 []byte + +//go:embed emojis/0027.svg +var Emoji_0027 []byte + +//go:embed emojis/0028.svg +var Emoji_0028 []byte + +//go:embed emojis/0029.svg +var Emoji_0029 []byte + +//go:embed emojis/0030.svg +var Emoji_0030 []byte + +//go:embed emojis/0031.svg +var Emoji_0031 []byte + +//go:embed emojis/0032.svg +var Emoji_0032 []byte + +//go:embed emojis/0033.svg +var Emoji_0033 []byte + +//go:embed emojis/0034.svg +var Emoji_0034 []byte + +//go:embed emojis/0035.svg +var Emoji_0035 []byte + +//go:embed emojis/0036.svg +var Emoji_0036 []byte + +//go:embed emojis/0037.svg +var Emoji_0037 []byte + +//go:embed emojis/0038.svg +var Emoji_0038 []byte + +//go:embed emojis/0039.svg +var Emoji_0039 []byte + +//go:embed emojis/0040.svg +var Emoji_0040 []byte + +//go:embed emojis/0041.svg +var Emoji_0041 []byte + +//go:embed emojis/0042.svg +var Emoji_0042 []byte + +//go:embed emojis/0043.svg +var Emoji_0043 []byte + +//go:embed emojis/0044.svg +var Emoji_0044 []byte + +//go:embed emojis/0045.svg +var Emoji_0045 []byte + +//go:embed emojis/0046.svg +var Emoji_0046 []byte + +//go:embed emojis/0047.svg +var Emoji_0047 []byte + +//go:embed emojis/0048.svg +var Emoji_0048 []byte + +//go:embed emojis/0049.svg +var Emoji_0049 []byte + +//go:embed emojis/0050.svg +var Emoji_0050 []byte + +//go:embed emojis/0051.svg +var Emoji_0051 []byte + +//go:embed emojis/0052.svg +var Emoji_0052 []byte + +//go:embed emojis/0053.svg +var Emoji_0053 []byte + +//go:embed emojis/0054.svg +var Emoji_0054 []byte + +//go:embed emojis/0055.svg +var Emoji_0055 []byte + +//go:embed emojis/0056.svg +var Emoji_0056 []byte + +//go:embed emojis/0057.svg +var Emoji_0057 []byte + +//go:embed emojis/0058.svg +var Emoji_0058 []byte + +//go:embed emojis/0059.svg +var Emoji_0059 []byte + +//go:embed emojis/0060.svg +var Emoji_0060 []byte + +//go:embed emojis/0061.svg +var Emoji_0061 []byte + +//go:embed emojis/0062.svg +var Emoji_0062 []byte + +//go:embed emojis/0063.svg +var Emoji_0063 []byte + +//go:embed emojis/0064.svg +var Emoji_0064 []byte + +//go:embed emojis/0065.svg +var Emoji_0065 []byte + +//go:embed emojis/0066.svg +var Emoji_0066 []byte + +//go:embed emojis/0067.svg +var Emoji_0067 []byte + +//go:embed emojis/0068.svg +var Emoji_0068 []byte + +//go:embed emojis/0069.svg +var Emoji_0069 []byte + +//go:embed emojis/0070.svg +var Emoji_0070 []byte + +//go:embed emojis/0071.svg +var Emoji_0071 []byte + +//go:embed emojis/0072.svg +var Emoji_0072 []byte + +//go:embed emojis/0073.svg +var Emoji_0073 []byte + +//go:embed emojis/0074.svg +var Emoji_0074 []byte + +//go:embed emojis/0075.svg +var Emoji_0075 []byte + +//go:embed emojis/0076.svg +var Emoji_0076 []byte + +//go:embed emojis/0077.svg +var Emoji_0077 []byte + +//go:embed emojis/0078.svg +var Emoji_0078 []byte + +//go:embed emojis/0079.svg +var Emoji_0079 []byte + +//go:embed emojis/0080.svg +var Emoji_0080 []byte + +//go:embed emojis/0081.svg +var Emoji_0081 []byte + +//go:embed emojis/0082.svg +var Emoji_0082 []byte + +//go:embed emojis/0083.svg +var Emoji_0083 []byte + +//go:embed emojis/0084.svg +var Emoji_0084 []byte + +//go:embed emojis/0085.svg +var Emoji_0085 []byte + +//go:embed emojis/0086.svg +var Emoji_0086 []byte + +//go:embed emojis/0087.svg +var Emoji_0087 []byte + +//go:embed emojis/0088.svg +var Emoji_0088 []byte + +//go:embed emojis/0089.svg +var Emoji_0089 []byte + +//go:embed emojis/0090.svg +var Emoji_0090 []byte + +//go:embed emojis/0091.svg +var Emoji_0091 []byte + +//go:embed emojis/0092.svg +var Emoji_0092 []byte + +//go:embed emojis/0093.svg +var Emoji_0093 []byte + +//go:embed emojis/0094.svg +var Emoji_0094 []byte + +//go:embed emojis/0095.svg +var Emoji_0095 []byte + +//go:embed emojis/0096.svg +var Emoji_0096 []byte + +//go:embed emojis/0097.svg +var Emoji_0097 []byte + +//go:embed emojis/0098.svg +var Emoji_0098 []byte + +//go:embed emojis/0099.svg +var Emoji_0099 []byte + +//go:embed emojis/0100.svg +var Emoji_0100 []byte + +//go:embed emojis/0101.svg +var Emoji_0101 []byte + +//go:embed emojis/0102.svg +var Emoji_0102 []byte + +//go:embed emojis/0103.svg +var Emoji_0103 []byte + +//go:embed emojis/0104.svg +var Emoji_0104 []byte + +//go:embed emojis/0105.svg +var Emoji_0105 []byte + +//go:embed emojis/0106.svg +var Emoji_0106 []byte + +//go:embed emojis/0107.svg +var Emoji_0107 []byte + +//go:embed emojis/0108.svg +var Emoji_0108 []byte + +//go:embed emojis/0109.svg +var Emoji_0109 []byte + +//go:embed emojis/0110.svg +var Emoji_0110 []byte + +//go:embed emojis/0111.svg +var Emoji_0111 []byte + +//go:embed emojis/0112.svg +var Emoji_0112 []byte + +//go:embed emojis/0113.svg +var Emoji_0113 []byte + +//go:embed emojis/0114.svg +var Emoji_0114 []byte + +//go:embed emojis/0115.svg +var Emoji_0115 []byte + +//go:embed emojis/0116.svg +var Emoji_0116 []byte + +//go:embed emojis/0117.svg +var Emoji_0117 []byte + +//go:embed emojis/0118.svg +var Emoji_0118 []byte + +//go:embed emojis/0119.svg +var Emoji_0119 []byte + +//go:embed emojis/0120.svg +var Emoji_0120 []byte + +//go:embed emojis/0121.svg +var Emoji_0121 []byte + +//go:embed emojis/0122.svg +var Emoji_0122 []byte + +//go:embed emojis/0123.svg +var Emoji_0123 []byte + +//go:embed emojis/0124.svg +var Emoji_0124 []byte + +//go:embed emojis/0125.svg +var Emoji_0125 []byte + +//go:embed emojis/0126.svg +var Emoji_0126 []byte + +//go:embed emojis/0127.svg +var Emoji_0127 []byte + +//go:embed emojis/0128.svg +var Emoji_0128 []byte + +//go:embed emojis/0129.svg +var Emoji_0129 []byte + +//go:embed emojis/0130.svg +var Emoji_0130 []byte + +//go:embed emojis/0131.svg +var Emoji_0131 []byte + +//go:embed emojis/0132.svg +var Emoji_0132 []byte + +//go:embed emojis/0133.svg +var Emoji_0133 []byte + +//go:embed emojis/0134.svg +var Emoji_0134 []byte + +//go:embed emojis/0135.svg +var Emoji_0135 []byte + +//go:embed emojis/0136.svg +var Emoji_0136 []byte + +//go:embed emojis/0137.svg +var Emoji_0137 []byte + +//go:embed emojis/0138.svg +var Emoji_0138 []byte + +//go:embed emojis/0139.svg +var Emoji_0139 []byte + +//go:embed emojis/0140.svg +var Emoji_0140 []byte + +//go:embed emojis/0141.svg +var Emoji_0141 []byte + +//go:embed emojis/0142.svg +var Emoji_0142 []byte + +//go:embed emojis/0143.svg +var Emoji_0143 []byte + +//go:embed emojis/0144.svg +var Emoji_0144 []byte + +//go:embed emojis/0145.svg +var Emoji_0145 []byte + +//go:embed emojis/0146.svg +var Emoji_0146 []byte + +//go:embed emojis/0147.svg +var Emoji_0147 []byte + +//go:embed emojis/0148.svg +var Emoji_0148 []byte + +//go:embed emojis/0149.svg +var Emoji_0149 []byte + +//go:embed emojis/0150.svg +var Emoji_0150 []byte + +//go:embed emojis/0151.svg +var Emoji_0151 []byte + +//go:embed emojis/0152.svg +var Emoji_0152 []byte + +//go:embed emojis/0153.svg +var Emoji_0153 []byte + +//go:embed emojis/0154.svg +var Emoji_0154 []byte + +//go:embed emojis/0155.svg +var Emoji_0155 []byte + +//go:embed emojis/0156.svg +var Emoji_0156 []byte + +//go:embed emojis/0157.svg +var Emoji_0157 []byte + +//go:embed emojis/0158.svg +var Emoji_0158 []byte + +//go:embed emojis/0159.svg +var Emoji_0159 []byte + +//go:embed emojis/0160.svg +var Emoji_0160 []byte + +//go:embed emojis/0161.svg +var Emoji_0161 []byte + +//go:embed emojis/0162.svg +var Emoji_0162 []byte + +//go:embed emojis/0163.svg +var Emoji_0163 []byte + +//go:embed emojis/0164.svg +var Emoji_0164 []byte + +//go:embed emojis/0165.svg +var Emoji_0165 []byte + +//go:embed emojis/0166.svg +var Emoji_0166 []byte + +//go:embed emojis/0167.svg +var Emoji_0167 []byte + +//go:embed emojis/0168.svg +var Emoji_0168 []byte + +//go:embed emojis/0169.svg +var Emoji_0169 []byte + +//go:embed emojis/0170.svg +var Emoji_0170 []byte + +//go:embed emojis/0171.svg +var Emoji_0171 []byte + +//go:embed emojis/0172.svg +var Emoji_0172 []byte + +//go:embed emojis/0173.svg +var Emoji_0173 []byte + +//go:embed emojis/0174.svg +var Emoji_0174 []byte + +//go:embed emojis/0175.svg +var Emoji_0175 []byte + +//go:embed emojis/0176.svg +var Emoji_0176 []byte + +//go:embed emojis/0177.svg +var Emoji_0177 []byte + +//go:embed emojis/0178.svg +var Emoji_0178 []byte + +//go:embed emojis/0179.svg +var Emoji_0179 []byte + +//go:embed emojis/0180.svg +var Emoji_0180 []byte + +//go:embed emojis/0181.svg +var Emoji_0181 []byte + +//go:embed emojis/0182.svg +var Emoji_0182 []byte + +//go:embed emojis/0183.svg +var Emoji_0183 []byte + +//go:embed emojis/0184.svg +var Emoji_0184 []byte + +//go:embed emojis/0185.svg +var Emoji_0185 []byte + +//go:embed emojis/0186.svg +var Emoji_0186 []byte + +//go:embed emojis/0187.svg +var Emoji_0187 []byte + +//go:embed emojis/0188.svg +var Emoji_0188 []byte + +//go:embed emojis/0189.svg +var Emoji_0189 []byte + +//go:embed emojis/0190.svg +var Emoji_0190 []byte + +//go:embed emojis/0191.svg +var Emoji_0191 []byte + +//go:embed emojis/0192.svg +var Emoji_0192 []byte + +//go:embed emojis/0193.svg +var Emoji_0193 []byte + +//go:embed emojis/0194.svg +var Emoji_0194 []byte + +//go:embed emojis/0195.svg +var Emoji_0195 []byte + +//go:embed emojis/0196.svg +var Emoji_0196 []byte + +//go:embed emojis/0197.svg +var Emoji_0197 []byte + +//go:embed emojis/0198.svg +var Emoji_0198 []byte + +//go:embed emojis/0199.svg +var Emoji_0199 []byte + +//go:embed emojis/0200.svg +var Emoji_0200 []byte + +//go:embed emojis/0201.svg +var Emoji_0201 []byte + +//go:embed emojis/0202.svg +var Emoji_0202 []byte + +//go:embed emojis/0203.svg +var Emoji_0203 []byte + +//go:embed emojis/0204.svg +var Emoji_0204 []byte + +//go:embed emojis/0205.svg +var Emoji_0205 []byte + +//go:embed emojis/0206.svg +var Emoji_0206 []byte + +//go:embed emojis/0207.svg +var Emoji_0207 []byte + +//go:embed emojis/0208.svg +var Emoji_0208 []byte + +//go:embed emojis/0209.svg +var Emoji_0209 []byte + +//go:embed emojis/0210.svg +var Emoji_0210 []byte + +//go:embed emojis/0211.svg +var Emoji_0211 []byte + +//go:embed emojis/0212.svg +var Emoji_0212 []byte + +//go:embed emojis/0213.svg +var Emoji_0213 []byte + +//go:embed emojis/0214.svg +var Emoji_0214 []byte + +//go:embed emojis/0215.svg +var Emoji_0215 []byte + +//go:embed emojis/0216.svg +var Emoji_0216 []byte + +//go:embed emojis/0217.svg +var Emoji_0217 []byte + +//go:embed emojis/0218.svg +var Emoji_0218 []byte + +//go:embed emojis/0219.svg +var Emoji_0219 []byte + +//go:embed emojis/0220.svg +var Emoji_0220 []byte + +//go:embed emojis/0221.svg +var Emoji_0221 []byte + +//go:embed emojis/0222.svg +var Emoji_0222 []byte + +//go:embed emojis/0223.svg +var Emoji_0223 []byte + +//go:embed emojis/0224.svg +var Emoji_0224 []byte + +//go:embed emojis/0225.svg +var Emoji_0225 []byte + +//go:embed emojis/0226.svg +var Emoji_0226 []byte + +//go:embed emojis/0227.svg +var Emoji_0227 []byte + +//go:embed emojis/0228.svg +var Emoji_0228 []byte + +//go:embed emojis/0229.svg +var Emoji_0229 []byte + +//go:embed emojis/0230.svg +var Emoji_0230 []byte + +//go:embed emojis/0231.svg +var Emoji_0231 []byte + +//go:embed emojis/0232.svg +var Emoji_0232 []byte + +//go:embed emojis/0233.svg +var Emoji_0233 []byte + +//go:embed emojis/0234.svg +var Emoji_0234 []byte + +//go:embed emojis/0235.svg +var Emoji_0235 []byte + +//go:embed emojis/0236.svg +var Emoji_0236 []byte + +//go:embed emojis/0237.svg +var Emoji_0237 []byte + +//go:embed emojis/0238.svg +var Emoji_0238 []byte + +//go:embed emojis/0239.svg +var Emoji_0239 []byte + +//go:embed emojis/0240.svg +var Emoji_0240 []byte + +//go:embed emojis/0241.svg +var Emoji_0241 []byte + +//go:embed emojis/0242.svg +var Emoji_0242 []byte + +//go:embed emojis/0243.svg +var Emoji_0243 []byte + +//go:embed emojis/0244.svg +var Emoji_0244 []byte + +//go:embed emojis/0245.svg +var Emoji_0245 []byte + +//go:embed emojis/0246.svg +var Emoji_0246 []byte + +//go:embed emojis/0247.svg +var Emoji_0247 []byte + +//go:embed emojis/0248.svg +var Emoji_0248 []byte + +//go:embed emojis/0249.svg +var Emoji_0249 []byte + +//go:embed emojis/0250.svg +var Emoji_0250 []byte + +//go:embed emojis/0251.svg +var Emoji_0251 []byte + +//go:embed emojis/0252.svg +var Emoji_0252 []byte + +//go:embed emojis/0253.svg +var Emoji_0253 []byte + +//go:embed emojis/0254.svg +var Emoji_0254 []byte + +//go:embed emojis/0255.svg +var Emoji_0255 []byte + +//go:embed emojis/0256.svg +var Emoji_0256 []byte + +//go:embed emojis/0257.svg +var Emoji_0257 []byte + +//go:embed emojis/0258.svg +var Emoji_0258 []byte + +//go:embed emojis/0259.svg +var Emoji_0259 []byte + +//go:embed emojis/0260.svg +var Emoji_0260 []byte + +//go:embed emojis/0261.svg +var Emoji_0261 []byte + +//go:embed emojis/0262.svg +var Emoji_0262 []byte + +//go:embed emojis/0263.svg +var Emoji_0263 []byte + +//go:embed emojis/0264.svg +var Emoji_0264 []byte + +//go:embed emojis/0265.svg +var Emoji_0265 []byte + +//go:embed emojis/0266.svg +var Emoji_0266 []byte + +//go:embed emojis/0267.svg +var Emoji_0267 []byte + +//go:embed emojis/0268.svg +var Emoji_0268 []byte + +//go:embed emojis/0269.svg +var Emoji_0269 []byte + +//go:embed emojis/0270.svg +var Emoji_0270 []byte + +//go:embed emojis/0271.svg +var Emoji_0271 []byte + +//go:embed emojis/0272.svg +var Emoji_0272 []byte + +//go:embed emojis/0273.svg +var Emoji_0273 []byte + +//go:embed emojis/0274.svg +var Emoji_0274 []byte + +//go:embed emojis/0275.svg +var Emoji_0275 []byte + +//go:embed emojis/0276.svg +var Emoji_0276 []byte + +//go:embed emojis/0277.svg +var Emoji_0277 []byte + +//go:embed emojis/0278.svg +var Emoji_0278 []byte + +//go:embed emojis/0279.svg +var Emoji_0279 []byte + +//go:embed emojis/0280.svg +var Emoji_0280 []byte + +//go:embed emojis/0281.svg +var Emoji_0281 []byte + +//go:embed emojis/0282.svg +var Emoji_0282 []byte + +//go:embed emojis/0283.svg +var Emoji_0283 []byte + +//go:embed emojis/0284.svg +var Emoji_0284 []byte + +//go:embed emojis/0285.svg +var Emoji_0285 []byte + +//go:embed emojis/0286.svg +var Emoji_0286 []byte + +//go:embed emojis/0287.svg +var Emoji_0287 []byte + +//go:embed emojis/0288.svg +var Emoji_0288 []byte + +//go:embed emojis/0289.svg +var Emoji_0289 []byte + +//go:embed emojis/0290.svg +var Emoji_0290 []byte + +//go:embed emojis/0291.svg +var Emoji_0291 []byte + +//go:embed emojis/0292.svg +var Emoji_0292 []byte + +//go:embed emojis/0293.svg +var Emoji_0293 []byte + +//go:embed emojis/0294.svg +var Emoji_0294 []byte + +//go:embed emojis/0295.svg +var Emoji_0295 []byte + +//go:embed emojis/0296.svg +var Emoji_0296 []byte + +//go:embed emojis/0297.svg +var Emoji_0297 []byte + +//go:embed emojis/0298.svg +var Emoji_0298 []byte + +//go:embed emojis/0299.svg +var Emoji_0299 []byte + +//go:embed emojis/0300.svg +var Emoji_0300 []byte + +//go:embed emojis/0301.svg +var Emoji_0301 []byte + +//go:embed emojis/0302.svg +var Emoji_0302 []byte + +//go:embed emojis/0303.svg +var Emoji_0303 []byte + +//go:embed emojis/0304.svg +var Emoji_0304 []byte + +//go:embed emojis/0305.svg +var Emoji_0305 []byte + +//go:embed emojis/0306.svg +var Emoji_0306 []byte + +//go:embed emojis/0307.svg +var Emoji_0307 []byte + +//go:embed emojis/0308.svg +var Emoji_0308 []byte + +//go:embed emojis/0309.svg +var Emoji_0309 []byte + +//go:embed emojis/0310.svg +var Emoji_0310 []byte + +//go:embed emojis/0311.svg +var Emoji_0311 []byte + +//go:embed emojis/0312.svg +var Emoji_0312 []byte + +//go:embed emojis/0313.svg +var Emoji_0313 []byte + +//go:embed emojis/0314.svg +var Emoji_0314 []byte + +//go:embed emojis/0315.svg +var Emoji_0315 []byte + +//go:embed emojis/0316.svg +var Emoji_0316 []byte + +//go:embed emojis/0317.svg +var Emoji_0317 []byte + +//go:embed emojis/0318.svg +var Emoji_0318 []byte + +//go:embed emojis/0319.svg +var Emoji_0319 []byte + +//go:embed emojis/0320.svg +var Emoji_0320 []byte + +//go:embed emojis/0321.svg +var Emoji_0321 []byte + +//go:embed emojis/0322.svg +var Emoji_0322 []byte + +//go:embed emojis/0323.svg +var Emoji_0323 []byte + +//go:embed emojis/0324.svg +var Emoji_0324 []byte + +//go:embed emojis/0325.svg +var Emoji_0325 []byte + +//go:embed emojis/0326.svg +var Emoji_0326 []byte + +//go:embed emojis/0327.svg +var Emoji_0327 []byte + +//go:embed emojis/0328.svg +var Emoji_0328 []byte + +//go:embed emojis/0329.svg +var Emoji_0329 []byte + +//go:embed emojis/0330.svg +var Emoji_0330 []byte + +//go:embed emojis/0331.svg +var Emoji_0331 []byte + +//go:embed emojis/0332.svg +var Emoji_0332 []byte + +//go:embed emojis/0333.svg +var Emoji_0333 []byte + +//go:embed emojis/0334.svg +var Emoji_0334 []byte + +//go:embed emojis/0335.svg +var Emoji_0335 []byte + +//go:embed emojis/0336.svg +var Emoji_0336 []byte + +//go:embed emojis/0337.svg +var Emoji_0337 []byte + +//go:embed emojis/0338.svg +var Emoji_0338 []byte + +//go:embed emojis/0339.svg +var Emoji_0339 []byte + +//go:embed emojis/0340.svg +var Emoji_0340 []byte + +//go:embed emojis/0341.svg +var Emoji_0341 []byte + +//go:embed emojis/0342.svg +var Emoji_0342 []byte + +//go:embed emojis/0343.svg +var Emoji_0343 []byte + +//go:embed emojis/0344.svg +var Emoji_0344 []byte + +//go:embed emojis/0345.svg +var Emoji_0345 []byte + +//go:embed emojis/0346.svg +var Emoji_0346 []byte + +//go:embed emojis/0347.svg +var Emoji_0347 []byte + +//go:embed emojis/0348.svg +var Emoji_0348 []byte + +//go:embed emojis/0349.svg +var Emoji_0349 []byte + +//go:embed emojis/0350.svg +var Emoji_0350 []byte + +//go:embed emojis/0351.svg +var Emoji_0351 []byte + +//go:embed emojis/0352.svg +var Emoji_0352 []byte + +//go:embed emojis/0353.svg +var Emoji_0353 []byte + +//go:embed emojis/0354.svg +var Emoji_0354 []byte + +//go:embed emojis/0355.svg +var Emoji_0355 []byte + +//go:embed emojis/0356.svg +var Emoji_0356 []byte + +//go:embed emojis/0357.svg +var Emoji_0357 []byte + +//go:embed emojis/0358.svg +var Emoji_0358 []byte + +//go:embed emojis/0359.svg +var Emoji_0359 []byte + +//go:embed emojis/0360.svg +var Emoji_0360 []byte + +//go:embed emojis/0361.svg +var Emoji_0361 []byte + +//go:embed emojis/0362.svg +var Emoji_0362 []byte + +//go:embed emojis/0363.svg +var Emoji_0363 []byte + +//go:embed emojis/0364.svg +var Emoji_0364 []byte + +//go:embed emojis/0365.svg +var Emoji_0365 []byte + +//go:embed emojis/0366.svg +var Emoji_0366 []byte + +//go:embed emojis/0367.svg +var Emoji_0367 []byte + +//go:embed emojis/0368.svg +var Emoji_0368 []byte + +//go:embed emojis/0369.svg +var Emoji_0369 []byte + +//go:embed emojis/0370.svg +var Emoji_0370 []byte + +//go:embed emojis/0371.svg +var Emoji_0371 []byte + +//go:embed emojis/0372.svg +var Emoji_0372 []byte + +//go:embed emojis/0373.svg +var Emoji_0373 []byte + +//go:embed emojis/0374.svg +var Emoji_0374 []byte + +//go:embed emojis/0375.svg +var Emoji_0375 []byte + +//go:embed emojis/0376.svg +var Emoji_0376 []byte + +//go:embed emojis/0377.svg +var Emoji_0377 []byte + +//go:embed emojis/0378.svg +var Emoji_0378 []byte + +//go:embed emojis/0379.svg +var Emoji_0379 []byte + +//go:embed emojis/0380.svg +var Emoji_0380 []byte + +//go:embed emojis/0381.svg +var Emoji_0381 []byte + +//go:embed emojis/0382.svg +var Emoji_0382 []byte + +//go:embed emojis/0383.svg +var Emoji_0383 []byte + +//go:embed emojis/0384.svg +var Emoji_0384 []byte + +//go:embed emojis/0385.svg +var Emoji_0385 []byte + +//go:embed emojis/0386.svg +var Emoji_0386 []byte + +//go:embed emojis/0387.svg +var Emoji_0387 []byte + +//go:embed emojis/0388.svg +var Emoji_0388 []byte + +//go:embed emojis/0389.svg +var Emoji_0389 []byte + +//go:embed emojis/0390.svg +var Emoji_0390 []byte + +//go:embed emojis/0391.svg +var Emoji_0391 []byte + +//go:embed emojis/0392.svg +var Emoji_0392 []byte + +//go:embed emojis/0393.svg +var Emoji_0393 []byte + +//go:embed emojis/0394.svg +var Emoji_0394 []byte + +//go:embed emojis/0395.svg +var Emoji_0395 []byte + +//go:embed emojis/0396.svg +var Emoji_0396 []byte + +//go:embed emojis/0397.svg +var Emoji_0397 []byte + +//go:embed emojis/0398.svg +var Emoji_0398 []byte + +//go:embed emojis/0399.svg +var Emoji_0399 []byte + +//go:embed emojis/0400.svg +var Emoji_0400 []byte + +//go:embed emojis/0401.svg +var Emoji_0401 []byte + +//go:embed emojis/0402.svg +var Emoji_0402 []byte + +//go:embed emojis/0403.svg +var Emoji_0403 []byte + +//go:embed emojis/0404.svg +var Emoji_0404 []byte + +//go:embed emojis/0405.svg +var Emoji_0405 []byte + +//go:embed emojis/0406.svg +var Emoji_0406 []byte + +//go:embed emojis/0407.svg +var Emoji_0407 []byte + +//go:embed emojis/0408.svg +var Emoji_0408 []byte + +//go:embed emojis/0409.svg +var Emoji_0409 []byte + +//go:embed emojis/0410.svg +var Emoji_0410 []byte + +//go:embed emojis/0411.svg +var Emoji_0411 []byte + +//go:embed emojis/0412.svg +var Emoji_0412 []byte + +//go:embed emojis/0413.svg +var Emoji_0413 []byte + +//go:embed emojis/0414.svg +var Emoji_0414 []byte + +//go:embed emojis/0415.svg +var Emoji_0415 []byte + +//go:embed emojis/0416.svg +var Emoji_0416 []byte + +//go:embed emojis/0417.svg +var Emoji_0417 []byte + +//go:embed emojis/0418.svg +var Emoji_0418 []byte + +//go:embed emojis/0419.svg +var Emoji_0419 []byte + +//go:embed emojis/0420.svg +var Emoji_0420 []byte + +//go:embed emojis/0421.svg +var Emoji_0421 []byte + +//go:embed emojis/0422.svg +var Emoji_0422 []byte + +//go:embed emojis/0423.svg +var Emoji_0423 []byte + +//go:embed emojis/0424.svg +var Emoji_0424 []byte + +//go:embed emojis/0425.svg +var Emoji_0425 []byte + +//go:embed emojis/0426.svg +var Emoji_0426 []byte + +//go:embed emojis/0427.svg +var Emoji_0427 []byte + +//go:embed emojis/0428.svg +var Emoji_0428 []byte + +//go:embed emojis/0429.svg +var Emoji_0429 []byte + +//go:embed emojis/0430.svg +var Emoji_0430 []byte + +//go:embed emojis/0431.svg +var Emoji_0431 []byte + +//go:embed emojis/0432.svg +var Emoji_0432 []byte + +//go:embed emojis/0433.svg +var Emoji_0433 []byte + +//go:embed emojis/0434.svg +var Emoji_0434 []byte + +//go:embed emojis/0435.svg +var Emoji_0435 []byte + +//go:embed emojis/0436.svg +var Emoji_0436 []byte + +//go:embed emojis/0437.svg +var Emoji_0437 []byte + +//go:embed emojis/0438.svg +var Emoji_0438 []byte + +//go:embed emojis/0439.svg +var Emoji_0439 []byte + +//go:embed emojis/0440.svg +var Emoji_0440 []byte + +//go:embed emojis/0441.svg +var Emoji_0441 []byte + +//go:embed emojis/0442.svg +var Emoji_0442 []byte + +//go:embed emojis/0443.svg +var Emoji_0443 []byte + +//go:embed emojis/0444.svg +var Emoji_0444 []byte + +//go:embed emojis/0445.svg +var Emoji_0445 []byte + +//go:embed emojis/0446.svg +var Emoji_0446 []byte + +//go:embed emojis/0447.svg +var Emoji_0447 []byte + +//go:embed emojis/0448.svg +var Emoji_0448 []byte + +//go:embed emojis/0449.svg +var Emoji_0449 []byte + +//go:embed emojis/0450.svg +var Emoji_0450 []byte + +//go:embed emojis/0451.svg +var Emoji_0451 []byte + +//go:embed emojis/0452.svg +var Emoji_0452 []byte + +//go:embed emojis/0453.svg +var Emoji_0453 []byte + +//go:embed emojis/0454.svg +var Emoji_0454 []byte + +//go:embed emojis/0455.svg +var Emoji_0455 []byte + +//go:embed emojis/0456.svg +var Emoji_0456 []byte + +//go:embed emojis/0457.svg +var Emoji_0457 []byte + +//go:embed emojis/0458.svg +var Emoji_0458 []byte + +//go:embed emojis/0459.svg +var Emoji_0459 []byte + +//go:embed emojis/0460.svg +var Emoji_0460 []byte + +//go:embed emojis/0461.svg +var Emoji_0461 []byte + +//go:embed emojis/0462.svg +var Emoji_0462 []byte + +//go:embed emojis/0463.svg +var Emoji_0463 []byte + +//go:embed emojis/0464.svg +var Emoji_0464 []byte + +//go:embed emojis/0465.svg +var Emoji_0465 []byte + +//go:embed emojis/0466.svg +var Emoji_0466 []byte + +//go:embed emojis/0467.svg +var Emoji_0467 []byte + +//go:embed emojis/0468.svg +var Emoji_0468 []byte + +//go:embed emojis/0469.svg +var Emoji_0469 []byte + +//go:embed emojis/0470.svg +var Emoji_0470 []byte + +//go:embed emojis/0471.svg +var Emoji_0471 []byte + +//go:embed emojis/0472.svg +var Emoji_0472 []byte + +//go:embed emojis/0473.svg +var Emoji_0473 []byte + +//go:embed emojis/0474.svg +var Emoji_0474 []byte + +//go:embed emojis/0475.svg +var Emoji_0475 []byte + +//go:embed emojis/0476.svg +var Emoji_0476 []byte + +//go:embed emojis/0477.svg +var Emoji_0477 []byte + +//go:embed emojis/0478.svg +var Emoji_0478 []byte + +//go:embed emojis/0479.svg +var Emoji_0479 []byte + +//go:embed emojis/0480.svg +var Emoji_0480 []byte + +//go:embed emojis/0481.svg +var Emoji_0481 []byte + +//go:embed emojis/0482.svg +var Emoji_0482 []byte + +//go:embed emojis/0483.svg +var Emoji_0483 []byte + +//go:embed emojis/0484.svg +var Emoji_0484 []byte + +//go:embed emojis/0485.svg +var Emoji_0485 []byte + +//go:embed emojis/0486.svg +var Emoji_0486 []byte + +//go:embed emojis/0487.svg +var Emoji_0487 []byte + +//go:embed emojis/0488.svg +var Emoji_0488 []byte + +//go:embed emojis/0489.svg +var Emoji_0489 []byte + +//go:embed emojis/0490.svg +var Emoji_0490 []byte + +//go:embed emojis/0491.svg +var Emoji_0491 []byte + +//go:embed emojis/0492.svg +var Emoji_0492 []byte + +//go:embed emojis/0493.svg +var Emoji_0493 []byte + +//go:embed emojis/0494.svg +var Emoji_0494 []byte + +//go:embed emojis/0495.svg +var Emoji_0495 []byte + +//go:embed emojis/0496.svg +var Emoji_0496 []byte + +//go:embed emojis/0497.svg +var Emoji_0497 []byte + +//go:embed emojis/0498.svg +var Emoji_0498 []byte + +//go:embed emojis/0499.svg +var Emoji_0499 []byte + +//go:embed emojis/0500.svg +var Emoji_0500 []byte + +//go:embed emojis/0501.svg +var Emoji_0501 []byte + +//go:embed emojis/0502.svg +var Emoji_0502 []byte + +//go:embed emojis/0503.svg +var Emoji_0503 []byte + +//go:embed emojis/0504.svg +var Emoji_0504 []byte + +//go:embed emojis/0505.svg +var Emoji_0505 []byte + +//go:embed emojis/0506.svg +var Emoji_0506 []byte + +//go:embed emojis/0507.svg +var Emoji_0507 []byte + +//go:embed emojis/0508.svg +var Emoji_0508 []byte + +//go:embed emojis/0509.svg +var Emoji_0509 []byte + +//go:embed emojis/0510.svg +var Emoji_0510 []byte + +//go:embed emojis/0511.svg +var Emoji_0511 []byte + +//go:embed emojis/0512.svg +var Emoji_0512 []byte + +//go:embed emojis/0513.svg +var Emoji_0513 []byte + +//go:embed emojis/0514.svg +var Emoji_0514 []byte + +//go:embed emojis/0515.svg +var Emoji_0515 []byte + +//go:embed emojis/0516.svg +var Emoji_0516 []byte + +//go:embed emojis/0517.svg +var Emoji_0517 []byte + +//go:embed emojis/0518.svg +var Emoji_0518 []byte + +//go:embed emojis/0519.svg +var Emoji_0519 []byte + +//go:embed emojis/0520.svg +var Emoji_0520 []byte + +//go:embed emojis/0521.svg +var Emoji_0521 []byte + +//go:embed emojis/0522.svg +var Emoji_0522 []byte + +//go:embed emojis/0523.svg +var Emoji_0523 []byte + +//go:embed emojis/0524.svg +var Emoji_0524 []byte + +//go:embed emojis/0525.svg +var Emoji_0525 []byte + +//go:embed emojis/0526.svg +var Emoji_0526 []byte + +//go:embed emojis/0527.svg +var Emoji_0527 []byte + +//go:embed emojis/0528.svg +var Emoji_0528 []byte + +//go:embed emojis/0529.svg +var Emoji_0529 []byte + +//go:embed emojis/0530.svg +var Emoji_0530 []byte + +//go:embed emojis/0531.svg +var Emoji_0531 []byte + +//go:embed emojis/0532.svg +var Emoji_0532 []byte + +//go:embed emojis/0533.svg +var Emoji_0533 []byte + +//go:embed emojis/0534.svg +var Emoji_0534 []byte + +//go:embed emojis/0535.svg +var Emoji_0535 []byte + +//go:embed emojis/0536.svg +var Emoji_0536 []byte + +//go:embed emojis/0537.svg +var Emoji_0537 []byte + +//go:embed emojis/0538.svg +var Emoji_0538 []byte + +//go:embed emojis/0539.svg +var Emoji_0539 []byte + +//go:embed emojis/0540.svg +var Emoji_0540 []byte + +//go:embed emojis/0541.svg +var Emoji_0541 []byte + +//go:embed emojis/0542.svg +var Emoji_0542 []byte + +//go:embed emojis/0543.svg +var Emoji_0543 []byte + +//go:embed emojis/0544.svg +var Emoji_0544 []byte + +//go:embed emojis/0545.svg +var Emoji_0545 []byte + +//go:embed emojis/0546.svg +var Emoji_0546 []byte + +//go:embed emojis/0547.svg +var Emoji_0547 []byte + +//go:embed emojis/0548.svg +var Emoji_0548 []byte + +//go:embed emojis/0549.svg +var Emoji_0549 []byte + +//go:embed emojis/0550.svg +var Emoji_0550 []byte + +//go:embed emojis/0551.svg +var Emoji_0551 []byte + +//go:embed emojis/0552.svg +var Emoji_0552 []byte + +//go:embed emojis/0553.svg +var Emoji_0553 []byte + +//go:embed emojis/0554.svg +var Emoji_0554 []byte + +//go:embed emojis/0555.svg +var Emoji_0555 []byte + +//go:embed emojis/0556.svg +var Emoji_0556 []byte + +//go:embed emojis/0557.svg +var Emoji_0557 []byte + +//go:embed emojis/0558.svg +var Emoji_0558 []byte + +//go:embed emojis/0559.svg +var Emoji_0559 []byte + +//go:embed emojis/0560.svg +var Emoji_0560 []byte + +//go:embed emojis/0561.svg +var Emoji_0561 []byte + +//go:embed emojis/0562.svg +var Emoji_0562 []byte + +//go:embed emojis/0563.svg +var Emoji_0563 []byte + +//go:embed emojis/0564.svg +var Emoji_0564 []byte + +//go:embed emojis/0565.svg +var Emoji_0565 []byte + +//go:embed emojis/0566.svg +var Emoji_0566 []byte + +//go:embed emojis/0567.svg +var Emoji_0567 []byte + +//go:embed emojis/0568.svg +var Emoji_0568 []byte + +//go:embed emojis/0569.svg +var Emoji_0569 []byte + +//go:embed emojis/0570.svg +var Emoji_0570 []byte + +//go:embed emojis/0571.svg +var Emoji_0571 []byte + +//go:embed emojis/0572.svg +var Emoji_0572 []byte + +//go:embed emojis/0573.svg +var Emoji_0573 []byte + +//go:embed emojis/0574.svg +var Emoji_0574 []byte + +//go:embed emojis/0575.svg +var Emoji_0575 []byte + +//go:embed emojis/0576.svg +var Emoji_0576 []byte + +//go:embed emojis/0577.svg +var Emoji_0577 []byte + +//go:embed emojis/0578.svg +var Emoji_0578 []byte + +//go:embed emojis/0579.svg +var Emoji_0579 []byte + +//go:embed emojis/0580.svg +var Emoji_0580 []byte + +//go:embed emojis/0581.svg +var Emoji_0581 []byte + +//go:embed emojis/0582.svg +var Emoji_0582 []byte + +//go:embed emojis/0583.svg +var Emoji_0583 []byte + +//go:embed emojis/0584.svg +var Emoji_0584 []byte + +//go:embed emojis/0585.svg +var Emoji_0585 []byte + +//go:embed emojis/0586.svg +var Emoji_0586 []byte + +//go:embed emojis/0587.svg +var Emoji_0587 []byte + +//go:embed emojis/0588.svg +var Emoji_0588 []byte + +//go:embed emojis/0589.svg +var Emoji_0589 []byte + +//go:embed emojis/0590.svg +var Emoji_0590 []byte + +//go:embed emojis/0591.svg +var Emoji_0591 []byte + +//go:embed emojis/0592.svg +var Emoji_0592 []byte + +//go:embed emojis/0593.svg +var Emoji_0593 []byte + +//go:embed emojis/0594.svg +var Emoji_0594 []byte + +//go:embed emojis/0595.svg +var Emoji_0595 []byte + +//go:embed emojis/0596.svg +var Emoji_0596 []byte + +//go:embed emojis/0597.svg +var Emoji_0597 []byte + +//go:embed emojis/0598.svg +var Emoji_0598 []byte + +//go:embed emojis/0599.svg +var Emoji_0599 []byte + +//go:embed emojis/0600.svg +var Emoji_0600 []byte + +//go:embed emojis/0601.svg +var Emoji_0601 []byte + +//go:embed emojis/0602.svg +var Emoji_0602 []byte + +//go:embed emojis/0603.svg +var Emoji_0603 []byte + +//go:embed emojis/0604.svg +var Emoji_0604 []byte + +//go:embed emojis/0605.svg +var Emoji_0605 []byte + +//go:embed emojis/0606.svg +var Emoji_0606 []byte + +//go:embed emojis/0607.svg +var Emoji_0607 []byte + +//go:embed emojis/0608.svg +var Emoji_0608 []byte + +//go:embed emojis/0609.svg +var Emoji_0609 []byte + +//go:embed emojis/0610.svg +var Emoji_0610 []byte + +//go:embed emojis/0611.svg +var Emoji_0611 []byte + +//go:embed emojis/0612.svg +var Emoji_0612 []byte + +//go:embed emojis/0613.svg +var Emoji_0613 []byte + +//go:embed emojis/0614.svg +var Emoji_0614 []byte + +//go:embed emojis/0615.svg +var Emoji_0615 []byte + +//go:embed emojis/0616.svg +var Emoji_0616 []byte + +//go:embed emojis/0617.svg +var Emoji_0617 []byte + +//go:embed emojis/0618.svg +var Emoji_0618 []byte + +//go:embed emojis/0619.svg +var Emoji_0619 []byte + +//go:embed emojis/0620.svg +var Emoji_0620 []byte + +//go:embed emojis/0621.svg +var Emoji_0621 []byte + +//go:embed emojis/0622.svg +var Emoji_0622 []byte + +//go:embed emojis/0623.svg +var Emoji_0623 []byte + +//go:embed emojis/0624.svg +var Emoji_0624 []byte + +//go:embed emojis/0625.svg +var Emoji_0625 []byte + +//go:embed emojis/0626.svg +var Emoji_0626 []byte + +//go:embed emojis/0627.svg +var Emoji_0627 []byte + +//go:embed emojis/0628.svg +var Emoji_0628 []byte + +//go:embed emojis/0629.svg +var Emoji_0629 []byte + +//go:embed emojis/0630.svg +var Emoji_0630 []byte + +//go:embed emojis/0631.svg +var Emoji_0631 []byte + +//go:embed emojis/0632.svg +var Emoji_0632 []byte + +//go:embed emojis/0633.svg +var Emoji_0633 []byte + +//go:embed emojis/0634.svg +var Emoji_0634 []byte + +//go:embed emojis/0635.svg +var Emoji_0635 []byte + +//go:embed emojis/0636.svg +var Emoji_0636 []byte + +//go:embed emojis/0637.svg +var Emoji_0637 []byte + +//go:embed emojis/0638.svg +var Emoji_0638 []byte + +//go:embed emojis/0639.svg +var Emoji_0639 []byte + +//go:embed emojis/0640.svg +var Emoji_0640 []byte + +//go:embed emojis/0641.svg +var Emoji_0641 []byte + +//go:embed emojis/0642.svg +var Emoji_0642 []byte + +//go:embed emojis/0643.svg +var Emoji_0643 []byte + +//go:embed emojis/0644.svg +var Emoji_0644 []byte + +//go:embed emojis/0645.svg +var Emoji_0645 []byte + +//go:embed emojis/0646.svg +var Emoji_0646 []byte + +//go:embed emojis/0647.svg +var Emoji_0647 []byte + +//go:embed emojis/0648.svg +var Emoji_0648 []byte + +//go:embed emojis/0649.svg +var Emoji_0649 []byte + +//go:embed emojis/0650.svg +var Emoji_0650 []byte + +//go:embed emojis/0651.svg +var Emoji_0651 []byte + +//go:embed emojis/0652.svg +var Emoji_0652 []byte + +//go:embed emojis/0653.svg +var Emoji_0653 []byte + +//go:embed emojis/0654.svg +var Emoji_0654 []byte + +//go:embed emojis/0655.svg +var Emoji_0655 []byte + +//go:embed emojis/0656.svg +var Emoji_0656 []byte + +//go:embed emojis/0657.svg +var Emoji_0657 []byte + +//go:embed emojis/0658.svg +var Emoji_0658 []byte + +//go:embed emojis/0659.svg +var Emoji_0659 []byte + +//go:embed emojis/0660.svg +var Emoji_0660 []byte + +//go:embed emojis/0661.svg +var Emoji_0661 []byte + +//go:embed emojis/0662.svg +var Emoji_0662 []byte + +//go:embed emojis/0663.svg +var Emoji_0663 []byte + +//go:embed emojis/0664.svg +var Emoji_0664 []byte + +//go:embed emojis/0665.svg +var Emoji_0665 []byte + +//go:embed emojis/0666.svg +var Emoji_0666 []byte + +//go:embed emojis/0667.svg +var Emoji_0667 []byte + +//go:embed emojis/0668.svg +var Emoji_0668 []byte + +//go:embed emojis/0669.svg +var Emoji_0669 []byte + +//go:embed emojis/0670.svg +var Emoji_0670 []byte + +//go:embed emojis/0671.svg +var Emoji_0671 []byte + +//go:embed emojis/0672.svg +var Emoji_0672 []byte + +//go:embed emojis/0673.svg +var Emoji_0673 []byte + +//go:embed emojis/0674.svg +var Emoji_0674 []byte + +//go:embed emojis/0675.svg +var Emoji_0675 []byte + +//go:embed emojis/0676.svg +var Emoji_0676 []byte + +//go:embed emojis/0677.svg +var Emoji_0677 []byte + +//go:embed emojis/0678.svg +var Emoji_0678 []byte + +//go:embed emojis/0679.svg +var Emoji_0679 []byte + +//go:embed emojis/0680.svg +var Emoji_0680 []byte + +//go:embed emojis/0681.svg +var Emoji_0681 []byte + +//go:embed emojis/0682.svg +var Emoji_0682 []byte + +//go:embed emojis/0683.svg +var Emoji_0683 []byte + +//go:embed emojis/0684.svg +var Emoji_0684 []byte + +//go:embed emojis/0685.svg +var Emoji_0685 []byte + +//go:embed emojis/0686.svg +var Emoji_0686 []byte + +//go:embed emojis/0687.svg +var Emoji_0687 []byte + +//go:embed emojis/0688.svg +var Emoji_0688 []byte + +//go:embed emojis/0689.svg +var Emoji_0689 []byte + +//go:embed emojis/0690.svg +var Emoji_0690 []byte + +//go:embed emojis/0691.svg +var Emoji_0691 []byte + +//go:embed emojis/0692.svg +var Emoji_0692 []byte + +//go:embed emojis/0693.svg +var Emoji_0693 []byte + +//go:embed emojis/0694.svg +var Emoji_0694 []byte + +//go:embed emojis/0695.svg +var Emoji_0695 []byte + +//go:embed emojis/0696.svg +var Emoji_0696 []byte + +//go:embed emojis/0697.svg +var Emoji_0697 []byte + +//go:embed emojis/0698.svg +var Emoji_0698 []byte + +//go:embed emojis/0699.svg +var Emoji_0699 []byte + +//go:embed emojis/0700.svg +var Emoji_0700 []byte + +//go:embed emojis/0701.svg +var Emoji_0701 []byte + +//go:embed emojis/0702.svg +var Emoji_0702 []byte + +//go:embed emojis/0703.svg +var Emoji_0703 []byte + +//go:embed emojis/0704.svg +var Emoji_0704 []byte + +//go:embed emojis/0705.svg +var Emoji_0705 []byte + +//go:embed emojis/0706.svg +var Emoji_0706 []byte + +//go:embed emojis/0707.svg +var Emoji_0707 []byte + +//go:embed emojis/0708.svg +var Emoji_0708 []byte + +//go:embed emojis/0709.svg +var Emoji_0709 []byte + +//go:embed emojis/0710.svg +var Emoji_0710 []byte + +//go:embed emojis/0711.svg +var Emoji_0711 []byte + +//go:embed emojis/0712.svg +var Emoji_0712 []byte + +//go:embed emojis/0713.svg +var Emoji_0713 []byte + +//go:embed emojis/0714.svg +var Emoji_0714 []byte + +//go:embed emojis/0715.svg +var Emoji_0715 []byte + +//go:embed emojis/0716.svg +var Emoji_0716 []byte + +//go:embed emojis/0717.svg +var Emoji_0717 []byte + +//go:embed emojis/0718.svg +var Emoji_0718 []byte + +//go:embed emojis/0719.svg +var Emoji_0719 []byte + +//go:embed emojis/0720.svg +var Emoji_0720 []byte + +//go:embed emojis/0721.svg +var Emoji_0721 []byte + +//go:embed emojis/0722.svg +var Emoji_0722 []byte + +//go:embed emojis/0723.svg +var Emoji_0723 []byte + +//go:embed emojis/0724.svg +var Emoji_0724 []byte + +//go:embed emojis/0725.svg +var Emoji_0725 []byte + +//go:embed emojis/0726.svg +var Emoji_0726 []byte + +//go:embed emojis/0727.svg +var Emoji_0727 []byte + +//go:embed emojis/0728.svg +var Emoji_0728 []byte + +//go:embed emojis/0729.svg +var Emoji_0729 []byte + +//go:embed emojis/0730.svg +var Emoji_0730 []byte + +//go:embed emojis/0731.svg +var Emoji_0731 []byte + +//go:embed emojis/0732.svg +var Emoji_0732 []byte + +//go:embed emojis/0733.svg +var Emoji_0733 []byte + +//go:embed emojis/0734.svg +var Emoji_0734 []byte + +//go:embed emojis/0735.svg +var Emoji_0735 []byte + +//go:embed emojis/0736.svg +var Emoji_0736 []byte + +//go:embed emojis/0737.svg +var Emoji_0737 []byte + +//go:embed emojis/0738.svg +var Emoji_0738 []byte + +//go:embed emojis/0739.svg +var Emoji_0739 []byte + +//go:embed emojis/0740.svg +var Emoji_0740 []byte + +//go:embed emojis/0741.svg +var Emoji_0741 []byte + +//go:embed emojis/0742.svg +var Emoji_0742 []byte + +//go:embed emojis/0743.svg +var Emoji_0743 []byte + +//go:embed emojis/0744.svg +var Emoji_0744 []byte + +//go:embed emojis/0745.svg +var Emoji_0745 []byte + +//go:embed emojis/0746.svg +var Emoji_0746 []byte + +//go:embed emojis/0747.svg +var Emoji_0747 []byte + +//go:embed emojis/0748.svg +var Emoji_0748 []byte + +//go:embed emojis/0749.svg +var Emoji_0749 []byte + +//go:embed emojis/0750.svg +var Emoji_0750 []byte + +//go:embed emojis/0751.svg +var Emoji_0751 []byte + +//go:embed emojis/0752.svg +var Emoji_0752 []byte + +//go:embed emojis/0753.svg +var Emoji_0753 []byte + +//go:embed emojis/0754.svg +var Emoji_0754 []byte + +//go:embed emojis/0755.svg +var Emoji_0755 []byte + +//go:embed emojis/0756.svg +var Emoji_0756 []byte + +//go:embed emojis/0757.svg +var Emoji_0757 []byte + +//go:embed emojis/0758.svg +var Emoji_0758 []byte + +//go:embed emojis/0759.svg +var Emoji_0759 []byte + +//go:embed emojis/0760.svg +var Emoji_0760 []byte + +//go:embed emojis/0761.svg +var Emoji_0761 []byte + +//go:embed emojis/0762.svg +var Emoji_0762 []byte + +//go:embed emojis/0763.svg +var Emoji_0763 []byte + +//go:embed emojis/0764.svg +var Emoji_0764 []byte + +//go:embed emojis/0765.svg +var Emoji_0765 []byte + +//go:embed emojis/0766.svg +var Emoji_0766 []byte + +//go:embed emojis/0767.svg +var Emoji_0767 []byte + +//go:embed emojis/0768.svg +var Emoji_0768 []byte + +//go:embed emojis/0769.svg +var Emoji_0769 []byte + +//go:embed emojis/0770.svg +var Emoji_0770 []byte + +//go:embed emojis/0771.svg +var Emoji_0771 []byte + +//go:embed emojis/0772.svg +var Emoji_0772 []byte + +//go:embed emojis/0773.svg +var Emoji_0773 []byte + +//go:embed emojis/0774.svg +var Emoji_0774 []byte + +//go:embed emojis/0775.svg +var Emoji_0775 []byte + +//go:embed emojis/0776.svg +var Emoji_0776 []byte + +//go:embed emojis/0777.svg +var Emoji_0777 []byte + +//go:embed emojis/0778.svg +var Emoji_0778 []byte + +//go:embed emojis/0779.svg +var Emoji_0779 []byte + +//go:embed emojis/0780.svg +var Emoji_0780 []byte + +//go:embed emojis/0781.svg +var Emoji_0781 []byte + +//go:embed emojis/0782.svg +var Emoji_0782 []byte + +//go:embed emojis/0783.svg +var Emoji_0783 []byte + +//go:embed emojis/0784.svg +var Emoji_0784 []byte + +//go:embed emojis/0785.svg +var Emoji_0785 []byte + +//go:embed emojis/0786.svg +var Emoji_0786 []byte + +//go:embed emojis/0787.svg +var Emoji_0787 []byte + +//go:embed emojis/0788.svg +var Emoji_0788 []byte + +//go:embed emojis/0789.svg +var Emoji_0789 []byte + +//go:embed emojis/0790.svg +var Emoji_0790 []byte + +//go:embed emojis/0791.svg +var Emoji_0791 []byte + +//go:embed emojis/0792.svg +var Emoji_0792 []byte + +//go:embed emojis/0793.svg +var Emoji_0793 []byte + +//go:embed emojis/0794.svg +var Emoji_0794 []byte + +//go:embed emojis/0795.svg +var Emoji_0795 []byte + +//go:embed emojis/0796.svg +var Emoji_0796 []byte + +//go:embed emojis/0797.svg +var Emoji_0797 []byte + +//go:embed emojis/0798.svg +var Emoji_0798 []byte + +//go:embed emojis/0799.svg +var Emoji_0799 []byte + +//go:embed emojis/0800.svg +var Emoji_0800 []byte + +//go:embed emojis/0801.svg +var Emoji_0801 []byte + +//go:embed emojis/0802.svg +var Emoji_0802 []byte + +//go:embed emojis/0803.svg +var Emoji_0803 []byte + +//go:embed emojis/0804.svg +var Emoji_0804 []byte + +//go:embed emojis/0805.svg +var Emoji_0805 []byte + +//go:embed emojis/0806.svg +var Emoji_0806 []byte + +//go:embed emojis/0807.svg +var Emoji_0807 []byte + +//go:embed emojis/0808.svg +var Emoji_0808 []byte + +//go:embed emojis/0809.svg +var Emoji_0809 []byte + +//go:embed emojis/0810.svg +var Emoji_0810 []byte + +//go:embed emojis/0811.svg +var Emoji_0811 []byte + +//go:embed emojis/0812.svg +var Emoji_0812 []byte + +//go:embed emojis/0813.svg +var Emoji_0813 []byte + +//go:embed emojis/0814.svg +var Emoji_0814 []byte + +//go:embed emojis/0815.svg +var Emoji_0815 []byte + +//go:embed emojis/0816.svg +var Emoji_0816 []byte + +//go:embed emojis/0817.svg +var Emoji_0817 []byte + +//go:embed emojis/0818.svg +var Emoji_0818 []byte + +//go:embed emojis/0819.svg +var Emoji_0819 []byte + +//go:embed emojis/0820.svg +var Emoji_0820 []byte + +//go:embed emojis/0821.svg +var Emoji_0821 []byte + +//go:embed emojis/0822.svg +var Emoji_0822 []byte + +//go:embed emojis/0823.svg +var Emoji_0823 []byte + +//go:embed emojis/0824.svg +var Emoji_0824 []byte + +//go:embed emojis/0825.svg +var Emoji_0825 []byte + +//go:embed emojis/0826.svg +var Emoji_0826 []byte + +//go:embed emojis/0827.svg +var Emoji_0827 []byte + +//go:embed emojis/0828.svg +var Emoji_0828 []byte + +//go:embed emojis/0829.svg +var Emoji_0829 []byte + +//go:embed emojis/0830.svg +var Emoji_0830 []byte + +//go:embed emojis/0831.svg +var Emoji_0831 []byte + +//go:embed emojis/0832.svg +var Emoji_0832 []byte + +//go:embed emojis/0833.svg +var Emoji_0833 []byte + +//go:embed emojis/0834.svg +var Emoji_0834 []byte + +//go:embed emojis/0835.svg +var Emoji_0835 []byte + +//go:embed emojis/0836.svg +var Emoji_0836 []byte + +//go:embed emojis/0837.svg +var Emoji_0837 []byte + +//go:embed emojis/0838.svg +var Emoji_0838 []byte + +//go:embed emojis/0839.svg +var Emoji_0839 []byte + +//go:embed emojis/0840.svg +var Emoji_0840 []byte + +//go:embed emojis/0841.svg +var Emoji_0841 []byte + +//go:embed emojis/0842.svg +var Emoji_0842 []byte + +//go:embed emojis/0843.svg +var Emoji_0843 []byte + +//go:embed emojis/0844.svg +var Emoji_0844 []byte + +//go:embed emojis/0845.svg +var Emoji_0845 []byte + +//go:embed emojis/0846.svg +var Emoji_0846 []byte + +//go:embed emojis/0847.svg +var Emoji_0847 []byte + +//go:embed emojis/0848.svg +var Emoji_0848 []byte + +//go:embed emojis/0849.svg +var Emoji_0849 []byte + +//go:embed emojis/0850.svg +var Emoji_0850 []byte + +//go:embed emojis/0851.svg +var Emoji_0851 []byte + +//go:embed emojis/0852.svg +var Emoji_0852 []byte + +//go:embed emojis/0853.svg +var Emoji_0853 []byte + +//go:embed emojis/0854.svg +var Emoji_0854 []byte + +//go:embed emojis/0855.svg +var Emoji_0855 []byte + +//go:embed emojis/0856.svg +var Emoji_0856 []byte + +//go:embed emojis/0857.svg +var Emoji_0857 []byte + +//go:embed emojis/0858.svg +var Emoji_0858 []byte + +//go:embed emojis/0859.svg +var Emoji_0859 []byte + +//go:embed emojis/0860.svg +var Emoji_0860 []byte + +//go:embed emojis/0861.svg +var Emoji_0861 []byte + +//go:embed emojis/0862.svg +var Emoji_0862 []byte + +//go:embed emojis/0863.svg +var Emoji_0863 []byte + +//go:embed emojis/0864.svg +var Emoji_0864 []byte + +//go:embed emojis/0865.svg +var Emoji_0865 []byte + +//go:embed emojis/0866.svg +var Emoji_0866 []byte + +//go:embed emojis/0867.svg +var Emoji_0867 []byte + +//go:embed emojis/0868.svg +var Emoji_0868 []byte + +//go:embed emojis/0869.svg +var Emoji_0869 []byte + +//go:embed emojis/0870.svg +var Emoji_0870 []byte + +//go:embed emojis/0871.svg +var Emoji_0871 []byte + +//go:embed emojis/0872.svg +var Emoji_0872 []byte + +//go:embed emojis/0873.svg +var Emoji_0873 []byte + +//go:embed emojis/0874.svg +var Emoji_0874 []byte + +//go:embed emojis/0875.svg +var Emoji_0875 []byte + +//go:embed emojis/0876.svg +var Emoji_0876 []byte + +//go:embed emojis/0877.svg +var Emoji_0877 []byte + +//go:embed emojis/0878.svg +var Emoji_0878 []byte + +//go:embed emojis/0879.svg +var Emoji_0879 []byte + +//go:embed emojis/0880.svg +var Emoji_0880 []byte + +//go:embed emojis/0881.svg +var Emoji_0881 []byte + +//go:embed emojis/0882.svg +var Emoji_0882 []byte + +//go:embed emojis/0883.svg +var Emoji_0883 []byte + +//go:embed emojis/0884.svg +var Emoji_0884 []byte + +//go:embed emojis/0885.svg +var Emoji_0885 []byte + +//go:embed emojis/0886.svg +var Emoji_0886 []byte + +//go:embed emojis/0887.svg +var Emoji_0887 []byte + +//go:embed emojis/0888.svg +var Emoji_0888 []byte + +//go:embed emojis/0889.svg +var Emoji_0889 []byte + +//go:embed emojis/0890.svg +var Emoji_0890 []byte + +//go:embed emojis/0891.svg +var Emoji_0891 []byte + +//go:embed emojis/0892.svg +var Emoji_0892 []byte + +//go:embed emojis/0893.svg +var Emoji_0893 []byte + +//go:embed emojis/0894.svg +var Emoji_0894 []byte + +//go:embed emojis/0895.svg +var Emoji_0895 []byte + +//go:embed emojis/0896.svg +var Emoji_0896 []byte + +//go:embed emojis/0897.svg +var Emoji_0897 []byte + +//go:embed emojis/0898.svg +var Emoji_0898 []byte + +//go:embed emojis/0899.svg +var Emoji_0899 []byte + +//go:embed emojis/0900.svg +var Emoji_0900 []byte + +//go:embed emojis/0901.svg +var Emoji_0901 []byte + +//go:embed emojis/0902.svg +var Emoji_0902 []byte + +//go:embed emojis/0903.svg +var Emoji_0903 []byte + +//go:embed emojis/0904.svg +var Emoji_0904 []byte + +//go:embed emojis/0905.svg +var Emoji_0905 []byte + +//go:embed emojis/0906.svg +var Emoji_0906 []byte + +//go:embed emojis/0907.svg +var Emoji_0907 []byte + +//go:embed emojis/0908.svg +var Emoji_0908 []byte + +//go:embed emojis/0909.svg +var Emoji_0909 []byte + +//go:embed emojis/0910.svg +var Emoji_0910 []byte + +//go:embed emojis/0911.svg +var Emoji_0911 []byte + +//go:embed emojis/0912.svg +var Emoji_0912 []byte + +//go:embed emojis/0913.svg +var Emoji_0913 []byte + +//go:embed emojis/0914.svg +var Emoji_0914 []byte + +//go:embed emojis/0915.svg +var Emoji_0915 []byte + +//go:embed emojis/0916.svg +var Emoji_0916 []byte + +//go:embed emojis/0917.svg +var Emoji_0917 []byte + +//go:embed emojis/0918.svg +var Emoji_0918 []byte + +//go:embed emojis/0919.svg +var Emoji_0919 []byte + +//go:embed emojis/0920.svg +var Emoji_0920 []byte + +//go:embed emojis/0921.svg +var Emoji_0921 []byte + +//go:embed emojis/0922.svg +var Emoji_0922 []byte + +//go:embed emojis/0923.svg +var Emoji_0923 []byte + +//go:embed emojis/0924.svg +var Emoji_0924 []byte + +//go:embed emojis/0925.svg +var Emoji_0925 []byte + +//go:embed emojis/0926.svg +var Emoji_0926 []byte + +//go:embed emojis/0927.svg +var Emoji_0927 []byte + +//go:embed emojis/0928.svg +var Emoji_0928 []byte + +//go:embed emojis/0929.svg +var Emoji_0929 []byte + +//go:embed emojis/0930.svg +var Emoji_0930 []byte + +//go:embed emojis/0931.svg +var Emoji_0931 []byte + +//go:embed emojis/0932.svg +var Emoji_0932 []byte + +//go:embed emojis/0933.svg +var Emoji_0933 []byte + +//go:embed emojis/0934.svg +var Emoji_0934 []byte + +//go:embed emojis/0935.svg +var Emoji_0935 []byte + +//go:embed emojis/0936.svg +var Emoji_0936 []byte + +//go:embed emojis/0937.svg +var Emoji_0937 []byte + +//go:embed emojis/0938.svg +var Emoji_0938 []byte + +//go:embed emojis/0939.svg +var Emoji_0939 []byte + +//go:embed emojis/0940.svg +var Emoji_0940 []byte + +//go:embed emojis/0941.svg +var Emoji_0941 []byte + +//go:embed emojis/0942.svg +var Emoji_0942 []byte + +//go:embed emojis/0943.svg +var Emoji_0943 []byte + +//go:embed emojis/0944.svg +var Emoji_0944 []byte + +//go:embed emojis/0945.svg +var Emoji_0945 []byte + +//go:embed emojis/0946.svg +var Emoji_0946 []byte + +//go:embed emojis/0947.svg +var Emoji_0947 []byte + +//go:embed emojis/0948.svg +var Emoji_0948 []byte + +//go:embed emojis/0949.svg +var Emoji_0949 []byte + +//go:embed emojis/0950.svg +var Emoji_0950 []byte + +//go:embed emojis/0951.svg +var Emoji_0951 []byte + +//go:embed emojis/0952.svg +var Emoji_0952 []byte + +//go:embed emojis/0953.svg +var Emoji_0953 []byte + +//go:embed emojis/0954.svg +var Emoji_0954 []byte + +//go:embed emojis/0955.svg +var Emoji_0955 []byte + +//go:embed emojis/0956.svg +var Emoji_0956 []byte + +//go:embed emojis/0957.svg +var Emoji_0957 []byte + +//go:embed emojis/0958.svg +var Emoji_0958 []byte + +//go:embed emojis/0959.svg +var Emoji_0959 []byte + +//go:embed emojis/0960.svg +var Emoji_0960 []byte + +//go:embed emojis/0961.svg +var Emoji_0961 []byte + +//go:embed emojis/0962.svg +var Emoji_0962 []byte + +//go:embed emojis/0963.svg +var Emoji_0963 []byte + +//go:embed emojis/0964.svg +var Emoji_0964 []byte + +//go:embed emojis/0965.svg +var Emoji_0965 []byte + +//go:embed emojis/0966.svg +var Emoji_0966 []byte + +//go:embed emojis/0967.svg +var Emoji_0967 []byte + +//go:embed emojis/0968.svg +var Emoji_0968 []byte + +//go:embed emojis/0969.svg +var Emoji_0969 []byte + +//go:embed emojis/0970.svg +var Emoji_0970 []byte + +//go:embed emojis/0971.svg +var Emoji_0971 []byte + +//go:embed emojis/0972.svg +var Emoji_0972 []byte + +//go:embed emojis/0973.svg +var Emoji_0973 []byte + +//go:embed emojis/0974.svg +var Emoji_0974 []byte + +//go:embed emojis/0975.svg +var Emoji_0975 []byte + +//go:embed emojis/0976.svg +var Emoji_0976 []byte + +//go:embed emojis/0977.svg +var Emoji_0977 []byte + +//go:embed emojis/0978.svg +var Emoji_0978 []byte + +//go:embed emojis/0979.svg +var Emoji_0979 []byte + +//go:embed emojis/0980.svg +var Emoji_0980 []byte + +//go:embed emojis/0981.svg +var Emoji_0981 []byte + +//go:embed emojis/0982.svg +var Emoji_0982 []byte + +//go:embed emojis/0983.svg +var Emoji_0983 []byte + +//go:embed emojis/0984.svg +var Emoji_0984 []byte + +//go:embed emojis/0985.svg +var Emoji_0985 []byte + +//go:embed emojis/0986.svg +var Emoji_0986 []byte + +//go:embed emojis/0987.svg +var Emoji_0987 []byte + +//go:embed emojis/0988.svg +var Emoji_0988 []byte + +//go:embed emojis/0989.svg +var Emoji_0989 []byte + +//go:embed emojis/0990.svg +var Emoji_0990 []byte + +//go:embed emojis/0991.svg +var Emoji_0991 []byte + +//go:embed emojis/0992.svg +var Emoji_0992 []byte + +//go:embed emojis/0993.svg +var Emoji_0993 []byte + +//go:embed emojis/0994.svg +var Emoji_0994 []byte + +//go:embed emojis/0995.svg +var Emoji_0995 []byte + +//go:embed emojis/0996.svg +var Emoji_0996 []byte + +//go:embed emojis/0997.svg +var Emoji_0997 []byte + +//go:embed emojis/0998.svg +var Emoji_0998 []byte + +//go:embed emojis/0999.svg +var Emoji_0999 []byte + +//go:embed emojis/1000.svg +var Emoji_1000 []byte + +//go:embed emojis/1001.svg +var Emoji_1001 []byte + +//go:embed emojis/1002.svg +var Emoji_1002 []byte + +//go:embed emojis/1003.svg +var Emoji_1003 []byte + +//go:embed emojis/1004.svg +var Emoji_1004 []byte + +//go:embed emojis/1005.svg +var Emoji_1005 []byte + +//go:embed emojis/1006.svg +var Emoji_1006 []byte + +//go:embed emojis/1007.svg +var Emoji_1007 []byte + +//go:embed emojis/1008.svg +var Emoji_1008 []byte + +//go:embed emojis/1009.svg +var Emoji_1009 []byte + +//go:embed emojis/1010.svg +var Emoji_1010 []byte + +//go:embed emojis/1011.svg +var Emoji_1011 []byte + +//go:embed emojis/1012.svg +var Emoji_1012 []byte + +//go:embed emojis/1013.svg +var Emoji_1013 []byte + +//go:embed emojis/1014.svg +var Emoji_1014 []byte + +//go:embed emojis/1015.svg +var Emoji_1015 []byte + +//go:embed emojis/1016.svg +var Emoji_1016 []byte + +//go:embed emojis/1017.svg +var Emoji_1017 []byte + +//go:embed emojis/1018.svg +var Emoji_1018 []byte + +//go:embed emojis/1019.svg +var Emoji_1019 []byte + +//go:embed emojis/1020.svg +var Emoji_1020 []byte + +//go:embed emojis/1021.svg +var Emoji_1021 []byte + +//go:embed emojis/1022.svg +var Emoji_1022 []byte + +//go:embed emojis/1023.svg +var Emoji_1023 []byte + +//go:embed emojis/1024.svg +var Emoji_1024 []byte + +//go:embed emojis/1025.svg +var Emoji_1025 []byte + +//go:embed emojis/1026.svg +var Emoji_1026 []byte + +//go:embed emojis/1027.svg +var Emoji_1027 []byte + +//go:embed emojis/1028.svg +var Emoji_1028 []byte + +//go:embed emojis/1029.svg +var Emoji_1029 []byte + +//go:embed emojis/1030.svg +var Emoji_1030 []byte + +//go:embed emojis/1031.svg +var Emoji_1031 []byte + +//go:embed emojis/1032.svg +var Emoji_1032 []byte + +//go:embed emojis/1033.svg +var Emoji_1033 []byte + +//go:embed emojis/1034.svg +var Emoji_1034 []byte + +//go:embed emojis/1035.svg +var Emoji_1035 []byte + +//go:embed emojis/1036.svg +var Emoji_1036 []byte + +//go:embed emojis/1037.svg +var Emoji_1037 []byte + +//go:embed emojis/1038.svg +var Emoji_1038 []byte + +//go:embed emojis/1039.svg +var Emoji_1039 []byte + +//go:embed emojis/1040.svg +var Emoji_1040 []byte + +//go:embed emojis/1041.svg +var Emoji_1041 []byte + +//go:embed emojis/1042.svg +var Emoji_1042 []byte + +//go:embed emojis/1043.svg +var Emoji_1043 []byte + +//go:embed emojis/1044.svg +var Emoji_1044 []byte + +//go:embed emojis/1045.svg +var Emoji_1045 []byte + +//go:embed emojis/1046.svg +var Emoji_1046 []byte + +//go:embed emojis/1047.svg +var Emoji_1047 []byte + +//go:embed emojis/1048.svg +var Emoji_1048 []byte + +//go:embed emojis/1049.svg +var Emoji_1049 []byte + +//go:embed emojis/1050.svg +var Emoji_1050 []byte + +//go:embed emojis/1051.svg +var Emoji_1051 []byte + +//go:embed emojis/1052.svg +var Emoji_1052 []byte + +//go:embed emojis/1053.svg +var Emoji_1053 []byte + +//go:embed emojis/1054.svg +var Emoji_1054 []byte + +//go:embed emojis/1055.svg +var Emoji_1055 []byte + +//go:embed emojis/1056.svg +var Emoji_1056 []byte + +//go:embed emojis/1057.svg +var Emoji_1057 []byte + +//go:embed emojis/1058.svg +var Emoji_1058 []byte + +//go:embed emojis/1059.svg +var Emoji_1059 []byte + +//go:embed emojis/1060.svg +var Emoji_1060 []byte + +//go:embed emojis/1061.svg +var Emoji_1061 []byte + +//go:embed emojis/1062.svg +var Emoji_1062 []byte + +//go:embed emojis/1063.svg +var Emoji_1063 []byte + +//go:embed emojis/1064.svg +var Emoji_1064 []byte + +//go:embed emojis/1065.svg +var Emoji_1065 []byte + +//go:embed emojis/1066.svg +var Emoji_1066 []byte + +//go:embed emojis/1067.svg +var Emoji_1067 []byte + +//go:embed emojis/1068.svg +var Emoji_1068 []byte + +//go:embed emojis/1069.svg +var Emoji_1069 []byte + +//go:embed emojis/1070.svg +var Emoji_1070 []byte + +//go:embed emojis/1071.svg +var Emoji_1071 []byte + +//go:embed emojis/1072.svg +var Emoji_1072 []byte + +//go:embed emojis/1073.svg +var Emoji_1073 []byte + +//go:embed emojis/1074.svg +var Emoji_1074 []byte + +//go:embed emojis/1075.svg +var Emoji_1075 []byte + +//go:embed emojis/1076.svg +var Emoji_1076 []byte + +//go:embed emojis/1077.svg +var Emoji_1077 []byte + +//go:embed emojis/1078.svg +var Emoji_1078 []byte + +//go:embed emojis/1079.svg +var Emoji_1079 []byte + +//go:embed emojis/1080.svg +var Emoji_1080 []byte + +//go:embed emojis/1081.svg +var Emoji_1081 []byte + +//go:embed emojis/1082.svg +var Emoji_1082 []byte + +//go:embed emojis/1083.svg +var Emoji_1083 []byte + +//go:embed emojis/1084.svg +var Emoji_1084 []byte + +//go:embed emojis/1085.svg +var Emoji_1085 []byte + +//go:embed emojis/1086.svg +var Emoji_1086 []byte + +//go:embed emojis/1087.svg +var Emoji_1087 []byte + +//go:embed emojis/1088.svg +var Emoji_1088 []byte + +//go:embed emojis/1089.svg +var Emoji_1089 []byte + +//go:embed emojis/1090.svg +var Emoji_1090 []byte + +//go:embed emojis/1091.svg +var Emoji_1091 []byte + +//go:embed emojis/1092.svg +var Emoji_1092 []byte + +//go:embed emojis/1093.svg +var Emoji_1093 []byte + +//go:embed emojis/1094.svg +var Emoji_1094 []byte + +//go:embed emojis/1095.svg +var Emoji_1095 []byte + +//go:embed emojis/1096.svg +var Emoji_1096 []byte + +//go:embed emojis/1097.svg +var Emoji_1097 []byte + +//go:embed emojis/1098.svg +var Emoji_1098 []byte + +//go:embed emojis/1099.svg +var Emoji_1099 []byte + +//go:embed emojis/1100.svg +var Emoji_1100 []byte + +//go:embed emojis/1101.svg +var Emoji_1101 []byte + +//go:embed emojis/1102.svg +var Emoji_1102 []byte + +//go:embed emojis/1103.svg +var Emoji_1103 []byte + +//go:embed emojis/1104.svg +var Emoji_1104 []byte + +//go:embed emojis/1105.svg +var Emoji_1105 []byte + +//go:embed emojis/1106.svg +var Emoji_1106 []byte + +//go:embed emojis/1107.svg +var Emoji_1107 []byte + +//go:embed emojis/1108.svg +var Emoji_1108 []byte + +//go:embed emojis/1109.svg +var Emoji_1109 []byte + +//go:embed emojis/1110.svg +var Emoji_1110 []byte + +//go:embed emojis/1111.svg +var Emoji_1111 []byte + +//go:embed emojis/1112.svg +var Emoji_1112 []byte + +//go:embed emojis/1113.svg +var Emoji_1113 []byte + +//go:embed emojis/1114.svg +var Emoji_1114 []byte + +//go:embed emojis/1115.svg +var Emoji_1115 []byte + +//go:embed emojis/1116.svg +var Emoji_1116 []byte + +//go:embed emojis/1117.svg +var Emoji_1117 []byte + +//go:embed emojis/1118.svg +var Emoji_1118 []byte + +//go:embed emojis/1119.svg +var Emoji_1119 []byte + +//go:embed emojis/1120.svg +var Emoji_1120 []byte + +//go:embed emojis/1121.svg +var Emoji_1121 []byte + +//go:embed emojis/1122.svg +var Emoji_1122 []byte + +//go:embed emojis/1123.svg +var Emoji_1123 []byte + +//go:embed emojis/1124.svg +var Emoji_1124 []byte + +//go:embed emojis/1125.svg +var Emoji_1125 []byte + +//go:embed emojis/1126.svg +var Emoji_1126 []byte + +//go:embed emojis/1127.svg +var Emoji_1127 []byte + +//go:embed emojis/1128.svg +var Emoji_1128 []byte + +//go:embed emojis/1129.svg +var Emoji_1129 []byte + +//go:embed emojis/1130.svg +var Emoji_1130 []byte + +//go:embed emojis/1131.svg +var Emoji_1131 []byte + +//go:embed emojis/1132.svg +var Emoji_1132 []byte + +//go:embed emojis/1133.svg +var Emoji_1133 []byte + +//go:embed emojis/1134.svg +var Emoji_1134 []byte + +//go:embed emojis/1135.svg +var Emoji_1135 []byte + +//go:embed emojis/1136.svg +var Emoji_1136 []byte + +//go:embed emojis/1137.svg +var Emoji_1137 []byte + +//go:embed emojis/1138.svg +var Emoji_1138 []byte + +//go:embed emojis/1139.svg +var Emoji_1139 []byte + +//go:embed emojis/1140.svg +var Emoji_1140 []byte + +//go:embed emojis/1141.svg +var Emoji_1141 []byte + +//go:embed emojis/1142.svg +var Emoji_1142 []byte + +//go:embed emojis/1143.svg +var Emoji_1143 []byte + +//go:embed emojis/1144.svg +var Emoji_1144 []byte + +//go:embed emojis/1145.svg +var Emoji_1145 []byte + +//go:embed emojis/1146.svg +var Emoji_1146 []byte + +//go:embed emojis/1147.svg +var Emoji_1147 []byte + +//go:embed emojis/1148.svg +var Emoji_1148 []byte + +//go:embed emojis/1149.svg +var Emoji_1149 []byte + +//go:embed emojis/1150.svg +var Emoji_1150 []byte + +//go:embed emojis/1151.svg +var Emoji_1151 []byte + +//go:embed emojis/1152.svg +var Emoji_1152 []byte + +//go:embed emojis/1153.svg +var Emoji_1153 []byte + +//go:embed emojis/1154.svg +var Emoji_1154 []byte + +//go:embed emojis/1155.svg +var Emoji_1155 []byte + +//go:embed emojis/1156.svg +var Emoji_1156 []byte + +//go:embed emojis/1157.svg +var Emoji_1157 []byte + +//go:embed emojis/1158.svg +var Emoji_1158 []byte + +//go:embed emojis/1159.svg +var Emoji_1159 []byte + +//go:embed emojis/1160.svg +var Emoji_1160 []byte + +//go:embed emojis/1161.svg +var Emoji_1161 []byte + +//go:embed emojis/1162.svg +var Emoji_1162 []byte + +//go:embed emojis/1163.svg +var Emoji_1163 []byte + +//go:embed emojis/1164.svg +var Emoji_1164 []byte + +//go:embed emojis/1165.svg +var Emoji_1165 []byte + +//go:embed emojis/1166.svg +var Emoji_1166 []byte + +//go:embed emojis/1167.svg +var Emoji_1167 []byte + +//go:embed emojis/1168.svg +var Emoji_1168 []byte + +//go:embed emojis/1169.svg +var Emoji_1169 []byte + +//go:embed emojis/1170.svg +var Emoji_1170 []byte + +//go:embed emojis/1171.svg +var Emoji_1171 []byte + +//go:embed emojis/1172.svg +var Emoji_1172 []byte + +//go:embed emojis/1173.svg +var Emoji_1173 []byte + +//go:embed emojis/1174.svg +var Emoji_1174 []byte + +//go:embed emojis/1175.svg +var Emoji_1175 []byte + +//go:embed emojis/1176.svg +var Emoji_1176 []byte + +//go:embed emojis/1177.svg +var Emoji_1177 []byte + +//go:embed emojis/1178.svg +var Emoji_1178 []byte + +//go:embed emojis/1179.svg +var Emoji_1179 []byte + +//go:embed emojis/1180.svg +var Emoji_1180 []byte + +//go:embed emojis/1181.svg +var Emoji_1181 []byte + +//go:embed emojis/1182.svg +var Emoji_1182 []byte + +//go:embed emojis/1183.svg +var Emoji_1183 []byte + +//go:embed emojis/1184.svg +var Emoji_1184 []byte + +//go:embed emojis/1185.svg +var Emoji_1185 []byte + +//go:embed emojis/1186.svg +var Emoji_1186 []byte + +//go:embed emojis/1187.svg +var Emoji_1187 []byte + +//go:embed emojis/1188.svg +var Emoji_1188 []byte + +//go:embed emojis/1189.svg +var Emoji_1189 []byte + +//go:embed emojis/1190.svg +var Emoji_1190 []byte + +//go:embed emojis/1191.svg +var Emoji_1191 []byte + +//go:embed emojis/1192.svg +var Emoji_1192 []byte + +//go:embed emojis/1193.svg +var Emoji_1193 []byte + +//go:embed emojis/1194.svg +var Emoji_1194 []byte + +//go:embed emojis/1195.svg +var Emoji_1195 []byte + +//go:embed emojis/1196.svg +var Emoji_1196 []byte + +//go:embed emojis/1197.svg +var Emoji_1197 []byte + +//go:embed emojis/1198.svg +var Emoji_1198 []byte + +//go:embed emojis/1199.svg +var Emoji_1199 []byte + +//go:embed emojis/1200.svg +var Emoji_1200 []byte + +//go:embed emojis/1201.svg +var Emoji_1201 []byte + +//go:embed emojis/1202.svg +var Emoji_1202 []byte + +//go:embed emojis/1203.svg +var Emoji_1203 []byte + +//go:embed emojis/1204.svg +var Emoji_1204 []byte + +//go:embed emojis/1205.svg +var Emoji_1205 []byte + +//go:embed emojis/1206.svg +var Emoji_1206 []byte + +//go:embed emojis/1207.svg +var Emoji_1207 []byte + +//go:embed emojis/1208.svg +var Emoji_1208 []byte + +//go:embed emojis/1209.svg +var Emoji_1209 []byte + +//go:embed emojis/1210.svg +var Emoji_1210 []byte + +//go:embed emojis/1211.svg +var Emoji_1211 []byte + +//go:embed emojis/1212.svg +var Emoji_1212 []byte + +//go:embed emojis/1213.svg +var Emoji_1213 []byte + +//go:embed emojis/1214.svg +var Emoji_1214 []byte + +//go:embed emojis/1215.svg +var Emoji_1215 []byte + +//go:embed emojis/1216.svg +var Emoji_1216 []byte + +//go:embed emojis/1217.svg +var Emoji_1217 []byte + +//go:embed emojis/1218.svg +var Emoji_1218 []byte + +//go:embed emojis/1219.svg +var Emoji_1219 []byte + +//go:embed emojis/1220.svg +var Emoji_1220 []byte + +//go:embed emojis/1221.svg +var Emoji_1221 []byte + +//go:embed emojis/1222.svg +var Emoji_1222 []byte + +//go:embed emojis/1223.svg +var Emoji_1223 []byte + +//go:embed emojis/1224.svg +var Emoji_1224 []byte + +//go:embed emojis/1225.svg +var Emoji_1225 []byte + +//go:embed emojis/1226.svg +var Emoji_1226 []byte + +//go:embed emojis/1227.svg +var Emoji_1227 []byte + +//go:embed emojis/1228.svg +var Emoji_1228 []byte + +//go:embed emojis/1229.svg +var Emoji_1229 []byte + +//go:embed emojis/1230.svg +var Emoji_1230 []byte + +//go:embed emojis/1231.svg +var Emoji_1231 []byte + +//go:embed emojis/1232.svg +var Emoji_1232 []byte + +//go:embed emojis/1233.svg +var Emoji_1233 []byte + +//go:embed emojis/1234.svg +var Emoji_1234 []byte + +//go:embed emojis/1235.svg +var Emoji_1235 []byte + +//go:embed emojis/1236.svg +var Emoji_1236 []byte + +//go:embed emojis/1237.svg +var Emoji_1237 []byte + +//go:embed emojis/1238.svg +var Emoji_1238 []byte + +//go:embed emojis/1239.svg +var Emoji_1239 []byte + +//go:embed emojis/1240.svg +var Emoji_1240 []byte + +//go:embed emojis/1241.svg +var Emoji_1241 []byte + +//go:embed emojis/1242.svg +var Emoji_1242 []byte + +//go:embed emojis/1243.svg +var Emoji_1243 []byte + +//go:embed emojis/1244.svg +var Emoji_1244 []byte + +//go:embed emojis/1245.svg +var Emoji_1245 []byte + +//go:embed emojis/1246.svg +var Emoji_1246 []byte + +//go:embed emojis/1247.svg +var Emoji_1247 []byte + +//go:embed emojis/1248.svg +var Emoji_1248 []byte + +//go:embed emojis/1249.svg +var Emoji_1249 []byte + +//go:embed emojis/1250.svg +var Emoji_1250 []byte + +//go:embed emojis/1251.svg +var Emoji_1251 []byte + +//go:embed emojis/1252.svg +var Emoji_1252 []byte + +//go:embed emojis/1253.svg +var Emoji_1253 []byte + +//go:embed emojis/1254.svg +var Emoji_1254 []byte + +//go:embed emojis/1255.svg +var Emoji_1255 []byte + +//go:embed emojis/1256.svg +var Emoji_1256 []byte + +//go:embed emojis/1257.svg +var Emoji_1257 []byte + +//go:embed emojis/1258.svg +var Emoji_1258 []byte + +//go:embed emojis/1259.svg +var Emoji_1259 []byte + +//go:embed emojis/1260.svg +var Emoji_1260 []byte + +//go:embed emojis/1261.svg +var Emoji_1261 []byte + +//go:embed emojis/1262.svg +var Emoji_1262 []byte + +//go:embed emojis/1263.svg +var Emoji_1263 []byte + +//go:embed emojis/1264.svg +var Emoji_1264 []byte + +//go:embed emojis/1265.svg +var Emoji_1265 []byte + +//go:embed emojis/1266.svg +var Emoji_1266 []byte + +//go:embed emojis/1267.svg +var Emoji_1267 []byte + +//go:embed emojis/1268.svg +var Emoji_1268 []byte + +//go:embed emojis/1269.svg +var Emoji_1269 []byte + +//go:embed emojis/1270.svg +var Emoji_1270 []byte + +//go:embed emojis/1271.svg +var Emoji_1271 []byte + +//go:embed emojis/1272.svg +var Emoji_1272 []byte + +//go:embed emojis/1273.svg +var Emoji_1273 []byte + +//go:embed emojis/1274.svg +var Emoji_1274 []byte + +//go:embed emojis/1275.svg +var Emoji_1275 []byte + +//go:embed emojis/1276.svg +var Emoji_1276 []byte + +//go:embed emojis/1277.svg +var Emoji_1277 []byte + +//go:embed emojis/1278.svg +var Emoji_1278 []byte + +//go:embed emojis/1279.svg +var Emoji_1279 []byte + +//go:embed emojis/1280.svg +var Emoji_1280 []byte + +//go:embed emojis/1281.svg +var Emoji_1281 []byte + +//go:embed emojis/1282.svg +var Emoji_1282 []byte + +//go:embed emojis/1283.svg +var Emoji_1283 []byte + +//go:embed emojis/1284.svg +var Emoji_1284 []byte + +//go:embed emojis/1285.svg +var Emoji_1285 []byte + +//go:embed emojis/1286.svg +var Emoji_1286 []byte + +//go:embed emojis/1287.svg +var Emoji_1287 []byte + +//go:embed emojis/1288.svg +var Emoji_1288 []byte + +//go:embed emojis/1289.svg +var Emoji_1289 []byte + +//go:embed emojis/1290.svg +var Emoji_1290 []byte + +//go:embed emojis/1291.svg +var Emoji_1291 []byte + +//go:embed emojis/1292.svg +var Emoji_1292 []byte + +//go:embed emojis/1293.svg +var Emoji_1293 []byte + +//go:embed emojis/1294.svg +var Emoji_1294 []byte + +//go:embed emojis/1295.svg +var Emoji_1295 []byte + +//go:embed emojis/1296.svg +var Emoji_1296 []byte + +//go:embed emojis/1297.svg +var Emoji_1297 []byte + +//go:embed emojis/1298.svg +var Emoji_1298 []byte + +//go:embed emojis/1299.svg +var Emoji_1299 []byte + +//go:embed emojis/1300.svg +var Emoji_1300 []byte + +//go:embed emojis/1301.svg +var Emoji_1301 []byte + +//go:embed emojis/1302.svg +var Emoji_1302 []byte + +//go:embed emojis/1303.svg +var Emoji_1303 []byte + +//go:embed emojis/1304.svg +var Emoji_1304 []byte + +//go:embed emojis/1305.svg +var Emoji_1305 []byte + +//go:embed emojis/1306.svg +var Emoji_1306 []byte + +//go:embed emojis/1307.svg +var Emoji_1307 []byte + +//go:embed emojis/1308.svg +var Emoji_1308 []byte + +//go:embed emojis/1309.svg +var Emoji_1309 []byte + +//go:embed emojis/1310.svg +var Emoji_1310 []byte + +//go:embed emojis/1311.svg +var Emoji_1311 []byte + +//go:embed emojis/1312.svg +var Emoji_1312 []byte + +//go:embed emojis/1313.svg +var Emoji_1313 []byte + +//go:embed emojis/1314.svg +var Emoji_1314 []byte + +//go:embed emojis/1315.svg +var Emoji_1315 []byte + +//go:embed emojis/1316.svg +var Emoji_1316 []byte + +//go:embed emojis/1317.svg +var Emoji_1317 []byte + +//go:embed emojis/1318.svg +var Emoji_1318 []byte + +//go:embed emojis/1319.svg +var Emoji_1319 []byte + +//go:embed emojis/1320.svg +var Emoji_1320 []byte + +//go:embed emojis/1321.svg +var Emoji_1321 []byte + +//go:embed emojis/1322.svg +var Emoji_1322 []byte + +//go:embed emojis/1323.svg +var Emoji_1323 []byte + +//go:embed emojis/1324.svg +var Emoji_1324 []byte + +//go:embed emojis/1325.svg +var Emoji_1325 []byte + +//go:embed emojis/1326.svg +var Emoji_1326 []byte + +//go:embed emojis/1327.svg +var Emoji_1327 []byte + +//go:embed emojis/1328.svg +var Emoji_1328 []byte + +//go:embed emojis/1329.svg +var Emoji_1329 []byte + +//go:embed emojis/1330.svg +var Emoji_1330 []byte + +//go:embed emojis/1331.svg +var Emoji_1331 []byte + +//go:embed emojis/1332.svg +var Emoji_1332 []byte + +//go:embed emojis/1333.svg +var Emoji_1333 []byte + +//go:embed emojis/1334.svg +var Emoji_1334 []byte + +//go:embed emojis/1335.svg +var Emoji_1335 []byte + +//go:embed emojis/1336.svg +var Emoji_1336 []byte + +//go:embed emojis/1337.svg +var Emoji_1337 []byte + +//go:embed emojis/1338.svg +var Emoji_1338 []byte + +//go:embed emojis/1339.svg +var Emoji_1339 []byte + +//go:embed emojis/1340.svg +var Emoji_1340 []byte + +//go:embed emojis/1341.svg +var Emoji_1341 []byte + +//go:embed emojis/1342.svg +var Emoji_1342 []byte + +//go:embed emojis/1343.svg +var Emoji_1343 []byte + +//go:embed emojis/1344.svg +var Emoji_1344 []byte + +//go:embed emojis/1345.svg +var Emoji_1345 []byte + +//go:embed emojis/1346.svg +var Emoji_1346 []byte + +//go:embed emojis/1347.svg +var Emoji_1347 []byte + +//go:embed emojis/1348.svg +var Emoji_1348 []byte + +//go:embed emojis/1349.svg +var Emoji_1349 []byte + +//go:embed emojis/1350.svg +var Emoji_1350 []byte + +//go:embed emojis/1351.svg +var Emoji_1351 []byte + +//go:embed emojis/1352.svg +var Emoji_1352 []byte + +//go:embed emojis/1353.svg +var Emoji_1353 []byte + +//go:embed emojis/1354.svg +var Emoji_1354 []byte + +//go:embed emojis/1355.svg +var Emoji_1355 []byte + +//go:embed emojis/1356.svg +var Emoji_1356 []byte + +//go:embed emojis/1357.svg +var Emoji_1357 []byte + +//go:embed emojis/1358.svg +var Emoji_1358 []byte + +//go:embed emojis/1359.svg +var Emoji_1359 []byte + +//go:embed emojis/1360.svg +var Emoji_1360 []byte + +//go:embed emojis/1361.svg +var Emoji_1361 []byte + +//go:embed emojis/1362.svg +var Emoji_1362 []byte + +//go:embed emojis/1363.svg +var Emoji_1363 []byte + +//go:embed emojis/1364.svg +var Emoji_1364 []byte + +//go:embed emojis/1365.svg +var Emoji_1365 []byte + +//go:embed emojis/1366.svg +var Emoji_1366 []byte + +//go:embed emojis/1367.svg +var Emoji_1367 []byte + +//go:embed emojis/1368.svg +var Emoji_1368 []byte + +//go:embed emojis/1369.svg +var Emoji_1369 []byte + +//go:embed emojis/1370.svg +var Emoji_1370 []byte + +//go:embed emojis/1371.svg +var Emoji_1371 []byte + +//go:embed emojis/1372.svg +var Emoji_1372 []byte + +//go:embed emojis/1373.svg +var Emoji_1373 []byte + +//go:embed emojis/1374.svg +var Emoji_1374 []byte + +//go:embed emojis/1375.svg +var Emoji_1375 []byte + +//go:embed emojis/1376.svg +var Emoji_1376 []byte + +//go:embed emojis/1377.svg +var Emoji_1377 []byte + +//go:embed emojis/1378.svg +var Emoji_1378 []byte + +//go:embed emojis/1379.svg +var Emoji_1379 []byte + +//go:embed emojis/1380.svg +var Emoji_1380 []byte + +//go:embed emojis/1381.svg +var Emoji_1381 []byte + +//go:embed emojis/1382.svg +var Emoji_1382 []byte + +//go:embed emojis/1383.svg +var Emoji_1383 []byte + +//go:embed emojis/1384.svg +var Emoji_1384 []byte + +//go:embed emojis/1385.svg +var Emoji_1385 []byte + +//go:embed emojis/1386.svg +var Emoji_1386 []byte + +//go:embed emojis/1387.svg +var Emoji_1387 []byte + +//go:embed emojis/1388.svg +var Emoji_1388 []byte + +//go:embed emojis/1389.svg +var Emoji_1389 []byte + +//go:embed emojis/1390.svg +var Emoji_1390 []byte + +//go:embed emojis/1391.svg +var Emoji_1391 []byte + +//go:embed emojis/1392.svg +var Emoji_1392 []byte + +//go:embed emojis/1393.svg +var Emoji_1393 []byte + +//go:embed emojis/1394.svg +var Emoji_1394 []byte + +//go:embed emojis/1395.svg +var Emoji_1395 []byte + +//go:embed emojis/1396.svg +var Emoji_1396 []byte + +//go:embed emojis/1397.svg +var Emoji_1397 []byte + +//go:embed emojis/1398.svg +var Emoji_1398 []byte + +//go:embed emojis/1399.svg +var Emoji_1399 []byte + +//go:embed emojis/1400.svg +var Emoji_1400 []byte + +//go:embed emojis/1401.svg +var Emoji_1401 []byte + +//go:embed emojis/1402.svg +var Emoji_1402 []byte + +//go:embed emojis/1403.svg +var Emoji_1403 []byte + +//go:embed emojis/1404.svg +var Emoji_1404 []byte + +//go:embed emojis/1405.svg +var Emoji_1405 []byte + +//go:embed emojis/1406.svg +var Emoji_1406 []byte + +//go:embed emojis/1407.svg +var Emoji_1407 []byte + +//go:embed emojis/1408.svg +var Emoji_1408 []byte + +//go:embed emojis/1409.svg +var Emoji_1409 []byte + +//go:embed emojis/1410.svg +var Emoji_1410 []byte + +//go:embed emojis/1411.svg +var Emoji_1411 []byte + +//go:embed emojis/1412.svg +var Emoji_1412 []byte + +//go:embed emojis/1413.svg +var Emoji_1413 []byte + +//go:embed emojis/1414.svg +var Emoji_1414 []byte + +//go:embed emojis/1415.svg +var Emoji_1415 []byte + +//go:embed emojis/1416.svg +var Emoji_1416 []byte + +//go:embed emojis/1417.svg +var Emoji_1417 []byte + +//go:embed emojis/1418.svg +var Emoji_1418 []byte + +//go:embed emojis/1419.svg +var Emoji_1419 []byte + +//go:embed emojis/1420.svg +var Emoji_1420 []byte + +//go:embed emojis/1421.svg +var Emoji_1421 []byte + +//go:embed emojis/1422.svg +var Emoji_1422 []byte + +//go:embed emojis/1423.svg +var Emoji_1423 []byte + +//go:embed emojis/1424.svg +var Emoji_1424 []byte + +//go:embed emojis/1425.svg +var Emoji_1425 []byte + +//go:embed emojis/1426.svg +var Emoji_1426 []byte + +//go:embed emojis/1427.svg +var Emoji_1427 []byte + +//go:embed emojis/1428.svg +var Emoji_1428 []byte + +//go:embed emojis/1429.svg +var Emoji_1429 []byte + +//go:embed emojis/1430.svg +var Emoji_1430 []byte + +//go:embed emojis/1431.svg +var Emoji_1431 []byte + +//go:embed emojis/1432.svg +var Emoji_1432 []byte + +//go:embed emojis/1433.svg +var Emoji_1433 []byte + +//go:embed emojis/1434.svg +var Emoji_1434 []byte + +//go:embed emojis/1435.svg +var Emoji_1435 []byte + +//go:embed emojis/1436.svg +var Emoji_1436 []byte + +//go:embed emojis/1437.svg +var Emoji_1437 []byte + +//go:embed emojis/1438.svg +var Emoji_1438 []byte + +//go:embed emojis/1439.svg +var Emoji_1439 []byte + +//go:embed emojis/1440.svg +var Emoji_1440 []byte + +//go:embed emojis/1441.svg +var Emoji_1441 []byte + +//go:embed emojis/1442.svg +var Emoji_1442 []byte + +//go:embed emojis/1443.svg +var Emoji_1443 []byte + +//go:embed emojis/1444.svg +var Emoji_1444 []byte + +//go:embed emojis/1445.svg +var Emoji_1445 []byte + +//go:embed emojis/1446.svg +var Emoji_1446 []byte + +//go:embed emojis/1447.svg +var Emoji_1447 []byte + +//go:embed emojis/1448.svg +var Emoji_1448 []byte + +//go:embed emojis/1449.svg +var Emoji_1449 []byte + +//go:embed emojis/1450.svg +var Emoji_1450 []byte + +//go:embed emojis/1451.svg +var Emoji_1451 []byte + +//go:embed emojis/1452.svg +var Emoji_1452 []byte + +//go:embed emojis/1453.svg +var Emoji_1453 []byte + +//go:embed emojis/1454.svg +var Emoji_1454 []byte + +//go:embed emojis/1455.svg +var Emoji_1455 []byte + +//go:embed emojis/1456.svg +var Emoji_1456 []byte + +//go:embed emojis/1457.svg +var Emoji_1457 []byte + +//go:embed emojis/1458.svg +var Emoji_1458 []byte + +//go:embed emojis/1459.svg +var Emoji_1459 []byte + +//go:embed emojis/1460.svg +var Emoji_1460 []byte + +//go:embed emojis/1461.svg +var Emoji_1461 []byte + +//go:embed emojis/1462.svg +var Emoji_1462 []byte + +//go:embed emojis/1463.svg +var Emoji_1463 []byte + +//go:embed emojis/1464.svg +var Emoji_1464 []byte + +//go:embed emojis/1465.svg +var Emoji_1465 []byte + +//go:embed emojis/1466.svg +var Emoji_1466 []byte + +//go:embed emojis/1467.svg +var Emoji_1467 []byte + +//go:embed emojis/1468.svg +var Emoji_1468 []byte + +//go:embed emojis/1469.svg +var Emoji_1469 []byte + +//go:embed emojis/1470.svg +var Emoji_1470 []byte + +//go:embed emojis/1471.svg +var Emoji_1471 []byte + +//go:embed emojis/1472.svg +var Emoji_1472 []byte + +//go:embed emojis/1473.svg +var Emoji_1473 []byte + +//go:embed emojis/1474.svg +var Emoji_1474 []byte + +//go:embed emojis/1475.svg +var Emoji_1475 []byte + +//go:embed emojis/1476.svg +var Emoji_1476 []byte + +//go:embed emojis/1477.svg +var Emoji_1477 []byte + +//go:embed emojis/1478.svg +var Emoji_1478 []byte + +//go:embed emojis/1479.svg +var Emoji_1479 []byte + +//go:embed emojis/1480.svg +var Emoji_1480 []byte + +//go:embed emojis/1481.svg +var Emoji_1481 []byte + +//go:embed emojis/1482.svg +var Emoji_1482 []byte + +//go:embed emojis/1483.svg +var Emoji_1483 []byte + +//go:embed emojis/1484.svg +var Emoji_1484 []byte + +//go:embed emojis/1485.svg +var Emoji_1485 []byte + +//go:embed emojis/1486.svg +var Emoji_1486 []byte + +//go:embed emojis/1487.svg +var Emoji_1487 []byte + +//go:embed emojis/1488.svg +var Emoji_1488 []byte + +//go:embed emojis/1489.svg +var Emoji_1489 []byte + +//go:embed emojis/1490.svg +var Emoji_1490 []byte + +//go:embed emojis/1491.svg +var Emoji_1491 []byte + +//go:embed emojis/1492.svg +var Emoji_1492 []byte + +//go:embed emojis/1493.svg +var Emoji_1493 []byte + +//go:embed emojis/1494.svg +var Emoji_1494 []byte + +//go:embed emojis/1495.svg +var Emoji_1495 []byte + +//go:embed emojis/1496.svg +var Emoji_1496 []byte + +//go:embed emojis/1497.svg +var Emoji_1497 []byte + +//go:embed emojis/1498.svg +var Emoji_1498 []byte + +//go:embed emojis/1499.svg +var Emoji_1499 []byte + +//go:embed emojis/1500.svg +var Emoji_1500 []byte + +//go:embed emojis/1501.svg +var Emoji_1501 []byte + +//go:embed emojis/1502.svg +var Emoji_1502 []byte + +//go:embed emojis/1503.svg +var Emoji_1503 []byte + +//go:embed emojis/1504.svg +var Emoji_1504 []byte + +//go:embed emojis/1505.svg +var Emoji_1505 []byte + +//go:embed emojis/1506.svg +var Emoji_1506 []byte + +//go:embed emojis/1507.svg +var Emoji_1507 []byte + +//go:embed emojis/1508.svg +var Emoji_1508 []byte + +//go:embed emojis/1509.svg +var Emoji_1509 []byte + +//go:embed emojis/1510.svg +var Emoji_1510 []byte + +//go:embed emojis/1511.svg +var Emoji_1511 []byte + +//go:embed emojis/1512.svg +var Emoji_1512 []byte + +//go:embed emojis/1513.svg +var Emoji_1513 []byte + +//go:embed emojis/1514.svg +var Emoji_1514 []byte + +//go:embed emojis/1515.svg +var Emoji_1515 []byte + +//go:embed emojis/1516.svg +var Emoji_1516 []byte + +//go:embed emojis/1517.svg +var Emoji_1517 []byte + +//go:embed emojis/1518.svg +var Emoji_1518 []byte + +//go:embed emojis/1519.svg +var Emoji_1519 []byte + +//go:embed emojis/1520.svg +var Emoji_1520 []byte + +//go:embed emojis/1521.svg +var Emoji_1521 []byte + +//go:embed emojis/1522.svg +var Emoji_1522 []byte + +//go:embed emojis/1523.svg +var Emoji_1523 []byte + +//go:embed emojis/1524.svg +var Emoji_1524 []byte + +//go:embed emojis/1525.svg +var Emoji_1525 []byte + +//go:embed emojis/1526.svg +var Emoji_1526 []byte + +//go:embed emojis/1527.svg +var Emoji_1527 []byte + +//go:embed emojis/1528.svg +var Emoji_1528 []byte + +//go:embed emojis/1529.svg +var Emoji_1529 []byte + +//go:embed emojis/1530.svg +var Emoji_1530 []byte + +//go:embed emojis/1531.svg +var Emoji_1531 []byte + +//go:embed emojis/1532.svg +var Emoji_1532 []byte + +//go:embed emojis/1533.svg +var Emoji_1533 []byte + +//go:embed emojis/1534.svg +var Emoji_1534 []byte + +//go:embed emojis/1535.svg +var Emoji_1535 []byte + +//go:embed emojis/1536.svg +var Emoji_1536 []byte + +//go:embed emojis/1537.svg +var Emoji_1537 []byte + +//go:embed emojis/1538.svg +var Emoji_1538 []byte + +//go:embed emojis/1539.svg +var Emoji_1539 []byte + +//go:embed emojis/1540.svg +var Emoji_1540 []byte + +//go:embed emojis/1541.svg +var Emoji_1541 []byte + +//go:embed emojis/1542.svg +var Emoji_1542 []byte + +//go:embed emojis/1543.svg +var Emoji_1543 []byte + +//go:embed emojis/1544.svg +var Emoji_1544 []byte + +//go:embed emojis/1545.svg +var Emoji_1545 []byte + +//go:embed emojis/1546.svg +var Emoji_1546 []byte + +//go:embed emojis/1547.svg +var Emoji_1547 []byte + +//go:embed emojis/1548.svg +var Emoji_1548 []byte + +//go:embed emojis/1549.svg +var Emoji_1549 []byte + +//go:embed emojis/1550.svg +var Emoji_1550 []byte + +//go:embed emojis/1551.svg +var Emoji_1551 []byte + +//go:embed emojis/1552.svg +var Emoji_1552 []byte + +//go:embed emojis/1553.svg +var Emoji_1553 []byte + +//go:embed emojis/1554.svg +var Emoji_1554 []byte + +//go:embed emojis/1555.svg +var Emoji_1555 []byte + +//go:embed emojis/1556.svg +var Emoji_1556 []byte + +//go:embed emojis/1557.svg +var Emoji_1557 []byte + +//go:embed emojis/1558.svg +var Emoji_1558 []byte + +//go:embed emojis/1559.svg +var Emoji_1559 []byte + +//go:embed emojis/1560.svg +var Emoji_1560 []byte + +//go:embed emojis/1561.svg +var Emoji_1561 []byte + +//go:embed emojis/1562.svg +var Emoji_1562 []byte + +//go:embed emojis/1563.svg +var Emoji_1563 []byte + +//go:embed emojis/1564.svg +var Emoji_1564 []byte + +//go:embed emojis/1565.svg +var Emoji_1565 []byte + +//go:embed emojis/1566.svg +var Emoji_1566 []byte + +//go:embed emojis/1567.svg +var Emoji_1567 []byte + +//go:embed emojis/1568.svg +var Emoji_1568 []byte + +//go:embed emojis/1569.svg +var Emoji_1569 []byte + +//go:embed emojis/1570.svg +var Emoji_1570 []byte + +//go:embed emojis/1571.svg +var Emoji_1571 []byte + +//go:embed emojis/1572.svg +var Emoji_1572 []byte + +//go:embed emojis/1573.svg +var Emoji_1573 []byte + +//go:embed emojis/1574.svg +var Emoji_1574 []byte + +//go:embed emojis/1575.svg +var Emoji_1575 []byte + +//go:embed emojis/1576.svg +var Emoji_1576 []byte + +//go:embed emojis/1577.svg +var Emoji_1577 []byte + +//go:embed emojis/1578.svg +var Emoji_1578 []byte + +//go:embed emojis/1579.svg +var Emoji_1579 []byte + +//go:embed emojis/1580.svg +var Emoji_1580 []byte + +//go:embed emojis/1581.svg +var Emoji_1581 []byte + +//go:embed emojis/1582.svg +var Emoji_1582 []byte + +//go:embed emojis/1583.svg +var Emoji_1583 []byte + +//go:embed emojis/1584.svg +var Emoji_1584 []byte + +//go:embed emojis/1585.svg +var Emoji_1585 []byte + +//go:embed emojis/1586.svg +var Emoji_1586 []byte + +//go:embed emojis/1587.svg +var Emoji_1587 []byte + +//go:embed emojis/1588.svg +var Emoji_1588 []byte + +//go:embed emojis/1589.svg +var Emoji_1589 []byte + +//go:embed emojis/1590.svg +var Emoji_1590 []byte + +//go:embed emojis/1591.svg +var Emoji_1591 []byte + +//go:embed emojis/1592.svg +var Emoji_1592 []byte + +//go:embed emojis/1593.svg +var Emoji_1593 []byte + +//go:embed emojis/1594.svg +var Emoji_1594 []byte + +//go:embed emojis/1595.svg +var Emoji_1595 []byte + +//go:embed emojis/1596.svg +var Emoji_1596 []byte + +//go:embed emojis/1597.svg +var Emoji_1597 []byte + +//go:embed emojis/1598.svg +var Emoji_1598 []byte + +//go:embed emojis/1599.svg +var Emoji_1599 []byte + +//go:embed emojis/1600.svg +var Emoji_1600 []byte + +//go:embed emojis/1601.svg +var Emoji_1601 []byte + +//go:embed emojis/1602.svg +var Emoji_1602 []byte + +//go:embed emojis/1603.svg +var Emoji_1603 []byte + +//go:embed emojis/1604.svg +var Emoji_1604 []byte + +//go:embed emojis/1605.svg +var Emoji_1605 []byte + +//go:embed emojis/1606.svg +var Emoji_1606 []byte + +//go:embed emojis/1607.svg +var Emoji_1607 []byte + +//go:embed emojis/1608.svg +var Emoji_1608 []byte + +//go:embed emojis/1609.svg +var Emoji_1609 []byte + +//go:embed emojis/1610.svg +var Emoji_1610 []byte + +//go:embed emojis/1611.svg +var Emoji_1611 []byte + +//go:embed emojis/1612.svg +var Emoji_1612 []byte + +//go:embed emojis/1613.svg +var Emoji_1613 []byte + +//go:embed emojis/1614.svg +var Emoji_1614 []byte + +//go:embed emojis/1615.svg +var Emoji_1615 []byte + +//go:embed emojis/1616.svg +var Emoji_1616 []byte + +//go:embed emojis/1617.svg +var Emoji_1617 []byte + +//go:embed emojis/1618.svg +var Emoji_1618 []byte + +//go:embed emojis/1619.svg +var Emoji_1619 []byte + +//go:embed emojis/1620.svg +var Emoji_1620 []byte + +//go:embed emojis/1621.svg +var Emoji_1621 []byte + +//go:embed emojis/1622.svg +var Emoji_1622 []byte + +//go:embed emojis/1623.svg +var Emoji_1623 []byte + +//go:embed emojis/1624.svg +var Emoji_1624 []byte + +//go:embed emojis/1625.svg +var Emoji_1625 []byte + +//go:embed emojis/1626.svg +var Emoji_1626 []byte + +//go:embed emojis/1627.svg +var Emoji_1627 []byte + +//go:embed emojis/1628.svg +var Emoji_1628 []byte + +//go:embed emojis/1629.svg +var Emoji_1629 []byte + +//go:embed emojis/1630.svg +var Emoji_1630 []byte + +//go:embed emojis/1631.svg +var Emoji_1631 []byte + +//go:embed emojis/1632.svg +var Emoji_1632 []byte + +//go:embed emojis/1633.svg +var Emoji_1633 []byte + +//go:embed emojis/1634.svg +var Emoji_1634 []byte + +//go:embed emojis/1635.svg +var Emoji_1635 []byte + +//go:embed emojis/1636.svg +var Emoji_1636 []byte + +//go:embed emojis/1637.svg +var Emoji_1637 []byte + +//go:embed emojis/1638.svg +var Emoji_1638 []byte + +//go:embed emojis/1639.svg +var Emoji_1639 []byte + +//go:embed emojis/1640.svg +var Emoji_1640 []byte + +//go:embed emojis/1641.svg +var Emoji_1641 []byte + +//go:embed emojis/1642.svg +var Emoji_1642 []byte + +//go:embed emojis/1643.svg +var Emoji_1643 []byte + +//go:embed emojis/1644.svg +var Emoji_1644 []byte + +//go:embed emojis/1645.svg +var Emoji_1645 []byte + +//go:embed emojis/1646.svg +var Emoji_1646 []byte + +//go:embed emojis/1647.svg +var Emoji_1647 []byte + +//go:embed emojis/1648.svg +var Emoji_1648 []byte + +//go:embed emojis/1649.svg +var Emoji_1649 []byte + +//go:embed emojis/1650.svg +var Emoji_1650 []byte + +//go:embed emojis/1651.svg +var Emoji_1651 []byte + +//go:embed emojis/1652.svg +var Emoji_1652 []byte + +//go:embed emojis/1653.svg +var Emoji_1653 []byte + +//go:embed emojis/1654.svg +var Emoji_1654 []byte + +//go:embed emojis/1655.svg +var Emoji_1655 []byte + +//go:embed emojis/1656.svg +var Emoji_1656 []byte + +//go:embed emojis/1657.svg +var Emoji_1657 []byte + +//go:embed emojis/1658.svg +var Emoji_1658 []byte + +//go:embed emojis/1659.svg +var Emoji_1659 []byte + +//go:embed emojis/1660.svg +var Emoji_1660 []byte + +//go:embed emojis/1661.svg +var Emoji_1661 []byte + +//go:embed emojis/1662.svg +var Emoji_1662 []byte + +//go:embed emojis/1663.svg +var Emoji_1663 []byte + +//go:embed emojis/1664.svg +var Emoji_1664 []byte + +//go:embed emojis/1665.svg +var Emoji_1665 []byte + +//go:embed emojis/1666.svg +var Emoji_1666 []byte + +//go:embed emojis/1667.svg +var Emoji_1667 []byte + +//go:embed emojis/1668.svg +var Emoji_1668 []byte + +//go:embed emojis/1669.svg +var Emoji_1669 []byte + +//go:embed emojis/1670.svg +var Emoji_1670 []byte + +//go:embed emojis/1671.svg +var Emoji_1671 []byte + +//go:embed emojis/1672.svg +var Emoji_1672 []byte + +//go:embed emojis/1673.svg +var Emoji_1673 []byte + +//go:embed emojis/1674.svg +var Emoji_1674 []byte + +//go:embed emojis/1675.svg +var Emoji_1675 []byte + +//go:embed emojis/1676.svg +var Emoji_1676 []byte + +//go:embed emojis/1677.svg +var Emoji_1677 []byte + +//go:embed emojis/1678.svg +var Emoji_1678 []byte + +//go:embed emojis/1679.svg +var Emoji_1679 []byte + +//go:embed emojis/1680.svg +var Emoji_1680 []byte + +//go:embed emojis/1681.svg +var Emoji_1681 []byte + +//go:embed emojis/1682.svg +var Emoji_1682 []byte + +//go:embed emojis/1683.svg +var Emoji_1683 []byte + +//go:embed emojis/1684.svg +var Emoji_1684 []byte + +//go:embed emojis/1685.svg +var Emoji_1685 []byte + +//go:embed emojis/1686.svg +var Emoji_1686 []byte + +//go:embed emojis/1687.svg +var Emoji_1687 []byte + +//go:embed emojis/1688.svg +var Emoji_1688 []byte + +//go:embed emojis/1689.svg +var Emoji_1689 []byte + +//go:embed emojis/1690.svg +var Emoji_1690 []byte + +//go:embed emojis/1691.svg +var Emoji_1691 []byte + +//go:embed emojis/1692.svg +var Emoji_1692 []byte + +//go:embed emojis/1693.svg +var Emoji_1693 []byte + +//go:embed emojis/1694.svg +var Emoji_1694 []byte + +//go:embed emojis/1695.svg +var Emoji_1695 []byte + +//go:embed emojis/1696.svg +var Emoji_1696 []byte + +//go:embed emojis/1697.svg +var Emoji_1697 []byte + +//go:embed emojis/1698.svg +var Emoji_1698 []byte + +//go:embed emojis/1699.svg +var Emoji_1699 []byte + +//go:embed emojis/1700.svg +var Emoji_1700 []byte + +//go:embed emojis/1701.svg +var Emoji_1701 []byte + +//go:embed emojis/1702.svg +var Emoji_1702 []byte + +//go:embed emojis/1703.svg +var Emoji_1703 []byte + +//go:embed emojis/1704.svg +var Emoji_1704 []byte + +//go:embed emojis/1705.svg +var Emoji_1705 []byte + +//go:embed emojis/1706.svg +var Emoji_1706 []byte + +//go:embed emojis/1707.svg +var Emoji_1707 []byte + +//go:embed emojis/1708.svg +var Emoji_1708 []byte + +//go:embed emojis/1709.svg +var Emoji_1709 []byte + +//go:embed emojis/1710.svg +var Emoji_1710 []byte + +//go:embed emojis/1711.svg +var Emoji_1711 []byte + +//go:embed emojis/1712.svg +var Emoji_1712 []byte + +//go:embed emojis/1713.svg +var Emoji_1713 []byte + +//go:embed emojis/1714.svg +var Emoji_1714 []byte + +//go:embed emojis/1715.svg +var Emoji_1715 []byte + +//go:embed emojis/1716.svg +var Emoji_1716 []byte + +//go:embed emojis/1717.svg +var Emoji_1717 []byte + +//go:embed emojis/1718.svg +var Emoji_1718 []byte + +//go:embed emojis/1719.svg +var Emoji_1719 []byte + +//go:embed emojis/1720.svg +var Emoji_1720 []byte + +//go:embed emojis/1721.svg +var Emoji_1721 []byte + +//go:embed emojis/1722.svg +var Emoji_1722 []byte + +//go:embed emojis/1723.svg +var Emoji_1723 []byte + +//go:embed emojis/1724.svg +var Emoji_1724 []byte + +//go:embed emojis/1725.svg +var Emoji_1725 []byte + +//go:embed emojis/1726.svg +var Emoji_1726 []byte + +//go:embed emojis/1727.svg +var Emoji_1727 []byte + +//go:embed emojis/1728.svg +var Emoji_1728 []byte + +//go:embed emojis/1729.svg +var Emoji_1729 []byte + +//go:embed emojis/1730.svg +var Emoji_1730 []byte + +//go:embed emojis/1731.svg +var Emoji_1731 []byte + +//go:embed emojis/1732.svg +var Emoji_1732 []byte + +//go:embed emojis/1733.svg +var Emoji_1733 []byte + +//go:embed emojis/1734.svg +var Emoji_1734 []byte + +//go:embed emojis/1735.svg +var Emoji_1735 []byte + +//go:embed emojis/1736.svg +var Emoji_1736 []byte + +//go:embed emojis/1737.svg +var Emoji_1737 []byte + +//go:embed emojis/1738.svg +var Emoji_1738 []byte + +//go:embed emojis/1739.svg +var Emoji_1739 []byte + +//go:embed emojis/1740.svg +var Emoji_1740 []byte + +//go:embed emojis/1741.svg +var Emoji_1741 []byte + +//go:embed emojis/1742.svg +var Emoji_1742 []byte + +//go:embed emojis/1743.svg +var Emoji_1743 []byte + +//go:embed emojis/1744.svg +var Emoji_1744 []byte + +//go:embed emojis/1745.svg +var Emoji_1745 []byte + +//go:embed emojis/1746.svg +var Emoji_1746 []byte + +//go:embed emojis/1747.svg +var Emoji_1747 []byte + +//go:embed emojis/1748.svg +var Emoji_1748 []byte + +//go:embed emojis/1749.svg +var Emoji_1749 []byte + +//go:embed emojis/1750.svg +var Emoji_1750 []byte + +//go:embed emojis/1751.svg +var Emoji_1751 []byte + +//go:embed emojis/1752.svg +var Emoji_1752 []byte + +//go:embed emojis/1753.svg +var Emoji_1753 []byte + +//go:embed emojis/1754.svg +var Emoji_1754 []byte + +//go:embed emojis/1755.svg +var Emoji_1755 []byte + +//go:embed emojis/1756.svg +var Emoji_1756 []byte + +//go:embed emojis/1757.svg +var Emoji_1757 []byte + +//go:embed emojis/1758.svg +var Emoji_1758 []byte + +//go:embed emojis/1759.svg +var Emoji_1759 []byte + +//go:embed emojis/1760.svg +var Emoji_1760 []byte + +//go:embed emojis/1761.svg +var Emoji_1761 []byte + +//go:embed emojis/1762.svg +var Emoji_1762 []byte + +//go:embed emojis/1763.svg +var Emoji_1763 []byte + +//go:embed emojis/1764.svg +var Emoji_1764 []byte + +//go:embed emojis/1765.svg +var Emoji_1765 []byte + +//go:embed emojis/1766.svg +var Emoji_1766 []byte + +//go:embed emojis/1767.svg +var Emoji_1767 []byte + +//go:embed emojis/1768.svg +var Emoji_1768 []byte + +//go:embed emojis/1769.svg +var Emoji_1769 []byte + +//go:embed emojis/1770.svg +var Emoji_1770 []byte + +//go:embed emojis/1771.svg +var Emoji_1771 []byte + +//go:embed emojis/1772.svg +var Emoji_1772 []byte + +//go:embed emojis/1773.svg +var Emoji_1773 []byte + +//go:embed emojis/1774.svg +var Emoji_1774 []byte + +//go:embed emojis/1775.svg +var Emoji_1775 []byte + +//go:embed emojis/1776.svg +var Emoji_1776 []byte + +//go:embed emojis/1777.svg +var Emoji_1777 []byte + +//go:embed emojis/1778.svg +var Emoji_1778 []byte + +//go:embed emojis/1779.svg +var Emoji_1779 []byte + +//go:embed emojis/1780.svg +var Emoji_1780 []byte + +//go:embed emojis/1781.svg +var Emoji_1781 []byte + +//go:embed emojis/1782.svg +var Emoji_1782 []byte + +//go:embed emojis/1783.svg +var Emoji_1783 []byte + +//go:embed emojis/1784.svg +var Emoji_1784 []byte + +//go:embed emojis/1785.svg +var Emoji_1785 []byte + +//go:embed emojis/1786.svg +var Emoji_1786 []byte + +//go:embed emojis/1787.svg +var Emoji_1787 []byte + +//go:embed emojis/1788.svg +var Emoji_1788 []byte + +//go:embed emojis/1789.svg +var Emoji_1789 []byte + +//go:embed emojis/1790.svg +var Emoji_1790 []byte + +//go:embed emojis/1791.svg +var Emoji_1791 []byte + +//go:embed emojis/1792.svg +var Emoji_1792 []byte + +//go:embed emojis/1793.svg +var Emoji_1793 []byte + +//go:embed emojis/1794.svg +var Emoji_1794 []byte + +//go:embed emojis/1795.svg +var Emoji_1795 []byte + +//go:embed emojis/1796.svg +var Emoji_1796 []byte + +//go:embed emojis/1797.svg +var Emoji_1797 []byte + +//go:embed emojis/1798.svg +var Emoji_1798 []byte + +//go:embed emojis/1799.svg +var Emoji_1799 []byte + +//go:embed emojis/1800.svg +var Emoji_1800 []byte + +//go:embed emojis/1801.svg +var Emoji_1801 []byte + +//go:embed emojis/1802.svg +var Emoji_1802 []byte + +//go:embed emojis/1803.svg +var Emoji_1803 []byte + +//go:embed emojis/1804.svg +var Emoji_1804 []byte + +//go:embed emojis/1805.svg +var Emoji_1805 []byte + +//go:embed emojis/1806.svg +var Emoji_1806 []byte + +//go:embed emojis/1807.svg +var Emoji_1807 []byte + +//go:embed emojis/1808.svg +var Emoji_1808 []byte + +//go:embed emojis/1809.svg +var Emoji_1809 []byte + +//go:embed emojis/1810.svg +var Emoji_1810 []byte + +//go:embed emojis/1811.svg +var Emoji_1811 []byte + +//go:embed emojis/1812.svg +var Emoji_1812 []byte + +//go:embed emojis/1813.svg +var Emoji_1813 []byte + +//go:embed emojis/1814.svg +var Emoji_1814 []byte + +//go:embed emojis/1815.svg +var Emoji_1815 []byte + +//go:embed emojis/1816.svg +var Emoji_1816 []byte + +//go:embed emojis/1817.svg +var Emoji_1817 []byte + +//go:embed emojis/1818.svg +var Emoji_1818 []byte + +//go:embed emojis/1819.svg +var Emoji_1819 []byte + +//go:embed emojis/1820.svg +var Emoji_1820 []byte + +//go:embed emojis/1821.svg +var Emoji_1821 []byte + +//go:embed emojis/1822.svg +var Emoji_1822 []byte + +//go:embed emojis/1823.svg +var Emoji_1823 []byte + +//go:embed emojis/1824.svg +var Emoji_1824 []byte + +//go:embed emojis/1825.svg +var Emoji_1825 []byte + +//go:embed emojis/1826.svg +var Emoji_1826 []byte + +//go:embed emojis/1827.svg +var Emoji_1827 []byte + +//go:embed emojis/1828.svg +var Emoji_1828 []byte + +//go:embed emojis/1829.svg +var Emoji_1829 []byte + +//go:embed emojis/1830.svg +var Emoji_1830 []byte + +//go:embed emojis/1831.svg +var Emoji_1831 []byte + +//go:embed emojis/1832.svg +var Emoji_1832 []byte + +//go:embed emojis/1833.svg +var Emoji_1833 []byte + +//go:embed emojis/1834.svg +var Emoji_1834 []byte + +//go:embed emojis/1835.svg +var Emoji_1835 []byte + +//go:embed emojis/1836.svg +var Emoji_1836 []byte + +//go:embed emojis/1837.svg +var Emoji_1837 []byte + +//go:embed emojis/1838.svg +var Emoji_1838 []byte + +//go:embed emojis/1839.svg +var Emoji_1839 []byte + +//go:embed emojis/1840.svg +var Emoji_1840 []byte + +//go:embed emojis/1841.svg +var Emoji_1841 []byte + +//go:embed emojis/1842.svg +var Emoji_1842 []byte + +//go:embed emojis/1843.svg +var Emoji_1843 []byte + +//go:embed emojis/1844.svg +var Emoji_1844 []byte + +//go:embed emojis/1845.svg +var Emoji_1845 []byte + +//go:embed emojis/1846.svg +var Emoji_1846 []byte + +//go:embed emojis/1847.svg +var Emoji_1847 []byte + +//go:embed emojis/1848.svg +var Emoji_1848 []byte + +//go:embed emojis/1849.svg +var Emoji_1849 []byte + +//go:embed emojis/1850.svg +var Emoji_1850 []byte + +//go:embed emojis/1851.svg +var Emoji_1851 []byte + +//go:embed emojis/1852.svg +var Emoji_1852 []byte + +//go:embed emojis/1853.svg +var Emoji_1853 []byte + +//go:embed emojis/1854.svg +var Emoji_1854 []byte + +//go:embed emojis/1855.svg +var Emoji_1855 []byte + +//go:embed emojis/1856.svg +var Emoji_1856 []byte + +//go:embed emojis/1857.svg +var Emoji_1857 []byte + +//go:embed emojis/1858.svg +var Emoji_1858 []byte + +//go:embed emojis/1859.svg +var Emoji_1859 []byte + +//go:embed emojis/1860.svg +var Emoji_1860 []byte + +//go:embed emojis/1861.svg +var Emoji_1861 []byte + +//go:embed emojis/1862.svg +var Emoji_1862 []byte + +//go:embed emojis/1863.svg +var Emoji_1863 []byte + +//go:embed emojis/1864.svg +var Emoji_1864 []byte + +//go:embed emojis/1865.svg +var Emoji_1865 []byte + +//go:embed emojis/1866.svg +var Emoji_1866 []byte + +//go:embed emojis/1867.svg +var Emoji_1867 []byte + +//go:embed emojis/1868.svg +var Emoji_1868 []byte + +//go:embed emojis/1869.svg +var Emoji_1869 []byte + +//go:embed emojis/1870.svg +var Emoji_1870 []byte + +//go:embed emojis/1871.svg +var Emoji_1871 []byte + +//go:embed emojis/1872.svg +var Emoji_1872 []byte + +//go:embed emojis/1873.svg +var Emoji_1873 []byte + +//go:embed emojis/1874.svg +var Emoji_1874 []byte + +//go:embed emojis/1875.svg +var Emoji_1875 []byte + +//go:embed emojis/1876.svg +var Emoji_1876 []byte + +//go:embed emojis/1877.svg +var Emoji_1877 []byte + +//go:embed emojis/1878.svg +var Emoji_1878 []byte + +//go:embed emojis/1879.svg +var Emoji_1879 []byte + +//go:embed emojis/1880.svg +var Emoji_1880 []byte + +//go:embed emojis/1881.svg +var Emoji_1881 []byte + +//go:embed emojis/1882.svg +var Emoji_1882 []byte + +//go:embed emojis/1883.svg +var Emoji_1883 []byte + +//go:embed emojis/1884.svg +var Emoji_1884 []byte + +//go:embed emojis/1885.svg +var Emoji_1885 []byte + +//go:embed emojis/1886.svg +var Emoji_1886 []byte + +//go:embed emojis/1887.svg +var Emoji_1887 []byte + +//go:embed emojis/1888.svg +var Emoji_1888 []byte + +//go:embed emojis/1889.svg +var Emoji_1889 []byte + +//go:embed emojis/1890.svg +var Emoji_1890 []byte + +//go:embed emojis/1891.svg +var Emoji_1891 []byte + +//go:embed emojis/1892.svg +var Emoji_1892 []byte + +//go:embed emojis/1893.svg +var Emoji_1893 []byte + +//go:embed emojis/1894.svg +var Emoji_1894 []byte + +//go:embed emojis/1895.svg +var Emoji_1895 []byte + +//go:embed emojis/1896.svg +var Emoji_1896 []byte + +//go:embed emojis/1897.svg +var Emoji_1897 []byte + +//go:embed emojis/1898.svg +var Emoji_1898 []byte + +//go:embed emojis/1899.svg +var Emoji_1899 []byte + +//go:embed emojis/1900.svg +var Emoji_1900 []byte + +//go:embed emojis/1901.svg +var Emoji_1901 []byte + +//go:embed emojis/1902.svg +var Emoji_1902 []byte + +//go:embed emojis/1903.svg +var Emoji_1903 []byte + +//go:embed emojis/1904.svg +var Emoji_1904 []byte + +//go:embed emojis/1905.svg +var Emoji_1905 []byte + +//go:embed emojis/1906.svg +var Emoji_1906 []byte + +//go:embed emojis/1907.svg +var Emoji_1907 []byte + +//go:embed emojis/1908.svg +var Emoji_1908 []byte + +//go:embed emojis/1909.svg +var Emoji_1909 []byte + +//go:embed emojis/1910.svg +var Emoji_1910 []byte + +//go:embed emojis/1911.svg +var Emoji_1911 []byte + +//go:embed emojis/1912.svg +var Emoji_1912 []byte + +//go:embed emojis/1913.svg +var Emoji_1913 []byte + +//go:embed emojis/1914.svg +var Emoji_1914 []byte + +//go:embed emojis/1915.svg +var Emoji_1915 []byte + +//go:embed emojis/1916.svg +var Emoji_1916 []byte + +//go:embed emojis/1917.svg +var Emoji_1917 []byte + +//go:embed emojis/1918.svg +var Emoji_1918 []byte + +//go:embed emojis/1919.svg +var Emoji_1919 []byte + +//go:embed emojis/1920.svg +var Emoji_1920 []byte + +//go:embed emojis/1921.svg +var Emoji_1921 []byte + +//go:embed emojis/1922.svg +var Emoji_1922 []byte + +//go:embed emojis/1923.svg +var Emoji_1923 []byte + +//go:embed emojis/1924.svg +var Emoji_1924 []byte + +//go:embed emojis/1925.svg +var Emoji_1925 []byte + +//go:embed emojis/1926.svg +var Emoji_1926 []byte + +//go:embed emojis/1927.svg +var Emoji_1927 []byte + +//go:embed emojis/1928.svg +var Emoji_1928 []byte + +//go:embed emojis/1929.svg +var Emoji_1929 []byte + +//go:embed emojis/1930.svg +var Emoji_1930 []byte + +//go:embed emojis/1931.svg +var Emoji_1931 []byte + +//go:embed emojis/1932.svg +var Emoji_1932 []byte + +//go:embed emojis/1933.svg +var Emoji_1933 []byte + +//go:embed emojis/1934.svg +var Emoji_1934 []byte + +//go:embed emojis/1935.svg +var Emoji_1935 []byte + +//go:embed emojis/1936.svg +var Emoji_1936 []byte + +//go:embed emojis/1937.svg +var Emoji_1937 []byte + +//go:embed emojis/1938.svg +var Emoji_1938 []byte + +//go:embed emojis/1939.svg +var Emoji_1939 []byte + +//go:embed emojis/1940.svg +var Emoji_1940 []byte + +//go:embed emojis/1941.svg +var Emoji_1941 []byte + +//go:embed emojis/1942.svg +var Emoji_1942 []byte + +//go:embed emojis/1943.svg +var Emoji_1943 []byte + +//go:embed emojis/1944.svg +var Emoji_1944 []byte + +//go:embed emojis/1945.svg +var Emoji_1945 []byte + +//go:embed emojis/1946.svg +var Emoji_1946 []byte + +//go:embed emojis/1947.svg +var Emoji_1947 []byte + +//go:embed emojis/1948.svg +var Emoji_1948 []byte + +//go:embed emojis/1949.svg +var Emoji_1949 []byte + +//go:embed emojis/1950.svg +var Emoji_1950 []byte + +//go:embed emojis/1951.svg +var Emoji_1951 []byte + +//go:embed emojis/1952.svg +var Emoji_1952 []byte + +//go:embed emojis/1953.svg +var Emoji_1953 []byte + +//go:embed emojis/1954.svg +var Emoji_1954 []byte + +//go:embed emojis/1955.svg +var Emoji_1955 []byte + +//go:embed emojis/1956.svg +var Emoji_1956 []byte + +//go:embed emojis/1957.svg +var Emoji_1957 []byte + +//go:embed emojis/1958.svg +var Emoji_1958 []byte + +//go:embed emojis/1959.svg +var Emoji_1959 []byte + +//go:embed emojis/1960.svg +var Emoji_1960 []byte + +//go:embed emojis/1961.svg +var Emoji_1961 []byte + +//go:embed emojis/1962.svg +var Emoji_1962 []byte + +//go:embed emojis/1963.svg +var Emoji_1963 []byte + +//go:embed emojis/1964.svg +var Emoji_1964 []byte + +//go:embed emojis/1965.svg +var Emoji_1965 []byte + +//go:embed emojis/1966.svg +var Emoji_1966 []byte + +//go:embed emojis/1967.svg +var Emoji_1967 []byte + +//go:embed emojis/1968.svg +var Emoji_1968 []byte + +//go:embed emojis/1969.svg +var Emoji_1969 []byte + +//go:embed emojis/1970.svg +var Emoji_1970 []byte + +//go:embed emojis/1971.svg +var Emoji_1971 []byte + +//go:embed emojis/1972.svg +var Emoji_1972 []byte + +//go:embed emojis/1973.svg +var Emoji_1973 []byte + +//go:embed emojis/1974.svg +var Emoji_1974 []byte + +//go:embed emojis/1975.svg +var Emoji_1975 []byte + +//go:embed emojis/1976.svg +var Emoji_1976 []byte + +//go:embed emojis/1977.svg +var Emoji_1977 []byte + +//go:embed emojis/1978.svg +var Emoji_1978 []byte + +//go:embed emojis/1979.svg +var Emoji_1979 []byte + +//go:embed emojis/1980.svg +var Emoji_1980 []byte + +//go:embed emojis/1981.svg +var Emoji_1981 []byte + +//go:embed emojis/1982.svg +var Emoji_1982 []byte + +//go:embed emojis/1983.svg +var Emoji_1983 []byte + +//go:embed emojis/1984.svg +var Emoji_1984 []byte + +//go:embed emojis/1985.svg +var Emoji_1985 []byte + +//go:embed emojis/1986.svg +var Emoji_1986 []byte + +//go:embed emojis/1987.svg +var Emoji_1987 []byte + +//go:embed emojis/1988.svg +var Emoji_1988 []byte + +//go:embed emojis/1989.svg +var Emoji_1989 []byte + +//go:embed emojis/1990.svg +var Emoji_1990 []byte + +//go:embed emojis/1991.svg +var Emoji_1991 []byte + +//go:embed emojis/1992.svg +var Emoji_1992 []byte + +//go:embed emojis/1993.svg +var Emoji_1993 []byte + +//go:embed emojis/1994.svg +var Emoji_1994 []byte + +//go:embed emojis/1995.svg +var Emoji_1995 []byte + +//go:embed emojis/1996.svg +var Emoji_1996 []byte + +//go:embed emojis/1997.svg +var Emoji_1997 []byte + +//go:embed emojis/1998.svg +var Emoji_1998 []byte + +//go:embed emojis/1999.svg +var Emoji_1999 []byte + +//go:embed emojis/2000.svg +var Emoji_2000 []byte + +//go:embed emojis/2001.svg +var Emoji_2001 []byte + +//go:embed emojis/2002.svg +var Emoji_2002 []byte + +//go:embed emojis/2003.svg +var Emoji_2003 []byte + +//go:embed emojis/2004.svg +var Emoji_2004 []byte + +//go:embed emojis/2005.svg +var Emoji_2005 []byte + +//go:embed emojis/2006.svg +var Emoji_2006 []byte + +//go:embed emojis/2007.svg +var Emoji_2007 []byte + +//go:embed emojis/2008.svg +var Emoji_2008 []byte + +//go:embed emojis/2009.svg +var Emoji_2009 []byte + +//go:embed emojis/2010.svg +var Emoji_2010 []byte + +//go:embed emojis/2011.svg +var Emoji_2011 []byte + +//go:embed emojis/2012.svg +var Emoji_2012 []byte + +//go:embed emojis/2013.svg +var Emoji_2013 []byte + +//go:embed emojis/2014.svg +var Emoji_2014 []byte + +//go:embed emojis/2015.svg +var Emoji_2015 []byte + +//go:embed emojis/2016.svg +var Emoji_2016 []byte + +//go:embed emojis/2017.svg +var Emoji_2017 []byte + +//go:embed emojis/2018.svg +var Emoji_2018 []byte + +//go:embed emojis/2019.svg +var Emoji_2019 []byte + +//go:embed emojis/2020.svg +var Emoji_2020 []byte + +//go:embed emojis/2021.svg +var Emoji_2021 []byte + +//go:embed emojis/2022.svg +var Emoji_2022 []byte + +//go:embed emojis/2023.svg +var Emoji_2023 []byte + +//go:embed emojis/2024.svg +var Emoji_2024 []byte + +//go:embed emojis/2025.svg +var Emoji_2025 []byte + +//go:embed emojis/2026.svg +var Emoji_2026 []byte + +//go:embed emojis/2027.svg +var Emoji_2027 []byte + +//go:embed emojis/2028.svg +var Emoji_2028 []byte + +//go:embed emojis/2029.svg +var Emoji_2029 []byte + +//go:embed emojis/2030.svg +var Emoji_2030 []byte + +//go:embed emojis/2031.svg +var Emoji_2031 []byte + +//go:embed emojis/2032.svg +var Emoji_2032 []byte + +//go:embed emojis/2033.svg +var Emoji_2033 []byte + +//go:embed emojis/2034.svg +var Emoji_2034 []byte + +//go:embed emojis/2035.svg +var Emoji_2035 []byte + +//go:embed emojis/2036.svg +var Emoji_2036 []byte + +//go:embed emojis/2037.svg +var Emoji_2037 []byte + +//go:embed emojis/2038.svg +var Emoji_2038 []byte + +//go:embed emojis/2039.svg +var Emoji_2039 []byte + +//go:embed emojis/2040.svg +var Emoji_2040 []byte + +//go:embed emojis/2041.svg +var Emoji_2041 []byte + +//go:embed emojis/2042.svg +var Emoji_2042 []byte + +//go:embed emojis/2043.svg +var Emoji_2043 []byte + +//go:embed emojis/2044.svg +var Emoji_2044 []byte + +//go:embed emojis/2045.svg +var Emoji_2045 []byte + +//go:embed emojis/2046.svg +var Emoji_2046 []byte + +//go:embed emojis/2047.svg +var Emoji_2047 []byte + +//go:embed emojis/2048.svg +var Emoji_2048 []byte + +//go:embed emojis/2049.svg +var Emoji_2049 []byte + +//go:embed emojis/2050.svg +var Emoji_2050 []byte + +//go:embed emojis/2051.svg +var Emoji_2051 []byte + +//go:embed emojis/2052.svg +var Emoji_2052 []byte + +//go:embed emojis/2053.svg +var Emoji_2053 []byte + +//go:embed emojis/2054.svg +var Emoji_2054 []byte + +//go:embed emojis/2055.svg +var Emoji_2055 []byte + +//go:embed emojis/2056.svg +var Emoji_2056 []byte + +//go:embed emojis/2057.svg +var Emoji_2057 []byte + +//go:embed emojis/2058.svg +var Emoji_2058 []byte + +//go:embed emojis/2059.svg +var Emoji_2059 []byte + +//go:embed emojis/2060.svg +var Emoji_2060 []byte + +//go:embed emojis/2061.svg +var Emoji_2061 []byte + +//go:embed emojis/2062.svg +var Emoji_2062 []byte + +//go:embed emojis/2063.svg +var Emoji_2063 []byte + +//go:embed emojis/2064.svg +var Emoji_2064 []byte + +//go:embed emojis/2065.svg +var Emoji_2065 []byte + +//go:embed emojis/2066.svg +var Emoji_2066 []byte + +//go:embed emojis/2067.svg +var Emoji_2067 []byte + +//go:embed emojis/2068.svg +var Emoji_2068 []byte + +//go:embed emojis/2069.svg +var Emoji_2069 []byte + +//go:embed emojis/2070.svg +var Emoji_2070 []byte + +//go:embed emojis/2071.svg +var Emoji_2071 []byte + +//go:embed emojis/2072.svg +var Emoji_2072 []byte + +//go:embed emojis/2073.svg +var Emoji_2073 []byte + +//go:embed emojis/2074.svg +var Emoji_2074 []byte + +//go:embed emojis/2075.svg +var Emoji_2075 []byte + +//go:embed emojis/2076.svg +var Emoji_2076 []byte + +//go:embed emojis/2077.svg +var Emoji_2077 []byte + +//go:embed emojis/2078.svg +var Emoji_2078 []byte + +//go:embed emojis/2079.svg +var Emoji_2079 []byte + +//go:embed emojis/2080.svg +var Emoji_2080 []byte + +//go:embed emojis/2081.svg +var Emoji_2081 []byte + +//go:embed emojis/2082.svg +var Emoji_2082 []byte + +//go:embed emojis/2083.svg +var Emoji_2083 []byte + +//go:embed emojis/2084.svg +var Emoji_2084 []byte + +//go:embed emojis/2085.svg +var Emoji_2085 []byte + +//go:embed emojis/2086.svg +var Emoji_2086 []byte + +//go:embed emojis/2087.svg +var Emoji_2087 []byte + +//go:embed emojis/2088.svg +var Emoji_2088 []byte + +//go:embed emojis/2089.svg +var Emoji_2089 []byte + +//go:embed emojis/2090.svg +var Emoji_2090 []byte + +//go:embed emojis/2091.svg +var Emoji_2091 []byte + +//go:embed emojis/2092.svg +var Emoji_2092 []byte + +//go:embed emojis/2093.svg +var Emoji_2093 []byte + +//go:embed emojis/2094.svg +var Emoji_2094 []byte + +//go:embed emojis/2095.svg +var Emoji_2095 []byte + +//go:embed emojis/2096.svg +var Emoji_2096 []byte + +//go:embed emojis/2097.svg +var Emoji_2097 []byte + +//go:embed emojis/2098.svg +var Emoji_2098 []byte + +//go:embed emojis/2099.svg +var Emoji_2099 []byte + +//go:embed emojis/2100.svg +var Emoji_2100 []byte + +//go:embed emojis/2101.svg +var Emoji_2101 []byte + +//go:embed emojis/2102.svg +var Emoji_2102 []byte + +//go:embed emojis/2103.svg +var Emoji_2103 []byte + +//go:embed emojis/2104.svg +var Emoji_2104 []byte + +//go:embed emojis/2105.svg +var Emoji_2105 []byte + +//go:embed emojis/2106.svg +var Emoji_2106 []byte + +//go:embed emojis/2107.svg +var Emoji_2107 []byte + +//go:embed emojis/2108.svg +var Emoji_2108 []byte + +//go:embed emojis/2109.svg +var Emoji_2109 []byte + +//go:embed emojis/2110.svg +var Emoji_2110 []byte + +//go:embed emojis/2111.svg +var Emoji_2111 []byte + +//go:embed emojis/2112.svg +var Emoji_2112 []byte + +//go:embed emojis/2113.svg +var Emoji_2113 []byte + +//go:embed emojis/2114.svg +var Emoji_2114 []byte + +//go:embed emojis/2115.svg +var Emoji_2115 []byte + +//go:embed emojis/2116.svg +var Emoji_2116 []byte + +//go:embed emojis/2117.svg +var Emoji_2117 []byte + +//go:embed emojis/2118.svg +var Emoji_2118 []byte + +//go:embed emojis/2119.svg +var Emoji_2119 []byte + +//go:embed emojis/2120.svg +var Emoji_2120 []byte + +//go:embed emojis/2121.svg +var Emoji_2121 []byte + +//go:embed emojis/2122.svg +var Emoji_2122 []byte + +//go:embed emojis/2123.svg +var Emoji_2123 []byte + +//go:embed emojis/2124.svg +var Emoji_2124 []byte + +//go:embed emojis/2125.svg +var Emoji_2125 []byte + +//go:embed emojis/2126.svg +var Emoji_2126 []byte + +//go:embed emojis/2127.svg +var Emoji_2127 []byte + +//go:embed emojis/2128.svg +var Emoji_2128 []byte + +//go:embed emojis/2129.svg +var Emoji_2129 []byte + +//go:embed emojis/2130.svg +var Emoji_2130 []byte + +//go:embed emojis/2131.svg +var Emoji_2131 []byte + +//go:embed emojis/2132.svg +var Emoji_2132 []byte + +//go:embed emojis/2133.svg +var Emoji_2133 []byte + +//go:embed emojis/2134.svg +var Emoji_2134 []byte + +//go:embed emojis/2135.svg +var Emoji_2135 []byte + +//go:embed emojis/2136.svg +var Emoji_2136 []byte + +//go:embed emojis/2137.svg +var Emoji_2137 []byte + +//go:embed emojis/2138.svg +var Emoji_2138 []byte + +//go:embed emojis/2139.svg +var Emoji_2139 []byte + +//go:embed emojis/2140.svg +var Emoji_2140 []byte + +//go:embed emojis/2141.svg +var Emoji_2141 []byte + +//go:embed emojis/2142.svg +var Emoji_2142 []byte + +//go:embed emojis/2143.svg +var Emoji_2143 []byte + +//go:embed emojis/2144.svg +var Emoji_2144 []byte + +//go:embed emojis/2145.svg +var Emoji_2145 []byte + +//go:embed emojis/2146.svg +var Emoji_2146 []byte + +//go:embed emojis/2147.svg +var Emoji_2147 []byte + +//go:embed emojis/2148.svg +var Emoji_2148 []byte + +//go:embed emojis/2149.svg +var Emoji_2149 []byte + +//go:embed emojis/2150.svg +var Emoji_2150 []byte + +//go:embed emojis/2151.svg +var Emoji_2151 []byte + +//go:embed emojis/2152.svg +var Emoji_2152 []byte + +//go:embed emojis/2153.svg +var Emoji_2153 []byte + +//go:embed emojis/2154.svg +var Emoji_2154 []byte + +//go:embed emojis/2155.svg +var Emoji_2155 []byte + +//go:embed emojis/2156.svg +var Emoji_2156 []byte + +//go:embed emojis/2157.svg +var Emoji_2157 []byte + +//go:embed emojis/2158.svg +var Emoji_2158 []byte + +//go:embed emojis/2159.svg +var Emoji_2159 []byte + +//go:embed emojis/2160.svg +var Emoji_2160 []byte + +//go:embed emojis/2161.svg +var Emoji_2161 []byte + +//go:embed emojis/2162.svg +var Emoji_2162 []byte + +//go:embed emojis/2163.svg +var Emoji_2163 []byte + +//go:embed emojis/2164.svg +var Emoji_2164 []byte + +//go:embed emojis/2165.svg +var Emoji_2165 []byte + +//go:embed emojis/2166.svg +var Emoji_2166 []byte + +//go:embed emojis/2167.svg +var Emoji_2167 []byte + +//go:embed emojis/2168.svg +var Emoji_2168 []byte + +//go:embed emojis/2169.svg +var Emoji_2169 []byte + +//go:embed emojis/2170.svg +var Emoji_2170 []byte + +//go:embed emojis/2171.svg +var Emoji_2171 []byte + +//go:embed emojis/2172.svg +var Emoji_2172 []byte + +//go:embed emojis/2173.svg +var Emoji_2173 []byte + +//go:embed emojis/2174.svg +var Emoji_2174 []byte + +//go:embed emojis/2175.svg +var Emoji_2175 []byte + +//go:embed emojis/2176.svg +var Emoji_2176 []byte + +//go:embed emojis/2177.svg +var Emoji_2177 []byte + +//go:embed emojis/2178.svg +var Emoji_2178 []byte + +//go:embed emojis/2179.svg +var Emoji_2179 []byte + +//go:embed emojis/2180.svg +var Emoji_2180 []byte + +//go:embed emojis/2181.svg +var Emoji_2181 []byte + +//go:embed emojis/2182.svg +var Emoji_2182 []byte + +//go:embed emojis/2183.svg +var Emoji_2183 []byte + +//go:embed emojis/2184.svg +var Emoji_2184 []byte + +//go:embed emojis/2185.svg +var Emoji_2185 []byte + +//go:embed emojis/2186.svg +var Emoji_2186 []byte + +//go:embed emojis/2187.svg +var Emoji_2187 []byte + +//go:embed emojis/2188.svg +var Emoji_2188 []byte + +//go:embed emojis/2189.svg +var Emoji_2189 []byte + +//go:embed emojis/2190.svg +var Emoji_2190 []byte + +//go:embed emojis/2191.svg +var Emoji_2191 []byte + +//go:embed emojis/2192.svg +var Emoji_2192 []byte + +//go:embed emojis/2193.svg +var Emoji_2193 []byte + +//go:embed emojis/2194.svg +var Emoji_2194 []byte + +//go:embed emojis/2195.svg +var Emoji_2195 []byte + +//go:embed emojis/2196.svg +var Emoji_2196 []byte + +//go:embed emojis/2197.svg +var Emoji_2197 []byte + +//go:embed emojis/2198.svg +var Emoji_2198 []byte + +//go:embed emojis/2199.svg +var Emoji_2199 []byte + +//go:embed emojis/2200.svg +var Emoji_2200 []byte + +//go:embed emojis/2201.svg +var Emoji_2201 []byte + +//go:embed emojis/2202.svg +var Emoji_2202 []byte + +//go:embed emojis/2203.svg +var Emoji_2203 []byte + +//go:embed emojis/2204.svg +var Emoji_2204 []byte + +//go:embed emojis/2205.svg +var Emoji_2205 []byte + +//go:embed emojis/2206.svg +var Emoji_2206 []byte + +//go:embed emojis/2207.svg +var Emoji_2207 []byte + +//go:embed emojis/2208.svg +var Emoji_2208 []byte + +//go:embed emojis/2209.svg +var Emoji_2209 []byte + +//go:embed emojis/2210.svg +var Emoji_2210 []byte + +//go:embed emojis/2211.svg +var Emoji_2211 []byte + +//go:embed emojis/2212.svg +var Emoji_2212 []byte + +//go:embed emojis/2213.svg +var Emoji_2213 []byte + +//go:embed emojis/2214.svg +var Emoji_2214 []byte + +//go:embed emojis/2215.svg +var Emoji_2215 []byte + +//go:embed emojis/2216.svg +var Emoji_2216 []byte + +//go:embed emojis/2217.svg +var Emoji_2217 []byte + +//go:embed emojis/2218.svg +var Emoji_2218 []byte + +//go:embed emojis/2219.svg +var Emoji_2219 []byte + +//go:embed emojis/2220.svg +var Emoji_2220 []byte + +//go:embed emojis/2221.svg +var Emoji_2221 []byte + +//go:embed emojis/2222.svg +var Emoji_2222 []byte + +//go:embed emojis/2223.svg +var Emoji_2223 []byte + +//go:embed emojis/2224.svg +var Emoji_2224 []byte + +//go:embed emojis/2225.svg +var Emoji_2225 []byte + +//go:embed emojis/2226.svg +var Emoji_2226 []byte + +//go:embed emojis/2227.svg +var Emoji_2227 []byte + +//go:embed emojis/2228.svg +var Emoji_2228 []byte + +//go:embed emojis/2229.svg +var Emoji_2229 []byte + +//go:embed emojis/2230.svg +var Emoji_2230 []byte + +//go:embed emojis/2231.svg +var Emoji_2231 []byte + +//go:embed emojis/2232.svg +var Emoji_2232 []byte + +//go:embed emojis/2233.svg +var Emoji_2233 []byte + +//go:embed emojis/2234.svg +var Emoji_2234 []byte + +//go:embed emojis/2235.svg +var Emoji_2235 []byte + +//go:embed emojis/2236.svg +var Emoji_2236 []byte + +//go:embed emojis/2237.svg +var Emoji_2237 []byte + +//go:embed emojis/2238.svg +var Emoji_2238 []byte + +//go:embed emojis/2239.svg +var Emoji_2239 []byte + +//go:embed emojis/2240.svg +var Emoji_2240 []byte + +//go:embed emojis/2241.svg +var Emoji_2241 []byte + +//go:embed emojis/2242.svg +var Emoji_2242 []byte + +//go:embed emojis/2243.svg +var Emoji_2243 []byte + +//go:embed emojis/2244.svg +var Emoji_2244 []byte + +//go:embed emojis/2245.svg +var Emoji_2245 []byte + +//go:embed emojis/2246.svg +var Emoji_2246 []byte + +//go:embed emojis/2247.svg +var Emoji_2247 []byte + +//go:embed emojis/2248.svg +var Emoji_2248 []byte + +//go:embed emojis/2249.svg +var Emoji_2249 []byte + +//go:embed emojis/2250.svg +var Emoji_2250 []byte + +//go:embed emojis/2251.svg +var Emoji_2251 []byte + +//go:embed emojis/2252.svg +var Emoji_2252 []byte + +//go:embed emojis/2253.svg +var Emoji_2253 []byte + +//go:embed emojis/2254.svg +var Emoji_2254 []byte + +//go:embed emojis/2255.svg +var Emoji_2255 []byte + +//go:embed emojis/2256.svg +var Emoji_2256 []byte + +//go:embed emojis/2257.svg +var Emoji_2257 []byte + +//go:embed emojis/2258.svg +var Emoji_2258 []byte + +//go:embed emojis/2259.svg +var Emoji_2259 []byte + +//go:embed emojis/2260.svg +var Emoji_2260 []byte + +//go:embed emojis/2261.svg +var Emoji_2261 []byte + +//go:embed emojis/2262.svg +var Emoji_2262 []byte + +//go:embed emojis/2263.svg +var Emoji_2263 []byte + +//go:embed emojis/2264.svg +var Emoji_2264 []byte + +//go:embed emojis/2265.svg +var Emoji_2265 []byte + +//go:embed emojis/2266.svg +var Emoji_2266 []byte + +//go:embed emojis/2267.svg +var Emoji_2267 []byte + +//go:embed emojis/2268.svg +var Emoji_2268 []byte + +//go:embed emojis/2269.svg +var Emoji_2269 []byte + +//go:embed emojis/2270.svg +var Emoji_2270 []byte + +//go:embed emojis/2271.svg +var Emoji_2271 []byte + +//go:embed emojis/2272.svg +var Emoji_2272 []byte + +//go:embed emojis/2273.svg +var Emoji_2273 []byte + +//go:embed emojis/2274.svg +var Emoji_2274 []byte + +//go:embed emojis/2275.svg +var Emoji_2275 []byte + +//go:embed emojis/2276.svg +var Emoji_2276 []byte + +//go:embed emojis/2277.svg +var Emoji_2277 []byte + +//go:embed emojis/2278.svg +var Emoji_2278 []byte + +//go:embed emojis/2279.svg +var Emoji_2279 []byte + +//go:embed emojis/2280.svg +var Emoji_2280 []byte + +//go:embed emojis/2281.svg +var Emoji_2281 []byte + +//go:embed emojis/2282.svg +var Emoji_2282 []byte + +//go:embed emojis/2283.svg +var Emoji_2283 []byte + +//go:embed emojis/2284.svg +var Emoji_2284 []byte + +//go:embed emojis/2285.svg +var Emoji_2285 []byte + +//go:embed emojis/2286.svg +var Emoji_2286 []byte + +//go:embed emojis/2287.svg +var Emoji_2287 []byte + +//go:embed emojis/2288.svg +var Emoji_2288 []byte + +//go:embed emojis/2289.svg +var Emoji_2289 []byte + +//go:embed emojis/2290.svg +var Emoji_2290 []byte + +//go:embed emojis/2291.svg +var Emoji_2291 []byte + +//go:embed emojis/2292.svg +var Emoji_2292 []byte + +//go:embed emojis/2293.svg +var Emoji_2293 []byte + +//go:embed emojis/2294.svg +var Emoji_2294 []byte + +//go:embed emojis/2295.svg +var Emoji_2295 []byte + +//go:embed emojis/2296.svg +var Emoji_2296 []byte + +//go:embed emojis/2297.svg +var Emoji_2297 []byte + +//go:embed emojis/2298.svg +var Emoji_2298 []byte + +//go:embed emojis/2299.svg +var Emoji_2299 []byte + +//go:embed emojis/2300.svg +var Emoji_2300 []byte + +//go:embed emojis/2301.svg +var Emoji_2301 []byte + +//go:embed emojis/2302.svg +var Emoji_2302 []byte + +//go:embed emojis/2303.svg +var Emoji_2303 []byte + +//go:embed emojis/2304.svg +var Emoji_2304 []byte + +//go:embed emojis/2305.svg +var Emoji_2305 []byte + +//go:embed emojis/2306.svg +var Emoji_2306 []byte + +//go:embed emojis/2307.svg +var Emoji_2307 []byte + +//go:embed emojis/2308.svg +var Emoji_2308 []byte + +//go:embed emojis/2309.svg +var Emoji_2309 []byte + +//go:embed emojis/2310.svg +var Emoji_2310 []byte + +//go:embed emojis/2311.svg +var Emoji_2311 []byte + +//go:embed emojis/2312.svg +var Emoji_2312 []byte + +//go:embed emojis/2313.svg +var Emoji_2313 []byte + +//go:embed emojis/2314.svg +var Emoji_2314 []byte + +//go:embed emojis/2315.svg +var Emoji_2315 []byte + +//go:embed emojis/2316.svg +var Emoji_2316 []byte + +//go:embed emojis/2317.svg +var Emoji_2317 []byte + +//go:embed emojis/2318.svg +var Emoji_2318 []byte + +//go:embed emojis/2319.svg +var Emoji_2319 []byte + +//go:embed emojis/2320.svg +var Emoji_2320 []byte + +//go:embed emojis/2321.svg +var Emoji_2321 []byte + +//go:embed emojis/2322.svg +var Emoji_2322 []byte + +//go:embed emojis/2323.svg +var Emoji_2323 []byte + +//go:embed emojis/2324.svg +var Emoji_2324 []byte + +//go:embed emojis/2325.svg +var Emoji_2325 []byte + +//go:embed emojis/2326.svg +var Emoji_2326 []byte + +//go:embed emojis/2327.svg +var Emoji_2327 []byte + +//go:embed emojis/2328.svg +var Emoji_2328 []byte + +//go:embed emojis/2329.svg +var Emoji_2329 []byte + +//go:embed emojis/2330.svg +var Emoji_2330 []byte + +//go:embed emojis/2331.svg +var Emoji_2331 []byte + +//go:embed emojis/2332.svg +var Emoji_2332 []byte + +//go:embed emojis/2333.svg +var Emoji_2333 []byte + +//go:embed emojis/2334.svg +var Emoji_2334 []byte + +//go:embed emojis/2335.svg +var Emoji_2335 []byte + +//go:embed emojis/2336.svg +var Emoji_2336 []byte + +//go:embed emojis/2337.svg +var Emoji_2337 []byte + +//go:embed emojis/2338.svg +var Emoji_2338 []byte + +//go:embed emojis/2339.svg +var Emoji_2339 []byte + +//go:embed emojis/2340.svg +var Emoji_2340 []byte + +//go:embed emojis/2341.svg +var Emoji_2341 []byte + +//go:embed emojis/2342.svg +var Emoji_2342 []byte + +//go:embed emojis/2343.svg +var Emoji_2343 []byte + +//go:embed emojis/2344.svg +var Emoji_2344 []byte + +//go:embed emojis/2345.svg +var Emoji_2345 []byte + +//go:embed emojis/2346.svg +var Emoji_2346 []byte + +//go:embed emojis/2347.svg +var Emoji_2347 []byte + +//go:embed emojis/2348.svg +var Emoji_2348 []byte + +//go:embed emojis/2349.svg +var Emoji_2349 []byte + +//go:embed emojis/2350.svg +var Emoji_2350 []byte + +//go:embed emojis/2351.svg +var Emoji_2351 []byte + +//go:embed emojis/2352.svg +var Emoji_2352 []byte + +//go:embed emojis/2353.svg +var Emoji_2353 []byte + +//go:embed emojis/2354.svg +var Emoji_2354 []byte + +//go:embed emojis/2355.svg +var Emoji_2355 []byte + +//go:embed emojis/2356.svg +var Emoji_2356 []byte + +//go:embed emojis/2357.svg +var Emoji_2357 []byte + +//go:embed emojis/2358.svg +var Emoji_2358 []byte + +//go:embed emojis/2359.svg +var Emoji_2359 []byte + +//go:embed emojis/2360.svg +var Emoji_2360 []byte + +//go:embed emojis/2361.svg +var Emoji_2361 []byte + +//go:embed emojis/2362.svg +var Emoji_2362 []byte + +//go:embed emojis/2363.svg +var Emoji_2363 []byte + +//go:embed emojis/2364.svg +var Emoji_2364 []byte + +//go:embed emojis/2365.svg +var Emoji_2365 []byte + +//go:embed emojis/2366.svg +var Emoji_2366 []byte + +//go:embed emojis/2367.svg +var Emoji_2367 []byte + +//go:embed emojis/2368.svg +var Emoji_2368 []byte + +//go:embed emojis/2369.svg +var Emoji_2369 []byte + +//go:embed emojis/2370.svg +var Emoji_2370 []byte + +//go:embed emojis/2371.svg +var Emoji_2371 []byte + +//go:embed emojis/2372.svg +var Emoji_2372 []byte + +//go:embed emojis/2373.svg +var Emoji_2373 []byte + +//go:embed emojis/2374.svg +var Emoji_2374 []byte + +//go:embed emojis/2375.svg +var Emoji_2375 []byte + +//go:embed emojis/2376.svg +var Emoji_2376 []byte + +//go:embed emojis/2377.svg +var Emoji_2377 []byte + +//go:embed emojis/2378.svg +var Emoji_2378 []byte + +//go:embed emojis/2379.svg +var Emoji_2379 []byte + +//go:embed emojis/2380.svg +var Emoji_2380 []byte + +//go:embed emojis/2381.svg +var Emoji_2381 []byte + +//go:embed emojis/2382.svg +var Emoji_2382 []byte + +//go:embed emojis/2383.svg +var Emoji_2383 []byte + +//go:embed emojis/2384.svg +var Emoji_2384 []byte + +//go:embed emojis/2385.svg +var Emoji_2385 []byte + +//go:embed emojis/2386.svg +var Emoji_2386 []byte + +//go:embed emojis/2387.svg +var Emoji_2387 []byte + +//go:embed emojis/2388.svg +var Emoji_2388 []byte + +//go:embed emojis/2389.svg +var Emoji_2389 []byte + +//go:embed emojis/2390.svg +var Emoji_2390 []byte + +//go:embed emojis/2391.svg +var Emoji_2391 []byte + +//go:embed emojis/2392.svg +var Emoji_2392 []byte + +//go:embed emojis/2393.svg +var Emoji_2393 []byte + +//go:embed emojis/2394.svg +var Emoji_2394 []byte + +//go:embed emojis/2395.svg +var Emoji_2395 []byte + +//go:embed emojis/2396.svg +var Emoji_2396 []byte + +//go:embed emojis/2397.svg +var Emoji_2397 []byte + +//go:embed emojis/2398.svg +var Emoji_2398 []byte + +//go:embed emojis/2399.svg +var Emoji_2399 []byte + +//go:embed emojis/2400.svg +var Emoji_2400 []byte + +//go:embed emojis/2401.svg +var Emoji_2401 []byte + +//go:embed emojis/2402.svg +var Emoji_2402 []byte + +//go:embed emojis/2403.svg +var Emoji_2403 []byte + +//go:embed emojis/2404.svg +var Emoji_2404 []byte + +//go:embed emojis/2405.svg +var Emoji_2405 []byte + +//go:embed emojis/2406.svg +var Emoji_2406 []byte + +//go:embed emojis/2407.svg +var Emoji_2407 []byte + +//go:embed emojis/2408.svg +var Emoji_2408 []byte + +//go:embed emojis/2409.svg +var Emoji_2409 []byte + +//go:embed emojis/2410.svg +var Emoji_2410 []byte + +//go:embed emojis/2411.svg +var Emoji_2411 []byte + +//go:embed emojis/2412.svg +var Emoji_2412 []byte + +//go:embed emojis/2413.svg +var Emoji_2413 []byte + +//go:embed emojis/2414.svg +var Emoji_2414 []byte + +//go:embed emojis/2415.svg +var Emoji_2415 []byte + +//go:embed emojis/2416.svg +var Emoji_2416 []byte + +//go:embed emojis/2417.svg +var Emoji_2417 []byte + +//go:embed emojis/2418.svg +var Emoji_2418 []byte + +//go:embed emojis/2419.svg +var Emoji_2419 []byte + +//go:embed emojis/2420.svg +var Emoji_2420 []byte + +//go:embed emojis/2421.svg +var Emoji_2421 []byte + +//go:embed emojis/2422.svg +var Emoji_2422 []byte + +//go:embed emojis/2423.svg +var Emoji_2423 []byte + +//go:embed emojis/2424.svg +var Emoji_2424 []byte + +//go:embed emojis/2425.svg +var Emoji_2425 []byte + +//go:embed emojis/2426.svg +var Emoji_2426 []byte + +//go:embed emojis/2427.svg +var Emoji_2427 []byte + +//go:embed emojis/2428.svg +var Emoji_2428 []byte + +//go:embed emojis/2429.svg +var Emoji_2429 []byte + +//go:embed emojis/2430.svg +var Emoji_2430 []byte + +//go:embed emojis/2431.svg +var Emoji_2431 []byte + +//go:embed emojis/2432.svg +var Emoji_2432 []byte + +//go:embed emojis/2433.svg +var Emoji_2433 []byte + +//go:embed emojis/2434.svg +var Emoji_2434 []byte + +//go:embed emojis/2435.svg +var Emoji_2435 []byte + +//go:embed emojis/2436.svg +var Emoji_2436 []byte + +//go:embed emojis/2437.svg +var Emoji_2437 []byte + +//go:embed emojis/2438.svg +var Emoji_2438 []byte + +//go:embed emojis/2439.svg +var Emoji_2439 []byte + +//go:embed emojis/2440.svg +var Emoji_2440 []byte + +//go:embed emojis/2441.svg +var Emoji_2441 []byte + +//go:embed emojis/2442.svg +var Emoji_2442 []byte + +//go:embed emojis/2443.svg +var Emoji_2443 []byte + +//go:embed emojis/2444.svg +var Emoji_2444 []byte + +//go:embed emojis/2445.svg +var Emoji_2445 []byte + +//go:embed emojis/2446.svg +var Emoji_2446 []byte + +//go:embed emojis/2447.svg +var Emoji_2447 []byte + +//go:embed emojis/2448.svg +var Emoji_2448 []byte + +//go:embed emojis/2449.svg +var Emoji_2449 []byte + +//go:embed emojis/2450.svg +var Emoji_2450 []byte + +//go:embed emojis/2451.svg +var Emoji_2451 []byte + +//go:embed emojis/2452.svg +var Emoji_2452 []byte + +//go:embed emojis/2453.svg +var Emoji_2453 []byte + +//go:embed emojis/2454.svg +var Emoji_2454 []byte + +//go:embed emojis/2455.svg +var Emoji_2455 []byte + +//go:embed emojis/2456.svg +var Emoji_2456 []byte + +//go:embed emojis/2457.svg +var Emoji_2457 []byte + +//go:embed emojis/2458.svg +var Emoji_2458 []byte + +//go:embed emojis/2459.svg +var Emoji_2459 []byte + +//go:embed emojis/2460.svg +var Emoji_2460 []byte + +//go:embed emojis/2461.svg +var Emoji_2461 []byte + +//go:embed emojis/2462.svg +var Emoji_2462 []byte + +//go:embed emojis/2463.svg +var Emoji_2463 []byte + +//go:embed emojis/2464.svg +var Emoji_2464 []byte + +//go:embed emojis/2465.svg +var Emoji_2465 []byte + +//go:embed emojis/2466.svg +var Emoji_2466 []byte + +//go:embed emojis/2467.svg +var Emoji_2467 []byte + +//go:embed emojis/2468.svg +var Emoji_2468 []byte + +//go:embed emojis/2469.svg +var Emoji_2469 []byte + +//go:embed emojis/2470.svg +var Emoji_2470 []byte + +//go:embed emojis/2471.svg +var Emoji_2471 []byte + +//go:embed emojis/2472.svg +var Emoji_2472 []byte + +//go:embed emojis/2473.svg +var Emoji_2473 []byte + +//go:embed emojis/2474.svg +var Emoji_2474 []byte + +//go:embed emojis/2475.svg +var Emoji_2475 []byte + +//go:embed emojis/2476.svg +var Emoji_2476 []byte + +//go:embed emojis/2477.svg +var Emoji_2477 []byte + +//go:embed emojis/2478.svg +var Emoji_2478 []byte + +//go:embed emojis/2479.svg +var Emoji_2479 []byte + +//go:embed emojis/2480.svg +var Emoji_2480 []byte + +//go:embed emojis/2481.svg +var Emoji_2481 []byte + +//go:embed emojis/2482.svg +var Emoji_2482 []byte + +//go:embed emojis/2483.svg +var Emoji_2483 []byte + +//go:embed emojis/2484.svg +var Emoji_2484 []byte + +//go:embed emojis/2485.svg +var Emoji_2485 []byte + +//go:embed emojis/2486.svg +var Emoji_2486 []byte + +//go:embed emojis/2487.svg +var Emoji_2487 []byte + +//go:embed emojis/2488.svg +var Emoji_2488 []byte + +//go:embed emojis/2489.svg +var Emoji_2489 []byte + +//go:embed emojis/2490.svg +var Emoji_2490 []byte + +//go:embed emojis/2491.svg +var Emoji_2491 []byte + +//go:embed emojis/2492.svg +var Emoji_2492 []byte + +//go:embed emojis/2493.svg +var Emoji_2493 []byte + +//go:embed emojis/2494.svg +var Emoji_2494 []byte + +//go:embed emojis/2495.svg +var Emoji_2495 []byte + +//go:embed emojis/2496.svg +var Emoji_2496 []byte + +//go:embed emojis/2497.svg +var Emoji_2497 []byte + +//go:embed emojis/2498.svg +var Emoji_2498 []byte + +//go:embed emojis/2499.svg +var Emoji_2499 []byte + +//go:embed emojis/2500.svg +var Emoji_2500 []byte + +//go:embed emojis/2501.svg +var Emoji_2501 []byte + +//go:embed emojis/2502.svg +var Emoji_2502 []byte + +//go:embed emojis/2503.svg +var Emoji_2503 []byte + +//go:embed emojis/2504.svg +var Emoji_2504 []byte + +//go:embed emojis/2505.svg +var Emoji_2505 []byte + +//go:embed emojis/2506.svg +var Emoji_2506 []byte + +//go:embed emojis/2507.svg +var Emoji_2507 []byte + +//go:embed emojis/2508.svg +var Emoji_2508 []byte + +//go:embed emojis/2509.svg +var Emoji_2509 []byte + +//go:embed emojis/2510.svg +var Emoji_2510 []byte + +//go:embed emojis/2511.svg +var Emoji_2511 []byte + +//go:embed emojis/2512.svg +var Emoji_2512 []byte + +//go:embed emojis/2513.svg +var Emoji_2513 []byte + +//go:embed emojis/2514.svg +var Emoji_2514 []byte + +//go:embed emojis/2515.svg +var Emoji_2515 []byte + +//go:embed emojis/2516.svg +var Emoji_2516 []byte + +//go:embed emojis/2517.svg +var Emoji_2517 []byte + +//go:embed emojis/2518.svg +var Emoji_2518 []byte + +//go:embed emojis/2519.svg +var Emoji_2519 []byte + +//go:embed emojis/2520.svg +var Emoji_2520 []byte + +//go:embed emojis/2521.svg +var Emoji_2521 []byte + +//go:embed emojis/2522.svg +var Emoji_2522 []byte + +//go:embed emojis/2523.svg +var Emoji_2523 []byte + +//go:embed emojis/2524.svg +var Emoji_2524 []byte + +//go:embed emojis/2525.svg +var Emoji_2525 []byte + +//go:embed emojis/2526.svg +var Emoji_2526 []byte + +//go:embed emojis/2527.svg +var Emoji_2527 []byte + +//go:embed emojis/2528.svg +var Emoji_2528 []byte + +//go:embed emojis/2529.svg +var Emoji_2529 []byte + +//go:embed emojis/2530.svg +var Emoji_2530 []byte + +//go:embed emojis/2531.svg +var Emoji_2531 []byte + +//go:embed emojis/2532.svg +var Emoji_2532 []byte + +//go:embed emojis/2533.svg +var Emoji_2533 []byte + +//go:embed emojis/2534.svg +var Emoji_2534 []byte + +//go:embed emojis/2535.svg +var Emoji_2535 []byte + +//go:embed emojis/2536.svg +var Emoji_2536 []byte + +//go:embed emojis/2537.svg +var Emoji_2537 []byte + +//go:embed emojis/2538.svg +var Emoji_2538 []byte + +//go:embed emojis/2539.svg +var Emoji_2539 []byte + +//go:embed emojis/2540.svg +var Emoji_2540 []byte + +//go:embed emojis/2541.svg +var Emoji_2541 []byte + +//go:embed emojis/2542.svg +var Emoji_2542 []byte + +//go:embed emojis/2543.svg +var Emoji_2543 []byte + +//go:embed emojis/2544.svg +var Emoji_2544 []byte + +//go:embed emojis/2545.svg +var Emoji_2545 []byte + +//go:embed emojis/2546.svg +var Emoji_2546 []byte + +//go:embed emojis/2547.svg +var Emoji_2547 []byte + +//go:embed emojis/2548.svg +var Emoji_2548 []byte + +//go:embed emojis/2549.svg +var Emoji_2549 []byte + +//go:embed emojis/2550.svg +var Emoji_2550 []byte + +//go:embed emojis/2551.svg +var Emoji_2551 []byte + +//go:embed emojis/2552.svg +var Emoji_2552 []byte + +//go:embed emojis/2553.svg +var Emoji_2553 []byte + +//go:embed emojis/2554.svg +var Emoji_2554 []byte + +//go:embed emojis/2555.svg +var Emoji_2555 []byte + +//go:embed emojis/2556.svg +var Emoji_2556 []byte + +//go:embed emojis/2557.svg +var Emoji_2557 []byte + +//go:embed emojis/2558.svg +var Emoji_2558 []byte + +//go:embed emojis/2559.svg +var Emoji_2559 []byte + +//go:embed emojis/2560.svg +var Emoji_2560 []byte + +//go:embed emojis/2561.svg +var Emoji_2561 []byte + +//go:embed emojis/2562.svg +var Emoji_2562 []byte + +//go:embed emojis/2563.svg +var Emoji_2563 []byte + +//go:embed emojis/2564.svg +var Emoji_2564 []byte + +//go:embed emojis/2565.svg +var Emoji_2565 []byte + +//go:embed emojis/2566.svg +var Emoji_2566 []byte + +//go:embed emojis/2567.svg +var Emoji_2567 []byte + +//go:embed emojis/2568.svg +var Emoji_2568 []byte + +//go:embed emojis/2569.svg +var Emoji_2569 []byte + +//go:embed emojis/2570.svg +var Emoji_2570 []byte + +//go:embed emojis/2571.svg +var Emoji_2571 []byte + +//go:embed emojis/2572.svg +var Emoji_2572 []byte + +//go:embed emojis/2573.svg +var Emoji_2573 []byte + +//go:embed emojis/2574.svg +var Emoji_2574 []byte + +//go:embed emojis/2575.svg +var Emoji_2575 []byte + +//go:embed emojis/2576.svg +var Emoji_2576 []byte + +//go:embed emojis/2577.svg +var Emoji_2577 []byte + +//go:embed emojis/2578.svg +var Emoji_2578 []byte + +//go:embed emojis/2579.svg +var Emoji_2579 []byte + +//go:embed emojis/2580.svg +var Emoji_2580 []byte + +//go:embed emojis/2581.svg +var Emoji_2581 []byte + +//go:embed emojis/2582.svg +var Emoji_2582 []byte + +//go:embed emojis/2583.svg +var Emoji_2583 []byte + +//go:embed emojis/2584.svg +var Emoji_2584 []byte + +//go:embed emojis/2585.svg +var Emoji_2585 []byte + +//go:embed emojis/2586.svg +var Emoji_2586 []byte + +//go:embed emojis/2587.svg +var Emoji_2587 []byte + +//go:embed emojis/2588.svg +var Emoji_2588 []byte + +//go:embed emojis/2589.svg +var Emoji_2589 []byte + +//go:embed emojis/2590.svg +var Emoji_2590 []byte + +//go:embed emojis/2591.svg +var Emoji_2591 []byte + +//go:embed emojis/2592.svg +var Emoji_2592 []byte + +//go:embed emojis/2593.svg +var Emoji_2593 []byte + +//go:embed emojis/2594.svg +var Emoji_2594 []byte + +//go:embed emojis/2595.svg +var Emoji_2595 []byte + +//go:embed emojis/2596.svg +var Emoji_2596 []byte + +//go:embed emojis/2597.svg +var Emoji_2597 []byte + +//go:embed emojis/2598.svg +var Emoji_2598 []byte + +//go:embed emojis/2599.svg +var Emoji_2599 []byte + +//go:embed emojis/2600.svg +var Emoji_2600 []byte + +//go:embed emojis/2601.svg +var Emoji_2601 []byte + +//go:embed emojis/2602.svg +var Emoji_2602 []byte + +//go:embed emojis/2603.svg +var Emoji_2603 []byte + +//go:embed emojis/2604.svg +var Emoji_2604 []byte + +//go:embed emojis/2605.svg +var Emoji_2605 []byte + +//go:embed emojis/2606.svg +var Emoji_2606 []byte + +//go:embed emojis/2607.svg +var Emoji_2607 []byte + +//go:embed emojis/2608.svg +var Emoji_2608 []byte + +//go:embed emojis/2609.svg +var Emoji_2609 []byte + +//go:embed emojis/2610.svg +var Emoji_2610 []byte + +//go:embed emojis/2611.svg +var Emoji_2611 []byte + +//go:embed emojis/2612.svg +var Emoji_2612 []byte + +//go:embed emojis/2613.svg +var Emoji_2613 []byte + +//go:embed emojis/2614.svg +var Emoji_2614 []byte + +//go:embed emojis/2615.svg +var Emoji_2615 []byte + +//go:embed emojis/2616.svg +var Emoji_2616 []byte + +//go:embed emojis/2617.svg +var Emoji_2617 []byte + +//go:embed emojis/2618.svg +var Emoji_2618 []byte + +//go:embed emojis/2619.svg +var Emoji_2619 []byte + +//go:embed emojis/2620.svg +var Emoji_2620 []byte + +//go:embed emojis/2621.svg +var Emoji_2621 []byte + +//go:embed emojis/2622.svg +var Emoji_2622 []byte + +//go:embed emojis/2623.svg +var Emoji_2623 []byte + +//go:embed emojis/2624.svg +var Emoji_2624 []byte + +//go:embed emojis/2625.svg +var Emoji_2625 []byte + +//go:embed emojis/2626.svg +var Emoji_2626 []byte + +//go:embed emojis/2627.svg +var Emoji_2627 []byte + +//go:embed emojis/2628.svg +var Emoji_2628 []byte + +//go:embed emojis/2629.svg +var Emoji_2629 []byte + +//go:embed emojis/2630.svg +var Emoji_2630 []byte + +//go:embed emojis/2631.svg +var Emoji_2631 []byte + +//go:embed emojis/2632.svg +var Emoji_2632 []byte + +//go:embed emojis/2633.svg +var Emoji_2633 []byte + +//go:embed emojis/2634.svg +var Emoji_2634 []byte + +//go:embed emojis/2635.svg +var Emoji_2635 []byte + +//go:embed emojis/2636.svg +var Emoji_2636 []byte + +//go:embed emojis/2637.svg +var Emoji_2637 []byte + +//go:embed emojis/2638.svg +var Emoji_2638 []byte + +//go:embed emojis/2639.svg +var Emoji_2639 []byte + +//go:embed emojis/2640.svg +var Emoji_2640 []byte + +//go:embed emojis/2641.svg +var Emoji_2641 []byte + +//go:embed emojis/2642.svg +var Emoji_2642 []byte + +//go:embed emojis/2643.svg +var Emoji_2643 []byte + +//go:embed emojis/2644.svg +var Emoji_2644 []byte + +//go:embed emojis/2645.svg +var Emoji_2645 []byte + +//go:embed emojis/2646.svg +var Emoji_2646 []byte + +//go:embed emojis/2647.svg +var Emoji_2647 []byte + +//go:embed emojis/2648.svg +var Emoji_2648 []byte + +//go:embed emojis/2649.svg +var Emoji_2649 []byte + +//go:embed emojis/2650.svg +var Emoji_2650 []byte + +//go:embed emojis/2651.svg +var Emoji_2651 []byte + +//go:embed emojis/2652.svg +var Emoji_2652 []byte + +//go:embed emojis/2653.svg +var Emoji_2653 []byte + +//go:embed emojis/2654.svg +var Emoji_2654 []byte + +//go:embed emojis/2655.svg +var Emoji_2655 []byte + +//go:embed emojis/2656.svg +var Emoji_2656 []byte + +//go:embed emojis/2657.svg +var Emoji_2657 []byte + +//go:embed emojis/2658.svg +var Emoji_2658 []byte + +//go:embed emojis/2659.svg +var Emoji_2659 []byte + +//go:embed emojis/2660.svg +var Emoji_2660 []byte + +//go:embed emojis/2661.svg +var Emoji_2661 []byte + +//go:embed emojis/2662.svg +var Emoji_2662 []byte + +//go:embed emojis/2663.svg +var Emoji_2663 []byte + +//go:embed emojis/2664.svg +var Emoji_2664 []byte + +//go:embed emojis/2665.svg +var Emoji_2665 []byte + +//go:embed emojis/2666.svg +var Emoji_2666 []byte + +//go:embed emojis/2667.svg +var Emoji_2667 []byte + +//go:embed emojis/2668.svg +var Emoji_2668 []byte + +//go:embed emojis/2669.svg +var Emoji_2669 []byte + +//go:embed emojis/2670.svg +var Emoji_2670 []byte + +//go:embed emojis/2671.svg +var Emoji_2671 []byte + +//go:embed emojis/2672.svg +var Emoji_2672 []byte + +//go:embed emojis/2673.svg +var Emoji_2673 []byte + +//go:embed emojis/2674.svg +var Emoji_2674 []byte + +//go:embed emojis/2675.svg +var Emoji_2675 []byte + +//go:embed emojis/2676.svg +var Emoji_2676 []byte + +//go:embed emojis/2677.svg +var Emoji_2677 []byte + +//go:embed emojis/2678.svg +var Emoji_2678 []byte + +//go:embed emojis/2679.svg +var Emoji_2679 []byte + +//go:embed emojis/2680.svg +var Emoji_2680 []byte + +//go:embed emojis/2681.svg +var Emoji_2681 []byte + +//go:embed emojis/2682.svg +var Emoji_2682 []byte + +//go:embed emojis/2683.svg +var Emoji_2683 []byte + +//go:embed emojis/2684.svg +var Emoji_2684 []byte + +//go:embed emojis/2685.svg +var Emoji_2685 []byte + +//go:embed emojis/2686.svg +var Emoji_2686 []byte + +//go:embed emojis/2687.svg +var Emoji_2687 []byte + +//go:embed emojis/2688.svg +var Emoji_2688 []byte + +//go:embed emojis/2689.svg +var Emoji_2689 []byte + +//go:embed emojis/2690.svg +var Emoji_2690 []byte + +//go:embed emojis/2691.svg +var Emoji_2691 []byte + +//go:embed emojis/2692.svg +var Emoji_2692 []byte + +//go:embed emojis/2693.svg +var Emoji_2693 []byte + +//go:embed emojis/2694.svg +var Emoji_2694 []byte + +//go:embed emojis/2695.svg +var Emoji_2695 []byte + +//go:embed emojis/2696.svg +var Emoji_2696 []byte + +//go:embed emojis/2697.svg +var Emoji_2697 []byte + +//go:embed emojis/2698.svg +var Emoji_2698 []byte + +//go:embed emojis/2699.svg +var Emoji_2699 []byte + +//go:embed emojis/2700.svg +var Emoji_2700 []byte + +//go:embed emojis/2701.svg +var Emoji_2701 []byte + +//go:embed emojis/2702.svg +var Emoji_2702 []byte + +//go:embed emojis/2703.svg +var Emoji_2703 []byte + +//go:embed emojis/2704.svg +var Emoji_2704 []byte + +//go:embed emojis/2705.svg +var Emoji_2705 []byte + +//go:embed emojis/2706.svg +var Emoji_2706 []byte + +//go:embed emojis/2707.svg +var Emoji_2707 []byte + +//go:embed emojis/2708.svg +var Emoji_2708 []byte + +//go:embed emojis/2709.svg +var Emoji_2709 []byte + +//go:embed emojis/2710.svg +var Emoji_2710 []byte + +//go:embed emojis/2711.svg +var Emoji_2711 []byte + +//go:embed emojis/2712.svg +var Emoji_2712 []byte + +//go:embed emojis/2713.svg +var Emoji_2713 []byte + +//go:embed emojis/2714.svg +var Emoji_2714 []byte + +//go:embed emojis/2715.svg +var Emoji_2715 []byte + +//go:embed emojis/2716.svg +var Emoji_2716 []byte + +//go:embed emojis/2717.svg +var Emoji_2717 []byte + +//go:embed emojis/2718.svg +var Emoji_2718 []byte + +//go:embed emojis/2719.svg +var Emoji_2719 []byte + +//go:embed emojis/2720.svg +var Emoji_2720 []byte + +//go:embed emojis/2721.svg +var Emoji_2721 []byte + +//go:embed emojis/2722.svg +var Emoji_2722 []byte + +//go:embed emojis/2723.svg +var Emoji_2723 []byte + +//go:embed emojis/2724.svg +var Emoji_2724 []byte + +//go:embed emojis/2725.svg +var Emoji_2725 []byte + +//go:embed emojis/2726.svg +var Emoji_2726 []byte + +//go:embed emojis/2727.svg +var Emoji_2727 []byte + +//go:embed emojis/2728.svg +var Emoji_2728 []byte + +//go:embed emojis/2729.svg +var Emoji_2729 []byte + +//go:embed emojis/2730.svg +var Emoji_2730 []byte + +//go:embed emojis/2731.svg +var Emoji_2731 []byte + +//go:embed emojis/2732.svg +var Emoji_2732 []byte + +//go:embed emojis/2733.svg +var Emoji_2733 []byte + +//go:embed emojis/2734.svg +var Emoji_2734 []byte + +//go:embed emojis/2735.svg +var Emoji_2735 []byte + +//go:embed emojis/2736.svg +var Emoji_2736 []byte + +//go:embed emojis/2737.svg +var Emoji_2737 []byte + +//go:embed emojis/2738.svg +var Emoji_2738 []byte + +//go:embed emojis/2739.svg +var Emoji_2739 []byte + +//go:embed emojis/2740.svg +var Emoji_2740 []byte + +//go:embed emojis/2741.svg +var Emoji_2741 []byte + +//go:embed emojis/2742.svg +var Emoji_2742 []byte + +//go:embed emojis/2743.svg +var Emoji_2743 []byte + +//go:embed emojis/2744.svg +var Emoji_2744 []byte + +//go:embed emojis/2745.svg +var Emoji_2745 []byte + +//go:embed emojis/2746.svg +var Emoji_2746 []byte + +//go:embed emojis/2747.svg +var Emoji_2747 []byte + +//go:embed emojis/2748.svg +var Emoji_2748 []byte + +//go:embed emojis/2749.svg +var Emoji_2749 []byte + +//go:embed emojis/2750.svg +var Emoji_2750 []byte + +//go:embed emojis/2751.svg +var Emoji_2751 []byte + +//go:embed emojis/2752.svg +var Emoji_2752 []byte + +//go:embed emojis/2753.svg +var Emoji_2753 []byte + +//go:embed emojis/2754.svg +var Emoji_2754 []byte + +//go:embed emojis/2755.svg +var Emoji_2755 []byte + +//go:embed emojis/2756.svg +var Emoji_2756 []byte + +//go:embed emojis/2757.svg +var Emoji_2757 []byte + +//go:embed emojis/2758.svg +var Emoji_2758 []byte + +//go:embed emojis/2759.svg +var Emoji_2759 []byte + +//go:embed emojis/2760.svg +var Emoji_2760 []byte + +//go:embed emojis/2761.svg +var Emoji_2761 []byte + +//go:embed emojis/2762.svg +var Emoji_2762 []byte + +//go:embed emojis/2763.svg +var Emoji_2763 []byte + +//go:embed emojis/2764.svg +var Emoji_2764 []byte + +//go:embed emojis/2765.svg +var Emoji_2765 []byte + +//go:embed emojis/2766.svg +var Emoji_2766 []byte + +//go:embed emojis/2767.svg +var Emoji_2767 []byte + +//go:embed emojis/2768.svg +var Emoji_2768 []byte + +//go:embed emojis/2769.svg +var Emoji_2769 []byte + +//go:embed emojis/2770.svg +var Emoji_2770 []byte + +//go:embed emojis/2771.svg +var Emoji_2771 []byte + +//go:embed emojis/2772.svg +var Emoji_2772 []byte + +//go:embed emojis/2773.svg +var Emoji_2773 []byte + +//go:embed emojis/2774.svg +var Emoji_2774 []byte + +//go:embed emojis/2775.svg +var Emoji_2775 []byte + +//go:embed emojis/2776.svg +var Emoji_2776 []byte + +//go:embed emojis/2777.svg +var Emoji_2777 []byte + +//go:embed emojis/2778.svg +var Emoji_2778 []byte + +//go:embed emojis/2779.svg +var Emoji_2779 []byte + +//go:embed emojis/2780.svg +var Emoji_2780 []byte + +//go:embed emojis/2781.svg +var Emoji_2781 []byte + +//go:embed emojis/2782.svg +var Emoji_2782 []byte + +//go:embed emojis/2783.svg +var Emoji_2783 []byte + +//go:embed emojis/2784.svg +var Emoji_2784 []byte + +//go:embed emojis/2785.svg +var Emoji_2785 []byte + +//go:embed emojis/2786.svg +var Emoji_2786 []byte + +//go:embed emojis/2787.svg +var Emoji_2787 []byte + +//go:embed emojis/2788.svg +var Emoji_2788 []byte + +//go:embed emojis/2789.svg +var Emoji_2789 []byte + +//go:embed emojis/2790.svg +var Emoji_2790 []byte + +//go:embed emojis/2791.svg +var Emoji_2791 []byte + +//go:embed emojis/2792.svg +var Emoji_2792 []byte + +//go:embed emojis/2793.svg +var Emoji_2793 []byte + +//go:embed emojis/2794.svg +var Emoji_2794 []byte + +//go:embed emojis/2795.svg +var Emoji_2795 []byte + +//go:embed emojis/2796.svg +var Emoji_2796 []byte + +//go:embed emojis/2797.svg +var Emoji_2797 []byte + +//go:embed emojis/2798.svg +var Emoji_2798 []byte + +//go:embed emojis/2799.svg +var Emoji_2799 []byte + +//go:embed emojis/2800.svg +var Emoji_2800 []byte + +//go:embed emojis/2801.svg +var Emoji_2801 []byte + +//go:embed emojis/2802.svg +var Emoji_2802 []byte + +//go:embed emojis/2803.svg +var Emoji_2803 []byte + +//go:embed emojis/2804.svg +var Emoji_2804 []byte + +//go:embed emojis/2805.svg +var Emoji_2805 []byte + +//go:embed emojis/2806.svg +var Emoji_2806 []byte + +//go:embed emojis/2807.svg +var Emoji_2807 []byte + +//go:embed emojis/2808.svg +var Emoji_2808 []byte + +//go:embed emojis/2809.svg +var Emoji_2809 []byte + +//go:embed emojis/2810.svg +var Emoji_2810 []byte + +//go:embed emojis/2811.svg +var Emoji_2811 []byte + +//go:embed emojis/2812.svg +var Emoji_2812 []byte + +//go:embed emojis/2813.svg +var Emoji_2813 []byte + +//go:embed emojis/2814.svg +var Emoji_2814 []byte + +//go:embed emojis/2815.svg +var Emoji_2815 []byte + +//go:embed emojis/2816.svg +var Emoji_2816 []byte + +//go:embed emojis/2817.svg +var Emoji_2817 []byte + +//go:embed emojis/2818.svg +var Emoji_2818 []byte + +//go:embed emojis/2819.svg +var Emoji_2819 []byte + +//go:embed emojis/2820.svg +var Emoji_2820 []byte + +//go:embed emojis/2821.svg +var Emoji_2821 []byte + +//go:embed emojis/2822.svg +var Emoji_2822 []byte + +//go:embed emojis/2823.svg +var Emoji_2823 []byte + +//go:embed emojis/2824.svg +var Emoji_2824 []byte + +//go:embed emojis/2825.svg +var Emoji_2825 []byte + +//go:embed emojis/2826.svg +var Emoji_2826 []byte + +//go:embed emojis/2827.svg +var Emoji_2827 []byte + +//go:embed emojis/2828.svg +var Emoji_2828 []byte + +//go:embed emojis/2829.svg +var Emoji_2829 []byte + +//go:embed emojis/2830.svg +var Emoji_2830 []byte + +//go:embed emojis/2831.svg +var Emoji_2831 []byte + +//go:embed emojis/2832.svg +var Emoji_2832 []byte + +//go:embed emojis/2833.svg +var Emoji_2833 []byte + +//go:embed emojis/2834.svg +var Emoji_2834 []byte + +//go:embed emojis/2835.svg +var Emoji_2835 []byte + +//go:embed emojis/2836.svg +var Emoji_2836 []byte + +//go:embed emojis/2837.svg +var Emoji_2837 []byte + +//go:embed emojis/2838.svg +var Emoji_2838 []byte + +//go:embed emojis/2839.svg +var Emoji_2839 []byte + +//go:embed emojis/2840.svg +var Emoji_2840 []byte + +//go:embed emojis/2841.svg +var Emoji_2841 []byte + +//go:embed emojis/2842.svg +var Emoji_2842 []byte + +//go:embed emojis/2843.svg +var Emoji_2843 []byte + +//go:embed emojis/2844.svg +var Emoji_2844 []byte + +//go:embed emojis/2845.svg +var Emoji_2845 []byte + +//go:embed emojis/2846.svg +var Emoji_2846 []byte + +//go:embed emojis/2847.svg +var Emoji_2847 []byte + +//go:embed emojis/2848.svg +var Emoji_2848 []byte + +//go:embed emojis/2849.svg +var Emoji_2849 []byte + +//go:embed emojis/2850.svg +var Emoji_2850 []byte + +//go:embed emojis/2851.svg +var Emoji_2851 []byte + +//go:embed emojis/2852.svg +var Emoji_2852 []byte + +//go:embed emojis/2853.svg +var Emoji_2853 []byte + +//go:embed emojis/2854.svg +var Emoji_2854 []byte + +//go:embed emojis/2855.svg +var Emoji_2855 []byte + +//go:embed emojis/2856.svg +var Emoji_2856 []byte + +//go:embed emojis/2857.svg +var Emoji_2857 []byte + +//go:embed emojis/2858.svg +var Emoji_2858 []byte + +//go:embed emojis/2859.svg +var Emoji_2859 []byte + +//go:embed emojis/2860.svg +var Emoji_2860 []byte + +//go:embed emojis/2861.svg +var Emoji_2861 []byte + +//go:embed emojis/2862.svg +var Emoji_2862 []byte + +//go:embed emojis/2863.svg +var Emoji_2863 []byte + +//go:embed emojis/2864.svg +var Emoji_2864 []byte + +//go:embed emojis/2865.svg +var Emoji_2865 []byte + +//go:embed emojis/2866.svg +var Emoji_2866 []byte + +//go:embed emojis/2867.svg +var Emoji_2867 []byte + +//go:embed emojis/2868.svg +var Emoji_2868 []byte + +//go:embed emojis/2869.svg +var Emoji_2869 []byte + +//go:embed emojis/2870.svg +var Emoji_2870 []byte + +//go:embed emojis/2871.svg +var Emoji_2871 []byte + +//go:embed emojis/2872.svg +var Emoji_2872 []byte + +//go:embed emojis/2873.svg +var Emoji_2873 []byte + +//go:embed emojis/2874.svg +var Emoji_2874 []byte + +//go:embed emojis/2875.svg +var Emoji_2875 []byte + +//go:embed emojis/2876.svg +var Emoji_2876 []byte + +//go:embed emojis/2877.svg +var Emoji_2877 []byte + +//go:embed emojis/2878.svg +var Emoji_2878 []byte + +//go:embed emojis/2879.svg +var Emoji_2879 []byte + +//go:embed emojis/2880.svg +var Emoji_2880 []byte + +//go:embed emojis/2881.svg +var Emoji_2881 []byte + +//go:embed emojis/2882.svg +var Emoji_2882 []byte + +//go:embed emojis/2883.svg +var Emoji_2883 []byte + +//go:embed emojis/2884.svg +var Emoji_2884 []byte + +//go:embed emojis/2885.svg +var Emoji_2885 []byte + +//go:embed emojis/2886.svg +var Emoji_2886 []byte + +//go:embed emojis/2887.svg +var Emoji_2887 []byte + +//go:embed emojis/2888.svg +var Emoji_2888 []byte + +//go:embed emojis/2889.svg +var Emoji_2889 []byte + +//go:embed emojis/2890.svg +var Emoji_2890 []byte + +//go:embed emojis/2891.svg +var Emoji_2891 []byte + +//go:embed emojis/2892.svg +var Emoji_2892 []byte + +//go:embed emojis/2893.svg +var Emoji_2893 []byte + +//go:embed emojis/2894.svg +var Emoji_2894 []byte + +//go:embed emojis/2895.svg +var Emoji_2895 []byte + +//go:embed emojis/2896.svg +var Emoji_2896 []byte + +//go:embed emojis/2897.svg +var Emoji_2897 []byte + +//go:embed emojis/2898.svg +var Emoji_2898 []byte + +//go:embed emojis/2899.svg +var Emoji_2899 []byte + +//go:embed emojis/2900.svg +var Emoji_2900 []byte + +//go:embed emojis/2901.svg +var Emoji_2901 []byte + +//go:embed emojis/2902.svg +var Emoji_2902 []byte + +//go:embed emojis/2903.svg +var Emoji_2903 []byte + +//go:embed emojis/2904.svg +var Emoji_2904 []byte + +//go:embed emojis/2905.svg +var Emoji_2905 []byte + +//go:embed emojis/2906.svg +var Emoji_2906 []byte + +//go:embed emojis/2907.svg +var Emoji_2907 []byte + +//go:embed emojis/2908.svg +var Emoji_2908 []byte + +//go:embed emojis/2909.svg +var Emoji_2909 []byte + +//go:embed emojis/2910.svg +var Emoji_2910 []byte + +//go:embed emojis/2911.svg +var Emoji_2911 []byte + +//go:embed emojis/2912.svg +var Emoji_2912 []byte + +//go:embed emojis/2913.svg +var Emoji_2913 []byte + +//go:embed emojis/2914.svg +var Emoji_2914 []byte + +//go:embed emojis/2915.svg +var Emoji_2915 []byte + +//go:embed emojis/2916.svg +var Emoji_2916 []byte + +//go:embed emojis/2917.svg +var Emoji_2917 []byte + +//go:embed emojis/2918.svg +var Emoji_2918 []byte + +//go:embed emojis/2919.svg +var Emoji_2919 []byte + +//go:embed emojis/2920.svg +var Emoji_2920 []byte + +//go:embed emojis/2921.svg +var Emoji_2921 []byte + +//go:embed emojis/2922.svg +var Emoji_2922 []byte + +//go:embed emojis/2923.svg +var Emoji_2923 []byte + +//go:embed emojis/2924.svg +var Emoji_2924 []byte + +//go:embed emojis/2925.svg +var Emoji_2925 []byte + +//go:embed emojis/2926.svg +var Emoji_2926 []byte + +//go:embed emojis/2927.svg +var Emoji_2927 []byte + +//go:embed emojis/2928.svg +var Emoji_2928 []byte + +//go:embed emojis/2929.svg +var Emoji_2929 []byte + +//go:embed emojis/2930.svg +var Emoji_2930 []byte + +//go:embed emojis/2931.svg +var Emoji_2931 []byte + +//go:embed emojis/2932.svg +var Emoji_2932 []byte + +//go:embed emojis/2933.svg +var Emoji_2933 []byte + +//go:embed emojis/2934.svg +var Emoji_2934 []byte + +//go:embed emojis/2935.svg +var Emoji_2935 []byte + +//go:embed emojis/2936.svg +var Emoji_2936 []byte + +//go:embed emojis/2937.svg +var Emoji_2937 []byte + +//go:embed emojis/2938.svg +var Emoji_2938 []byte + +//go:embed emojis/2939.svg +var Emoji_2939 []byte + +//go:embed emojis/2940.svg +var Emoji_2940 []byte + +//go:embed emojis/2941.svg +var Emoji_2941 []byte + +//go:embed emojis/2942.svg +var Emoji_2942 []byte + +//go:embed emojis/2943.svg +var Emoji_2943 []byte + +//go:embed emojis/2944.svg +var Emoji_2944 []byte + +//go:embed emojis/2945.svg +var Emoji_2945 []byte + +//go:embed emojis/2946.svg +var Emoji_2946 []byte + +//go:embed emojis/2947.svg +var Emoji_2947 []byte + +//go:embed emojis/2948.svg +var Emoji_2948 []byte + +//go:embed emojis/2949.svg +var Emoji_2949 []byte + +//go:embed emojis/2950.svg +var Emoji_2950 []byte + +//go:embed emojis/2951.svg +var Emoji_2951 []byte + +//go:embed emojis/2952.svg +var Emoji_2952 []byte + +//go:embed emojis/2953.svg +var Emoji_2953 []byte + +//go:embed emojis/2954.svg +var Emoji_2954 []byte + +//go:embed emojis/2955.svg +var Emoji_2955 []byte + +//go:embed emojis/2956.svg +var Emoji_2956 []byte + +//go:embed emojis/2957.svg +var Emoji_2957 []byte + +//go:embed emojis/2958.svg +var Emoji_2958 []byte + +//go:embed emojis/2959.svg +var Emoji_2959 []byte + +//go:embed emojis/2960.svg +var Emoji_2960 []byte + +//go:embed emojis/2961.svg +var Emoji_2961 []byte + +//go:embed emojis/2962.svg +var Emoji_2962 []byte + +//go:embed emojis/2963.svg +var Emoji_2963 []byte + +//go:embed emojis/2964.svg +var Emoji_2964 []byte + +//go:embed emojis/2965.svg +var Emoji_2965 []byte + +//go:embed emojis/2966.svg +var Emoji_2966 []byte + +//go:embed emojis/2967.svg +var Emoji_2967 []byte + +//go:embed emojis/2968.svg +var Emoji_2968 []byte + +//go:embed emojis/2969.svg +var Emoji_2969 []byte + +//go:embed emojis/2970.svg +var Emoji_2970 []byte + +//go:embed emojis/2971.svg +var Emoji_2971 []byte + +//go:embed emojis/2972.svg +var Emoji_2972 []byte + +//go:embed emojis/2973.svg +var Emoji_2973 []byte + +//go:embed emojis/2974.svg +var Emoji_2974 []byte + +//go:embed emojis/2975.svg +var Emoji_2975 []byte + +//go:embed emojis/2976.svg +var Emoji_2976 []byte + +//go:embed emojis/2977.svg +var Emoji_2977 []byte + +//go:embed emojis/2978.svg +var Emoji_2978 []byte + +//go:embed emojis/2979.svg +var Emoji_2979 []byte + +//go:embed emojis/2980.svg +var Emoji_2980 []byte + +//go:embed emojis/2981.svg +var Emoji_2981 []byte + +//go:embed emojis/2982.svg +var Emoji_2982 []byte + +//go:embed emojis/2983.svg +var Emoji_2983 []byte + +//go:embed emojis/2984.svg +var Emoji_2984 []byte + +//go:embed emojis/2985.svg +var Emoji_2985 []byte + +//go:embed emojis/2986.svg +var Emoji_2986 []byte + +//go:embed emojis/2987.svg +var Emoji_2987 []byte + +//go:embed emojis/2988.svg +var Emoji_2988 []byte + +//go:embed emojis/2989.svg +var Emoji_2989 []byte + +//go:embed emojis/2990.svg +var Emoji_2990 []byte + +//go:embed emojis/2991.svg +var Emoji_2991 []byte + +//go:embed emojis/2992.svg +var Emoji_2992 []byte + +//go:embed emojis/2993.svg +var Emoji_2993 []byte + +//go:embed emojis/2994.svg +var Emoji_2994 []byte + +//go:embed emojis/2995.svg +var Emoji_2995 []byte + +//go:embed emojis/2996.svg +var Emoji_2996 []byte + +//go:embed emojis/2997.svg +var Emoji_2997 []byte + +//go:embed emojis/2998.svg +var Emoji_2998 []byte + +//go:embed emojis/2999.svg +var Emoji_2999 []byte + +//go:embed emojis/3000.svg +var Emoji_3000 []byte + +//go:embed emojis/3001.svg +var Emoji_3001 []byte + +//go:embed emojis/3002.svg +var Emoji_3002 []byte + +//go:embed emojis/3003.svg +var Emoji_3003 []byte + +//go:embed emojis/3004.svg +var Emoji_3004 []byte + +//go:embed emojis/3005.svg +var Emoji_3005 []byte + +//go:embed emojis/3006.svg +var Emoji_3006 []byte + +//go:embed emojis/3007.svg +var Emoji_3007 []byte + +//go:embed emojis/3008.svg +var Emoji_3008 []byte + +//go:embed emojis/3009.svg +var Emoji_3009 []byte + +//go:embed emojis/3010.svg +var Emoji_3010 []byte + +//go:embed emojis/3011.svg +var Emoji_3011 []byte + +//go:embed emojis/3012.svg +var Emoji_3012 []byte + +//go:embed emojis/3013.svg +var Emoji_3013 []byte + +//go:embed emojis/3014.svg +var Emoji_3014 []byte + +//go:embed emojis/3015.svg +var Emoji_3015 []byte + +//go:embed emojis/3016.svg +var Emoji_3016 []byte + +//go:embed emojis/3017.svg +var Emoji_3017 []byte + +//go:embed emojis/3018.svg +var Emoji_3018 []byte + +//go:embed emojis/3019.svg +var Emoji_3019 []byte + +//go:embed emojis/3020.svg +var Emoji_3020 []byte + +//go:embed emojis/3021.svg +var Emoji_3021 []byte + +//go:embed emojis/3022.svg +var Emoji_3022 []byte + +//go:embed emojis/3023.svg +var Emoji_3023 []byte + +//go:embed emojis/3024.svg +var Emoji_3024 []byte + +//go:embed emojis/3025.svg +var Emoji_3025 []byte + +//go:embed emojis/3026.svg +var Emoji_3026 []byte + +//go:embed emojis/3027.svg +var Emoji_3027 []byte + +//go:embed emojis/3028.svg +var Emoji_3028 []byte + +//go:embed emojis/3029.svg +var Emoji_3029 []byte + +//go:embed emojis/3030.svg +var Emoji_3030 []byte + +//go:embed emojis/3031.svg +var Emoji_3031 []byte + +//go:embed emojis/3032.svg +var Emoji_3032 []byte + +//go:embed emojis/3033.svg +var Emoji_3033 []byte + +//go:embed emojis/3034.svg +var Emoji_3034 []byte + +//go:embed emojis/3035.svg +var Emoji_3035 []byte + +//go:embed emojis/3036.svg +var Emoji_3036 []byte + +//go:embed emojis/3037.svg +var Emoji_3037 []byte + +//go:embed emojis/3038.svg +var Emoji_3038 []byte + +//go:embed emojis/3039.svg +var Emoji_3039 []byte + +//go:embed emojis/3040.svg +var Emoji_3040 []byte + +//go:embed emojis/3041.svg +var Emoji_3041 []byte + +//go:embed emojis/3042.svg +var Emoji_3042 []byte + +//go:embed emojis/3043.svg +var Emoji_3043 []byte + +//go:embed emojis/3044.svg +var Emoji_3044 []byte + +//go:embed emojis/3045.svg +var Emoji_3045 []byte + +//go:embed emojis/3046.svg +var Emoji_3046 []byte + +//go:embed emojis/3047.svg +var Emoji_3047 []byte + +//go:embed emojis/3048.svg +var Emoji_3048 []byte + +//go:embed emojis/3049.svg +var Emoji_3049 []byte + +//go:embed emojis/3050.svg +var Emoji_3050 []byte + +//go:embed emojis/3051.svg +var Emoji_3051 []byte + +//go:embed emojis/3052.svg +var Emoji_3052 []byte + +//go:embed emojis/3053.svg +var Emoji_3053 []byte + +//go:embed emojis/3054.svg +var Emoji_3054 []byte + +//go:embed emojis/3055.svg +var Emoji_3055 []byte + +//go:embed emojis/3056.svg +var Emoji_3056 []byte + +//go:embed emojis/3057.svg +var Emoji_3057 []byte + +//go:embed emojis/3058.svg +var Emoji_3058 []byte + +//go:embed emojis/3059.svg +var Emoji_3059 []byte + +//go:embed emojis/3060.svg +var Emoji_3060 []byte + +//go:embed emojis/3061.svg +var Emoji_3061 []byte + +//go:embed emojis/3062.svg +var Emoji_3062 []byte + +//go:embed emojis/3063.svg +var Emoji_3063 []byte + +//go:embed emojis/3064.svg +var Emoji_3064 []byte + +//go:embed emojis/3065.svg +var Emoji_3065 []byte + +//go:embed emojis/3066.svg +var Emoji_3066 []byte + +//go:embed emojis/3067.svg +var Emoji_3067 []byte + +//go:embed emojis/3068.svg +var Emoji_3068 []byte + +//go:embed emojis/3069.svg +var Emoji_3069 []byte + +//go:embed emojis/3070.svg +var Emoji_3070 []byte + +//go:embed emojis/3071.svg +var Emoji_3071 []byte + +//go:embed emojis/3072.svg +var Emoji_3072 []byte + +//go:embed emojis/3073.svg +var Emoji_3073 []byte + +//go:embed emojis/3074.svg +var Emoji_3074 []byte + +//go:embed emojis/3075.svg +var Emoji_3075 []byte + +//go:embed emojis/3076.svg +var Emoji_3076 []byte + +//go:embed emojis/3077.svg +var Emoji_3077 []byte + +//go:embed emojis/3078.svg +var Emoji_3078 []byte + +//go:embed emojis/3079.svg +var Emoji_3079 []byte + +//go:embed emojis/3080.svg +var Emoji_3080 []byte + +//go:embed emojis/3081.svg +var Emoji_3081 []byte + +//go:embed emojis/3082.svg +var Emoji_3082 []byte + +//go:embed emojis/3083.svg +var Emoji_3083 []byte + +//go:embed emojis/3084.svg +var Emoji_3084 []byte + +//go:embed emojis/3085.svg +var Emoji_3085 []byte + +//go:embed emojis/3086.svg +var Emoji_3086 []byte + +//go:embed emojis/3087.svg +var Emoji_3087 []byte + +//go:embed emojis/3088.svg +var Emoji_3088 []byte + +//go:embed emojis/3089.svg +var Emoji_3089 []byte + +//go:embed emojis/3090.svg +var Emoji_3090 []byte + +//go:embed emojis/3091.svg +var Emoji_3091 []byte + +//go:embed emojis/3092.svg +var Emoji_3092 []byte + +//go:embed emojis/3093.svg +var Emoji_3093 []byte + +//go:embed emojis/3094.svg +var Emoji_3094 []byte + +//go:embed emojis/3095.svg +var Emoji_3095 []byte + +//go:embed emojis/3096.svg +var Emoji_3096 []byte + +//go:embed emojis/3097.svg +var Emoji_3097 []byte + +//go:embed emojis/3098.svg +var Emoji_3098 []byte + +//go:embed emojis/3099.svg +var Emoji_3099 []byte + +//go:embed emojis/3100.svg +var Emoji_3100 []byte + +//go:embed emojis/3101.svg +var Emoji_3101 []byte + +//go:embed emojis/3102.svg +var Emoji_3102 []byte + +//go:embed emojis/3103.svg +var Emoji_3103 []byte + +//go:embed emojis/3104.svg +var Emoji_3104 []byte + +//go:embed emojis/3105.svg +var Emoji_3105 []byte + +//go:embed emojis/3106.svg +var Emoji_3106 []byte + +//go:embed emojis/3107.svg +var Emoji_3107 []byte + +//go:embed emojis/3108.svg +var Emoji_3108 []byte + +//go:embed emojis/3109.svg +var Emoji_3109 []byte + +//go:embed emojis/3110.svg +var Emoji_3110 []byte + +//go:embed emojis/3111.svg +var Emoji_3111 []byte + +//go:embed emojis/3112.svg +var Emoji_3112 []byte + +//go:embed emojis/3113.svg +var Emoji_3113 []byte + +//go:embed emojis/3114.svg +var Emoji_3114 []byte + +//go:embed emojis/3115.svg +var Emoji_3115 []byte + +//go:embed emojis/3116.svg +var Emoji_3116 []byte + +//go:embed emojis/3117.svg +var Emoji_3117 []byte + +//go:embed emojis/3118.svg +var Emoji_3118 []byte + +//go:embed emojis/3119.svg +var Emoji_3119 []byte + +//go:embed emojis/3120.svg +var Emoji_3120 []byte + +//go:embed emojis/3121.svg +var Emoji_3121 []byte + +//go:embed emojis/3122.svg +var Emoji_3122 []byte + +//go:embed emojis/3123.svg +var Emoji_3123 []byte + +//go:embed emojis/3124.svg +var Emoji_3124 []byte + +//go:embed emojis/3125.svg +var Emoji_3125 []byte + +//go:embed emojis/3126.svg +var Emoji_3126 []byte + +//go:embed emojis/3127.svg +var Emoji_3127 []byte + +//go:embed emojis/3128.svg +var Emoji_3128 []byte + +//go:embed emojis/3129.svg +var Emoji_3129 []byte + +//go:embed emojis/3130.svg +var Emoji_3130 []byte + +//go:embed emojis/3131.svg +var Emoji_3131 []byte + +//go:embed emojis/3132.svg +var Emoji_3132 []byte + +//go:embed emojis/3133.svg +var Emoji_3133 []byte + +//go:embed emojis/3134.svg +var Emoji_3134 []byte + +//go:embed emojis/3135.svg +var Emoji_3135 []byte + +//go:embed emojis/3136.svg +var Emoji_3136 []byte + +//go:embed emojis/3137.svg +var Emoji_3137 []byte + +//go:embed emojis/3138.svg +var Emoji_3138 []byte + +//go:embed emojis/3139.svg +var Emoji_3139 []byte + +//go:embed emojis/3140.svg +var Emoji_3140 []byte + +//go:embed emojis/3141.svg +var Emoji_3141 []byte + +//go:embed emojis/3142.svg +var Emoji_3142 []byte + +//go:embed emojis/3143.svg +var Emoji_3143 []byte + +//go:embed emojis/3144.svg +var Emoji_3144 []byte + +//go:embed emojis/3145.svg +var Emoji_3145 []byte + +//go:embed emojis/3146.svg +var Emoji_3146 []byte + +//go:embed emojis/3147.svg +var Emoji_3147 []byte + +//go:embed emojis/3148.svg +var Emoji_3148 []byte + +//go:embed emojis/3149.svg +var Emoji_3149 []byte + +//go:embed emojis/3150.svg +var Emoji_3150 []byte + +//go:embed emojis/3151.svg +var Emoji_3151 []byte + +//go:embed emojis/3152.svg +var Emoji_3152 []byte + +//go:embed emojis/3153.svg +var Emoji_3153 []byte + +//go:embed emojis/3154.svg +var Emoji_3154 []byte + +//go:embed emojis/3155.svg +var Emoji_3155 []byte + +//go:embed emojis/3156.svg +var Emoji_3156 []byte + +//go:embed emojis/3157.svg +var Emoji_3157 []byte + +//go:embed emojis/3158.svg +var Emoji_3158 []byte + +//go:embed emojis/3159.svg +var Emoji_3159 []byte + +//go:embed emojis/3160.svg +var Emoji_3160 []byte + +//go:embed emojis/3161.svg +var Emoji_3161 []byte + +//go:embed emojis/3162.svg +var Emoji_3162 []byte + +//go:embed emojis/3163.svg +var Emoji_3163 []byte + +//go:embed emojis/3164.svg +var Emoji_3164 []byte + +//go:embed emojis/3165.svg +var Emoji_3165 []byte + +//go:embed emojis/3166.svg +var Emoji_3166 []byte + +//go:embed emojis/3167.svg +var Emoji_3167 []byte + +//go:embed emojis/3168.svg +var Emoji_3168 []byte + +//go:embed emojis/3169.svg +var Emoji_3169 []byte + +//go:embed emojis/3170.svg +var Emoji_3170 []byte + +//go:embed emojis/3171.svg +var Emoji_3171 []byte + +//go:embed emojis/3172.svg +var Emoji_3172 []byte + +//go:embed emojis/3173.svg +var Emoji_3173 []byte + +//go:embed emojis/3174.svg +var Emoji_3174 []byte + +//go:embed emojis/3175.svg +var Emoji_3175 []byte + +//go:embed emojis/3176.svg +var Emoji_3176 []byte + +//go:embed emojis/3177.svg +var Emoji_3177 []byte + +//go:embed emojis/3178.svg +var Emoji_3178 []byte + +//go:embed emojis/3179.svg +var Emoji_3179 []byte + +//go:embed emojis/3180.svg +var Emoji_3180 []byte + +//go:embed emojis/3181.svg +var Emoji_3181 []byte + +//go:embed emojis/3182.svg +var Emoji_3182 []byte + +//go:embed emojis/3183.svg +var Emoji_3183 []byte + +//go:embed emojis/3184.svg +var Emoji_3184 []byte + +//go:embed emojis/3185.svg +var Emoji_3185 []byte + +//go:embed emojis/3186.svg +var Emoji_3186 []byte + +//go:embed emojis/3187.svg +var Emoji_3187 []byte + +//go:embed emojis/3188.svg +var Emoji_3188 []byte + +//go:embed emojis/3189.svg +var Emoji_3189 []byte + +//go:embed emojis/3190.svg +var Emoji_3190 []byte + +//go:embed emojis/3191.svg +var Emoji_3191 []byte + +//go:embed emojis/3192.svg +var Emoji_3192 []byte + +//go:embed emojis/3193.svg +var Emoji_3193 []byte + +//go:embed emojis/3194.svg +var Emoji_3194 []byte + +//go:embed emojis/3195.svg +var Emoji_3195 []byte + +//go:embed emojis/3196.svg +var Emoji_3196 []byte + +//go:embed emojis/3197.svg +var Emoji_3197 []byte + +//go:embed emojis/3198.svg +var Emoji_3198 []byte + +//go:embed emojis/3199.svg +var Emoji_3199 []byte + +//go:embed emojis/3200.svg +var Emoji_3200 []byte + +//go:embed emojis/3201.svg +var Emoji_3201 []byte + +//go:embed emojis/3202.svg +var Emoji_3202 []byte + +//go:embed emojis/3203.svg +var Emoji_3203 []byte + +//go:embed emojis/3204.svg +var Emoji_3204 []byte + +//go:embed emojis/3205.svg +var Emoji_3205 []byte + +//go:embed emojis/3206.svg +var Emoji_3206 []byte + +//go:embed emojis/3207.svg +var Emoji_3207 []byte + +//go:embed emojis/3208.svg +var Emoji_3208 []byte + +//go:embed emojis/3209.svg +var Emoji_3209 []byte + +//go:embed emojis/3210.svg +var Emoji_3210 []byte + +//go:embed emojis/3211.svg +var Emoji_3211 []byte + +//go:embed emojis/3212.svg +var Emoji_3212 []byte + +//go:embed emojis/3213.svg +var Emoji_3213 []byte + +//go:embed emojis/3214.svg +var Emoji_3214 []byte + +//go:embed emojis/3215.svg +var Emoji_3215 []byte + +//go:embed emojis/3216.svg +var Emoji_3216 []byte + +//go:embed emojis/3217.svg +var Emoji_3217 []byte + +//go:embed emojis/3218.svg +var Emoji_3218 []byte + +//go:embed emojis/3219.svg +var Emoji_3219 []byte + +//go:embed emojis/3220.svg +var Emoji_3220 []byte + +//go:embed emojis/3221.svg +var Emoji_3221 []byte + +//go:embed emojis/3222.svg +var Emoji_3222 []byte + +//go:embed emojis/3223.svg +var Emoji_3223 []byte + +//go:embed emojis/3224.svg +var Emoji_3224 []byte + +//go:embed emojis/3225.svg +var Emoji_3225 []byte + +//go:embed emojis/3226.svg +var Emoji_3226 []byte + +//go:embed emojis/3227.svg +var Emoji_3227 []byte + +//go:embed emojis/3228.svg +var Emoji_3228 []byte + +//go:embed emojis/3229.svg +var Emoji_3229 []byte + +//go:embed emojis/3230.svg +var Emoji_3230 []byte + +//go:embed emojis/3231.svg +var Emoji_3231 []byte + +//go:embed emojis/3232.svg +var Emoji_3232 []byte + +//go:embed emojis/3233.svg +var Emoji_3233 []byte + +//go:embed emojis/3234.svg +var Emoji_3234 []byte + +//go:embed emojis/3235.svg +var Emoji_3235 []byte + +//go:embed emojis/3236.svg +var Emoji_3236 []byte + +//go:embed emojis/3237.svg +var Emoji_3237 []byte + +//go:embed emojis/3238.svg +var Emoji_3238 []byte + +//go:embed emojis/3239.svg +var Emoji_3239 []byte + +//go:embed emojis/3240.svg +var Emoji_3240 []byte + +//go:embed emojis/3241.svg +var Emoji_3241 []byte + +//go:embed emojis/3242.svg +var Emoji_3242 []byte + +//go:embed emojis/3243.svg +var Emoji_3243 []byte + +//go:embed emojis/3244.svg +var Emoji_3244 []byte + +//go:embed emojis/3245.svg +var Emoji_3245 []byte + +//go:embed emojis/3246.svg +var Emoji_3246 []byte + +//go:embed emojis/3247.svg +var Emoji_3247 []byte + +//go:embed emojis/3248.svg +var Emoji_3248 []byte + +//go:embed emojis/3249.svg +var Emoji_3249 []byte + +//go:embed emojis/3250.svg +var Emoji_3250 []byte + +//go:embed emojis/3251.svg +var Emoji_3251 []byte + +//go:embed emojis/3252.svg +var Emoji_3252 []byte + +//go:embed emojis/3253.svg +var Emoji_3253 []byte + +//go:embed emojis/3254.svg +var Emoji_3254 []byte + +//go:embed emojis/3255.svg +var Emoji_3255 []byte + +//go:embed emojis/3256.svg +var Emoji_3256 []byte + +//go:embed emojis/3257.svg +var Emoji_3257 []byte + +//go:embed emojis/3258.svg +var Emoji_3258 []byte + +//go:embed emojis/3259.svg +var Emoji_3259 []byte + +//go:embed emojis/3260.svg +var Emoji_3260 []byte + +//go:embed emojis/3261.svg +var Emoji_3261 []byte + +//go:embed emojis/3262.svg +var Emoji_3262 []byte + +//go:embed emojis/3263.svg +var Emoji_3263 []byte + +//go:embed emojis/3264.svg +var Emoji_3264 []byte + +//go:embed emojis/3265.svg +var Emoji_3265 []byte + +//go:embed emojis/3266.svg +var Emoji_3266 []byte + +//go:embed emojis/3267.svg +var Emoji_3267 []byte + +//go:embed emojis/3268.svg +var Emoji_3268 []byte + +//go:embed emojis/3269.svg +var Emoji_3269 []byte + +//go:embed emojis/3270.svg +var Emoji_3270 []byte + +//go:embed emojis/3271.svg +var Emoji_3271 []byte + +//go:embed emojis/3272.svg +var Emoji_3272 []byte + +//go:embed emojis/3273.svg +var Emoji_3273 []byte + +//go:embed emojis/3274.svg +var Emoji_3274 []byte + +//go:embed emojis/3275.svg +var Emoji_3275 []byte + +//go:embed emojis/3276.svg +var Emoji_3276 []byte + +//go:embed emojis/3277.svg +var Emoji_3277 []byte + +//go:embed emojis/3278.svg +var Emoji_3278 []byte + +//go:embed emojis/3279.svg +var Emoji_3279 []byte + +//go:embed emojis/3280.svg +var Emoji_3280 []byte + +//go:embed emojis/3281.svg +var Emoji_3281 []byte + +//go:embed emojis/3282.svg +var Emoji_3282 []byte + +//go:embed emojis/3283.svg +var Emoji_3283 []byte + +//go:embed emojis/3284.svg +var Emoji_3284 []byte + +//go:embed emojis/3285.svg +var Emoji_3285 []byte + +//go:embed emojis/3286.svg +var Emoji_3286 []byte + +//go:embed emojis/3287.svg +var Emoji_3287 []byte + +//go:embed emojis/3288.svg +var Emoji_3288 []byte + +//go:embed emojis/3289.svg +var Emoji_3289 []byte + +//go:embed emojis/3290.svg +var Emoji_3290 []byte + +//go:embed emojis/3291.svg +var Emoji_3291 []byte + +//go:embed emojis/3292.svg +var Emoji_3292 []byte + +//go:embed emojis/3293.svg +var Emoji_3293 []byte + +//go:embed emojis/3294.svg +var Emoji_3294 []byte + +//go:embed emojis/3295.svg +var Emoji_3295 []byte + +//go:embed emojis/3296.svg +var Emoji_3296 []byte + +//go:embed emojis/3297.svg +var Emoji_3297 []byte + +//go:embed emojis/3298.svg +var Emoji_3298 []byte + +//go:embed emojis/3299.svg +var Emoji_3299 []byte + +//go:embed emojis/3300.svg +var Emoji_3300 []byte + +//go:embed emojis/3301.svg +var Emoji_3301 []byte + +//go:embed emojis/3302.svg +var Emoji_3302 []byte + +//go:embed emojis/3303.svg +var Emoji_3303 []byte + +//go:embed emojis/3304.svg +var Emoji_3304 []byte + +//go:embed emojis/3305.svg +var Emoji_3305 []byte + +//go:embed emojis/3306.svg +var Emoji_3306 []byte + +//go:embed emojis/3307.svg +var Emoji_3307 []byte + +//go:embed emojis/3308.svg +var Emoji_3308 []byte + +//go:embed emojis/3309.svg +var Emoji_3309 []byte + +//go:embed emojis/3310.svg +var Emoji_3310 []byte + +//go:embed emojis/3311.svg +var Emoji_3311 []byte + +//go:embed emojis/3312.svg +var Emoji_3312 []byte + +//go:embed emojis/3313.svg +var Emoji_3313 []byte + +//go:embed emojis/3314.svg +var Emoji_3314 []byte + +//go:embed emojis/3315.svg +var Emoji_3315 []byte + +//go:embed emojis/3316.svg +var Emoji_3316 []byte + +//go:embed emojis/3317.svg +var Emoji_3317 []byte + +//go:embed emojis/3318.svg +var Emoji_3318 []byte + +//go:embed emojis/3319.svg +var Emoji_3319 []byte + +//go:embed emojis/3320.svg +var Emoji_3320 []byte + +//go:embed emojis/3321.svg +var Emoji_3321 []byte + +//go:embed emojis/3322.svg +var Emoji_3322 []byte + +//go:embed emojis/3323.svg +var Emoji_3323 []byte + +//go:embed emojis/3324.svg +var Emoji_3324 []byte + +//go:embed emojis/3325.svg +var Emoji_3325 []byte + +//go:embed emojis/3326.svg +var Emoji_3326 []byte + +//go:embed emojis/3327.svg +var Emoji_3327 []byte + +//go:embed emojis/3328.svg +var Emoji_3328 []byte + +//go:embed emojis/3329.svg +var Emoji_3329 []byte + +//go:embed emojis/3330.svg +var Emoji_3330 []byte + +//go:embed emojis/3331.svg +var Emoji_3331 []byte + +//go:embed emojis/3332.svg +var Emoji_3332 []byte + +//go:embed emojis/3333.svg +var Emoji_3333 []byte + +//go:embed emojis/3334.svg +var Emoji_3334 []byte + +//go:embed emojis/3335.svg +var Emoji_3335 []byte + +//go:embed emojis/3336.svg +var Emoji_3336 []byte + +//go:embed emojis/3337.svg +var Emoji_3337 []byte + +//go:embed emojis/3338.svg +var Emoji_3338 []byte + +//go:embed emojis/3339.svg +var Emoji_3339 []byte + +//go:embed emojis/3340.svg +var Emoji_3340 []byte + +//go:embed emojis/3341.svg +var Emoji_3341 []byte + +//go:embed emojis/3342.svg +var Emoji_3342 []byte + +//go:embed emojis/3343.svg +var Emoji_3343 []byte + +//go:embed emojis/3344.svg +var Emoji_3344 []byte + +//go:embed emojis/3345.svg +var Emoji_3345 []byte + +//go:embed emojis/3346.svg +var Emoji_3346 []byte + +//go:embed emojis/3347.svg +var Emoji_3347 []byte + +//go:embed emojis/3348.svg +var Emoji_3348 []byte + +//go:embed emojis/3349.svg +var Emoji_3349 []byte + +//go:embed emojis/3350.svg +var Emoji_3350 []byte + +//go:embed emojis/3351.svg +var Emoji_3351 []byte + +//go:embed emojis/3352.svg +var Emoji_3352 []byte + +//go:embed emojis/3353.svg +var Emoji_3353 []byte + +//go:embed emojis/3354.svg +var Emoji_3354 []byte + +//go:embed emojis/3355.svg +var Emoji_3355 []byte + +//go:embed emojis/3356.svg +var Emoji_3356 []byte + +//go:embed emojis/3357.svg +var Emoji_3357 []byte + +//go:embed emojis/3358.svg +var Emoji_3358 []byte + +//go:embed emojis/3359.svg +var Emoji_3359 []byte + +//go:embed emojis/3360.svg +var Emoji_3360 []byte + +//go:embed emojis/3361.svg +var Emoji_3361 []byte + +//go:embed emojis/3362.svg +var Emoji_3362 []byte + +//go:embed emojis/3363.svg +var Emoji_3363 []byte + +//go:embed emojis/3364.svg +var Emoji_3364 []byte + +//go:embed emojis/3365.svg +var Emoji_3365 []byte + +//go:embed emojis/3366.svg +var Emoji_3366 []byte + +//go:embed emojis/3367.svg +var Emoji_3367 []byte + +//go:embed emojis/3368.svg +var Emoji_3368 []byte + +//go:embed emojis/3369.svg +var Emoji_3369 []byte + +//go:embed emojis/3370.svg +var Emoji_3370 []byte + +//go:embed emojis/3371.svg +var Emoji_3371 []byte + +//go:embed emojis/3372.svg +var Emoji_3372 []byte + +//go:embed emojis/3373.svg +var Emoji_3373 []byte + +//go:embed emojis/3374.svg +var Emoji_3374 []byte + +//go:embed emojis/3375.svg +var Emoji_3375 []byte + +//go:embed emojis/3376.svg +var Emoji_3376 []byte + +//go:embed emojis/3377.svg +var Emoji_3377 []byte + +//go:embed emojis/3378.svg +var Emoji_3378 []byte + +//go:embed emojis/3379.svg +var Emoji_3379 []byte + +//go:embed emojis/3380.svg +var Emoji_3380 []byte + +//go:embed emojis/3381.svg +var Emoji_3381 []byte + +//go:embed emojis/3382.svg +var Emoji_3382 []byte + +//go:embed emojis/3383.svg +var Emoji_3383 []byte + +//go:embed emojis/3384.svg +var Emoji_3384 []byte + +//go:embed emojis/3385.svg +var Emoji_3385 []byte + +//go:embed emojis/3386.svg +var Emoji_3386 []byte + +//go:embed emojis/3387.svg +var Emoji_3387 []byte + +//go:embed emojis/3388.svg +var Emoji_3388 []byte + +//go:embed emojis/3389.svg +var Emoji_3389 []byte + +//go:embed emojis/3390.svg +var Emoji_3390 []byte + +//go:embed emojis/3391.svg +var Emoji_3391 []byte + +//go:embed emojis/3392.svg +var Emoji_3392 []byte + +//go:embed emojis/3393.svg +var Emoji_3393 []byte + +//go:embed emojis/3394.svg +var Emoji_3394 []byte + +//go:embed emojis/3395.svg +var Emoji_3395 []byte + +//go:embed emojis/3396.svg +var Emoji_3396 []byte + +//go:embed emojis/3397.svg +var Emoji_3397 []byte + +//go:embed emojis/3398.svg +var Emoji_3398 []byte + +//go:embed emojis/3399.svg +var Emoji_3399 []byte + +//go:embed emojis/3400.svg +var Emoji_3400 []byte + +//go:embed emojis/3401.svg +var Emoji_3401 []byte + +//go:embed emojis/3402.svg +var Emoji_3402 []byte + +//go:embed emojis/3403.svg +var Emoji_3403 []byte + +//go:embed emojis/3404.svg +var Emoji_3404 []byte + +//go:embed emojis/3405.svg +var Emoji_3405 []byte + +//go:embed emojis/3406.svg +var Emoji_3406 []byte + +//go:embed emojis/3407.svg +var Emoji_3407 []byte + +//go:embed emojis/3408.svg +var Emoji_3408 []byte + +//go:embed emojis/3409.svg +var Emoji_3409 []byte + +//go:embed emojis/3410.svg +var Emoji_3410 []byte + +//go:embed emojis/3411.svg +var Emoji_3411 []byte + +//go:embed emojis/3412.svg +var Emoji_3412 []byte + +//go:embed emojis/3413.svg +var Emoji_3413 []byte + +//go:embed emojis/3414.svg +var Emoji_3414 []byte + +//go:embed emojis/3415.svg +var Emoji_3415 []byte + +//go:embed emojis/3416.svg +var Emoji_3416 []byte + +//go:embed emojis/3417.svg +var Emoji_3417 []byte + +//go:embed emojis/3418.svg +var Emoji_3418 []byte + +//go:embed emojis/3419.svg +var Emoji_3419 []byte + +//go:embed emojis/3420.svg +var Emoji_3420 []byte + +//go:embed emojis/3421.svg +var Emoji_3421 []byte + +//go:embed emojis/3422.svg +var Emoji_3422 []byte + +//go:embed emojis/3423.svg +var Emoji_3423 []byte + +//go:embed emojis/3424.svg +var Emoji_3424 []byte + +//go:embed emojis/3425.svg +var Emoji_3425 []byte + +//go:embed emojis/3426.svg +var Emoji_3426 []byte + +//go:embed emojis/3427.svg +var Emoji_3427 []byte + +//go:embed emojis/3428.svg +var Emoji_3428 []byte + +//go:embed emojis/3429.svg +var Emoji_3429 []byte + +//go:embed emojis/3430.svg +var Emoji_3430 []byte + +//go:embed emojis/3431.svg +var Emoji_3431 []byte + +//go:embed emojis/3432.svg +var Emoji_3432 []byte + +//go:embed emojis/3433.svg +var Emoji_3433 []byte + +//go:embed emojis/3434.svg +var Emoji_3434 []byte + +//go:embed emojis/3435.svg +var Emoji_3435 []byte + +//go:embed emojis/3436.svg +var Emoji_3436 []byte + +//go:embed emojis/3437.svg +var Emoji_3437 []byte + +//go:embed emojis/3438.svg +var Emoji_3438 []byte + +//go:embed emojis/3439.svg +var Emoji_3439 []byte + +//go:embed emojis/3440.svg +var Emoji_3440 []byte + +//go:embed emojis/3441.svg +var Emoji_3441 []byte + +//go:embed emojis/3442.svg +var Emoji_3442 []byte + +//go:embed emojis/3443.svg +var Emoji_3443 []byte + +//go:embed emojis/3444.svg +var Emoji_3444 []byte + +//go:embed emojis/3445.svg +var Emoji_3445 []byte + +//go:embed emojis/3446.svg +var Emoji_3446 []byte + +//go:embed emojis/3447.svg +var Emoji_3447 []byte + +//go:embed emojis/3448.svg +var Emoji_3448 []byte + +//go:embed emojis/3449.svg +var Emoji_3449 []byte + +//go:embed emojis/3450.svg +var Emoji_3450 []byte + +//go:embed emojis/3451.svg +var Emoji_3451 []byte + +//go:embed emojis/3452.svg +var Emoji_3452 []byte + +//go:embed emojis/3453.svg +var Emoji_3453 []byte + +//go:embed emojis/3454.svg +var Emoji_3454 []byte + +//go:embed emojis/3455.svg +var Emoji_3455 []byte + +//go:embed emojis/3456.svg +var Emoji_3456 []byte + +//go:embed emojis/3457.svg +var Emoji_3457 []byte + +//go:embed emojis/3458.svg +var Emoji_3458 []byte + +//go:embed emojis/3459.svg +var Emoji_3459 []byte + +//go:embed emojis/3460.svg +var Emoji_3460 []byte + +//go:embed emojis/3461.svg +var Emoji_3461 []byte + +//go:embed emojis/3462.svg +var Emoji_3462 []byte + +//go:embed emojis/3463.svg +var Emoji_3463 []byte + +//go:embed emojis/3464.svg +var Emoji_3464 []byte + +//go:embed emojis/3465.svg +var Emoji_3465 []byte + +//go:embed emojis/3466.svg +var Emoji_3466 []byte + +//go:embed emojis/3467.svg +var Emoji_3467 []byte + +//go:embed emojis/3468.svg +var Emoji_3468 []byte + +//go:embed emojis/3469.svg +var Emoji_3469 []byte + +//go:embed emojis/3470.svg +var Emoji_3470 []byte + +//go:embed emojis/3471.svg +var Emoji_3471 []byte + +//go:embed emojis/3472.svg +var Emoji_3472 []byte + +//go:embed emojis/3473.svg +var Emoji_3473 []byte + +//go:embed emojis/3474.svg +var Emoji_3474 []byte + +//go:embed emojis/3475.svg +var Emoji_3475 []byte + +//go:embed emojis/3476.svg +var Emoji_3476 []byte + +//go:embed emojis/3477.svg +var Emoji_3477 []byte + +//go:embed emojis/3478.svg +var Emoji_3478 []byte + +//go:embed emojis/3479.svg +var Emoji_3479 []byte + +//go:embed emojis/3480.svg +var Emoji_3480 []byte + +//go:embed emojis/3481.svg +var Emoji_3481 []byte + +//go:embed emojis/3482.svg +var Emoji_3482 []byte + +//go:embed emojis/3483.svg +var Emoji_3483 []byte + +//go:embed emojis/3484.svg +var Emoji_3484 []byte + +//go:embed emojis/3485.svg +var Emoji_3485 []byte + +//go:embed emojis/3486.svg +var Emoji_3486 []byte + +//go:embed emojis/3487.svg +var Emoji_3487 []byte + +//go:embed emojis/3488.svg +var Emoji_3488 []byte + +//go:embed emojis/3489.svg +var Emoji_3489 []byte + +//go:embed emojis/3490.svg +var Emoji_3490 []byte + +//go:embed emojis/3491.svg +var Emoji_3491 []byte + +//go:embed emojis/3492.svg +var Emoji_3492 []byte + +//go:embed emojis/3493.svg +var Emoji_3493 []byte + +//go:embed emojis/3494.svg +var Emoji_3494 []byte + +//go:embed emojis/3495.svg +var Emoji_3495 []byte + +//go:embed emojis/3496.svg +var Emoji_3496 []byte + +//go:embed emojis/3497.svg +var Emoji_3497 []byte + +//go:embed emojis/3498.svg +var Emoji_3498 []byte + +//go:embed emojis/3499.svg +var Emoji_3499 []byte + +//go:embed emojis/3500.svg +var Emoji_3500 []byte + +//go:embed emojis/3501.svg +var Emoji_3501 []byte + +//go:embed emojis/3502.svg +var Emoji_3502 []byte + +//go:embed emojis/3503.svg +var Emoji_3503 []byte + +//go:embed emojis/3504.svg +var Emoji_3504 []byte + +//go:embed emojis/3505.svg +var Emoji_3505 []byte + +//go:embed emojis/3506.svg +var Emoji_3506 []byte + +//go:embed emojis/3507.svg +var Emoji_3507 []byte + +//go:embed emojis/3508.svg +var Emoji_3508 []byte + +//go:embed emojis/3509.svg +var Emoji_3509 []byte + +//go:embed emojis/3510.svg +var Emoji_3510 []byte + +//go:embed emojis/3511.svg +var Emoji_3511 []byte + +//go:embed emojis/3512.svg +var Emoji_3512 []byte + +//go:embed emojis/3513.svg +var Emoji_3513 []byte + +//go:embed emojis/3514.svg +var Emoji_3514 []byte + +//go:embed emojis/3515.svg +var Emoji_3515 []byte + +//go:embed emojis/3516.svg +var Emoji_3516 []byte + +//go:embed emojis/3517.svg +var Emoji_3517 []byte + +//go:embed emojis/3518.svg +var Emoji_3518 []byte + +//go:embed emojis/3519.svg +var Emoji_3519 []byte + +//go:embed emojis/3520.svg +var Emoji_3520 []byte + +//go:embed emojis/3521.svg +var Emoji_3521 []byte + +//go:embed emojis/3522.svg +var Emoji_3522 []byte + +//go:embed emojis/3523.svg +var Emoji_3523 []byte + +//go:embed emojis/3524.svg +var Emoji_3524 []byte + +//go:embed emojis/3525.svg +var Emoji_3525 []byte + +//go:embed emojis/3526.svg +var Emoji_3526 []byte + +//go:embed emojis/3527.svg +var Emoji_3527 []byte + +//go:embed emojis/3528.svg +var Emoji_3528 []byte + +//go:embed emojis/3529.svg +var Emoji_3529 []byte + +//go:embed emojis/3530.svg +var Emoji_3530 []byte + +//go:embed emojis/3531.svg +var Emoji_3531 []byte + +//go:embed emojis/3532.svg +var Emoji_3532 []byte + +//go:embed emojis/3533.svg +var Emoji_3533 []byte + +//go:embed emojis/3534.svg +var Emoji_3534 []byte + +//go:embed emojis/3535.svg +var Emoji_3535 []byte + +//go:embed emojis/3536.svg +var Emoji_3536 []byte + +//go:embed emojis/3537.svg +var Emoji_3537 []byte + +//go:embed emojis/3538.svg +var Emoji_3538 []byte + +//go:embed emojis/3539.svg +var Emoji_3539 []byte + +//go:embed emojis/3540.svg +var Emoji_3540 []byte + +//go:embed emojis/3541.svg +var Emoji_3541 []byte + +//go:embed emojis/3542.svg +var Emoji_3542 []byte + +//go:embed emojis/3543.svg +var Emoji_3543 []byte + +//go:embed emojis/3544.svg +var Emoji_3544 []byte + +//go:embed emojis/3545.svg +var Emoji_3545 []byte + +//go:embed emojis/3546.svg +var Emoji_3546 []byte + +//go:embed emojis/3547.svg +var Emoji_3547 []byte + +//go:embed emojis/3548.svg +var Emoji_3548 []byte + +//go:embed emojis/3549.svg +var Emoji_3549 []byte + +//go:embed emojis/3550.svg +var Emoji_3550 []byte + +//go:embed emojis/3551.svg +var Emoji_3551 []byte + +//go:embed emojis/3552.svg +var Emoji_3552 []byte + +//go:embed emojis/3553.svg +var Emoji_3553 []byte + +//go:embed emojis/3554.svg +var Emoji_3554 []byte + +//go:embed emojis/3555.svg +var Emoji_3555 []byte + +//go:embed emojis/3556.svg +var Emoji_3556 []byte + +//go:embed emojis/3557.svg +var Emoji_3557 []byte + +//go:embed emojis/3558.svg +var Emoji_3558 []byte + +//go:embed emojis/3559.svg +var Emoji_3559 []byte + +//go:embed emojis/3560.svg +var Emoji_3560 []byte + +//go:embed emojis/3561.svg +var Emoji_3561 []byte + +//go:embed emojis/3562.svg +var Emoji_3562 []byte + +//go:embed emojis/3563.svg +var Emoji_3563 []byte + +//go:embed emojis/3564.svg +var Emoji_3564 []byte + +//go:embed emojis/3565.svg +var Emoji_3565 []byte + +//go:embed emojis/3566.svg +var Emoji_3566 []byte + +//go:embed emojis/3567.svg +var Emoji_3567 []byte + +//go:embed emojis/3568.svg +var Emoji_3568 []byte + +//go:embed emojis/3569.svg +var Emoji_3569 []byte + +//go:embed emojis/3570.svg +var Emoji_3570 []byte + +//go:embed emojis/3571.svg +var Emoji_3571 []byte + +//go:embed emojis/3572.svg +var Emoji_3572 []byte + +//go:embed emojis/3573.svg +var Emoji_3573 []byte + +//go:embed emojis/3574.svg +var Emoji_3574 []byte + +//go:embed emojis/3575.svg +var Emoji_3575 []byte + +//go:embed emojis/3576.svg +var Emoji_3576 []byte + +//go:embed emojis/3577.svg +var Emoji_3577 []byte + +//go:embed emojis/3578.svg +var Emoji_3578 []byte + +//go:embed emojis/3579.svg +var Emoji_3579 []byte + +//go:embed emojis/3580.svg +var Emoji_3580 []byte + +//go:embed emojis/3581.svg +var Emoji_3581 []byte + +//go:embed emojis/3582.svg +var Emoji_3582 []byte + +//go:embed emojis/3583.svg +var Emoji_3583 []byte + +//go:embed emojis/3584.svg +var Emoji_3584 []byte + +//go:embed emojis/3585.svg +var Emoji_3585 []byte + +//go:embed emojis/3586.svg +var Emoji_3586 []byte + +//go:embed emojis/3587.svg +var Emoji_3587 []byte + +//go:embed emojis/3588.svg +var Emoji_3588 []byte + +//go:embed emojis/3589.svg +var Emoji_3589 []byte + +//go:embed emojis/3590.svg +var Emoji_3590 []byte + +//go:embed emojis/3591.svg +var Emoji_3591 []byte + +//go:embed emojis/3592.svg +var Emoji_3592 []byte + +//go:embed emojis/3593.svg +var Emoji_3593 []byte + +//go:embed emojis/3594.svg +var Emoji_3594 []byte + +//go:embed emojis/3595.svg +var Emoji_3595 []byte + +//go:embed emojis/3596.svg +var Emoji_3596 []byte + +//go:embed emojis/3597.svg +var Emoji_3597 []byte + +//go:embed emojis/3598.svg +var Emoji_3598 []byte + +//go:embed emojis/3599.svg +var Emoji_3599 []byte + +//go:embed emojis/3600.svg +var Emoji_3600 []byte + +//go:embed emojis/3601.svg +var Emoji_3601 []byte + +//go:embed emojis/3602.svg +var Emoji_3602 []byte + +//go:embed emojis/3603.svg +var Emoji_3603 []byte + +//go:embed emojis/3604.svg +var Emoji_3604 []byte + +//go:embed emojis/3605.svg +var Emoji_3605 []byte + +//go:embed emojis/3606.svg +var Emoji_3606 []byte + +//go:embed emojis/3607.svg +var Emoji_3607 []byte + +//go:embed emojis/3608.svg +var Emoji_3608 []byte + +//go:embed emojis/3609.svg +var Emoji_3609 []byte + +//go:embed emojis/3610.svg +var Emoji_3610 []byte + +//go:embed emojis/3611.svg +var Emoji_3611 []byte + +//go:embed emojis/3612.svg +var Emoji_3612 []byte + +//go:embed emojis/3613.svg +var Emoji_3613 []byte + +//go:embed emojis/3614.svg +var Emoji_3614 []byte + +//go:embed emojis/3615.svg +var Emoji_3615 []byte + +//go:embed emojis/3616.svg +var Emoji_3616 []byte + +//go:embed emojis/3617.svg +var Emoji_3617 []byte + +//go:embed emojis/3618.svg +var Emoji_3618 []byte + +//go:embed emojis/3619.svg +var Emoji_3619 []byte + +//go:embed emojis/3620.svg +var Emoji_3620 []byte + +//go:embed emojis/3621.svg +var Emoji_3621 []byte + +//go:embed emojis/3622.svg +var Emoji_3622 []byte + +//go:embed emojis/3623.svg +var Emoji_3623 []byte + +//go:embed emojis/3624.svg +var Emoji_3624 []byte + +//go:embed emojis/3625.svg +var Emoji_3625 []byte + +//go:embed emojis/3626.svg +var Emoji_3626 []byte + +//go:embed emojis/3627.svg +var Emoji_3627 []byte + +//go:embed emojis/3628.svg +var Emoji_3628 []byte + +//go:embed emojis/3629.svg +var Emoji_3629 []byte + +//go:embed emojis/3630.svg +var Emoji_3630 []byte + +//go:embed emojis/3631.svg +var Emoji_3631 []byte + +func GetEmojiFileBytesFromIdentifier(inputIdentifier int)([]byte, error){ + + switch inputIdentifier{ + + case 1:{ + return Emoji_0001, nil + } + case 2:{ + return Emoji_0002, nil + } + case 3:{ + return Emoji_0003, nil + } + case 4:{ + return Emoji_0004, nil + } + case 5:{ + return Emoji_0005, nil + } + case 6:{ + return Emoji_0006, nil + } + case 7:{ + return Emoji_0007, nil + } + case 8:{ + return Emoji_0008, nil + } + case 9:{ + return Emoji_0009, nil + } + case 10:{ + return Emoji_0010, nil + } + case 11:{ + return Emoji_0011, nil + } + case 12:{ + return Emoji_0012, nil + } + case 13:{ + return Emoji_0013, nil + } + case 14:{ + return Emoji_0014, nil + } + case 15:{ + return Emoji_0015, nil + } + case 16:{ + return Emoji_0016, nil + } + case 17:{ + return Emoji_0017, nil + } + case 18:{ + return Emoji_0018, nil + } + case 19:{ + return Emoji_0019, nil + } + case 20:{ + return Emoji_0020, nil + } + case 21:{ + return Emoji_0021, nil + } + case 22:{ + return Emoji_0022, nil + } + case 23:{ + return Emoji_0023, nil + } + case 24:{ + return Emoji_0024, nil + } + case 25:{ + return Emoji_0025, nil + } + case 26:{ + return Emoji_0026, nil + } + case 27:{ + return Emoji_0027, nil + } + case 28:{ + return Emoji_0028, nil + } + case 29:{ + return Emoji_0029, nil + } + case 30:{ + return Emoji_0030, nil + } + case 31:{ + return Emoji_0031, nil + } + case 32:{ + return Emoji_0032, nil + } + case 33:{ + return Emoji_0033, nil + } + case 34:{ + return Emoji_0034, nil + } + case 35:{ + return Emoji_0035, nil + } + case 36:{ + return Emoji_0036, nil + } + case 37:{ + return Emoji_0037, nil + } + case 38:{ + return Emoji_0038, nil + } + case 39:{ + return Emoji_0039, nil + } + case 40:{ + return Emoji_0040, nil + } + case 41:{ + return Emoji_0041, nil + } + case 42:{ + return Emoji_0042, nil + } + case 43:{ + return Emoji_0043, nil + } + case 44:{ + return Emoji_0044, nil + } + case 45:{ + return Emoji_0045, nil + } + case 46:{ + return Emoji_0046, nil + } + case 47:{ + return Emoji_0047, nil + } + case 48:{ + return Emoji_0048, nil + } + case 49:{ + return Emoji_0049, nil + } + case 50:{ + return Emoji_0050, nil + } + case 51:{ + return Emoji_0051, nil + } + case 52:{ + return Emoji_0052, nil + } + case 53:{ + return Emoji_0053, nil + } + case 54:{ + return Emoji_0054, nil + } + case 55:{ + return Emoji_0055, nil + } + case 56:{ + return Emoji_0056, nil + } + case 57:{ + return Emoji_0057, nil + } + case 58:{ + return Emoji_0058, nil + } + case 59:{ + return Emoji_0059, nil + } + case 60:{ + return Emoji_0060, nil + } + case 61:{ + return Emoji_0061, nil + } + case 62:{ + return Emoji_0062, nil + } + case 63:{ + return Emoji_0063, nil + } + case 64:{ + return Emoji_0064, nil + } + case 65:{ + return Emoji_0065, nil + } + case 66:{ + return Emoji_0066, nil + } + case 67:{ + return Emoji_0067, nil + } + case 68:{ + return Emoji_0068, nil + } + case 69:{ + return Emoji_0069, nil + } + case 70:{ + return Emoji_0070, nil + } + case 71:{ + return Emoji_0071, nil + } + case 72:{ + return Emoji_0072, nil + } + case 73:{ + return Emoji_0073, nil + } + case 74:{ + return Emoji_0074, nil + } + case 75:{ + return Emoji_0075, nil + } + case 76:{ + return Emoji_0076, nil + } + case 77:{ + return Emoji_0077, nil + } + case 78:{ + return Emoji_0078, nil + } + case 79:{ + return Emoji_0079, nil + } + case 80:{ + return Emoji_0080, nil + } + case 81:{ + return Emoji_0081, nil + } + case 82:{ + return Emoji_0082, nil + } + case 83:{ + return Emoji_0083, nil + } + case 84:{ + return Emoji_0084, nil + } + case 85:{ + return Emoji_0085, nil + } + case 86:{ + return Emoji_0086, nil + } + case 87:{ + return Emoji_0087, nil + } + case 88:{ + return Emoji_0088, nil + } + case 89:{ + return Emoji_0089, nil + } + case 90:{ + return Emoji_0090, nil + } + case 91:{ + return Emoji_0091, nil + } + case 92:{ + return Emoji_0092, nil + } + case 93:{ + return Emoji_0093, nil + } + case 94:{ + return Emoji_0094, nil + } + case 95:{ + return Emoji_0095, nil + } + case 96:{ + return Emoji_0096, nil + } + case 97:{ + return Emoji_0097, nil + } + case 98:{ + return Emoji_0098, nil + } + case 99:{ + return Emoji_0099, nil + } + case 100:{ + return Emoji_0100, nil + } + case 101:{ + return Emoji_0101, nil + } + case 102:{ + return Emoji_0102, nil + } + case 103:{ + return Emoji_0103, nil + } + case 104:{ + return Emoji_0104, nil + } + case 105:{ + return Emoji_0105, nil + } + case 106:{ + return Emoji_0106, nil + } + case 107:{ + return Emoji_0107, nil + } + case 108:{ + return Emoji_0108, nil + } + case 109:{ + return Emoji_0109, nil + } + case 110:{ + return Emoji_0110, nil + } + case 111:{ + return Emoji_0111, nil + } + case 112:{ + return Emoji_0112, nil + } + case 113:{ + return Emoji_0113, nil + } + case 114:{ + return Emoji_0114, nil + } + case 115:{ + return Emoji_0115, nil + } + case 116:{ + return Emoji_0116, nil + } + case 117:{ + return Emoji_0117, nil + } + case 118:{ + return Emoji_0118, nil + } + case 119:{ + return Emoji_0119, nil + } + case 120:{ + return Emoji_0120, nil + } + case 121:{ + return Emoji_0121, nil + } + case 122:{ + return Emoji_0122, nil + } + case 123:{ + return Emoji_0123, nil + } + case 124:{ + return Emoji_0124, nil + } + case 125:{ + return Emoji_0125, nil + } + case 126:{ + return Emoji_0126, nil + } + case 127:{ + return Emoji_0127, nil + } + case 128:{ + return Emoji_0128, nil + } + case 129:{ + return Emoji_0129, nil + } + case 130:{ + return Emoji_0130, nil + } + case 131:{ + return Emoji_0131, nil + } + case 132:{ + return Emoji_0132, nil + } + case 133:{ + return Emoji_0133, nil + } + case 134:{ + return Emoji_0134, nil + } + case 135:{ + return Emoji_0135, nil + } + case 136:{ + return Emoji_0136, nil + } + case 137:{ + return Emoji_0137, nil + } + case 138:{ + return Emoji_0138, nil + } + case 139:{ + return Emoji_0139, nil + } + case 140:{ + return Emoji_0140, nil + } + case 141:{ + return Emoji_0141, nil + } + case 142:{ + return Emoji_0142, nil + } + case 143:{ + return Emoji_0143, nil + } + case 144:{ + return Emoji_0144, nil + } + case 145:{ + return Emoji_0145, nil + } + case 146:{ + return Emoji_0146, nil + } + case 147:{ + return Emoji_0147, nil + } + case 148:{ + return Emoji_0148, nil + } + case 149:{ + return Emoji_0149, nil + } + case 150:{ + return Emoji_0150, nil + } + case 151:{ + return Emoji_0151, nil + } + case 152:{ + return Emoji_0152, nil + } + case 153:{ + return Emoji_0153, nil + } + case 154:{ + return Emoji_0154, nil + } + case 155:{ + return Emoji_0155, nil + } + case 156:{ + return Emoji_0156, nil + } + case 157:{ + return Emoji_0157, nil + } + case 158:{ + return Emoji_0158, nil + } + case 159:{ + return Emoji_0159, nil + } + case 160:{ + return Emoji_0160, nil + } + case 161:{ + return Emoji_0161, nil + } + case 162:{ + return Emoji_0162, nil + } + case 163:{ + return Emoji_0163, nil + } + case 164:{ + return Emoji_0164, nil + } + case 165:{ + return Emoji_0165, nil + } + case 166:{ + return Emoji_0166, nil + } + case 167:{ + return Emoji_0167, nil + } + case 168:{ + return Emoji_0168, nil + } + case 169:{ + return Emoji_0169, nil + } + case 170:{ + return Emoji_0170, nil + } + case 171:{ + return Emoji_0171, nil + } + case 172:{ + return Emoji_0172, nil + } + case 173:{ + return Emoji_0173, nil + } + case 174:{ + return Emoji_0174, nil + } + case 175:{ + return Emoji_0175, nil + } + case 176:{ + return Emoji_0176, nil + } + case 177:{ + return Emoji_0177, nil + } + case 178:{ + return Emoji_0178, nil + } + case 179:{ + return Emoji_0179, nil + } + case 180:{ + return Emoji_0180, nil + } + case 181:{ + return Emoji_0181, nil + } + case 182:{ + return Emoji_0182, nil + } + case 183:{ + return Emoji_0183, nil + } + case 184:{ + return Emoji_0184, nil + } + case 185:{ + return Emoji_0185, nil + } + case 186:{ + return Emoji_0186, nil + } + case 187:{ + return Emoji_0187, nil + } + case 188:{ + return Emoji_0188, nil + } + case 189:{ + return Emoji_0189, nil + } + case 190:{ + return Emoji_0190, nil + } + case 191:{ + return Emoji_0191, nil + } + case 192:{ + return Emoji_0192, nil + } + case 193:{ + return Emoji_0193, nil + } + case 194:{ + return Emoji_0194, nil + } + case 195:{ + return Emoji_0195, nil + } + case 196:{ + return Emoji_0196, nil + } + case 197:{ + return Emoji_0197, nil + } + case 198:{ + return Emoji_0198, nil + } + case 199:{ + return Emoji_0199, nil + } + case 200:{ + return Emoji_0200, nil + } + case 201:{ + return Emoji_0201, nil + } + case 202:{ + return Emoji_0202, nil + } + case 203:{ + return Emoji_0203, nil + } + case 204:{ + return Emoji_0204, nil + } + case 205:{ + return Emoji_0205, nil + } + case 206:{ + return Emoji_0206, nil + } + case 207:{ + return Emoji_0207, nil + } + case 208:{ + return Emoji_0208, nil + } + case 209:{ + return Emoji_0209, nil + } + case 210:{ + return Emoji_0210, nil + } + case 211:{ + return Emoji_0211, nil + } + case 212:{ + return Emoji_0212, nil + } + case 213:{ + return Emoji_0213, nil + } + case 214:{ + return Emoji_0214, nil + } + case 215:{ + return Emoji_0215, nil + } + case 216:{ + return Emoji_0216, nil + } + case 217:{ + return Emoji_0217, nil + } + case 218:{ + return Emoji_0218, nil + } + case 219:{ + return Emoji_0219, nil + } + case 220:{ + return Emoji_0220, nil + } + case 221:{ + return Emoji_0221, nil + } + case 222:{ + return Emoji_0222, nil + } + case 223:{ + return Emoji_0223, nil + } + case 224:{ + return Emoji_0224, nil + } + case 225:{ + return Emoji_0225, nil + } + case 226:{ + return Emoji_0226, nil + } + case 227:{ + return Emoji_0227, nil + } + case 228:{ + return Emoji_0228, nil + } + case 229:{ + return Emoji_0229, nil + } + case 230:{ + return Emoji_0230, nil + } + case 231:{ + return Emoji_0231, nil + } + case 232:{ + return Emoji_0232, nil + } + case 233:{ + return Emoji_0233, nil + } + case 234:{ + return Emoji_0234, nil + } + case 235:{ + return Emoji_0235, nil + } + case 236:{ + return Emoji_0236, nil + } + case 237:{ + return Emoji_0237, nil + } + case 238:{ + return Emoji_0238, nil + } + case 239:{ + return Emoji_0239, nil + } + case 240:{ + return Emoji_0240, nil + } + case 241:{ + return Emoji_0241, nil + } + case 242:{ + return Emoji_0242, nil + } + case 243:{ + return Emoji_0243, nil + } + case 244:{ + return Emoji_0244, nil + } + case 245:{ + return Emoji_0245, nil + } + case 246:{ + return Emoji_0246, nil + } + case 247:{ + return Emoji_0247, nil + } + case 248:{ + return Emoji_0248, nil + } + case 249:{ + return Emoji_0249, nil + } + case 250:{ + return Emoji_0250, nil + } + case 251:{ + return Emoji_0251, nil + } + case 252:{ + return Emoji_0252, nil + } + case 253:{ + return Emoji_0253, nil + } + case 254:{ + return Emoji_0254, nil + } + case 255:{ + return Emoji_0255, nil + } + case 256:{ + return Emoji_0256, nil + } + case 257:{ + return Emoji_0257, nil + } + case 258:{ + return Emoji_0258, nil + } + case 259:{ + return Emoji_0259, nil + } + case 260:{ + return Emoji_0260, nil + } + case 261:{ + return Emoji_0261, nil + } + case 262:{ + return Emoji_0262, nil + } + case 263:{ + return Emoji_0263, nil + } + case 264:{ + return Emoji_0264, nil + } + case 265:{ + return Emoji_0265, nil + } + case 266:{ + return Emoji_0266, nil + } + case 267:{ + return Emoji_0267, nil + } + case 268:{ + return Emoji_0268, nil + } + case 269:{ + return Emoji_0269, nil + } + case 270:{ + return Emoji_0270, nil + } + case 271:{ + return Emoji_0271, nil + } + case 272:{ + return Emoji_0272, nil + } + case 273:{ + return Emoji_0273, nil + } + case 274:{ + return Emoji_0274, nil + } + case 275:{ + return Emoji_0275, nil + } + case 276:{ + return Emoji_0276, nil + } + case 277:{ + return Emoji_0277, nil + } + case 278:{ + return Emoji_0278, nil + } + case 279:{ + return Emoji_0279, nil + } + case 280:{ + return Emoji_0280, nil + } + case 281:{ + return Emoji_0281, nil + } + case 282:{ + return Emoji_0282, nil + } + case 283:{ + return Emoji_0283, nil + } + case 284:{ + return Emoji_0284, nil + } + case 285:{ + return Emoji_0285, nil + } + case 286:{ + return Emoji_0286, nil + } + case 287:{ + return Emoji_0287, nil + } + case 288:{ + return Emoji_0288, nil + } + case 289:{ + return Emoji_0289, nil + } + case 290:{ + return Emoji_0290, nil + } + case 291:{ + return Emoji_0291, nil + } + case 292:{ + return Emoji_0292, nil + } + case 293:{ + return Emoji_0293, nil + } + case 294:{ + return Emoji_0294, nil + } + case 295:{ + return Emoji_0295, nil + } + case 296:{ + return Emoji_0296, nil + } + case 297:{ + return Emoji_0297, nil + } + case 298:{ + return Emoji_0298, nil + } + case 299:{ + return Emoji_0299, nil + } + case 300:{ + return Emoji_0300, nil + } + case 301:{ + return Emoji_0301, nil + } + case 302:{ + return Emoji_0302, nil + } + case 303:{ + return Emoji_0303, nil + } + case 304:{ + return Emoji_0304, nil + } + case 305:{ + return Emoji_0305, nil + } + case 306:{ + return Emoji_0306, nil + } + case 307:{ + return Emoji_0307, nil + } + case 308:{ + return Emoji_0308, nil + } + case 309:{ + return Emoji_0309, nil + } + case 310:{ + return Emoji_0310, nil + } + case 311:{ + return Emoji_0311, nil + } + case 312:{ + return Emoji_0312, nil + } + case 313:{ + return Emoji_0313, nil + } + case 314:{ + return Emoji_0314, nil + } + case 315:{ + return Emoji_0315, nil + } + case 316:{ + return Emoji_0316, nil + } + case 317:{ + return Emoji_0317, nil + } + case 318:{ + return Emoji_0318, nil + } + case 319:{ + return Emoji_0319, nil + } + case 320:{ + return Emoji_0320, nil + } + case 321:{ + return Emoji_0321, nil + } + case 322:{ + return Emoji_0322, nil + } + case 323:{ + return Emoji_0323, nil + } + case 324:{ + return Emoji_0324, nil + } + case 325:{ + return Emoji_0325, nil + } + case 326:{ + return Emoji_0326, nil + } + case 327:{ + return Emoji_0327, nil + } + case 328:{ + return Emoji_0328, nil + } + case 329:{ + return Emoji_0329, nil + } + case 330:{ + return Emoji_0330, nil + } + case 331:{ + return Emoji_0331, nil + } + case 332:{ + return Emoji_0332, nil + } + case 333:{ + return Emoji_0333, nil + } + case 334:{ + return Emoji_0334, nil + } + case 335:{ + return Emoji_0335, nil + } + case 336:{ + return Emoji_0336, nil + } + case 337:{ + return Emoji_0337, nil + } + case 338:{ + return Emoji_0338, nil + } + case 339:{ + return Emoji_0339, nil + } + case 340:{ + return Emoji_0340, nil + } + case 341:{ + return Emoji_0341, nil + } + case 342:{ + return Emoji_0342, nil + } + case 343:{ + return Emoji_0343, nil + } + case 344:{ + return Emoji_0344, nil + } + case 345:{ + return Emoji_0345, nil + } + case 346:{ + return Emoji_0346, nil + } + case 347:{ + return Emoji_0347, nil + } + case 348:{ + return Emoji_0348, nil + } + case 349:{ + return Emoji_0349, nil + } + case 350:{ + return Emoji_0350, nil + } + case 351:{ + return Emoji_0351, nil + } + case 352:{ + return Emoji_0352, nil + } + case 353:{ + return Emoji_0353, nil + } + case 354:{ + return Emoji_0354, nil + } + case 355:{ + return Emoji_0355, nil + } + case 356:{ + return Emoji_0356, nil + } + case 357:{ + return Emoji_0357, nil + } + case 358:{ + return Emoji_0358, nil + } + case 359:{ + return Emoji_0359, nil + } + case 360:{ + return Emoji_0360, nil + } + case 361:{ + return Emoji_0361, nil + } + case 362:{ + return Emoji_0362, nil + } + case 363:{ + return Emoji_0363, nil + } + case 364:{ + return Emoji_0364, nil + } + case 365:{ + return Emoji_0365, nil + } + case 366:{ + return Emoji_0366, nil + } + case 367:{ + return Emoji_0367, nil + } + case 368:{ + return Emoji_0368, nil + } + case 369:{ + return Emoji_0369, nil + } + case 370:{ + return Emoji_0370, nil + } + case 371:{ + return Emoji_0371, nil + } + case 372:{ + return Emoji_0372, nil + } + case 373:{ + return Emoji_0373, nil + } + case 374:{ + return Emoji_0374, nil + } + case 375:{ + return Emoji_0375, nil + } + case 376:{ + return Emoji_0376, nil + } + case 377:{ + return Emoji_0377, nil + } + case 378:{ + return Emoji_0378, nil + } + case 379:{ + return Emoji_0379, nil + } + case 380:{ + return Emoji_0380, nil + } + case 381:{ + return Emoji_0381, nil + } + case 382:{ + return Emoji_0382, nil + } + case 383:{ + return Emoji_0383, nil + } + case 384:{ + return Emoji_0384, nil + } + case 385:{ + return Emoji_0385, nil + } + case 386:{ + return Emoji_0386, nil + } + case 387:{ + return Emoji_0387, nil + } + case 388:{ + return Emoji_0388, nil + } + case 389:{ + return Emoji_0389, nil + } + case 390:{ + return Emoji_0390, nil + } + case 391:{ + return Emoji_0391, nil + } + case 392:{ + return Emoji_0392, nil + } + case 393:{ + return Emoji_0393, nil + } + case 394:{ + return Emoji_0394, nil + } + case 395:{ + return Emoji_0395, nil + } + case 396:{ + return Emoji_0396, nil + } + case 397:{ + return Emoji_0397, nil + } + case 398:{ + return Emoji_0398, nil + } + case 399:{ + return Emoji_0399, nil + } + case 400:{ + return Emoji_0400, nil + } + case 401:{ + return Emoji_0401, nil + } + case 402:{ + return Emoji_0402, nil + } + case 403:{ + return Emoji_0403, nil + } + case 404:{ + return Emoji_0404, nil + } + case 405:{ + return Emoji_0405, nil + } + case 406:{ + return Emoji_0406, nil + } + case 407:{ + return Emoji_0407, nil + } + case 408:{ + return Emoji_0408, nil + } + case 409:{ + return Emoji_0409, nil + } + case 410:{ + return Emoji_0410, nil + } + case 411:{ + return Emoji_0411, nil + } + case 412:{ + return Emoji_0412, nil + } + case 413:{ + return Emoji_0413, nil + } + case 414:{ + return Emoji_0414, nil + } + case 415:{ + return Emoji_0415, nil + } + case 416:{ + return Emoji_0416, nil + } + case 417:{ + return Emoji_0417, nil + } + case 418:{ + return Emoji_0418, nil + } + case 419:{ + return Emoji_0419, nil + } + case 420:{ + return Emoji_0420, nil + } + case 421:{ + return Emoji_0421, nil + } + case 422:{ + return Emoji_0422, nil + } + case 423:{ + return Emoji_0423, nil + } + case 424:{ + return Emoji_0424, nil + } + case 425:{ + return Emoji_0425, nil + } + case 426:{ + return Emoji_0426, nil + } + case 427:{ + return Emoji_0427, nil + } + case 428:{ + return Emoji_0428, nil + } + case 429:{ + return Emoji_0429, nil + } + case 430:{ + return Emoji_0430, nil + } + case 431:{ + return Emoji_0431, nil + } + case 432:{ + return Emoji_0432, nil + } + case 433:{ + return Emoji_0433, nil + } + case 434:{ + return Emoji_0434, nil + } + case 435:{ + return Emoji_0435, nil + } + case 436:{ + return Emoji_0436, nil + } + case 437:{ + return Emoji_0437, nil + } + case 438:{ + return Emoji_0438, nil + } + case 439:{ + return Emoji_0439, nil + } + case 440:{ + return Emoji_0440, nil + } + case 441:{ + return Emoji_0441, nil + } + case 442:{ + return Emoji_0442, nil + } + case 443:{ + return Emoji_0443, nil + } + case 444:{ + return Emoji_0444, nil + } + case 445:{ + return Emoji_0445, nil + } + case 446:{ + return Emoji_0446, nil + } + case 447:{ + return Emoji_0447, nil + } + case 448:{ + return Emoji_0448, nil + } + case 449:{ + return Emoji_0449, nil + } + case 450:{ + return Emoji_0450, nil + } + case 451:{ + return Emoji_0451, nil + } + case 452:{ + return Emoji_0452, nil + } + case 453:{ + return Emoji_0453, nil + } + case 454:{ + return Emoji_0454, nil + } + case 455:{ + return Emoji_0455, nil + } + case 456:{ + return Emoji_0456, nil + } + case 457:{ + return Emoji_0457, nil + } + case 458:{ + return Emoji_0458, nil + } + case 459:{ + return Emoji_0459, nil + } + case 460:{ + return Emoji_0460, nil + } + case 461:{ + return Emoji_0461, nil + } + case 462:{ + return Emoji_0462, nil + } + case 463:{ + return Emoji_0463, nil + } + case 464:{ + return Emoji_0464, nil + } + case 465:{ + return Emoji_0465, nil + } + case 466:{ + return Emoji_0466, nil + } + case 467:{ + return Emoji_0467, nil + } + case 468:{ + return Emoji_0468, nil + } + case 469:{ + return Emoji_0469, nil + } + case 470:{ + return Emoji_0470, nil + } + case 471:{ + return Emoji_0471, nil + } + case 472:{ + return Emoji_0472, nil + } + case 473:{ + return Emoji_0473, nil + } + case 474:{ + return Emoji_0474, nil + } + case 475:{ + return Emoji_0475, nil + } + case 476:{ + return Emoji_0476, nil + } + case 477:{ + return Emoji_0477, nil + } + case 478:{ + return Emoji_0478, nil + } + case 479:{ + return Emoji_0479, nil + } + case 480:{ + return Emoji_0480, nil + } + case 481:{ + return Emoji_0481, nil + } + case 482:{ + return Emoji_0482, nil + } + case 483:{ + return Emoji_0483, nil + } + case 484:{ + return Emoji_0484, nil + } + case 485:{ + return Emoji_0485, nil + } + case 486:{ + return Emoji_0486, nil + } + case 487:{ + return Emoji_0487, nil + } + case 488:{ + return Emoji_0488, nil + } + case 489:{ + return Emoji_0489, nil + } + case 490:{ + return Emoji_0490, nil + } + case 491:{ + return Emoji_0491, nil + } + case 492:{ + return Emoji_0492, nil + } + case 493:{ + return Emoji_0493, nil + } + case 494:{ + return Emoji_0494, nil + } + case 495:{ + return Emoji_0495, nil + } + case 496:{ + return Emoji_0496, nil + } + case 497:{ + return Emoji_0497, nil + } + case 498:{ + return Emoji_0498, nil + } + case 499:{ + return Emoji_0499, nil + } + case 500:{ + return Emoji_0500, nil + } + case 501:{ + return Emoji_0501, nil + } + case 502:{ + return Emoji_0502, nil + } + case 503:{ + return Emoji_0503, nil + } + case 504:{ + return Emoji_0504, nil + } + case 505:{ + return Emoji_0505, nil + } + case 506:{ + return Emoji_0506, nil + } + case 507:{ + return Emoji_0507, nil + } + case 508:{ + return Emoji_0508, nil + } + case 509:{ + return Emoji_0509, nil + } + case 510:{ + return Emoji_0510, nil + } + case 511:{ + return Emoji_0511, nil + } + case 512:{ + return Emoji_0512, nil + } + case 513:{ + return Emoji_0513, nil + } + case 514:{ + return Emoji_0514, nil + } + case 515:{ + return Emoji_0515, nil + } + case 516:{ + return Emoji_0516, nil + } + case 517:{ + return Emoji_0517, nil + } + case 518:{ + return Emoji_0518, nil + } + case 519:{ + return Emoji_0519, nil + } + case 520:{ + return Emoji_0520, nil + } + case 521:{ + return Emoji_0521, nil + } + case 522:{ + return Emoji_0522, nil + } + case 523:{ + return Emoji_0523, nil + } + case 524:{ + return Emoji_0524, nil + } + case 525:{ + return Emoji_0525, nil + } + case 526:{ + return Emoji_0526, nil + } + case 527:{ + return Emoji_0527, nil + } + case 528:{ + return Emoji_0528, nil + } + case 529:{ + return Emoji_0529, nil + } + case 530:{ + return Emoji_0530, nil + } + case 531:{ + return Emoji_0531, nil + } + case 532:{ + return Emoji_0532, nil + } + case 533:{ + return Emoji_0533, nil + } + case 534:{ + return Emoji_0534, nil + } + case 535:{ + return Emoji_0535, nil + } + case 536:{ + return Emoji_0536, nil + } + case 537:{ + return Emoji_0537, nil + } + case 538:{ + return Emoji_0538, nil + } + case 539:{ + return Emoji_0539, nil + } + case 540:{ + return Emoji_0540, nil + } + case 541:{ + return Emoji_0541, nil + } + case 542:{ + return Emoji_0542, nil + } + case 543:{ + return Emoji_0543, nil + } + case 544:{ + return Emoji_0544, nil + } + case 545:{ + return Emoji_0545, nil + } + case 546:{ + return Emoji_0546, nil + } + case 547:{ + return Emoji_0547, nil + } + case 548:{ + return Emoji_0548, nil + } + case 549:{ + return Emoji_0549, nil + } + case 550:{ + return Emoji_0550, nil + } + case 551:{ + return Emoji_0551, nil + } + case 552:{ + return Emoji_0552, nil + } + case 553:{ + return Emoji_0553, nil + } + case 554:{ + return Emoji_0554, nil + } + case 555:{ + return Emoji_0555, nil + } + case 556:{ + return Emoji_0556, nil + } + case 557:{ + return Emoji_0557, nil + } + case 558:{ + return Emoji_0558, nil + } + case 559:{ + return Emoji_0559, nil + } + case 560:{ + return Emoji_0560, nil + } + case 561:{ + return Emoji_0561, nil + } + case 562:{ + return Emoji_0562, nil + } + case 563:{ + return Emoji_0563, nil + } + case 564:{ + return Emoji_0564, nil + } + case 565:{ + return Emoji_0565, nil + } + case 566:{ + return Emoji_0566, nil + } + case 567:{ + return Emoji_0567, nil + } + case 568:{ + return Emoji_0568, nil + } + case 569:{ + return Emoji_0569, nil + } + case 570:{ + return Emoji_0570, nil + } + case 571:{ + return Emoji_0571, nil + } + case 572:{ + return Emoji_0572, nil + } + case 573:{ + return Emoji_0573, nil + } + case 574:{ + return Emoji_0574, nil + } + case 575:{ + return Emoji_0575, nil + } + case 576:{ + return Emoji_0576, nil + } + case 577:{ + return Emoji_0577, nil + } + case 578:{ + return Emoji_0578, nil + } + case 579:{ + return Emoji_0579, nil + } + case 580:{ + return Emoji_0580, nil + } + case 581:{ + return Emoji_0581, nil + } + case 582:{ + return Emoji_0582, nil + } + case 583:{ + return Emoji_0583, nil + } + case 584:{ + return Emoji_0584, nil + } + case 585:{ + return Emoji_0585, nil + } + case 586:{ + return Emoji_0586, nil + } + case 587:{ + return Emoji_0587, nil + } + case 588:{ + return Emoji_0588, nil + } + case 589:{ + return Emoji_0589, nil + } + case 590:{ + return Emoji_0590, nil + } + case 591:{ + return Emoji_0591, nil + } + case 592:{ + return Emoji_0592, nil + } + case 593:{ + return Emoji_0593, nil + } + case 594:{ + return Emoji_0594, nil + } + case 595:{ + return Emoji_0595, nil + } + case 596:{ + return Emoji_0596, nil + } + case 597:{ + return Emoji_0597, nil + } + case 598:{ + return Emoji_0598, nil + } + case 599:{ + return Emoji_0599, nil + } + case 600:{ + return Emoji_0600, nil + } + case 601:{ + return Emoji_0601, nil + } + case 602:{ + return Emoji_0602, nil + } + case 603:{ + return Emoji_0603, nil + } + case 604:{ + return Emoji_0604, nil + } + case 605:{ + return Emoji_0605, nil + } + case 606:{ + return Emoji_0606, nil + } + case 607:{ + return Emoji_0607, nil + } + case 608:{ + return Emoji_0608, nil + } + case 609:{ + return Emoji_0609, nil + } + case 610:{ + return Emoji_0610, nil + } + case 611:{ + return Emoji_0611, nil + } + case 612:{ + return Emoji_0612, nil + } + case 613:{ + return Emoji_0613, nil + } + case 614:{ + return Emoji_0614, nil + } + case 615:{ + return Emoji_0615, nil + } + case 616:{ + return Emoji_0616, nil + } + case 617:{ + return Emoji_0617, nil + } + case 618:{ + return Emoji_0618, nil + } + case 619:{ + return Emoji_0619, nil + } + case 620:{ + return Emoji_0620, nil + } + case 621:{ + return Emoji_0621, nil + } + case 622:{ + return Emoji_0622, nil + } + case 623:{ + return Emoji_0623, nil + } + case 624:{ + return Emoji_0624, nil + } + case 625:{ + return Emoji_0625, nil + } + case 626:{ + return Emoji_0626, nil + } + case 627:{ + return Emoji_0627, nil + } + case 628:{ + return Emoji_0628, nil + } + case 629:{ + return Emoji_0629, nil + } + case 630:{ + return Emoji_0630, nil + } + case 631:{ + return Emoji_0631, nil + } + case 632:{ + return Emoji_0632, nil + } + case 633:{ + return Emoji_0633, nil + } + case 634:{ + return Emoji_0634, nil + } + case 635:{ + return Emoji_0635, nil + } + case 636:{ + return Emoji_0636, nil + } + case 637:{ + return Emoji_0637, nil + } + case 638:{ + return Emoji_0638, nil + } + case 639:{ + return Emoji_0639, nil + } + case 640:{ + return Emoji_0640, nil + } + case 641:{ + return Emoji_0641, nil + } + case 642:{ + return Emoji_0642, nil + } + case 643:{ + return Emoji_0643, nil + } + case 644:{ + return Emoji_0644, nil + } + case 645:{ + return Emoji_0645, nil + } + case 646:{ + return Emoji_0646, nil + } + case 647:{ + return Emoji_0647, nil + } + case 648:{ + return Emoji_0648, nil + } + case 649:{ + return Emoji_0649, nil + } + case 650:{ + return Emoji_0650, nil + } + case 651:{ + return Emoji_0651, nil + } + case 652:{ + return Emoji_0652, nil + } + case 653:{ + return Emoji_0653, nil + } + case 654:{ + return Emoji_0654, nil + } + case 655:{ + return Emoji_0655, nil + } + case 656:{ + return Emoji_0656, nil + } + case 657:{ + return Emoji_0657, nil + } + case 658:{ + return Emoji_0658, nil + } + case 659:{ + return Emoji_0659, nil + } + case 660:{ + return Emoji_0660, nil + } + case 661:{ + return Emoji_0661, nil + } + case 662:{ + return Emoji_0662, nil + } + case 663:{ + return Emoji_0663, nil + } + case 664:{ + return Emoji_0664, nil + } + case 665:{ + return Emoji_0665, nil + } + case 666:{ + return Emoji_0666, nil + } + case 667:{ + return Emoji_0667, nil + } + case 668:{ + return Emoji_0668, nil + } + case 669:{ + return Emoji_0669, nil + } + case 670:{ + return Emoji_0670, nil + } + case 671:{ + return Emoji_0671, nil + } + case 672:{ + return Emoji_0672, nil + } + case 673:{ + return Emoji_0673, nil + } + case 674:{ + return Emoji_0674, nil + } + case 675:{ + return Emoji_0675, nil + } + case 676:{ + return Emoji_0676, nil + } + case 677:{ + return Emoji_0677, nil + } + case 678:{ + return Emoji_0678, nil + } + case 679:{ + return Emoji_0679, nil + } + case 680:{ + return Emoji_0680, nil + } + case 681:{ + return Emoji_0681, nil + } + case 682:{ + return Emoji_0682, nil + } + case 683:{ + return Emoji_0683, nil + } + case 684:{ + return Emoji_0684, nil + } + case 685:{ + return Emoji_0685, nil + } + case 686:{ + return Emoji_0686, nil + } + case 687:{ + return Emoji_0687, nil + } + case 688:{ + return Emoji_0688, nil + } + case 689:{ + return Emoji_0689, nil + } + case 690:{ + return Emoji_0690, nil + } + case 691:{ + return Emoji_0691, nil + } + case 692:{ + return Emoji_0692, nil + } + case 693:{ + return Emoji_0693, nil + } + case 694:{ + return Emoji_0694, nil + } + case 695:{ + return Emoji_0695, nil + } + case 696:{ + return Emoji_0696, nil + } + case 697:{ + return Emoji_0697, nil + } + case 698:{ + return Emoji_0698, nil + } + case 699:{ + return Emoji_0699, nil + } + case 700:{ + return Emoji_0700, nil + } + case 701:{ + return Emoji_0701, nil + } + case 702:{ + return Emoji_0702, nil + } + case 703:{ + return Emoji_0703, nil + } + case 704:{ + return Emoji_0704, nil + } + case 705:{ + return Emoji_0705, nil + } + case 706:{ + return Emoji_0706, nil + } + case 707:{ + return Emoji_0707, nil + } + case 708:{ + return Emoji_0708, nil + } + case 709:{ + return Emoji_0709, nil + } + case 710:{ + return Emoji_0710, nil + } + case 711:{ + return Emoji_0711, nil + } + case 712:{ + return Emoji_0712, nil + } + case 713:{ + return Emoji_0713, nil + } + case 714:{ + return Emoji_0714, nil + } + case 715:{ + return Emoji_0715, nil + } + case 716:{ + return Emoji_0716, nil + } + case 717:{ + return Emoji_0717, nil + } + case 718:{ + return Emoji_0718, nil + } + case 719:{ + return Emoji_0719, nil + } + case 720:{ + return Emoji_0720, nil + } + case 721:{ + return Emoji_0721, nil + } + case 722:{ + return Emoji_0722, nil + } + case 723:{ + return Emoji_0723, nil + } + case 724:{ + return Emoji_0724, nil + } + case 725:{ + return Emoji_0725, nil + } + case 726:{ + return Emoji_0726, nil + } + case 727:{ + return Emoji_0727, nil + } + case 728:{ + return Emoji_0728, nil + } + case 729:{ + return Emoji_0729, nil + } + case 730:{ + return Emoji_0730, nil + } + case 731:{ + return Emoji_0731, nil + } + case 732:{ + return Emoji_0732, nil + } + case 733:{ + return Emoji_0733, nil + } + case 734:{ + return Emoji_0734, nil + } + case 735:{ + return Emoji_0735, nil + } + case 736:{ + return Emoji_0736, nil + } + case 737:{ + return Emoji_0737, nil + } + case 738:{ + return Emoji_0738, nil + } + case 739:{ + return Emoji_0739, nil + } + case 740:{ + return Emoji_0740, nil + } + case 741:{ + return Emoji_0741, nil + } + case 742:{ + return Emoji_0742, nil + } + case 743:{ + return Emoji_0743, nil + } + case 744:{ + return Emoji_0744, nil + } + case 745:{ + return Emoji_0745, nil + } + case 746:{ + return Emoji_0746, nil + } + case 747:{ + return Emoji_0747, nil + } + case 748:{ + return Emoji_0748, nil + } + case 749:{ + return Emoji_0749, nil + } + case 750:{ + return Emoji_0750, nil + } + case 751:{ + return Emoji_0751, nil + } + case 752:{ + return Emoji_0752, nil + } + case 753:{ + return Emoji_0753, nil + } + case 754:{ + return Emoji_0754, nil + } + case 755:{ + return Emoji_0755, nil + } + case 756:{ + return Emoji_0756, nil + } + case 757:{ + return Emoji_0757, nil + } + case 758:{ + return Emoji_0758, nil + } + case 759:{ + return Emoji_0759, nil + } + case 760:{ + return Emoji_0760, nil + } + case 761:{ + return Emoji_0761, nil + } + case 762:{ + return Emoji_0762, nil + } + case 763:{ + return Emoji_0763, nil + } + case 764:{ + return Emoji_0764, nil + } + case 765:{ + return Emoji_0765, nil + } + case 766:{ + return Emoji_0766, nil + } + case 767:{ + return Emoji_0767, nil + } + case 768:{ + return Emoji_0768, nil + } + case 769:{ + return Emoji_0769, nil + } + case 770:{ + return Emoji_0770, nil + } + case 771:{ + return Emoji_0771, nil + } + case 772:{ + return Emoji_0772, nil + } + case 773:{ + return Emoji_0773, nil + } + case 774:{ + return Emoji_0774, nil + } + case 775:{ + return Emoji_0775, nil + } + case 776:{ + return Emoji_0776, nil + } + case 777:{ + return Emoji_0777, nil + } + case 778:{ + return Emoji_0778, nil + } + case 779:{ + return Emoji_0779, nil + } + case 780:{ + return Emoji_0780, nil + } + case 781:{ + return Emoji_0781, nil + } + case 782:{ + return Emoji_0782, nil + } + case 783:{ + return Emoji_0783, nil + } + case 784:{ + return Emoji_0784, nil + } + case 785:{ + return Emoji_0785, nil + } + case 786:{ + return Emoji_0786, nil + } + case 787:{ + return Emoji_0787, nil + } + case 788:{ + return Emoji_0788, nil + } + case 789:{ + return Emoji_0789, nil + } + case 790:{ + return Emoji_0790, nil + } + case 791:{ + return Emoji_0791, nil + } + case 792:{ + return Emoji_0792, nil + } + case 793:{ + return Emoji_0793, nil + } + case 794:{ + return Emoji_0794, nil + } + case 795:{ + return Emoji_0795, nil + } + case 796:{ + return Emoji_0796, nil + } + case 797:{ + return Emoji_0797, nil + } + case 798:{ + return Emoji_0798, nil + } + case 799:{ + return Emoji_0799, nil + } + case 800:{ + return Emoji_0800, nil + } + case 801:{ + return Emoji_0801, nil + } + case 802:{ + return Emoji_0802, nil + } + case 803:{ + return Emoji_0803, nil + } + case 804:{ + return Emoji_0804, nil + } + case 805:{ + return Emoji_0805, nil + } + case 806:{ + return Emoji_0806, nil + } + case 807:{ + return Emoji_0807, nil + } + case 808:{ + return Emoji_0808, nil + } + case 809:{ + return Emoji_0809, nil + } + case 810:{ + return Emoji_0810, nil + } + case 811:{ + return Emoji_0811, nil + } + case 812:{ + return Emoji_0812, nil + } + case 813:{ + return Emoji_0813, nil + } + case 814:{ + return Emoji_0814, nil + } + case 815:{ + return Emoji_0815, nil + } + case 816:{ + return Emoji_0816, nil + } + case 817:{ + return Emoji_0817, nil + } + case 818:{ + return Emoji_0818, nil + } + case 819:{ + return Emoji_0819, nil + } + case 820:{ + return Emoji_0820, nil + } + case 821:{ + return Emoji_0821, nil + } + case 822:{ + return Emoji_0822, nil + } + case 823:{ + return Emoji_0823, nil + } + case 824:{ + return Emoji_0824, nil + } + case 825:{ + return Emoji_0825, nil + } + case 826:{ + return Emoji_0826, nil + } + case 827:{ + return Emoji_0827, nil + } + case 828:{ + return Emoji_0828, nil + } + case 829:{ + return Emoji_0829, nil + } + case 830:{ + return Emoji_0830, nil + } + case 831:{ + return Emoji_0831, nil + } + case 832:{ + return Emoji_0832, nil + } + case 833:{ + return Emoji_0833, nil + } + case 834:{ + return Emoji_0834, nil + } + case 835:{ + return Emoji_0835, nil + } + case 836:{ + return Emoji_0836, nil + } + case 837:{ + return Emoji_0837, nil + } + case 838:{ + return Emoji_0838, nil + } + case 839:{ + return Emoji_0839, nil + } + case 840:{ + return Emoji_0840, nil + } + case 841:{ + return Emoji_0841, nil + } + case 842:{ + return Emoji_0842, nil + } + case 843:{ + return Emoji_0843, nil + } + case 844:{ + return Emoji_0844, nil + } + case 845:{ + return Emoji_0845, nil + } + case 846:{ + return Emoji_0846, nil + } + case 847:{ + return Emoji_0847, nil + } + case 848:{ + return Emoji_0848, nil + } + case 849:{ + return Emoji_0849, nil + } + case 850:{ + return Emoji_0850, nil + } + case 851:{ + return Emoji_0851, nil + } + case 852:{ + return Emoji_0852, nil + } + case 853:{ + return Emoji_0853, nil + } + case 854:{ + return Emoji_0854, nil + } + case 855:{ + return Emoji_0855, nil + } + case 856:{ + return Emoji_0856, nil + } + case 857:{ + return Emoji_0857, nil + } + case 858:{ + return Emoji_0858, nil + } + case 859:{ + return Emoji_0859, nil + } + case 860:{ + return Emoji_0860, nil + } + case 861:{ + return Emoji_0861, nil + } + case 862:{ + return Emoji_0862, nil + } + case 863:{ + return Emoji_0863, nil + } + case 864:{ + return Emoji_0864, nil + } + case 865:{ + return Emoji_0865, nil + } + case 866:{ + return Emoji_0866, nil + } + case 867:{ + return Emoji_0867, nil + } + case 868:{ + return Emoji_0868, nil + } + case 869:{ + return Emoji_0869, nil + } + case 870:{ + return Emoji_0870, nil + } + case 871:{ + return Emoji_0871, nil + } + case 872:{ + return Emoji_0872, nil + } + case 873:{ + return Emoji_0873, nil + } + case 874:{ + return Emoji_0874, nil + } + case 875:{ + return Emoji_0875, nil + } + case 876:{ + return Emoji_0876, nil + } + case 877:{ + return Emoji_0877, nil + } + case 878:{ + return Emoji_0878, nil + } + case 879:{ + return Emoji_0879, nil + } + case 880:{ + return Emoji_0880, nil + } + case 881:{ + return Emoji_0881, nil + } + case 882:{ + return Emoji_0882, nil + } + case 883:{ + return Emoji_0883, nil + } + case 884:{ + return Emoji_0884, nil + } + case 885:{ + return Emoji_0885, nil + } + case 886:{ + return Emoji_0886, nil + } + case 887:{ + return Emoji_0887, nil + } + case 888:{ + return Emoji_0888, nil + } + case 889:{ + return Emoji_0889, nil + } + case 890:{ + return Emoji_0890, nil + } + case 891:{ + return Emoji_0891, nil + } + case 892:{ + return Emoji_0892, nil + } + case 893:{ + return Emoji_0893, nil + } + case 894:{ + return Emoji_0894, nil + } + case 895:{ + return Emoji_0895, nil + } + case 896:{ + return Emoji_0896, nil + } + case 897:{ + return Emoji_0897, nil + } + case 898:{ + return Emoji_0898, nil + } + case 899:{ + return Emoji_0899, nil + } + case 900:{ + return Emoji_0900, nil + } + case 901:{ + return Emoji_0901, nil + } + case 902:{ + return Emoji_0902, nil + } + case 903:{ + return Emoji_0903, nil + } + case 904:{ + return Emoji_0904, nil + } + case 905:{ + return Emoji_0905, nil + } + case 906:{ + return Emoji_0906, nil + } + case 907:{ + return Emoji_0907, nil + } + case 908:{ + return Emoji_0908, nil + } + case 909:{ + return Emoji_0909, nil + } + case 910:{ + return Emoji_0910, nil + } + case 911:{ + return Emoji_0911, nil + } + case 912:{ + return Emoji_0912, nil + } + case 913:{ + return Emoji_0913, nil + } + case 914:{ + return Emoji_0914, nil + } + case 915:{ + return Emoji_0915, nil + } + case 916:{ + return Emoji_0916, nil + } + case 917:{ + return Emoji_0917, nil + } + case 918:{ + return Emoji_0918, nil + } + case 919:{ + return Emoji_0919, nil + } + case 920:{ + return Emoji_0920, nil + } + case 921:{ + return Emoji_0921, nil + } + case 922:{ + return Emoji_0922, nil + } + case 923:{ + return Emoji_0923, nil + } + case 924:{ + return Emoji_0924, nil + } + case 925:{ + return Emoji_0925, nil + } + case 926:{ + return Emoji_0926, nil + } + case 927:{ + return Emoji_0927, nil + } + case 928:{ + return Emoji_0928, nil + } + case 929:{ + return Emoji_0929, nil + } + case 930:{ + return Emoji_0930, nil + } + case 931:{ + return Emoji_0931, nil + } + case 932:{ + return Emoji_0932, nil + } + case 933:{ + return Emoji_0933, nil + } + case 934:{ + return Emoji_0934, nil + } + case 935:{ + return Emoji_0935, nil + } + case 936:{ + return Emoji_0936, nil + } + case 937:{ + return Emoji_0937, nil + } + case 938:{ + return Emoji_0938, nil + } + case 939:{ + return Emoji_0939, nil + } + case 940:{ + return Emoji_0940, nil + } + case 941:{ + return Emoji_0941, nil + } + case 942:{ + return Emoji_0942, nil + } + case 943:{ + return Emoji_0943, nil + } + case 944:{ + return Emoji_0944, nil + } + case 945:{ + return Emoji_0945, nil + } + case 946:{ + return Emoji_0946, nil + } + case 947:{ + return Emoji_0947, nil + } + case 948:{ + return Emoji_0948, nil + } + case 949:{ + return Emoji_0949, nil + } + case 950:{ + return Emoji_0950, nil + } + case 951:{ + return Emoji_0951, nil + } + case 952:{ + return Emoji_0952, nil + } + case 953:{ + return Emoji_0953, nil + } + case 954:{ + return Emoji_0954, nil + } + case 955:{ + return Emoji_0955, nil + } + case 956:{ + return Emoji_0956, nil + } + case 957:{ + return Emoji_0957, nil + } + case 958:{ + return Emoji_0958, nil + } + case 959:{ + return Emoji_0959, nil + } + case 960:{ + return Emoji_0960, nil + } + case 961:{ + return Emoji_0961, nil + } + case 962:{ + return Emoji_0962, nil + } + case 963:{ + return Emoji_0963, nil + } + case 964:{ + return Emoji_0964, nil + } + case 965:{ + return Emoji_0965, nil + } + case 966:{ + return Emoji_0966, nil + } + case 967:{ + return Emoji_0967, nil + } + case 968:{ + return Emoji_0968, nil + } + case 969:{ + return Emoji_0969, nil + } + case 970:{ + return Emoji_0970, nil + } + case 971:{ + return Emoji_0971, nil + } + case 972:{ + return Emoji_0972, nil + } + case 973:{ + return Emoji_0973, nil + } + case 974:{ + return Emoji_0974, nil + } + case 975:{ + return Emoji_0975, nil + } + case 976:{ + return Emoji_0976, nil + } + case 977:{ + return Emoji_0977, nil + } + case 978:{ + return Emoji_0978, nil + } + case 979:{ + return Emoji_0979, nil + } + case 980:{ + return Emoji_0980, nil + } + case 981:{ + return Emoji_0981, nil + } + case 982:{ + return Emoji_0982, nil + } + case 983:{ + return Emoji_0983, nil + } + case 984:{ + return Emoji_0984, nil + } + case 985:{ + return Emoji_0985, nil + } + case 986:{ + return Emoji_0986, nil + } + case 987:{ + return Emoji_0987, nil + } + case 988:{ + return Emoji_0988, nil + } + case 989:{ + return Emoji_0989, nil + } + case 990:{ + return Emoji_0990, nil + } + case 991:{ + return Emoji_0991, nil + } + case 992:{ + return Emoji_0992, nil + } + case 993:{ + return Emoji_0993, nil + } + case 994:{ + return Emoji_0994, nil + } + case 995:{ + return Emoji_0995, nil + } + case 996:{ + return Emoji_0996, nil + } + case 997:{ + return Emoji_0997, nil + } + case 998:{ + return Emoji_0998, nil + } + case 999:{ + return Emoji_0999, nil + } + case 1000:{ + return Emoji_1000, nil + } + case 1001:{ + return Emoji_1001, nil + } + case 1002:{ + return Emoji_1002, nil + } + case 1003:{ + return Emoji_1003, nil + } + case 1004:{ + return Emoji_1004, nil + } + case 1005:{ + return Emoji_1005, nil + } + case 1006:{ + return Emoji_1006, nil + } + case 1007:{ + return Emoji_1007, nil + } + case 1008:{ + return Emoji_1008, nil + } + case 1009:{ + return Emoji_1009, nil + } + case 1010:{ + return Emoji_1010, nil + } + case 1011:{ + return Emoji_1011, nil + } + case 1012:{ + return Emoji_1012, nil + } + case 1013:{ + return Emoji_1013, nil + } + case 1014:{ + return Emoji_1014, nil + } + case 1015:{ + return Emoji_1015, nil + } + case 1016:{ + return Emoji_1016, nil + } + case 1017:{ + return Emoji_1017, nil + } + case 1018:{ + return Emoji_1018, nil + } + case 1019:{ + return Emoji_1019, nil + } + case 1020:{ + return Emoji_1020, nil + } + case 1021:{ + return Emoji_1021, nil + } + case 1022:{ + return Emoji_1022, nil + } + case 1023:{ + return Emoji_1023, nil + } + case 1024:{ + return Emoji_1024, nil + } + case 1025:{ + return Emoji_1025, nil + } + case 1026:{ + return Emoji_1026, nil + } + case 1027:{ + return Emoji_1027, nil + } + case 1028:{ + return Emoji_1028, nil + } + case 1029:{ + return Emoji_1029, nil + } + case 1030:{ + return Emoji_1030, nil + } + case 1031:{ + return Emoji_1031, nil + } + case 1032:{ + return Emoji_1032, nil + } + case 1033:{ + return Emoji_1033, nil + } + case 1034:{ + return Emoji_1034, nil + } + case 1035:{ + return Emoji_1035, nil + } + case 1036:{ + return Emoji_1036, nil + } + case 1037:{ + return Emoji_1037, nil + } + case 1038:{ + return Emoji_1038, nil + } + case 1039:{ + return Emoji_1039, nil + } + case 1040:{ + return Emoji_1040, nil + } + case 1041:{ + return Emoji_1041, nil + } + case 1042:{ + return Emoji_1042, nil + } + case 1043:{ + return Emoji_1043, nil + } + case 1044:{ + return Emoji_1044, nil + } + case 1045:{ + return Emoji_1045, nil + } + case 1046:{ + return Emoji_1046, nil + } + case 1047:{ + return Emoji_1047, nil + } + case 1048:{ + return Emoji_1048, nil + } + case 1049:{ + return Emoji_1049, nil + } + case 1050:{ + return Emoji_1050, nil + } + case 1051:{ + return Emoji_1051, nil + } + case 1052:{ + return Emoji_1052, nil + } + case 1053:{ + return Emoji_1053, nil + } + case 1054:{ + return Emoji_1054, nil + } + case 1055:{ + return Emoji_1055, nil + } + case 1056:{ + return Emoji_1056, nil + } + case 1057:{ + return Emoji_1057, nil + } + case 1058:{ + return Emoji_1058, nil + } + case 1059:{ + return Emoji_1059, nil + } + case 1060:{ + return Emoji_1060, nil + } + case 1061:{ + return Emoji_1061, nil + } + case 1062:{ + return Emoji_1062, nil + } + case 1063:{ + return Emoji_1063, nil + } + case 1064:{ + return Emoji_1064, nil + } + case 1065:{ + return Emoji_1065, nil + } + case 1066:{ + return Emoji_1066, nil + } + case 1067:{ + return Emoji_1067, nil + } + case 1068:{ + return Emoji_1068, nil + } + case 1069:{ + return Emoji_1069, nil + } + case 1070:{ + return Emoji_1070, nil + } + case 1071:{ + return Emoji_1071, nil + } + case 1072:{ + return Emoji_1072, nil + } + case 1073:{ + return Emoji_1073, nil + } + case 1074:{ + return Emoji_1074, nil + } + case 1075:{ + return Emoji_1075, nil + } + case 1076:{ + return Emoji_1076, nil + } + case 1077:{ + return Emoji_1077, nil + } + case 1078:{ + return Emoji_1078, nil + } + case 1079:{ + return Emoji_1079, nil + } + case 1080:{ + return Emoji_1080, nil + } + case 1081:{ + return Emoji_1081, nil + } + case 1082:{ + return Emoji_1082, nil + } + case 1083:{ + return Emoji_1083, nil + } + case 1084:{ + return Emoji_1084, nil + } + case 1085:{ + return Emoji_1085, nil + } + case 1086:{ + return Emoji_1086, nil + } + case 1087:{ + return Emoji_1087, nil + } + case 1088:{ + return Emoji_1088, nil + } + case 1089:{ + return Emoji_1089, nil + } + case 1090:{ + return Emoji_1090, nil + } + case 1091:{ + return Emoji_1091, nil + } + case 1092:{ + return Emoji_1092, nil + } + case 1093:{ + return Emoji_1093, nil + } + case 1094:{ + return Emoji_1094, nil + } + case 1095:{ + return Emoji_1095, nil + } + case 1096:{ + return Emoji_1096, nil + } + case 1097:{ + return Emoji_1097, nil + } + case 1098:{ + return Emoji_1098, nil + } + case 1099:{ + return Emoji_1099, nil + } + case 1100:{ + return Emoji_1100, nil + } + case 1101:{ + return Emoji_1101, nil + } + case 1102:{ + return Emoji_1102, nil + } + case 1103:{ + return Emoji_1103, nil + } + case 1104:{ + return Emoji_1104, nil + } + case 1105:{ + return Emoji_1105, nil + } + case 1106:{ + return Emoji_1106, nil + } + case 1107:{ + return Emoji_1107, nil + } + case 1108:{ + return Emoji_1108, nil + } + case 1109:{ + return Emoji_1109, nil + } + case 1110:{ + return Emoji_1110, nil + } + case 1111:{ + return Emoji_1111, nil + } + case 1112:{ + return Emoji_1112, nil + } + case 1113:{ + return Emoji_1113, nil + } + case 1114:{ + return Emoji_1114, nil + } + case 1115:{ + return Emoji_1115, nil + } + case 1116:{ + return Emoji_1116, nil + } + case 1117:{ + return Emoji_1117, nil + } + case 1118:{ + return Emoji_1118, nil + } + case 1119:{ + return Emoji_1119, nil + } + case 1120:{ + return Emoji_1120, nil + } + case 1121:{ + return Emoji_1121, nil + } + case 1122:{ + return Emoji_1122, nil + } + case 1123:{ + return Emoji_1123, nil + } + case 1124:{ + return Emoji_1124, nil + } + case 1125:{ + return Emoji_1125, nil + } + case 1126:{ + return Emoji_1126, nil + } + case 1127:{ + return Emoji_1127, nil + } + case 1128:{ + return Emoji_1128, nil + } + case 1129:{ + return Emoji_1129, nil + } + case 1130:{ + return Emoji_1130, nil + } + case 1131:{ + return Emoji_1131, nil + } + case 1132:{ + return Emoji_1132, nil + } + case 1133:{ + return Emoji_1133, nil + } + case 1134:{ + return Emoji_1134, nil + } + case 1135:{ + return Emoji_1135, nil + } + case 1136:{ + return Emoji_1136, nil + } + case 1137:{ + return Emoji_1137, nil + } + case 1138:{ + return Emoji_1138, nil + } + case 1139:{ + return Emoji_1139, nil + } + case 1140:{ + return Emoji_1140, nil + } + case 1141:{ + return Emoji_1141, nil + } + case 1142:{ + return Emoji_1142, nil + } + case 1143:{ + return Emoji_1143, nil + } + case 1144:{ + return Emoji_1144, nil + } + case 1145:{ + return Emoji_1145, nil + } + case 1146:{ + return Emoji_1146, nil + } + case 1147:{ + return Emoji_1147, nil + } + case 1148:{ + return Emoji_1148, nil + } + case 1149:{ + return Emoji_1149, nil + } + case 1150:{ + return Emoji_1150, nil + } + case 1151:{ + return Emoji_1151, nil + } + case 1152:{ + return Emoji_1152, nil + } + case 1153:{ + return Emoji_1153, nil + } + case 1154:{ + return Emoji_1154, nil + } + case 1155:{ + return Emoji_1155, nil + } + case 1156:{ + return Emoji_1156, nil + } + case 1157:{ + return Emoji_1157, nil + } + case 1158:{ + return Emoji_1158, nil + } + case 1159:{ + return Emoji_1159, nil + } + case 1160:{ + return Emoji_1160, nil + } + case 1161:{ + return Emoji_1161, nil + } + case 1162:{ + return Emoji_1162, nil + } + case 1163:{ + return Emoji_1163, nil + } + case 1164:{ + return Emoji_1164, nil + } + case 1165:{ + return Emoji_1165, nil + } + case 1166:{ + return Emoji_1166, nil + } + case 1167:{ + return Emoji_1167, nil + } + case 1168:{ + return Emoji_1168, nil + } + case 1169:{ + return Emoji_1169, nil + } + case 1170:{ + return Emoji_1170, nil + } + case 1171:{ + return Emoji_1171, nil + } + case 1172:{ + return Emoji_1172, nil + } + case 1173:{ + return Emoji_1173, nil + } + case 1174:{ + return Emoji_1174, nil + } + case 1175:{ + return Emoji_1175, nil + } + case 1176:{ + return Emoji_1176, nil + } + case 1177:{ + return Emoji_1177, nil + } + case 1178:{ + return Emoji_1178, nil + } + case 1179:{ + return Emoji_1179, nil + } + case 1180:{ + return Emoji_1180, nil + } + case 1181:{ + return Emoji_1181, nil + } + case 1182:{ + return Emoji_1182, nil + } + case 1183:{ + return Emoji_1183, nil + } + case 1184:{ + return Emoji_1184, nil + } + case 1185:{ + return Emoji_1185, nil + } + case 1186:{ + return Emoji_1186, nil + } + case 1187:{ + return Emoji_1187, nil + } + case 1188:{ + return Emoji_1188, nil + } + case 1189:{ + return Emoji_1189, nil + } + case 1190:{ + return Emoji_1190, nil + } + case 1191:{ + return Emoji_1191, nil + } + case 1192:{ + return Emoji_1192, nil + } + case 1193:{ + return Emoji_1193, nil + } + case 1194:{ + return Emoji_1194, nil + } + case 1195:{ + return Emoji_1195, nil + } + case 1196:{ + return Emoji_1196, nil + } + case 1197:{ + return Emoji_1197, nil + } + case 1198:{ + return Emoji_1198, nil + } + case 1199:{ + return Emoji_1199, nil + } + case 1200:{ + return Emoji_1200, nil + } + case 1201:{ + return Emoji_1201, nil + } + case 1202:{ + return Emoji_1202, nil + } + case 1203:{ + return Emoji_1203, nil + } + case 1204:{ + return Emoji_1204, nil + } + case 1205:{ + return Emoji_1205, nil + } + case 1206:{ + return Emoji_1206, nil + } + case 1207:{ + return Emoji_1207, nil + } + case 1208:{ + return Emoji_1208, nil + } + case 1209:{ + return Emoji_1209, nil + } + case 1210:{ + return Emoji_1210, nil + } + case 1211:{ + return Emoji_1211, nil + } + case 1212:{ + return Emoji_1212, nil + } + case 1213:{ + return Emoji_1213, nil + } + case 1214:{ + return Emoji_1214, nil + } + case 1215:{ + return Emoji_1215, nil + } + case 1216:{ + return Emoji_1216, nil + } + case 1217:{ + return Emoji_1217, nil + } + case 1218:{ + return Emoji_1218, nil + } + case 1219:{ + return Emoji_1219, nil + } + case 1220:{ + return Emoji_1220, nil + } + case 1221:{ + return Emoji_1221, nil + } + case 1222:{ + return Emoji_1222, nil + } + case 1223:{ + return Emoji_1223, nil + } + case 1224:{ + return Emoji_1224, nil + } + case 1225:{ + return Emoji_1225, nil + } + case 1226:{ + return Emoji_1226, nil + } + case 1227:{ + return Emoji_1227, nil + } + case 1228:{ + return Emoji_1228, nil + } + case 1229:{ + return Emoji_1229, nil + } + case 1230:{ + return Emoji_1230, nil + } + case 1231:{ + return Emoji_1231, nil + } + case 1232:{ + return Emoji_1232, nil + } + case 1233:{ + return Emoji_1233, nil + } + case 1234:{ + return Emoji_1234, nil + } + case 1235:{ + return Emoji_1235, nil + } + case 1236:{ + return Emoji_1236, nil + } + case 1237:{ + return Emoji_1237, nil + } + case 1238:{ + return Emoji_1238, nil + } + case 1239:{ + return Emoji_1239, nil + } + case 1240:{ + return Emoji_1240, nil + } + case 1241:{ + return Emoji_1241, nil + } + case 1242:{ + return Emoji_1242, nil + } + case 1243:{ + return Emoji_1243, nil + } + case 1244:{ + return Emoji_1244, nil + } + case 1245:{ + return Emoji_1245, nil + } + case 1246:{ + return Emoji_1246, nil + } + case 1247:{ + return Emoji_1247, nil + } + case 1248:{ + return Emoji_1248, nil + } + case 1249:{ + return Emoji_1249, nil + } + case 1250:{ + return Emoji_1250, nil + } + case 1251:{ + return Emoji_1251, nil + } + case 1252:{ + return Emoji_1252, nil + } + case 1253:{ + return Emoji_1253, nil + } + case 1254:{ + return Emoji_1254, nil + } + case 1255:{ + return Emoji_1255, nil + } + case 1256:{ + return Emoji_1256, nil + } + case 1257:{ + return Emoji_1257, nil + } + case 1258:{ + return Emoji_1258, nil + } + case 1259:{ + return Emoji_1259, nil + } + case 1260:{ + return Emoji_1260, nil + } + case 1261:{ + return Emoji_1261, nil + } + case 1262:{ + return Emoji_1262, nil + } + case 1263:{ + return Emoji_1263, nil + } + case 1264:{ + return Emoji_1264, nil + } + case 1265:{ + return Emoji_1265, nil + } + case 1266:{ + return Emoji_1266, nil + } + case 1267:{ + return Emoji_1267, nil + } + case 1268:{ + return Emoji_1268, nil + } + case 1269:{ + return Emoji_1269, nil + } + case 1270:{ + return Emoji_1270, nil + } + case 1271:{ + return Emoji_1271, nil + } + case 1272:{ + return Emoji_1272, nil + } + case 1273:{ + return Emoji_1273, nil + } + case 1274:{ + return Emoji_1274, nil + } + case 1275:{ + return Emoji_1275, nil + } + case 1276:{ + return Emoji_1276, nil + } + case 1277:{ + return Emoji_1277, nil + } + case 1278:{ + return Emoji_1278, nil + } + case 1279:{ + return Emoji_1279, nil + } + case 1280:{ + return Emoji_1280, nil + } + case 1281:{ + return Emoji_1281, nil + } + case 1282:{ + return Emoji_1282, nil + } + case 1283:{ + return Emoji_1283, nil + } + case 1284:{ + return Emoji_1284, nil + } + case 1285:{ + return Emoji_1285, nil + } + case 1286:{ + return Emoji_1286, nil + } + case 1287:{ + return Emoji_1287, nil + } + case 1288:{ + return Emoji_1288, nil + } + case 1289:{ + return Emoji_1289, nil + } + case 1290:{ + return Emoji_1290, nil + } + case 1291:{ + return Emoji_1291, nil + } + case 1292:{ + return Emoji_1292, nil + } + case 1293:{ + return Emoji_1293, nil + } + case 1294:{ + return Emoji_1294, nil + } + case 1295:{ + return Emoji_1295, nil + } + case 1296:{ + return Emoji_1296, nil + } + case 1297:{ + return Emoji_1297, nil + } + case 1298:{ + return Emoji_1298, nil + } + case 1299:{ + return Emoji_1299, nil + } + case 1300:{ + return Emoji_1300, nil + } + case 1301:{ + return Emoji_1301, nil + } + case 1302:{ + return Emoji_1302, nil + } + case 1303:{ + return Emoji_1303, nil + } + case 1304:{ + return Emoji_1304, nil + } + case 1305:{ + return Emoji_1305, nil + } + case 1306:{ + return Emoji_1306, nil + } + case 1307:{ + return Emoji_1307, nil + } + case 1308:{ + return Emoji_1308, nil + } + case 1309:{ + return Emoji_1309, nil + } + case 1310:{ + return Emoji_1310, nil + } + case 1311:{ + return Emoji_1311, nil + } + case 1312:{ + return Emoji_1312, nil + } + case 1313:{ + return Emoji_1313, nil + } + case 1314:{ + return Emoji_1314, nil + } + case 1315:{ + return Emoji_1315, nil + } + case 1316:{ + return Emoji_1316, nil + } + case 1317:{ + return Emoji_1317, nil + } + case 1318:{ + return Emoji_1318, nil + } + case 1319:{ + return Emoji_1319, nil + } + case 1320:{ + return Emoji_1320, nil + } + case 1321:{ + return Emoji_1321, nil + } + case 1322:{ + return Emoji_1322, nil + } + case 1323:{ + return Emoji_1323, nil + } + case 1324:{ + return Emoji_1324, nil + } + case 1325:{ + return Emoji_1325, nil + } + case 1326:{ + return Emoji_1326, nil + } + case 1327:{ + return Emoji_1327, nil + } + case 1328:{ + return Emoji_1328, nil + } + case 1329:{ + return Emoji_1329, nil + } + case 1330:{ + return Emoji_1330, nil + } + case 1331:{ + return Emoji_1331, nil + } + case 1332:{ + return Emoji_1332, nil + } + case 1333:{ + return Emoji_1333, nil + } + case 1334:{ + return Emoji_1334, nil + } + case 1335:{ + return Emoji_1335, nil + } + case 1336:{ + return Emoji_1336, nil + } + case 1337:{ + return Emoji_1337, nil + } + case 1338:{ + return Emoji_1338, nil + } + case 1339:{ + return Emoji_1339, nil + } + case 1340:{ + return Emoji_1340, nil + } + case 1341:{ + return Emoji_1341, nil + } + case 1342:{ + return Emoji_1342, nil + } + case 1343:{ + return Emoji_1343, nil + } + case 1344:{ + return Emoji_1344, nil + } + case 1345:{ + return Emoji_1345, nil + } + case 1346:{ + return Emoji_1346, nil + } + case 1347:{ + return Emoji_1347, nil + } + case 1348:{ + return Emoji_1348, nil + } + case 1349:{ + return Emoji_1349, nil + } + case 1350:{ + return Emoji_1350, nil + } + case 1351:{ + return Emoji_1351, nil + } + case 1352:{ + return Emoji_1352, nil + } + case 1353:{ + return Emoji_1353, nil + } + case 1354:{ + return Emoji_1354, nil + } + case 1355:{ + return Emoji_1355, nil + } + case 1356:{ + return Emoji_1356, nil + } + case 1357:{ + return Emoji_1357, nil + } + case 1358:{ + return Emoji_1358, nil + } + case 1359:{ + return Emoji_1359, nil + } + case 1360:{ + return Emoji_1360, nil + } + case 1361:{ + return Emoji_1361, nil + } + case 1362:{ + return Emoji_1362, nil + } + case 1363:{ + return Emoji_1363, nil + } + case 1364:{ + return Emoji_1364, nil + } + case 1365:{ + return Emoji_1365, nil + } + case 1366:{ + return Emoji_1366, nil + } + case 1367:{ + return Emoji_1367, nil + } + case 1368:{ + return Emoji_1368, nil + } + case 1369:{ + return Emoji_1369, nil + } + case 1370:{ + return Emoji_1370, nil + } + case 1371:{ + return Emoji_1371, nil + } + case 1372:{ + return Emoji_1372, nil + } + case 1373:{ + return Emoji_1373, nil + } + case 1374:{ + return Emoji_1374, nil + } + case 1375:{ + return Emoji_1375, nil + } + case 1376:{ + return Emoji_1376, nil + } + case 1377:{ + return Emoji_1377, nil + } + case 1378:{ + return Emoji_1378, nil + } + case 1379:{ + return Emoji_1379, nil + } + case 1380:{ + return Emoji_1380, nil + } + case 1381:{ + return Emoji_1381, nil + } + case 1382:{ + return Emoji_1382, nil + } + case 1383:{ + return Emoji_1383, nil + } + case 1384:{ + return Emoji_1384, nil + } + case 1385:{ + return Emoji_1385, nil + } + case 1386:{ + return Emoji_1386, nil + } + case 1387:{ + return Emoji_1387, nil + } + case 1388:{ + return Emoji_1388, nil + } + case 1389:{ + return Emoji_1389, nil + } + case 1390:{ + return Emoji_1390, nil + } + case 1391:{ + return Emoji_1391, nil + } + case 1392:{ + return Emoji_1392, nil + } + case 1393:{ + return Emoji_1393, nil + } + case 1394:{ + return Emoji_1394, nil + } + case 1395:{ + return Emoji_1395, nil + } + case 1396:{ + return Emoji_1396, nil + } + case 1397:{ + return Emoji_1397, nil + } + case 1398:{ + return Emoji_1398, nil + } + case 1399:{ + return Emoji_1399, nil + } + case 1400:{ + return Emoji_1400, nil + } + case 1401:{ + return Emoji_1401, nil + } + case 1402:{ + return Emoji_1402, nil + } + case 1403:{ + return Emoji_1403, nil + } + case 1404:{ + return Emoji_1404, nil + } + case 1405:{ + return Emoji_1405, nil + } + case 1406:{ + return Emoji_1406, nil + } + case 1407:{ + return Emoji_1407, nil + } + case 1408:{ + return Emoji_1408, nil + } + case 1409:{ + return Emoji_1409, nil + } + case 1410:{ + return Emoji_1410, nil + } + case 1411:{ + return Emoji_1411, nil + } + case 1412:{ + return Emoji_1412, nil + } + case 1413:{ + return Emoji_1413, nil + } + case 1414:{ + return Emoji_1414, nil + } + case 1415:{ + return Emoji_1415, nil + } + case 1416:{ + return Emoji_1416, nil + } + case 1417:{ + return Emoji_1417, nil + } + case 1418:{ + return Emoji_1418, nil + } + case 1419:{ + return Emoji_1419, nil + } + case 1420:{ + return Emoji_1420, nil + } + case 1421:{ + return Emoji_1421, nil + } + case 1422:{ + return Emoji_1422, nil + } + case 1423:{ + return Emoji_1423, nil + } + case 1424:{ + return Emoji_1424, nil + } + case 1425:{ + return Emoji_1425, nil + } + case 1426:{ + return Emoji_1426, nil + } + case 1427:{ + return Emoji_1427, nil + } + case 1428:{ + return Emoji_1428, nil + } + case 1429:{ + return Emoji_1429, nil + } + case 1430:{ + return Emoji_1430, nil + } + case 1431:{ + return Emoji_1431, nil + } + case 1432:{ + return Emoji_1432, nil + } + case 1433:{ + return Emoji_1433, nil + } + case 1434:{ + return Emoji_1434, nil + } + case 1435:{ + return Emoji_1435, nil + } + case 1436:{ + return Emoji_1436, nil + } + case 1437:{ + return Emoji_1437, nil + } + case 1438:{ + return Emoji_1438, nil + } + case 1439:{ + return Emoji_1439, nil + } + case 1440:{ + return Emoji_1440, nil + } + case 1441:{ + return Emoji_1441, nil + } + case 1442:{ + return Emoji_1442, nil + } + case 1443:{ + return Emoji_1443, nil + } + case 1444:{ + return Emoji_1444, nil + } + case 1445:{ + return Emoji_1445, nil + } + case 1446:{ + return Emoji_1446, nil + } + case 1447:{ + return Emoji_1447, nil + } + case 1448:{ + return Emoji_1448, nil + } + case 1449:{ + return Emoji_1449, nil + } + case 1450:{ + return Emoji_1450, nil + } + case 1451:{ + return Emoji_1451, nil + } + case 1452:{ + return Emoji_1452, nil + } + case 1453:{ + return Emoji_1453, nil + } + case 1454:{ + return Emoji_1454, nil + } + case 1455:{ + return Emoji_1455, nil + } + case 1456:{ + return Emoji_1456, nil + } + case 1457:{ + return Emoji_1457, nil + } + case 1458:{ + return Emoji_1458, nil + } + case 1459:{ + return Emoji_1459, nil + } + case 1460:{ + return Emoji_1460, nil + } + case 1461:{ + return Emoji_1461, nil + } + case 1462:{ + return Emoji_1462, nil + } + case 1463:{ + return Emoji_1463, nil + } + case 1464:{ + return Emoji_1464, nil + } + case 1465:{ + return Emoji_1465, nil + } + case 1466:{ + return Emoji_1466, nil + } + case 1467:{ + return Emoji_1467, nil + } + case 1468:{ + return Emoji_1468, nil + } + case 1469:{ + return Emoji_1469, nil + } + case 1470:{ + return Emoji_1470, nil + } + case 1471:{ + return Emoji_1471, nil + } + case 1472:{ + return Emoji_1472, nil + } + case 1473:{ + return Emoji_1473, nil + } + case 1474:{ + return Emoji_1474, nil + } + case 1475:{ + return Emoji_1475, nil + } + case 1476:{ + return Emoji_1476, nil + } + case 1477:{ + return Emoji_1477, nil + } + case 1478:{ + return Emoji_1478, nil + } + case 1479:{ + return Emoji_1479, nil + } + case 1480:{ + return Emoji_1480, nil + } + case 1481:{ + return Emoji_1481, nil + } + case 1482:{ + return Emoji_1482, nil + } + case 1483:{ + return Emoji_1483, nil + } + case 1484:{ + return Emoji_1484, nil + } + case 1485:{ + return Emoji_1485, nil + } + case 1486:{ + return Emoji_1486, nil + } + case 1487:{ + return Emoji_1487, nil + } + case 1488:{ + return Emoji_1488, nil + } + case 1489:{ + return Emoji_1489, nil + } + case 1490:{ + return Emoji_1490, nil + } + case 1491:{ + return Emoji_1491, nil + } + case 1492:{ + return Emoji_1492, nil + } + case 1493:{ + return Emoji_1493, nil + } + case 1494:{ + return Emoji_1494, nil + } + case 1495:{ + return Emoji_1495, nil + } + case 1496:{ + return Emoji_1496, nil + } + case 1497:{ + return Emoji_1497, nil + } + case 1498:{ + return Emoji_1498, nil + } + case 1499:{ + return Emoji_1499, nil + } + case 1500:{ + return Emoji_1500, nil + } + case 1501:{ + return Emoji_1501, nil + } + case 1502:{ + return Emoji_1502, nil + } + case 1503:{ + return Emoji_1503, nil + } + case 1504:{ + return Emoji_1504, nil + } + case 1505:{ + return Emoji_1505, nil + } + case 1506:{ + return Emoji_1506, nil + } + case 1507:{ + return Emoji_1507, nil + } + case 1508:{ + return Emoji_1508, nil + } + case 1509:{ + return Emoji_1509, nil + } + case 1510:{ + return Emoji_1510, nil + } + case 1511:{ + return Emoji_1511, nil + } + case 1512:{ + return Emoji_1512, nil + } + case 1513:{ + return Emoji_1513, nil + } + case 1514:{ + return Emoji_1514, nil + } + case 1515:{ + return Emoji_1515, nil + } + case 1516:{ + return Emoji_1516, nil + } + case 1517:{ + return Emoji_1517, nil + } + case 1518:{ + return Emoji_1518, nil + } + case 1519:{ + return Emoji_1519, nil + } + case 1520:{ + return Emoji_1520, nil + } + case 1521:{ + return Emoji_1521, nil + } + case 1522:{ + return Emoji_1522, nil + } + case 1523:{ + return Emoji_1523, nil + } + case 1524:{ + return Emoji_1524, nil + } + case 1525:{ + return Emoji_1525, nil + } + case 1526:{ + return Emoji_1526, nil + } + case 1527:{ + return Emoji_1527, nil + } + case 1528:{ + return Emoji_1528, nil + } + case 1529:{ + return Emoji_1529, nil + } + case 1530:{ + return Emoji_1530, nil + } + case 1531:{ + return Emoji_1531, nil + } + case 1532:{ + return Emoji_1532, nil + } + case 1533:{ + return Emoji_1533, nil + } + case 1534:{ + return Emoji_1534, nil + } + case 1535:{ + return Emoji_1535, nil + } + case 1536:{ + return Emoji_1536, nil + } + case 1537:{ + return Emoji_1537, nil + } + case 1538:{ + return Emoji_1538, nil + } + case 1539:{ + return Emoji_1539, nil + } + case 1540:{ + return Emoji_1540, nil + } + case 1541:{ + return Emoji_1541, nil + } + case 1542:{ + return Emoji_1542, nil + } + case 1543:{ + return Emoji_1543, nil + } + case 1544:{ + return Emoji_1544, nil + } + case 1545:{ + return Emoji_1545, nil + } + case 1546:{ + return Emoji_1546, nil + } + case 1547:{ + return Emoji_1547, nil + } + case 1548:{ + return Emoji_1548, nil + } + case 1549:{ + return Emoji_1549, nil + } + case 1550:{ + return Emoji_1550, nil + } + case 1551:{ + return Emoji_1551, nil + } + case 1552:{ + return Emoji_1552, nil + } + case 1553:{ + return Emoji_1553, nil + } + case 1554:{ + return Emoji_1554, nil + } + case 1555:{ + return Emoji_1555, nil + } + case 1556:{ + return Emoji_1556, nil + } + case 1557:{ + return Emoji_1557, nil + } + case 1558:{ + return Emoji_1558, nil + } + case 1559:{ + return Emoji_1559, nil + } + case 1560:{ + return Emoji_1560, nil + } + case 1561:{ + return Emoji_1561, nil + } + case 1562:{ + return Emoji_1562, nil + } + case 1563:{ + return Emoji_1563, nil + } + case 1564:{ + return Emoji_1564, nil + } + case 1565:{ + return Emoji_1565, nil + } + case 1566:{ + return Emoji_1566, nil + } + case 1567:{ + return Emoji_1567, nil + } + case 1568:{ + return Emoji_1568, nil + } + case 1569:{ + return Emoji_1569, nil + } + case 1570:{ + return Emoji_1570, nil + } + case 1571:{ + return Emoji_1571, nil + } + case 1572:{ + return Emoji_1572, nil + } + case 1573:{ + return Emoji_1573, nil + } + case 1574:{ + return Emoji_1574, nil + } + case 1575:{ + return Emoji_1575, nil + } + case 1576:{ + return Emoji_1576, nil + } + case 1577:{ + return Emoji_1577, nil + } + case 1578:{ + return Emoji_1578, nil + } + case 1579:{ + return Emoji_1579, nil + } + case 1580:{ + return Emoji_1580, nil + } + case 1581:{ + return Emoji_1581, nil + } + case 1582:{ + return Emoji_1582, nil + } + case 1583:{ + return Emoji_1583, nil + } + case 1584:{ + return Emoji_1584, nil + } + case 1585:{ + return Emoji_1585, nil + } + case 1586:{ + return Emoji_1586, nil + } + case 1587:{ + return Emoji_1587, nil + } + case 1588:{ + return Emoji_1588, nil + } + case 1589:{ + return Emoji_1589, nil + } + case 1590:{ + return Emoji_1590, nil + } + case 1591:{ + return Emoji_1591, nil + } + case 1592:{ + return Emoji_1592, nil + } + case 1593:{ + return Emoji_1593, nil + } + case 1594:{ + return Emoji_1594, nil + } + case 1595:{ + return Emoji_1595, nil + } + case 1596:{ + return Emoji_1596, nil + } + case 1597:{ + return Emoji_1597, nil + } + case 1598:{ + return Emoji_1598, nil + } + case 1599:{ + return Emoji_1599, nil + } + case 1600:{ + return Emoji_1600, nil + } + case 1601:{ + return Emoji_1601, nil + } + case 1602:{ + return Emoji_1602, nil + } + case 1603:{ + return Emoji_1603, nil + } + case 1604:{ + return Emoji_1604, nil + } + case 1605:{ + return Emoji_1605, nil + } + case 1606:{ + return Emoji_1606, nil + } + case 1607:{ + return Emoji_1607, nil + } + case 1608:{ + return Emoji_1608, nil + } + case 1609:{ + return Emoji_1609, nil + } + case 1610:{ + return Emoji_1610, nil + } + case 1611:{ + return Emoji_1611, nil + } + case 1612:{ + return Emoji_1612, nil + } + case 1613:{ + return Emoji_1613, nil + } + case 1614:{ + return Emoji_1614, nil + } + case 1615:{ + return Emoji_1615, nil + } + case 1616:{ + return Emoji_1616, nil + } + case 1617:{ + return Emoji_1617, nil + } + case 1618:{ + return Emoji_1618, nil + } + case 1619:{ + return Emoji_1619, nil + } + case 1620:{ + return Emoji_1620, nil + } + case 1621:{ + return Emoji_1621, nil + } + case 1622:{ + return Emoji_1622, nil + } + case 1623:{ + return Emoji_1623, nil + } + case 1624:{ + return Emoji_1624, nil + } + case 1625:{ + return Emoji_1625, nil + } + case 1626:{ + return Emoji_1626, nil + } + case 1627:{ + return Emoji_1627, nil + } + case 1628:{ + return Emoji_1628, nil + } + case 1629:{ + return Emoji_1629, nil + } + case 1630:{ + return Emoji_1630, nil + } + case 1631:{ + return Emoji_1631, nil + } + case 1632:{ + return Emoji_1632, nil + } + case 1633:{ + return Emoji_1633, nil + } + case 1634:{ + return Emoji_1634, nil + } + case 1635:{ + return Emoji_1635, nil + } + case 1636:{ + return Emoji_1636, nil + } + case 1637:{ + return Emoji_1637, nil + } + case 1638:{ + return Emoji_1638, nil + } + case 1639:{ + return Emoji_1639, nil + } + case 1640:{ + return Emoji_1640, nil + } + case 1641:{ + return Emoji_1641, nil + } + case 1642:{ + return Emoji_1642, nil + } + case 1643:{ + return Emoji_1643, nil + } + case 1644:{ + return Emoji_1644, nil + } + case 1645:{ + return Emoji_1645, nil + } + case 1646:{ + return Emoji_1646, nil + } + case 1647:{ + return Emoji_1647, nil + } + case 1648:{ + return Emoji_1648, nil + } + case 1649:{ + return Emoji_1649, nil + } + case 1650:{ + return Emoji_1650, nil + } + case 1651:{ + return Emoji_1651, nil + } + case 1652:{ + return Emoji_1652, nil + } + case 1653:{ + return Emoji_1653, nil + } + case 1654:{ + return Emoji_1654, nil + } + case 1655:{ + return Emoji_1655, nil + } + case 1656:{ + return Emoji_1656, nil + } + case 1657:{ + return Emoji_1657, nil + } + case 1658:{ + return Emoji_1658, nil + } + case 1659:{ + return Emoji_1659, nil + } + case 1660:{ + return Emoji_1660, nil + } + case 1661:{ + return Emoji_1661, nil + } + case 1662:{ + return Emoji_1662, nil + } + case 1663:{ + return Emoji_1663, nil + } + case 1664:{ + return Emoji_1664, nil + } + case 1665:{ + return Emoji_1665, nil + } + case 1666:{ + return Emoji_1666, nil + } + case 1667:{ + return Emoji_1667, nil + } + case 1668:{ + return Emoji_1668, nil + } + case 1669:{ + return Emoji_1669, nil + } + case 1670:{ + return Emoji_1670, nil + } + case 1671:{ + return Emoji_1671, nil + } + case 1672:{ + return Emoji_1672, nil + } + case 1673:{ + return Emoji_1673, nil + } + case 1674:{ + return Emoji_1674, nil + } + case 1675:{ + return Emoji_1675, nil + } + case 1676:{ + return Emoji_1676, nil + } + case 1677:{ + return Emoji_1677, nil + } + case 1678:{ + return Emoji_1678, nil + } + case 1679:{ + return Emoji_1679, nil + } + case 1680:{ + return Emoji_1680, nil + } + case 1681:{ + return Emoji_1681, nil + } + case 1682:{ + return Emoji_1682, nil + } + case 1683:{ + return Emoji_1683, nil + } + case 1684:{ + return Emoji_1684, nil + } + case 1685:{ + return Emoji_1685, nil + } + case 1686:{ + return Emoji_1686, nil + } + case 1687:{ + return Emoji_1687, nil + } + case 1688:{ + return Emoji_1688, nil + } + case 1689:{ + return Emoji_1689, nil + } + case 1690:{ + return Emoji_1690, nil + } + case 1691:{ + return Emoji_1691, nil + } + case 1692:{ + return Emoji_1692, nil + } + case 1693:{ + return Emoji_1693, nil + } + case 1694:{ + return Emoji_1694, nil + } + case 1695:{ + return Emoji_1695, nil + } + case 1696:{ + return Emoji_1696, nil + } + case 1697:{ + return Emoji_1697, nil + } + case 1698:{ + return Emoji_1698, nil + } + case 1699:{ + return Emoji_1699, nil + } + case 1700:{ + return Emoji_1700, nil + } + case 1701:{ + return Emoji_1701, nil + } + case 1702:{ + return Emoji_1702, nil + } + case 1703:{ + return Emoji_1703, nil + } + case 1704:{ + return Emoji_1704, nil + } + case 1705:{ + return Emoji_1705, nil + } + case 1706:{ + return Emoji_1706, nil + } + case 1707:{ + return Emoji_1707, nil + } + case 1708:{ + return Emoji_1708, nil + } + case 1709:{ + return Emoji_1709, nil + } + case 1710:{ + return Emoji_1710, nil + } + case 1711:{ + return Emoji_1711, nil + } + case 1712:{ + return Emoji_1712, nil + } + case 1713:{ + return Emoji_1713, nil + } + case 1714:{ + return Emoji_1714, nil + } + case 1715:{ + return Emoji_1715, nil + } + case 1716:{ + return Emoji_1716, nil + } + case 1717:{ + return Emoji_1717, nil + } + case 1718:{ + return Emoji_1718, nil + } + case 1719:{ + return Emoji_1719, nil + } + case 1720:{ + return Emoji_1720, nil + } + case 1721:{ + return Emoji_1721, nil + } + case 1722:{ + return Emoji_1722, nil + } + case 1723:{ + return Emoji_1723, nil + } + case 1724:{ + return Emoji_1724, nil + } + case 1725:{ + return Emoji_1725, nil + } + case 1726:{ + return Emoji_1726, nil + } + case 1727:{ + return Emoji_1727, nil + } + case 1728:{ + return Emoji_1728, nil + } + case 1729:{ + return Emoji_1729, nil + } + case 1730:{ + return Emoji_1730, nil + } + case 1731:{ + return Emoji_1731, nil + } + case 1732:{ + return Emoji_1732, nil + } + case 1733:{ + return Emoji_1733, nil + } + case 1734:{ + return Emoji_1734, nil + } + case 1735:{ + return Emoji_1735, nil + } + case 1736:{ + return Emoji_1736, nil + } + case 1737:{ + return Emoji_1737, nil + } + case 1738:{ + return Emoji_1738, nil + } + case 1739:{ + return Emoji_1739, nil + } + case 1740:{ + return Emoji_1740, nil + } + case 1741:{ + return Emoji_1741, nil + } + case 1742:{ + return Emoji_1742, nil + } + case 1743:{ + return Emoji_1743, nil + } + case 1744:{ + return Emoji_1744, nil + } + case 1745:{ + return Emoji_1745, nil + } + case 1746:{ + return Emoji_1746, nil + } + case 1747:{ + return Emoji_1747, nil + } + case 1748:{ + return Emoji_1748, nil + } + case 1749:{ + return Emoji_1749, nil + } + case 1750:{ + return Emoji_1750, nil + } + case 1751:{ + return Emoji_1751, nil + } + case 1752:{ + return Emoji_1752, nil + } + case 1753:{ + return Emoji_1753, nil + } + case 1754:{ + return Emoji_1754, nil + } + case 1755:{ + return Emoji_1755, nil + } + case 1756:{ + return Emoji_1756, nil + } + case 1757:{ + return Emoji_1757, nil + } + case 1758:{ + return Emoji_1758, nil + } + case 1759:{ + return Emoji_1759, nil + } + case 1760:{ + return Emoji_1760, nil + } + case 1761:{ + return Emoji_1761, nil + } + case 1762:{ + return Emoji_1762, nil + } + case 1763:{ + return Emoji_1763, nil + } + case 1764:{ + return Emoji_1764, nil + } + case 1765:{ + return Emoji_1765, nil + } + case 1766:{ + return Emoji_1766, nil + } + case 1767:{ + return Emoji_1767, nil + } + case 1768:{ + return Emoji_1768, nil + } + case 1769:{ + return Emoji_1769, nil + } + case 1770:{ + return Emoji_1770, nil + } + case 1771:{ + return Emoji_1771, nil + } + case 1772:{ + return Emoji_1772, nil + } + case 1773:{ + return Emoji_1773, nil + } + case 1774:{ + return Emoji_1774, nil + } + case 1775:{ + return Emoji_1775, nil + } + case 1776:{ + return Emoji_1776, nil + } + case 1777:{ + return Emoji_1777, nil + } + case 1778:{ + return Emoji_1778, nil + } + case 1779:{ + return Emoji_1779, nil + } + case 1780:{ + return Emoji_1780, nil + } + case 1781:{ + return Emoji_1781, nil + } + case 1782:{ + return Emoji_1782, nil + } + case 1783:{ + return Emoji_1783, nil + } + case 1784:{ + return Emoji_1784, nil + } + case 1785:{ + return Emoji_1785, nil + } + case 1786:{ + return Emoji_1786, nil + } + case 1787:{ + return Emoji_1787, nil + } + case 1788:{ + return Emoji_1788, nil + } + case 1789:{ + return Emoji_1789, nil + } + case 1790:{ + return Emoji_1790, nil + } + case 1791:{ + return Emoji_1791, nil + } + case 1792:{ + return Emoji_1792, nil + } + case 1793:{ + return Emoji_1793, nil + } + case 1794:{ + return Emoji_1794, nil + } + case 1795:{ + return Emoji_1795, nil + } + case 1796:{ + return Emoji_1796, nil + } + case 1797:{ + return Emoji_1797, nil + } + case 1798:{ + return Emoji_1798, nil + } + case 1799:{ + return Emoji_1799, nil + } + case 1800:{ + return Emoji_1800, nil + } + case 1801:{ + return Emoji_1801, nil + } + case 1802:{ + return Emoji_1802, nil + } + case 1803:{ + return Emoji_1803, nil + } + case 1804:{ + return Emoji_1804, nil + } + case 1805:{ + return Emoji_1805, nil + } + case 1806:{ + return Emoji_1806, nil + } + case 1807:{ + return Emoji_1807, nil + } + case 1808:{ + return Emoji_1808, nil + } + case 1809:{ + return Emoji_1809, nil + } + case 1810:{ + return Emoji_1810, nil + } + case 1811:{ + return Emoji_1811, nil + } + case 1812:{ + return Emoji_1812, nil + } + case 1813:{ + return Emoji_1813, nil + } + case 1814:{ + return Emoji_1814, nil + } + case 1815:{ + return Emoji_1815, nil + } + case 1816:{ + return Emoji_1816, nil + } + case 1817:{ + return Emoji_1817, nil + } + case 1818:{ + return Emoji_1818, nil + } + case 1819:{ + return Emoji_1819, nil + } + case 1820:{ + return Emoji_1820, nil + } + case 1821:{ + return Emoji_1821, nil + } + case 1822:{ + return Emoji_1822, nil + } + case 1823:{ + return Emoji_1823, nil + } + case 1824:{ + return Emoji_1824, nil + } + case 1825:{ + return Emoji_1825, nil + } + case 1826:{ + return Emoji_1826, nil + } + case 1827:{ + return Emoji_1827, nil + } + case 1828:{ + return Emoji_1828, nil + } + case 1829:{ + return Emoji_1829, nil + } + case 1830:{ + return Emoji_1830, nil + } + case 1831:{ + return Emoji_1831, nil + } + case 1832:{ + return Emoji_1832, nil + } + case 1833:{ + return Emoji_1833, nil + } + case 1834:{ + return Emoji_1834, nil + } + case 1835:{ + return Emoji_1835, nil + } + case 1836:{ + return Emoji_1836, nil + } + case 1837:{ + return Emoji_1837, nil + } + case 1838:{ + return Emoji_1838, nil + } + case 1839:{ + return Emoji_1839, nil + } + case 1840:{ + return Emoji_1840, nil + } + case 1841:{ + return Emoji_1841, nil + } + case 1842:{ + return Emoji_1842, nil + } + case 1843:{ + return Emoji_1843, nil + } + case 1844:{ + return Emoji_1844, nil + } + case 1845:{ + return Emoji_1845, nil + } + case 1846:{ + return Emoji_1846, nil + } + case 1847:{ + return Emoji_1847, nil + } + case 1848:{ + return Emoji_1848, nil + } + case 1849:{ + return Emoji_1849, nil + } + case 1850:{ + return Emoji_1850, nil + } + case 1851:{ + return Emoji_1851, nil + } + case 1852:{ + return Emoji_1852, nil + } + case 1853:{ + return Emoji_1853, nil + } + case 1854:{ + return Emoji_1854, nil + } + case 1855:{ + return Emoji_1855, nil + } + case 1856:{ + return Emoji_1856, nil + } + case 1857:{ + return Emoji_1857, nil + } + case 1858:{ + return Emoji_1858, nil + } + case 1859:{ + return Emoji_1859, nil + } + case 1860:{ + return Emoji_1860, nil + } + case 1861:{ + return Emoji_1861, nil + } + case 1862:{ + return Emoji_1862, nil + } + case 1863:{ + return Emoji_1863, nil + } + case 1864:{ + return Emoji_1864, nil + } + case 1865:{ + return Emoji_1865, nil + } + case 1866:{ + return Emoji_1866, nil + } + case 1867:{ + return Emoji_1867, nil + } + case 1868:{ + return Emoji_1868, nil + } + case 1869:{ + return Emoji_1869, nil + } + case 1870:{ + return Emoji_1870, nil + } + case 1871:{ + return Emoji_1871, nil + } + case 1872:{ + return Emoji_1872, nil + } + case 1873:{ + return Emoji_1873, nil + } + case 1874:{ + return Emoji_1874, nil + } + case 1875:{ + return Emoji_1875, nil + } + case 1876:{ + return Emoji_1876, nil + } + case 1877:{ + return Emoji_1877, nil + } + case 1878:{ + return Emoji_1878, nil + } + case 1879:{ + return Emoji_1879, nil + } + case 1880:{ + return Emoji_1880, nil + } + case 1881:{ + return Emoji_1881, nil + } + case 1882:{ + return Emoji_1882, nil + } + case 1883:{ + return Emoji_1883, nil + } + case 1884:{ + return Emoji_1884, nil + } + case 1885:{ + return Emoji_1885, nil + } + case 1886:{ + return Emoji_1886, nil + } + case 1887:{ + return Emoji_1887, nil + } + case 1888:{ + return Emoji_1888, nil + } + case 1889:{ + return Emoji_1889, nil + } + case 1890:{ + return Emoji_1890, nil + } + case 1891:{ + return Emoji_1891, nil + } + case 1892:{ + return Emoji_1892, nil + } + case 1893:{ + return Emoji_1893, nil + } + case 1894:{ + return Emoji_1894, nil + } + case 1895:{ + return Emoji_1895, nil + } + case 1896:{ + return Emoji_1896, nil + } + case 1897:{ + return Emoji_1897, nil + } + case 1898:{ + return Emoji_1898, nil + } + case 1899:{ + return Emoji_1899, nil + } + case 1900:{ + return Emoji_1900, nil + } + case 1901:{ + return Emoji_1901, nil + } + case 1902:{ + return Emoji_1902, nil + } + case 1903:{ + return Emoji_1903, nil + } + case 1904:{ + return Emoji_1904, nil + } + case 1905:{ + return Emoji_1905, nil + } + case 1906:{ + return Emoji_1906, nil + } + case 1907:{ + return Emoji_1907, nil + } + case 1908:{ + return Emoji_1908, nil + } + case 1909:{ + return Emoji_1909, nil + } + case 1910:{ + return Emoji_1910, nil + } + case 1911:{ + return Emoji_1911, nil + } + case 1912:{ + return Emoji_1912, nil + } + case 1913:{ + return Emoji_1913, nil + } + case 1914:{ + return Emoji_1914, nil + } + case 1915:{ + return Emoji_1915, nil + } + case 1916:{ + return Emoji_1916, nil + } + case 1917:{ + return Emoji_1917, nil + } + case 1918:{ + return Emoji_1918, nil + } + case 1919:{ + return Emoji_1919, nil + } + case 1920:{ + return Emoji_1920, nil + } + case 1921:{ + return Emoji_1921, nil + } + case 1922:{ + return Emoji_1922, nil + } + case 1923:{ + return Emoji_1923, nil + } + case 1924:{ + return Emoji_1924, nil + } + case 1925:{ + return Emoji_1925, nil + } + case 1926:{ + return Emoji_1926, nil + } + case 1927:{ + return Emoji_1927, nil + } + case 1928:{ + return Emoji_1928, nil + } + case 1929:{ + return Emoji_1929, nil + } + case 1930:{ + return Emoji_1930, nil + } + case 1931:{ + return Emoji_1931, nil + } + case 1932:{ + return Emoji_1932, nil + } + case 1933:{ + return Emoji_1933, nil + } + case 1934:{ + return Emoji_1934, nil + } + case 1935:{ + return Emoji_1935, nil + } + case 1936:{ + return Emoji_1936, nil + } + case 1937:{ + return Emoji_1937, nil + } + case 1938:{ + return Emoji_1938, nil + } + case 1939:{ + return Emoji_1939, nil + } + case 1940:{ + return Emoji_1940, nil + } + case 1941:{ + return Emoji_1941, nil + } + case 1942:{ + return Emoji_1942, nil + } + case 1943:{ + return Emoji_1943, nil + } + case 1944:{ + return Emoji_1944, nil + } + case 1945:{ + return Emoji_1945, nil + } + case 1946:{ + return Emoji_1946, nil + } + case 1947:{ + return Emoji_1947, nil + } + case 1948:{ + return Emoji_1948, nil + } + case 1949:{ + return Emoji_1949, nil + } + case 1950:{ + return Emoji_1950, nil + } + case 1951:{ + return Emoji_1951, nil + } + case 1952:{ + return Emoji_1952, nil + } + case 1953:{ + return Emoji_1953, nil + } + case 1954:{ + return Emoji_1954, nil + } + case 1955:{ + return Emoji_1955, nil + } + case 1956:{ + return Emoji_1956, nil + } + case 1957:{ + return Emoji_1957, nil + } + case 1958:{ + return Emoji_1958, nil + } + case 1959:{ + return Emoji_1959, nil + } + case 1960:{ + return Emoji_1960, nil + } + case 1961:{ + return Emoji_1961, nil + } + case 1962:{ + return Emoji_1962, nil + } + case 1963:{ + return Emoji_1963, nil + } + case 1964:{ + return Emoji_1964, nil + } + case 1965:{ + return Emoji_1965, nil + } + case 1966:{ + return Emoji_1966, nil + } + case 1967:{ + return Emoji_1967, nil + } + case 1968:{ + return Emoji_1968, nil + } + case 1969:{ + return Emoji_1969, nil + } + case 1970:{ + return Emoji_1970, nil + } + case 1971:{ + return Emoji_1971, nil + } + case 1972:{ + return Emoji_1972, nil + } + case 1973:{ + return Emoji_1973, nil + } + case 1974:{ + return Emoji_1974, nil + } + case 1975:{ + return Emoji_1975, nil + } + case 1976:{ + return Emoji_1976, nil + } + case 1977:{ + return Emoji_1977, nil + } + case 1978:{ + return Emoji_1978, nil + } + case 1979:{ + return Emoji_1979, nil + } + case 1980:{ + return Emoji_1980, nil + } + case 1981:{ + return Emoji_1981, nil + } + case 1982:{ + return Emoji_1982, nil + } + case 1983:{ + return Emoji_1983, nil + } + case 1984:{ + return Emoji_1984, nil + } + case 1985:{ + return Emoji_1985, nil + } + case 1986:{ + return Emoji_1986, nil + } + case 1987:{ + return Emoji_1987, nil + } + case 1988:{ + return Emoji_1988, nil + } + case 1989:{ + return Emoji_1989, nil + } + case 1990:{ + return Emoji_1990, nil + } + case 1991:{ + return Emoji_1991, nil + } + case 1992:{ + return Emoji_1992, nil + } + case 1993:{ + return Emoji_1993, nil + } + case 1994:{ + return Emoji_1994, nil + } + case 1995:{ + return Emoji_1995, nil + } + case 1996:{ + return Emoji_1996, nil + } + case 1997:{ + return Emoji_1997, nil + } + case 1998:{ + return Emoji_1998, nil + } + case 1999:{ + return Emoji_1999, nil + } + case 2000:{ + return Emoji_2000, nil + } + case 2001:{ + return Emoji_2001, nil + } + case 2002:{ + return Emoji_2002, nil + } + case 2003:{ + return Emoji_2003, nil + } + case 2004:{ + return Emoji_2004, nil + } + case 2005:{ + return Emoji_2005, nil + } + case 2006:{ + return Emoji_2006, nil + } + case 2007:{ + return Emoji_2007, nil + } + case 2008:{ + return Emoji_2008, nil + } + case 2009:{ + return Emoji_2009, nil + } + case 2010:{ + return Emoji_2010, nil + } + case 2011:{ + return Emoji_2011, nil + } + case 2012:{ + return Emoji_2012, nil + } + case 2013:{ + return Emoji_2013, nil + } + case 2014:{ + return Emoji_2014, nil + } + case 2015:{ + return Emoji_2015, nil + } + case 2016:{ + return Emoji_2016, nil + } + case 2017:{ + return Emoji_2017, nil + } + case 2018:{ + return Emoji_2018, nil + } + case 2019:{ + return Emoji_2019, nil + } + case 2020:{ + return Emoji_2020, nil + } + case 2021:{ + return Emoji_2021, nil + } + case 2022:{ + return Emoji_2022, nil + } + case 2023:{ + return Emoji_2023, nil + } + case 2024:{ + return Emoji_2024, nil + } + case 2025:{ + return Emoji_2025, nil + } + case 2026:{ + return Emoji_2026, nil + } + case 2027:{ + return Emoji_2027, nil + } + case 2028:{ + return Emoji_2028, nil + } + case 2029:{ + return Emoji_2029, nil + } + case 2030:{ + return Emoji_2030, nil + } + case 2031:{ + return Emoji_2031, nil + } + case 2032:{ + return Emoji_2032, nil + } + case 2033:{ + return Emoji_2033, nil + } + case 2034:{ + return Emoji_2034, nil + } + case 2035:{ + return Emoji_2035, nil + } + case 2036:{ + return Emoji_2036, nil + } + case 2037:{ + return Emoji_2037, nil + } + case 2038:{ + return Emoji_2038, nil + } + case 2039:{ + return Emoji_2039, nil + } + case 2040:{ + return Emoji_2040, nil + } + case 2041:{ + return Emoji_2041, nil + } + case 2042:{ + return Emoji_2042, nil + } + case 2043:{ + return Emoji_2043, nil + } + case 2044:{ + return Emoji_2044, nil + } + case 2045:{ + return Emoji_2045, nil + } + case 2046:{ + return Emoji_2046, nil + } + case 2047:{ + return Emoji_2047, nil + } + case 2048:{ + return Emoji_2048, nil + } + case 2049:{ + return Emoji_2049, nil + } + case 2050:{ + return Emoji_2050, nil + } + case 2051:{ + return Emoji_2051, nil + } + case 2052:{ + return Emoji_2052, nil + } + case 2053:{ + return Emoji_2053, nil + } + case 2054:{ + return Emoji_2054, nil + } + case 2055:{ + return Emoji_2055, nil + } + case 2056:{ + return Emoji_2056, nil + } + case 2057:{ + return Emoji_2057, nil + } + case 2058:{ + return Emoji_2058, nil + } + case 2059:{ + return Emoji_2059, nil + } + case 2060:{ + return Emoji_2060, nil + } + case 2061:{ + return Emoji_2061, nil + } + case 2062:{ + return Emoji_2062, nil + } + case 2063:{ + return Emoji_2063, nil + } + case 2064:{ + return Emoji_2064, nil + } + case 2065:{ + return Emoji_2065, nil + } + case 2066:{ + return Emoji_2066, nil + } + case 2067:{ + return Emoji_2067, nil + } + case 2068:{ + return Emoji_2068, nil + } + case 2069:{ + return Emoji_2069, nil + } + case 2070:{ + return Emoji_2070, nil + } + case 2071:{ + return Emoji_2071, nil + } + case 2072:{ + return Emoji_2072, nil + } + case 2073:{ + return Emoji_2073, nil + } + case 2074:{ + return Emoji_2074, nil + } + case 2075:{ + return Emoji_2075, nil + } + case 2076:{ + return Emoji_2076, nil + } + case 2077:{ + return Emoji_2077, nil + } + case 2078:{ + return Emoji_2078, nil + } + case 2079:{ + return Emoji_2079, nil + } + case 2080:{ + return Emoji_2080, nil + } + case 2081:{ + return Emoji_2081, nil + } + case 2082:{ + return Emoji_2082, nil + } + case 2083:{ + return Emoji_2083, nil + } + case 2084:{ + return Emoji_2084, nil + } + case 2085:{ + return Emoji_2085, nil + } + case 2086:{ + return Emoji_2086, nil + } + case 2087:{ + return Emoji_2087, nil + } + case 2088:{ + return Emoji_2088, nil + } + case 2089:{ + return Emoji_2089, nil + } + case 2090:{ + return Emoji_2090, nil + } + case 2091:{ + return Emoji_2091, nil + } + case 2092:{ + return Emoji_2092, nil + } + case 2093:{ + return Emoji_2093, nil + } + case 2094:{ + return Emoji_2094, nil + } + case 2095:{ + return Emoji_2095, nil + } + case 2096:{ + return Emoji_2096, nil + } + case 2097:{ + return Emoji_2097, nil + } + case 2098:{ + return Emoji_2098, nil + } + case 2099:{ + return Emoji_2099, nil + } + case 2100:{ + return Emoji_2100, nil + } + case 2101:{ + return Emoji_2101, nil + } + case 2102:{ + return Emoji_2102, nil + } + case 2103:{ + return Emoji_2103, nil + } + case 2104:{ + return Emoji_2104, nil + } + case 2105:{ + return Emoji_2105, nil + } + case 2106:{ + return Emoji_2106, nil + } + case 2107:{ + return Emoji_2107, nil + } + case 2108:{ + return Emoji_2108, nil + } + case 2109:{ + return Emoji_2109, nil + } + case 2110:{ + return Emoji_2110, nil + } + case 2111:{ + return Emoji_2111, nil + } + case 2112:{ + return Emoji_2112, nil + } + case 2113:{ + return Emoji_2113, nil + } + case 2114:{ + return Emoji_2114, nil + } + case 2115:{ + return Emoji_2115, nil + } + case 2116:{ + return Emoji_2116, nil + } + case 2117:{ + return Emoji_2117, nil + } + case 2118:{ + return Emoji_2118, nil + } + case 2119:{ + return Emoji_2119, nil + } + case 2120:{ + return Emoji_2120, nil + } + case 2121:{ + return Emoji_2121, nil + } + case 2122:{ + return Emoji_2122, nil + } + case 2123:{ + return Emoji_2123, nil + } + case 2124:{ + return Emoji_2124, nil + } + case 2125:{ + return Emoji_2125, nil + } + case 2126:{ + return Emoji_2126, nil + } + case 2127:{ + return Emoji_2127, nil + } + case 2128:{ + return Emoji_2128, nil + } + case 2129:{ + return Emoji_2129, nil + } + case 2130:{ + return Emoji_2130, nil + } + case 2131:{ + return Emoji_2131, nil + } + case 2132:{ + return Emoji_2132, nil + } + case 2133:{ + return Emoji_2133, nil + } + case 2134:{ + return Emoji_2134, nil + } + case 2135:{ + return Emoji_2135, nil + } + case 2136:{ + return Emoji_2136, nil + } + case 2137:{ + return Emoji_2137, nil + } + case 2138:{ + return Emoji_2138, nil + } + case 2139:{ + return Emoji_2139, nil + } + case 2140:{ + return Emoji_2140, nil + } + case 2141:{ + return Emoji_2141, nil + } + case 2142:{ + return Emoji_2142, nil + } + case 2143:{ + return Emoji_2143, nil + } + case 2144:{ + return Emoji_2144, nil + } + case 2145:{ + return Emoji_2145, nil + } + case 2146:{ + return Emoji_2146, nil + } + case 2147:{ + return Emoji_2147, nil + } + case 2148:{ + return Emoji_2148, nil + } + case 2149:{ + return Emoji_2149, nil + } + case 2150:{ + return Emoji_2150, nil + } + case 2151:{ + return Emoji_2151, nil + } + case 2152:{ + return Emoji_2152, nil + } + case 2153:{ + return Emoji_2153, nil + } + case 2154:{ + return Emoji_2154, nil + } + case 2155:{ + return Emoji_2155, nil + } + case 2156:{ + return Emoji_2156, nil + } + case 2157:{ + return Emoji_2157, nil + } + case 2158:{ + return Emoji_2158, nil + } + case 2159:{ + return Emoji_2159, nil + } + case 2160:{ + return Emoji_2160, nil + } + case 2161:{ + return Emoji_2161, nil + } + case 2162:{ + return Emoji_2162, nil + } + case 2163:{ + return Emoji_2163, nil + } + case 2164:{ + return Emoji_2164, nil + } + case 2165:{ + return Emoji_2165, nil + } + case 2166:{ + return Emoji_2166, nil + } + case 2167:{ + return Emoji_2167, nil + } + case 2168:{ + return Emoji_2168, nil + } + case 2169:{ + return Emoji_2169, nil + } + case 2170:{ + return Emoji_2170, nil + } + case 2171:{ + return Emoji_2171, nil + } + case 2172:{ + return Emoji_2172, nil + } + case 2173:{ + return Emoji_2173, nil + } + case 2174:{ + return Emoji_2174, nil + } + case 2175:{ + return Emoji_2175, nil + } + case 2176:{ + return Emoji_2176, nil + } + case 2177:{ + return Emoji_2177, nil + } + case 2178:{ + return Emoji_2178, nil + } + case 2179:{ + return Emoji_2179, nil + } + case 2180:{ + return Emoji_2180, nil + } + case 2181:{ + return Emoji_2181, nil + } + case 2182:{ + return Emoji_2182, nil + } + case 2183:{ + return Emoji_2183, nil + } + case 2184:{ + return Emoji_2184, nil + } + case 2185:{ + return Emoji_2185, nil + } + case 2186:{ + return Emoji_2186, nil + } + case 2187:{ + return Emoji_2187, nil + } + case 2188:{ + return Emoji_2188, nil + } + case 2189:{ + return Emoji_2189, nil + } + case 2190:{ + return Emoji_2190, nil + } + case 2191:{ + return Emoji_2191, nil + } + case 2192:{ + return Emoji_2192, nil + } + case 2193:{ + return Emoji_2193, nil + } + case 2194:{ + return Emoji_2194, nil + } + case 2195:{ + return Emoji_2195, nil + } + case 2196:{ + return Emoji_2196, nil + } + case 2197:{ + return Emoji_2197, nil + } + case 2198:{ + return Emoji_2198, nil + } + case 2199:{ + return Emoji_2199, nil + } + case 2200:{ + return Emoji_2200, nil + } + case 2201:{ + return Emoji_2201, nil + } + case 2202:{ + return Emoji_2202, nil + } + case 2203:{ + return Emoji_2203, nil + } + case 2204:{ + return Emoji_2204, nil + } + case 2205:{ + return Emoji_2205, nil + } + case 2206:{ + return Emoji_2206, nil + } + case 2207:{ + return Emoji_2207, nil + } + case 2208:{ + return Emoji_2208, nil + } + case 2209:{ + return Emoji_2209, nil + } + case 2210:{ + return Emoji_2210, nil + } + case 2211:{ + return Emoji_2211, nil + } + case 2212:{ + return Emoji_2212, nil + } + case 2213:{ + return Emoji_2213, nil + } + case 2214:{ + return Emoji_2214, nil + } + case 2215:{ + return Emoji_2215, nil + } + case 2216:{ + return Emoji_2216, nil + } + case 2217:{ + return Emoji_2217, nil + } + case 2218:{ + return Emoji_2218, nil + } + case 2219:{ + return Emoji_2219, nil + } + case 2220:{ + return Emoji_2220, nil + } + case 2221:{ + return Emoji_2221, nil + } + case 2222:{ + return Emoji_2222, nil + } + case 2223:{ + return Emoji_2223, nil + } + case 2224:{ + return Emoji_2224, nil + } + case 2225:{ + return Emoji_2225, nil + } + case 2226:{ + return Emoji_2226, nil + } + case 2227:{ + return Emoji_2227, nil + } + case 2228:{ + return Emoji_2228, nil + } + case 2229:{ + return Emoji_2229, nil + } + case 2230:{ + return Emoji_2230, nil + } + case 2231:{ + return Emoji_2231, nil + } + case 2232:{ + return Emoji_2232, nil + } + case 2233:{ + return Emoji_2233, nil + } + case 2234:{ + return Emoji_2234, nil + } + case 2235:{ + return Emoji_2235, nil + } + case 2236:{ + return Emoji_2236, nil + } + case 2237:{ + return Emoji_2237, nil + } + case 2238:{ + return Emoji_2238, nil + } + case 2239:{ + return Emoji_2239, nil + } + case 2240:{ + return Emoji_2240, nil + } + case 2241:{ + return Emoji_2241, nil + } + case 2242:{ + return Emoji_2242, nil + } + case 2243:{ + return Emoji_2243, nil + } + case 2244:{ + return Emoji_2244, nil + } + case 2245:{ + return Emoji_2245, nil + } + case 2246:{ + return Emoji_2246, nil + } + case 2247:{ + return Emoji_2247, nil + } + case 2248:{ + return Emoji_2248, nil + } + case 2249:{ + return Emoji_2249, nil + } + case 2250:{ + return Emoji_2250, nil + } + case 2251:{ + return Emoji_2251, nil + } + case 2252:{ + return Emoji_2252, nil + } + case 2253:{ + return Emoji_2253, nil + } + case 2254:{ + return Emoji_2254, nil + } + case 2255:{ + return Emoji_2255, nil + } + case 2256:{ + return Emoji_2256, nil + } + case 2257:{ + return Emoji_2257, nil + } + case 2258:{ + return Emoji_2258, nil + } + case 2259:{ + return Emoji_2259, nil + } + case 2260:{ + return Emoji_2260, nil + } + case 2261:{ + return Emoji_2261, nil + } + case 2262:{ + return Emoji_2262, nil + } + case 2263:{ + return Emoji_2263, nil + } + case 2264:{ + return Emoji_2264, nil + } + case 2265:{ + return Emoji_2265, nil + } + case 2266:{ + return Emoji_2266, nil + } + case 2267:{ + return Emoji_2267, nil + } + case 2268:{ + return Emoji_2268, nil + } + case 2269:{ + return Emoji_2269, nil + } + case 2270:{ + return Emoji_2270, nil + } + case 2271:{ + return Emoji_2271, nil + } + case 2272:{ + return Emoji_2272, nil + } + case 2273:{ + return Emoji_2273, nil + } + case 2274:{ + return Emoji_2274, nil + } + case 2275:{ + return Emoji_2275, nil + } + case 2276:{ + return Emoji_2276, nil + } + case 2277:{ + return Emoji_2277, nil + } + case 2278:{ + return Emoji_2278, nil + } + case 2279:{ + return Emoji_2279, nil + } + case 2280:{ + return Emoji_2280, nil + } + case 2281:{ + return Emoji_2281, nil + } + case 2282:{ + return Emoji_2282, nil + } + case 2283:{ + return Emoji_2283, nil + } + case 2284:{ + return Emoji_2284, nil + } + case 2285:{ + return Emoji_2285, nil + } + case 2286:{ + return Emoji_2286, nil + } + case 2287:{ + return Emoji_2287, nil + } + case 2288:{ + return Emoji_2288, nil + } + case 2289:{ + return Emoji_2289, nil + } + case 2290:{ + return Emoji_2290, nil + } + case 2291:{ + return Emoji_2291, nil + } + case 2292:{ + return Emoji_2292, nil + } + case 2293:{ + return Emoji_2293, nil + } + case 2294:{ + return Emoji_2294, nil + } + case 2295:{ + return Emoji_2295, nil + } + case 2296:{ + return Emoji_2296, nil + } + case 2297:{ + return Emoji_2297, nil + } + case 2298:{ + return Emoji_2298, nil + } + case 2299:{ + return Emoji_2299, nil + } + case 2300:{ + return Emoji_2300, nil + } + case 2301:{ + return Emoji_2301, nil + } + case 2302:{ + return Emoji_2302, nil + } + case 2303:{ + return Emoji_2303, nil + } + case 2304:{ + return Emoji_2304, nil + } + case 2305:{ + return Emoji_2305, nil + } + case 2306:{ + return Emoji_2306, nil + } + case 2307:{ + return Emoji_2307, nil + } + case 2308:{ + return Emoji_2308, nil + } + case 2309:{ + return Emoji_2309, nil + } + case 2310:{ + return Emoji_2310, nil + } + case 2311:{ + return Emoji_2311, nil + } + case 2312:{ + return Emoji_2312, nil + } + case 2313:{ + return Emoji_2313, nil + } + case 2314:{ + return Emoji_2314, nil + } + case 2315:{ + return Emoji_2315, nil + } + case 2316:{ + return Emoji_2316, nil + } + case 2317:{ + return Emoji_2317, nil + } + case 2318:{ + return Emoji_2318, nil + } + case 2319:{ + return Emoji_2319, nil + } + case 2320:{ + return Emoji_2320, nil + } + case 2321:{ + return Emoji_2321, nil + } + case 2322:{ + return Emoji_2322, nil + } + case 2323:{ + return Emoji_2323, nil + } + case 2324:{ + return Emoji_2324, nil + } + case 2325:{ + return Emoji_2325, nil + } + case 2326:{ + return Emoji_2326, nil + } + case 2327:{ + return Emoji_2327, nil + } + case 2328:{ + return Emoji_2328, nil + } + case 2329:{ + return Emoji_2329, nil + } + case 2330:{ + return Emoji_2330, nil + } + case 2331:{ + return Emoji_2331, nil + } + case 2332:{ + return Emoji_2332, nil + } + case 2333:{ + return Emoji_2333, nil + } + case 2334:{ + return Emoji_2334, nil + } + case 2335:{ + return Emoji_2335, nil + } + case 2336:{ + return Emoji_2336, nil + } + case 2337:{ + return Emoji_2337, nil + } + case 2338:{ + return Emoji_2338, nil + } + case 2339:{ + return Emoji_2339, nil + } + case 2340:{ + return Emoji_2340, nil + } + case 2341:{ + return Emoji_2341, nil + } + case 2342:{ + return Emoji_2342, nil + } + case 2343:{ + return Emoji_2343, nil + } + case 2344:{ + return Emoji_2344, nil + } + case 2345:{ + return Emoji_2345, nil + } + case 2346:{ + return Emoji_2346, nil + } + case 2347:{ + return Emoji_2347, nil + } + case 2348:{ + return Emoji_2348, nil + } + case 2349:{ + return Emoji_2349, nil + } + case 2350:{ + return Emoji_2350, nil + } + case 2351:{ + return Emoji_2351, nil + } + case 2352:{ + return Emoji_2352, nil + } + case 2353:{ + return Emoji_2353, nil + } + case 2354:{ + return Emoji_2354, nil + } + case 2355:{ + return Emoji_2355, nil + } + case 2356:{ + return Emoji_2356, nil + } + case 2357:{ + return Emoji_2357, nil + } + case 2358:{ + return Emoji_2358, nil + } + case 2359:{ + return Emoji_2359, nil + } + case 2360:{ + return Emoji_2360, nil + } + case 2361:{ + return Emoji_2361, nil + } + case 2362:{ + return Emoji_2362, nil + } + case 2363:{ + return Emoji_2363, nil + } + case 2364:{ + return Emoji_2364, nil + } + case 2365:{ + return Emoji_2365, nil + } + case 2366:{ + return Emoji_2366, nil + } + case 2367:{ + return Emoji_2367, nil + } + case 2368:{ + return Emoji_2368, nil + } + case 2369:{ + return Emoji_2369, nil + } + case 2370:{ + return Emoji_2370, nil + } + case 2371:{ + return Emoji_2371, nil + } + case 2372:{ + return Emoji_2372, nil + } + case 2373:{ + return Emoji_2373, nil + } + case 2374:{ + return Emoji_2374, nil + } + case 2375:{ + return Emoji_2375, nil + } + case 2376:{ + return Emoji_2376, nil + } + case 2377:{ + return Emoji_2377, nil + } + case 2378:{ + return Emoji_2378, nil + } + case 2379:{ + return Emoji_2379, nil + } + case 2380:{ + return Emoji_2380, nil + } + case 2381:{ + return Emoji_2381, nil + } + case 2382:{ + return Emoji_2382, nil + } + case 2383:{ + return Emoji_2383, nil + } + case 2384:{ + return Emoji_2384, nil + } + case 2385:{ + return Emoji_2385, nil + } + case 2386:{ + return Emoji_2386, nil + } + case 2387:{ + return Emoji_2387, nil + } + case 2388:{ + return Emoji_2388, nil + } + case 2389:{ + return Emoji_2389, nil + } + case 2390:{ + return Emoji_2390, nil + } + case 2391:{ + return Emoji_2391, nil + } + case 2392:{ + return Emoji_2392, nil + } + case 2393:{ + return Emoji_2393, nil + } + case 2394:{ + return Emoji_2394, nil + } + case 2395:{ + return Emoji_2395, nil + } + case 2396:{ + return Emoji_2396, nil + } + case 2397:{ + return Emoji_2397, nil + } + case 2398:{ + return Emoji_2398, nil + } + case 2399:{ + return Emoji_2399, nil + } + case 2400:{ + return Emoji_2400, nil + } + case 2401:{ + return Emoji_2401, nil + } + case 2402:{ + return Emoji_2402, nil + } + case 2403:{ + return Emoji_2403, nil + } + case 2404:{ + return Emoji_2404, nil + } + case 2405:{ + return Emoji_2405, nil + } + case 2406:{ + return Emoji_2406, nil + } + case 2407:{ + return Emoji_2407, nil + } + case 2408:{ + return Emoji_2408, nil + } + case 2409:{ + return Emoji_2409, nil + } + case 2410:{ + return Emoji_2410, nil + } + case 2411:{ + return Emoji_2411, nil + } + case 2412:{ + return Emoji_2412, nil + } + case 2413:{ + return Emoji_2413, nil + } + case 2414:{ + return Emoji_2414, nil + } + case 2415:{ + return Emoji_2415, nil + } + case 2416:{ + return Emoji_2416, nil + } + case 2417:{ + return Emoji_2417, nil + } + case 2418:{ + return Emoji_2418, nil + } + case 2419:{ + return Emoji_2419, nil + } + case 2420:{ + return Emoji_2420, nil + } + case 2421:{ + return Emoji_2421, nil + } + case 2422:{ + return Emoji_2422, nil + } + case 2423:{ + return Emoji_2423, nil + } + case 2424:{ + return Emoji_2424, nil + } + case 2425:{ + return Emoji_2425, nil + } + case 2426:{ + return Emoji_2426, nil + } + case 2427:{ + return Emoji_2427, nil + } + case 2428:{ + return Emoji_2428, nil + } + case 2429:{ + return Emoji_2429, nil + } + case 2430:{ + return Emoji_2430, nil + } + case 2431:{ + return Emoji_2431, nil + } + case 2432:{ + return Emoji_2432, nil + } + case 2433:{ + return Emoji_2433, nil + } + case 2434:{ + return Emoji_2434, nil + } + case 2435:{ + return Emoji_2435, nil + } + case 2436:{ + return Emoji_2436, nil + } + case 2437:{ + return Emoji_2437, nil + } + case 2438:{ + return Emoji_2438, nil + } + case 2439:{ + return Emoji_2439, nil + } + case 2440:{ + return Emoji_2440, nil + } + case 2441:{ + return Emoji_2441, nil + } + case 2442:{ + return Emoji_2442, nil + } + case 2443:{ + return Emoji_2443, nil + } + case 2444:{ + return Emoji_2444, nil + } + case 2445:{ + return Emoji_2445, nil + } + case 2446:{ + return Emoji_2446, nil + } + case 2447:{ + return Emoji_2447, nil + } + case 2448:{ + return Emoji_2448, nil + } + case 2449:{ + return Emoji_2449, nil + } + case 2450:{ + return Emoji_2450, nil + } + case 2451:{ + return Emoji_2451, nil + } + case 2452:{ + return Emoji_2452, nil + } + case 2453:{ + return Emoji_2453, nil + } + case 2454:{ + return Emoji_2454, nil + } + case 2455:{ + return Emoji_2455, nil + } + case 2456:{ + return Emoji_2456, nil + } + case 2457:{ + return Emoji_2457, nil + } + case 2458:{ + return Emoji_2458, nil + } + case 2459:{ + return Emoji_2459, nil + } + case 2460:{ + return Emoji_2460, nil + } + case 2461:{ + return Emoji_2461, nil + } + case 2462:{ + return Emoji_2462, nil + } + case 2463:{ + return Emoji_2463, nil + } + case 2464:{ + return Emoji_2464, nil + } + case 2465:{ + return Emoji_2465, nil + } + case 2466:{ + return Emoji_2466, nil + } + case 2467:{ + return Emoji_2467, nil + } + case 2468:{ + return Emoji_2468, nil + } + case 2469:{ + return Emoji_2469, nil + } + case 2470:{ + return Emoji_2470, nil + } + case 2471:{ + return Emoji_2471, nil + } + case 2472:{ + return Emoji_2472, nil + } + case 2473:{ + return Emoji_2473, nil + } + case 2474:{ + return Emoji_2474, nil + } + case 2475:{ + return Emoji_2475, nil + } + case 2476:{ + return Emoji_2476, nil + } + case 2477:{ + return Emoji_2477, nil + } + case 2478:{ + return Emoji_2478, nil + } + case 2479:{ + return Emoji_2479, nil + } + case 2480:{ + return Emoji_2480, nil + } + case 2481:{ + return Emoji_2481, nil + } + case 2482:{ + return Emoji_2482, nil + } + case 2483:{ + return Emoji_2483, nil + } + case 2484:{ + return Emoji_2484, nil + } + case 2485:{ + return Emoji_2485, nil + } + case 2486:{ + return Emoji_2486, nil + } + case 2487:{ + return Emoji_2487, nil + } + case 2488:{ + return Emoji_2488, nil + } + case 2489:{ + return Emoji_2489, nil + } + case 2490:{ + return Emoji_2490, nil + } + case 2491:{ + return Emoji_2491, nil + } + case 2492:{ + return Emoji_2492, nil + } + case 2493:{ + return Emoji_2493, nil + } + case 2494:{ + return Emoji_2494, nil + } + case 2495:{ + return Emoji_2495, nil + } + case 2496:{ + return Emoji_2496, nil + } + case 2497:{ + return Emoji_2497, nil + } + case 2498:{ + return Emoji_2498, nil + } + case 2499:{ + return Emoji_2499, nil + } + case 2500:{ + return Emoji_2500, nil + } + case 2501:{ + return Emoji_2501, nil + } + case 2502:{ + return Emoji_2502, nil + } + case 2503:{ + return Emoji_2503, nil + } + case 2504:{ + return Emoji_2504, nil + } + case 2505:{ + return Emoji_2505, nil + } + case 2506:{ + return Emoji_2506, nil + } + case 2507:{ + return Emoji_2507, nil + } + case 2508:{ + return Emoji_2508, nil + } + case 2509:{ + return Emoji_2509, nil + } + case 2510:{ + return Emoji_2510, nil + } + case 2511:{ + return Emoji_2511, nil + } + case 2512:{ + return Emoji_2512, nil + } + case 2513:{ + return Emoji_2513, nil + } + case 2514:{ + return Emoji_2514, nil + } + case 2515:{ + return Emoji_2515, nil + } + case 2516:{ + return Emoji_2516, nil + } + case 2517:{ + return Emoji_2517, nil + } + case 2518:{ + return Emoji_2518, nil + } + case 2519:{ + return Emoji_2519, nil + } + case 2520:{ + return Emoji_2520, nil + } + case 2521:{ + return Emoji_2521, nil + } + case 2522:{ + return Emoji_2522, nil + } + case 2523:{ + return Emoji_2523, nil + } + case 2524:{ + return Emoji_2524, nil + } + case 2525:{ + return Emoji_2525, nil + } + case 2526:{ + return Emoji_2526, nil + } + case 2527:{ + return Emoji_2527, nil + } + case 2528:{ + return Emoji_2528, nil + } + case 2529:{ + return Emoji_2529, nil + } + case 2530:{ + return Emoji_2530, nil + } + case 2531:{ + return Emoji_2531, nil + } + case 2532:{ + return Emoji_2532, nil + } + case 2533:{ + return Emoji_2533, nil + } + case 2534:{ + return Emoji_2534, nil + } + case 2535:{ + return Emoji_2535, nil + } + case 2536:{ + return Emoji_2536, nil + } + case 2537:{ + return Emoji_2537, nil + } + case 2538:{ + return Emoji_2538, nil + } + case 2539:{ + return Emoji_2539, nil + } + case 2540:{ + return Emoji_2540, nil + } + case 2541:{ + return Emoji_2541, nil + } + case 2542:{ + return Emoji_2542, nil + } + case 2543:{ + return Emoji_2543, nil + } + case 2544:{ + return Emoji_2544, nil + } + case 2545:{ + return Emoji_2545, nil + } + case 2546:{ + return Emoji_2546, nil + } + case 2547:{ + return Emoji_2547, nil + } + case 2548:{ + return Emoji_2548, nil + } + case 2549:{ + return Emoji_2549, nil + } + case 2550:{ + return Emoji_2550, nil + } + case 2551:{ + return Emoji_2551, nil + } + case 2552:{ + return Emoji_2552, nil + } + case 2553:{ + return Emoji_2553, nil + } + case 2554:{ + return Emoji_2554, nil + } + case 2555:{ + return Emoji_2555, nil + } + case 2556:{ + return Emoji_2556, nil + } + case 2557:{ + return Emoji_2557, nil + } + case 2558:{ + return Emoji_2558, nil + } + case 2559:{ + return Emoji_2559, nil + } + case 2560:{ + return Emoji_2560, nil + } + case 2561:{ + return Emoji_2561, nil + } + case 2562:{ + return Emoji_2562, nil + } + case 2563:{ + return Emoji_2563, nil + } + case 2564:{ + return Emoji_2564, nil + } + case 2565:{ + return Emoji_2565, nil + } + case 2566:{ + return Emoji_2566, nil + } + case 2567:{ + return Emoji_2567, nil + } + case 2568:{ + return Emoji_2568, nil + } + case 2569:{ + return Emoji_2569, nil + } + case 2570:{ + return Emoji_2570, nil + } + case 2571:{ + return Emoji_2571, nil + } + case 2572:{ + return Emoji_2572, nil + } + case 2573:{ + return Emoji_2573, nil + } + case 2574:{ + return Emoji_2574, nil + } + case 2575:{ + return Emoji_2575, nil + } + case 2576:{ + return Emoji_2576, nil + } + case 2577:{ + return Emoji_2577, nil + } + case 2578:{ + return Emoji_2578, nil + } + case 2579:{ + return Emoji_2579, nil + } + case 2580:{ + return Emoji_2580, nil + } + case 2581:{ + return Emoji_2581, nil + } + case 2582:{ + return Emoji_2582, nil + } + case 2583:{ + return Emoji_2583, nil + } + case 2584:{ + return Emoji_2584, nil + } + case 2585:{ + return Emoji_2585, nil + } + case 2586:{ + return Emoji_2586, nil + } + case 2587:{ + return Emoji_2587, nil + } + case 2588:{ + return Emoji_2588, nil + } + case 2589:{ + return Emoji_2589, nil + } + case 2590:{ + return Emoji_2590, nil + } + case 2591:{ + return Emoji_2591, nil + } + case 2592:{ + return Emoji_2592, nil + } + case 2593:{ + return Emoji_2593, nil + } + case 2594:{ + return Emoji_2594, nil + } + case 2595:{ + return Emoji_2595, nil + } + case 2596:{ + return Emoji_2596, nil + } + case 2597:{ + return Emoji_2597, nil + } + case 2598:{ + return Emoji_2598, nil + } + case 2599:{ + return Emoji_2599, nil + } + case 2600:{ + return Emoji_2600, nil + } + case 2601:{ + return Emoji_2601, nil + } + case 2602:{ + return Emoji_2602, nil + } + case 2603:{ + return Emoji_2603, nil + } + case 2604:{ + return Emoji_2604, nil + } + case 2605:{ + return Emoji_2605, nil + } + case 2606:{ + return Emoji_2606, nil + } + case 2607:{ + return Emoji_2607, nil + } + case 2608:{ + return Emoji_2608, nil + } + case 2609:{ + return Emoji_2609, nil + } + case 2610:{ + return Emoji_2610, nil + } + case 2611:{ + return Emoji_2611, nil + } + case 2612:{ + return Emoji_2612, nil + } + case 2613:{ + return Emoji_2613, nil + } + case 2614:{ + return Emoji_2614, nil + } + case 2615:{ + return Emoji_2615, nil + } + case 2616:{ + return Emoji_2616, nil + } + case 2617:{ + return Emoji_2617, nil + } + case 2618:{ + return Emoji_2618, nil + } + case 2619:{ + return Emoji_2619, nil + } + case 2620:{ + return Emoji_2620, nil + } + case 2621:{ + return Emoji_2621, nil + } + case 2622:{ + return Emoji_2622, nil + } + case 2623:{ + return Emoji_2623, nil + } + case 2624:{ + return Emoji_2624, nil + } + case 2625:{ + return Emoji_2625, nil + } + case 2626:{ + return Emoji_2626, nil + } + case 2627:{ + return Emoji_2627, nil + } + case 2628:{ + return Emoji_2628, nil + } + case 2629:{ + return Emoji_2629, nil + } + case 2630:{ + return Emoji_2630, nil + } + case 2631:{ + return Emoji_2631, nil + } + case 2632:{ + return Emoji_2632, nil + } + case 2633:{ + return Emoji_2633, nil + } + case 2634:{ + return Emoji_2634, nil + } + case 2635:{ + return Emoji_2635, nil + } + case 2636:{ + return Emoji_2636, nil + } + case 2637:{ + return Emoji_2637, nil + } + case 2638:{ + return Emoji_2638, nil + } + case 2639:{ + return Emoji_2639, nil + } + case 2640:{ + return Emoji_2640, nil + } + case 2641:{ + return Emoji_2641, nil + } + case 2642:{ + return Emoji_2642, nil + } + case 2643:{ + return Emoji_2643, nil + } + case 2644:{ + return Emoji_2644, nil + } + case 2645:{ + return Emoji_2645, nil + } + case 2646:{ + return Emoji_2646, nil + } + case 2647:{ + return Emoji_2647, nil + } + case 2648:{ + return Emoji_2648, nil + } + case 2649:{ + return Emoji_2649, nil + } + case 2650:{ + return Emoji_2650, nil + } + case 2651:{ + return Emoji_2651, nil + } + case 2652:{ + return Emoji_2652, nil + } + case 2653:{ + return Emoji_2653, nil + } + case 2654:{ + return Emoji_2654, nil + } + case 2655:{ + return Emoji_2655, nil + } + case 2656:{ + return Emoji_2656, nil + } + case 2657:{ + return Emoji_2657, nil + } + case 2658:{ + return Emoji_2658, nil + } + case 2659:{ + return Emoji_2659, nil + } + case 2660:{ + return Emoji_2660, nil + } + case 2661:{ + return Emoji_2661, nil + } + case 2662:{ + return Emoji_2662, nil + } + case 2663:{ + return Emoji_2663, nil + } + case 2664:{ + return Emoji_2664, nil + } + case 2665:{ + return Emoji_2665, nil + } + case 2666:{ + return Emoji_2666, nil + } + case 2667:{ + return Emoji_2667, nil + } + case 2668:{ + return Emoji_2668, nil + } + case 2669:{ + return Emoji_2669, nil + } + case 2670:{ + return Emoji_2670, nil + } + case 2671:{ + return Emoji_2671, nil + } + case 2672:{ + return Emoji_2672, nil + } + case 2673:{ + return Emoji_2673, nil + } + case 2674:{ + return Emoji_2674, nil + } + case 2675:{ + return Emoji_2675, nil + } + case 2676:{ + return Emoji_2676, nil + } + case 2677:{ + return Emoji_2677, nil + } + case 2678:{ + return Emoji_2678, nil + } + case 2679:{ + return Emoji_2679, nil + } + case 2680:{ + return Emoji_2680, nil + } + case 2681:{ + return Emoji_2681, nil + } + case 2682:{ + return Emoji_2682, nil + } + case 2683:{ + return Emoji_2683, nil + } + case 2684:{ + return Emoji_2684, nil + } + case 2685:{ + return Emoji_2685, nil + } + case 2686:{ + return Emoji_2686, nil + } + case 2687:{ + return Emoji_2687, nil + } + case 2688:{ + return Emoji_2688, nil + } + case 2689:{ + return Emoji_2689, nil + } + case 2690:{ + return Emoji_2690, nil + } + case 2691:{ + return Emoji_2691, nil + } + case 2692:{ + return Emoji_2692, nil + } + case 2693:{ + return Emoji_2693, nil + } + case 2694:{ + return Emoji_2694, nil + } + case 2695:{ + return Emoji_2695, nil + } + case 2696:{ + return Emoji_2696, nil + } + case 2697:{ + return Emoji_2697, nil + } + case 2698:{ + return Emoji_2698, nil + } + case 2699:{ + return Emoji_2699, nil + } + case 2700:{ + return Emoji_2700, nil + } + case 2701:{ + return Emoji_2701, nil + } + case 2702:{ + return Emoji_2702, nil + } + case 2703:{ + return Emoji_2703, nil + } + case 2704:{ + return Emoji_2704, nil + } + case 2705:{ + return Emoji_2705, nil + } + case 2706:{ + return Emoji_2706, nil + } + case 2707:{ + return Emoji_2707, nil + } + case 2708:{ + return Emoji_2708, nil + } + case 2709:{ + return Emoji_2709, nil + } + case 2710:{ + return Emoji_2710, nil + } + case 2711:{ + return Emoji_2711, nil + } + case 2712:{ + return Emoji_2712, nil + } + case 2713:{ + return Emoji_2713, nil + } + case 2714:{ + return Emoji_2714, nil + } + case 2715:{ + return Emoji_2715, nil + } + case 2716:{ + return Emoji_2716, nil + } + case 2717:{ + return Emoji_2717, nil + } + case 2718:{ + return Emoji_2718, nil + } + case 2719:{ + return Emoji_2719, nil + } + case 2720:{ + return Emoji_2720, nil + } + case 2721:{ + return Emoji_2721, nil + } + case 2722:{ + return Emoji_2722, nil + } + case 2723:{ + return Emoji_2723, nil + } + case 2724:{ + return Emoji_2724, nil + } + case 2725:{ + return Emoji_2725, nil + } + case 2726:{ + return Emoji_2726, nil + } + case 2727:{ + return Emoji_2727, nil + } + case 2728:{ + return Emoji_2728, nil + } + case 2729:{ + return Emoji_2729, nil + } + case 2730:{ + return Emoji_2730, nil + } + case 2731:{ + return Emoji_2731, nil + } + case 2732:{ + return Emoji_2732, nil + } + case 2733:{ + return Emoji_2733, nil + } + case 2734:{ + return Emoji_2734, nil + } + case 2735:{ + return Emoji_2735, nil + } + case 2736:{ + return Emoji_2736, nil + } + case 2737:{ + return Emoji_2737, nil + } + case 2738:{ + return Emoji_2738, nil + } + case 2739:{ + return Emoji_2739, nil + } + case 2740:{ + return Emoji_2740, nil + } + case 2741:{ + return Emoji_2741, nil + } + case 2742:{ + return Emoji_2742, nil + } + case 2743:{ + return Emoji_2743, nil + } + case 2744:{ + return Emoji_2744, nil + } + case 2745:{ + return Emoji_2745, nil + } + case 2746:{ + return Emoji_2746, nil + } + case 2747:{ + return Emoji_2747, nil + } + case 2748:{ + return Emoji_2748, nil + } + case 2749:{ + return Emoji_2749, nil + } + case 2750:{ + return Emoji_2750, nil + } + case 2751:{ + return Emoji_2751, nil + } + case 2752:{ + return Emoji_2752, nil + } + case 2753:{ + return Emoji_2753, nil + } + case 2754:{ + return Emoji_2754, nil + } + case 2755:{ + return Emoji_2755, nil + } + case 2756:{ + return Emoji_2756, nil + } + case 2757:{ + return Emoji_2757, nil + } + case 2758:{ + return Emoji_2758, nil + } + case 2759:{ + return Emoji_2759, nil + } + case 2760:{ + return Emoji_2760, nil + } + case 2761:{ + return Emoji_2761, nil + } + case 2762:{ + return Emoji_2762, nil + } + case 2763:{ + return Emoji_2763, nil + } + case 2764:{ + return Emoji_2764, nil + } + case 2765:{ + return Emoji_2765, nil + } + case 2766:{ + return Emoji_2766, nil + } + case 2767:{ + return Emoji_2767, nil + } + case 2768:{ + return Emoji_2768, nil + } + case 2769:{ + return Emoji_2769, nil + } + case 2770:{ + return Emoji_2770, nil + } + case 2771:{ + return Emoji_2771, nil + } + case 2772:{ + return Emoji_2772, nil + } + case 2773:{ + return Emoji_2773, nil + } + case 2774:{ + return Emoji_2774, nil + } + case 2775:{ + return Emoji_2775, nil + } + case 2776:{ + return Emoji_2776, nil + } + case 2777:{ + return Emoji_2777, nil + } + case 2778:{ + return Emoji_2778, nil + } + case 2779:{ + return Emoji_2779, nil + } + case 2780:{ + return Emoji_2780, nil + } + case 2781:{ + return Emoji_2781, nil + } + case 2782:{ + return Emoji_2782, nil + } + case 2783:{ + return Emoji_2783, nil + } + case 2784:{ + return Emoji_2784, nil + } + case 2785:{ + return Emoji_2785, nil + } + case 2786:{ + return Emoji_2786, nil + } + case 2787:{ + return Emoji_2787, nil + } + case 2788:{ + return Emoji_2788, nil + } + case 2789:{ + return Emoji_2789, nil + } + case 2790:{ + return Emoji_2790, nil + } + case 2791:{ + return Emoji_2791, nil + } + case 2792:{ + return Emoji_2792, nil + } + case 2793:{ + return Emoji_2793, nil + } + case 2794:{ + return Emoji_2794, nil + } + case 2795:{ + return Emoji_2795, nil + } + case 2796:{ + return Emoji_2796, nil + } + case 2797:{ + return Emoji_2797, nil + } + case 2798:{ + return Emoji_2798, nil + } + case 2799:{ + return Emoji_2799, nil + } + case 2800:{ + return Emoji_2800, nil + } + case 2801:{ + return Emoji_2801, nil + } + case 2802:{ + return Emoji_2802, nil + } + case 2803:{ + return Emoji_2803, nil + } + case 2804:{ + return Emoji_2804, nil + } + case 2805:{ + return Emoji_2805, nil + } + case 2806:{ + return Emoji_2806, nil + } + case 2807:{ + return Emoji_2807, nil + } + case 2808:{ + return Emoji_2808, nil + } + case 2809:{ + return Emoji_2809, nil + } + case 2810:{ + return Emoji_2810, nil + } + case 2811:{ + return Emoji_2811, nil + } + case 2812:{ + return Emoji_2812, nil + } + case 2813:{ + return Emoji_2813, nil + } + case 2814:{ + return Emoji_2814, nil + } + case 2815:{ + return Emoji_2815, nil + } + case 2816:{ + return Emoji_2816, nil + } + case 2817:{ + return Emoji_2817, nil + } + case 2818:{ + return Emoji_2818, nil + } + case 2819:{ + return Emoji_2819, nil + } + case 2820:{ + return Emoji_2820, nil + } + case 2821:{ + return Emoji_2821, nil + } + case 2822:{ + return Emoji_2822, nil + } + case 2823:{ + return Emoji_2823, nil + } + case 2824:{ + return Emoji_2824, nil + } + case 2825:{ + return Emoji_2825, nil + } + case 2826:{ + return Emoji_2826, nil + } + case 2827:{ + return Emoji_2827, nil + } + case 2828:{ + return Emoji_2828, nil + } + case 2829:{ + return Emoji_2829, nil + } + case 2830:{ + return Emoji_2830, nil + } + case 2831:{ + return Emoji_2831, nil + } + case 2832:{ + return Emoji_2832, nil + } + case 2833:{ + return Emoji_2833, nil + } + case 2834:{ + return Emoji_2834, nil + } + case 2835:{ + return Emoji_2835, nil + } + case 2836:{ + return Emoji_2836, nil + } + case 2837:{ + return Emoji_2837, nil + } + case 2838:{ + return Emoji_2838, nil + } + case 2839:{ + return Emoji_2839, nil + } + case 2840:{ + return Emoji_2840, nil + } + case 2841:{ + return Emoji_2841, nil + } + case 2842:{ + return Emoji_2842, nil + } + case 2843:{ + return Emoji_2843, nil + } + case 2844:{ + return Emoji_2844, nil + } + case 2845:{ + return Emoji_2845, nil + } + case 2846:{ + return Emoji_2846, nil + } + case 2847:{ + return Emoji_2847, nil + } + case 2848:{ + return Emoji_2848, nil + } + case 2849:{ + return Emoji_2849, nil + } + case 2850:{ + return Emoji_2850, nil + } + case 2851:{ + return Emoji_2851, nil + } + case 2852:{ + return Emoji_2852, nil + } + case 2853:{ + return Emoji_2853, nil + } + case 2854:{ + return Emoji_2854, nil + } + case 2855:{ + return Emoji_2855, nil + } + case 2856:{ + return Emoji_2856, nil + } + case 2857:{ + return Emoji_2857, nil + } + case 2858:{ + return Emoji_2858, nil + } + case 2859:{ + return Emoji_2859, nil + } + case 2860:{ + return Emoji_2860, nil + } + case 2861:{ + return Emoji_2861, nil + } + case 2862:{ + return Emoji_2862, nil + } + case 2863:{ + return Emoji_2863, nil + } + case 2864:{ + return Emoji_2864, nil + } + case 2865:{ + return Emoji_2865, nil + } + case 2866:{ + return Emoji_2866, nil + } + case 2867:{ + return Emoji_2867, nil + } + case 2868:{ + return Emoji_2868, nil + } + case 2869:{ + return Emoji_2869, nil + } + case 2870:{ + return Emoji_2870, nil + } + case 2871:{ + return Emoji_2871, nil + } + case 2872:{ + return Emoji_2872, nil + } + case 2873:{ + return Emoji_2873, nil + } + case 2874:{ + return Emoji_2874, nil + } + case 2875:{ + return Emoji_2875, nil + } + case 2876:{ + return Emoji_2876, nil + } + case 2877:{ + return Emoji_2877, nil + } + case 2878:{ + return Emoji_2878, nil + } + case 2879:{ + return Emoji_2879, nil + } + case 2880:{ + return Emoji_2880, nil + } + case 2881:{ + return Emoji_2881, nil + } + case 2882:{ + return Emoji_2882, nil + } + case 2883:{ + return Emoji_2883, nil + } + case 2884:{ + return Emoji_2884, nil + } + case 2885:{ + return Emoji_2885, nil + } + case 2886:{ + return Emoji_2886, nil + } + case 2887:{ + return Emoji_2887, nil + } + case 2888:{ + return Emoji_2888, nil + } + case 2889:{ + return Emoji_2889, nil + } + case 2890:{ + return Emoji_2890, nil + } + case 2891:{ + return Emoji_2891, nil + } + case 2892:{ + return Emoji_2892, nil + } + case 2893:{ + return Emoji_2893, nil + } + case 2894:{ + return Emoji_2894, nil + } + case 2895:{ + return Emoji_2895, nil + } + case 2896:{ + return Emoji_2896, nil + } + case 2897:{ + return Emoji_2897, nil + } + case 2898:{ + return Emoji_2898, nil + } + case 2899:{ + return Emoji_2899, nil + } + case 2900:{ + return Emoji_2900, nil + } + case 2901:{ + return Emoji_2901, nil + } + case 2902:{ + return Emoji_2902, nil + } + case 2903:{ + return Emoji_2903, nil + } + case 2904:{ + return Emoji_2904, nil + } + case 2905:{ + return Emoji_2905, nil + } + case 2906:{ + return Emoji_2906, nil + } + case 2907:{ + return Emoji_2907, nil + } + case 2908:{ + return Emoji_2908, nil + } + case 2909:{ + return Emoji_2909, nil + } + case 2910:{ + return Emoji_2910, nil + } + case 2911:{ + return Emoji_2911, nil + } + case 2912:{ + return Emoji_2912, nil + } + case 2913:{ + return Emoji_2913, nil + } + case 2914:{ + return Emoji_2914, nil + } + case 2915:{ + return Emoji_2915, nil + } + case 2916:{ + return Emoji_2916, nil + } + case 2917:{ + return Emoji_2917, nil + } + case 2918:{ + return Emoji_2918, nil + } + case 2919:{ + return Emoji_2919, nil + } + case 2920:{ + return Emoji_2920, nil + } + case 2921:{ + return Emoji_2921, nil + } + case 2922:{ + return Emoji_2922, nil + } + case 2923:{ + return Emoji_2923, nil + } + case 2924:{ + return Emoji_2924, nil + } + case 2925:{ + return Emoji_2925, nil + } + case 2926:{ + return Emoji_2926, nil + } + case 2927:{ + return Emoji_2927, nil + } + case 2928:{ + return Emoji_2928, nil + } + case 2929:{ + return Emoji_2929, nil + } + case 2930:{ + return Emoji_2930, nil + } + case 2931:{ + return Emoji_2931, nil + } + case 2932:{ + return Emoji_2932, nil + } + case 2933:{ + return Emoji_2933, nil + } + case 2934:{ + return Emoji_2934, nil + } + case 2935:{ + return Emoji_2935, nil + } + case 2936:{ + return Emoji_2936, nil + } + case 2937:{ + return Emoji_2937, nil + } + case 2938:{ + return Emoji_2938, nil + } + case 2939:{ + return Emoji_2939, nil + } + case 2940:{ + return Emoji_2940, nil + } + case 2941:{ + return Emoji_2941, nil + } + case 2942:{ + return Emoji_2942, nil + } + case 2943:{ + return Emoji_2943, nil + } + case 2944:{ + return Emoji_2944, nil + } + case 2945:{ + return Emoji_2945, nil + } + case 2946:{ + return Emoji_2946, nil + } + case 2947:{ + return Emoji_2947, nil + } + case 2948:{ + return Emoji_2948, nil + } + case 2949:{ + return Emoji_2949, nil + } + case 2950:{ + return Emoji_2950, nil + } + case 2951:{ + return Emoji_2951, nil + } + case 2952:{ + return Emoji_2952, nil + } + case 2953:{ + return Emoji_2953, nil + } + case 2954:{ + return Emoji_2954, nil + } + case 2955:{ + return Emoji_2955, nil + } + case 2956:{ + return Emoji_2956, nil + } + case 2957:{ + return Emoji_2957, nil + } + case 2958:{ + return Emoji_2958, nil + } + case 2959:{ + return Emoji_2959, nil + } + case 2960:{ + return Emoji_2960, nil + } + case 2961:{ + return Emoji_2961, nil + } + case 2962:{ + return Emoji_2962, nil + } + case 2963:{ + return Emoji_2963, nil + } + case 2964:{ + return Emoji_2964, nil + } + case 2965:{ + return Emoji_2965, nil + } + case 2966:{ + return Emoji_2966, nil + } + case 2967:{ + return Emoji_2967, nil + } + case 2968:{ + return Emoji_2968, nil + } + case 2969:{ + return Emoji_2969, nil + } + case 2970:{ + return Emoji_2970, nil + } + case 2971:{ + return Emoji_2971, nil + } + case 2972:{ + return Emoji_2972, nil + } + case 2973:{ + return Emoji_2973, nil + } + case 2974:{ + return Emoji_2974, nil + } + case 2975:{ + return Emoji_2975, nil + } + case 2976:{ + return Emoji_2976, nil + } + case 2977:{ + return Emoji_2977, nil + } + case 2978:{ + return Emoji_2978, nil + } + case 2979:{ + return Emoji_2979, nil + } + case 2980:{ + return Emoji_2980, nil + } + case 2981:{ + return Emoji_2981, nil + } + case 2982:{ + return Emoji_2982, nil + } + case 2983:{ + return Emoji_2983, nil + } + case 2984:{ + return Emoji_2984, nil + } + case 2985:{ + return Emoji_2985, nil + } + case 2986:{ + return Emoji_2986, nil + } + case 2987:{ + return Emoji_2987, nil + } + case 2988:{ + return Emoji_2988, nil + } + case 2989:{ + return Emoji_2989, nil + } + case 2990:{ + return Emoji_2990, nil + } + case 2991:{ + return Emoji_2991, nil + } + case 2992:{ + return Emoji_2992, nil + } + case 2993:{ + return Emoji_2993, nil + } + case 2994:{ + return Emoji_2994, nil + } + case 2995:{ + return Emoji_2995, nil + } + case 2996:{ + return Emoji_2996, nil + } + case 2997:{ + return Emoji_2997, nil + } + case 2998:{ + return Emoji_2998, nil + } + case 2999:{ + return Emoji_2999, nil + } + case 3000:{ + return Emoji_3000, nil + } + case 3001:{ + return Emoji_3001, nil + } + case 3002:{ + return Emoji_3002, nil + } + case 3003:{ + return Emoji_3003, nil + } + case 3004:{ + return Emoji_3004, nil + } + case 3005:{ + return Emoji_3005, nil + } + case 3006:{ + return Emoji_3006, nil + } + case 3007:{ + return Emoji_3007, nil + } + case 3008:{ + return Emoji_3008, nil + } + case 3009:{ + return Emoji_3009, nil + } + case 3010:{ + return Emoji_3010, nil + } + case 3011:{ + return Emoji_3011, nil + } + case 3012:{ + return Emoji_3012, nil + } + case 3013:{ + return Emoji_3013, nil + } + case 3014:{ + return Emoji_3014, nil + } + case 3015:{ + return Emoji_3015, nil + } + case 3016:{ + return Emoji_3016, nil + } + case 3017:{ + return Emoji_3017, nil + } + case 3018:{ + return Emoji_3018, nil + } + case 3019:{ + return Emoji_3019, nil + } + case 3020:{ + return Emoji_3020, nil + } + case 3021:{ + return Emoji_3021, nil + } + case 3022:{ + return Emoji_3022, nil + } + case 3023:{ + return Emoji_3023, nil + } + case 3024:{ + return Emoji_3024, nil + } + case 3025:{ + return Emoji_3025, nil + } + case 3026:{ + return Emoji_3026, nil + } + case 3027:{ + return Emoji_3027, nil + } + case 3028:{ + return Emoji_3028, nil + } + case 3029:{ + return Emoji_3029, nil + } + case 3030:{ + return Emoji_3030, nil + } + case 3031:{ + return Emoji_3031, nil + } + case 3032:{ + return Emoji_3032, nil + } + case 3033:{ + return Emoji_3033, nil + } + case 3034:{ + return Emoji_3034, nil + } + case 3035:{ + return Emoji_3035, nil + } + case 3036:{ + return Emoji_3036, nil + } + case 3037:{ + return Emoji_3037, nil + } + case 3038:{ + return Emoji_3038, nil + } + case 3039:{ + return Emoji_3039, nil + } + case 3040:{ + return Emoji_3040, nil + } + case 3041:{ + return Emoji_3041, nil + } + case 3042:{ + return Emoji_3042, nil + } + case 3043:{ + return Emoji_3043, nil + } + case 3044:{ + return Emoji_3044, nil + } + case 3045:{ + return Emoji_3045, nil + } + case 3046:{ + return Emoji_3046, nil + } + case 3047:{ + return Emoji_3047, nil + } + case 3048:{ + return Emoji_3048, nil + } + case 3049:{ + return Emoji_3049, nil + } + case 3050:{ + return Emoji_3050, nil + } + case 3051:{ + return Emoji_3051, nil + } + case 3052:{ + return Emoji_3052, nil + } + case 3053:{ + return Emoji_3053, nil + } + case 3054:{ + return Emoji_3054, nil + } + case 3055:{ + return Emoji_3055, nil + } + case 3056:{ + return Emoji_3056, nil + } + case 3057:{ + return Emoji_3057, nil + } + case 3058:{ + return Emoji_3058, nil + } + case 3059:{ + return Emoji_3059, nil + } + case 3060:{ + return Emoji_3060, nil + } + case 3061:{ + return Emoji_3061, nil + } + case 3062:{ + return Emoji_3062, nil + } + case 3063:{ + return Emoji_3063, nil + } + case 3064:{ + return Emoji_3064, nil + } + case 3065:{ + return Emoji_3065, nil + } + case 3066:{ + return Emoji_3066, nil + } + case 3067:{ + return Emoji_3067, nil + } + case 3068:{ + return Emoji_3068, nil + } + case 3069:{ + return Emoji_3069, nil + } + case 3070:{ + return Emoji_3070, nil + } + case 3071:{ + return Emoji_3071, nil + } + case 3072:{ + return Emoji_3072, nil + } + case 3073:{ + return Emoji_3073, nil + } + case 3074:{ + return Emoji_3074, nil + } + case 3075:{ + return Emoji_3075, nil + } + case 3076:{ + return Emoji_3076, nil + } + case 3077:{ + return Emoji_3077, nil + } + case 3078:{ + return Emoji_3078, nil + } + case 3079:{ + return Emoji_3079, nil + } + case 3080:{ + return Emoji_3080, nil + } + case 3081:{ + return Emoji_3081, nil + } + case 3082:{ + return Emoji_3082, nil + } + case 3083:{ + return Emoji_3083, nil + } + case 3084:{ + return Emoji_3084, nil + } + case 3085:{ + return Emoji_3085, nil + } + case 3086:{ + return Emoji_3086, nil + } + case 3087:{ + return Emoji_3087, nil + } + case 3088:{ + return Emoji_3088, nil + } + case 3089:{ + return Emoji_3089, nil + } + case 3090:{ + return Emoji_3090, nil + } + case 3091:{ + return Emoji_3091, nil + } + case 3092:{ + return Emoji_3092, nil + } + case 3093:{ + return Emoji_3093, nil + } + case 3094:{ + return Emoji_3094, nil + } + case 3095:{ + return Emoji_3095, nil + } + case 3096:{ + return Emoji_3096, nil + } + case 3097:{ + return Emoji_3097, nil + } + case 3098:{ + return Emoji_3098, nil + } + case 3099:{ + return Emoji_3099, nil + } + case 3100:{ + return Emoji_3100, nil + } + case 3101:{ + return Emoji_3101, nil + } + case 3102:{ + return Emoji_3102, nil + } + case 3103:{ + return Emoji_3103, nil + } + case 3104:{ + return Emoji_3104, nil + } + case 3105:{ + return Emoji_3105, nil + } + case 3106:{ + return Emoji_3106, nil + } + case 3107:{ + return Emoji_3107, nil + } + case 3108:{ + return Emoji_3108, nil + } + case 3109:{ + return Emoji_3109, nil + } + case 3110:{ + return Emoji_3110, nil + } + case 3111:{ + return Emoji_3111, nil + } + case 3112:{ + return Emoji_3112, nil + } + case 3113:{ + return Emoji_3113, nil + } + case 3114:{ + return Emoji_3114, nil + } + case 3115:{ + return Emoji_3115, nil + } + case 3116:{ + return Emoji_3116, nil + } + case 3117:{ + return Emoji_3117, nil + } + case 3118:{ + return Emoji_3118, nil + } + case 3119:{ + return Emoji_3119, nil + } + case 3120:{ + return Emoji_3120, nil + } + case 3121:{ + return Emoji_3121, nil + } + case 3122:{ + return Emoji_3122, nil + } + case 3123:{ + return Emoji_3123, nil + } + case 3124:{ + return Emoji_3124, nil + } + case 3125:{ + return Emoji_3125, nil + } + case 3126:{ + return Emoji_3126, nil + } + case 3127:{ + return Emoji_3127, nil + } + case 3128:{ + return Emoji_3128, nil + } + case 3129:{ + return Emoji_3129, nil + } + case 3130:{ + return Emoji_3130, nil + } + case 3131:{ + return Emoji_3131, nil + } + case 3132:{ + return Emoji_3132, nil + } + case 3133:{ + return Emoji_3133, nil + } + case 3134:{ + return Emoji_3134, nil + } + case 3135:{ + return Emoji_3135, nil + } + case 3136:{ + return Emoji_3136, nil + } + case 3137:{ + return Emoji_3137, nil + } + case 3138:{ + return Emoji_3138, nil + } + case 3139:{ + return Emoji_3139, nil + } + case 3140:{ + return Emoji_3140, nil + } + case 3141:{ + return Emoji_3141, nil + } + case 3142:{ + return Emoji_3142, nil + } + case 3143:{ + return Emoji_3143, nil + } + case 3144:{ + return Emoji_3144, nil + } + case 3145:{ + return Emoji_3145, nil + } + case 3146:{ + return Emoji_3146, nil + } + case 3147:{ + return Emoji_3147, nil + } + case 3148:{ + return Emoji_3148, nil + } + case 3149:{ + return Emoji_3149, nil + } + case 3150:{ + return Emoji_3150, nil + } + case 3151:{ + return Emoji_3151, nil + } + case 3152:{ + return Emoji_3152, nil + } + case 3153:{ + return Emoji_3153, nil + } + case 3154:{ + return Emoji_3154, nil + } + case 3155:{ + return Emoji_3155, nil + } + case 3156:{ + return Emoji_3156, nil + } + case 3157:{ + return Emoji_3157, nil + } + case 3158:{ + return Emoji_3158, nil + } + case 3159:{ + return Emoji_3159, nil + } + case 3160:{ + return Emoji_3160, nil + } + case 3161:{ + return Emoji_3161, nil + } + case 3162:{ + return Emoji_3162, nil + } + case 3163:{ + return Emoji_3163, nil + } + case 3164:{ + return Emoji_3164, nil + } + case 3165:{ + return Emoji_3165, nil + } + case 3166:{ + return Emoji_3166, nil + } + case 3167:{ + return Emoji_3167, nil + } + case 3168:{ + return Emoji_3168, nil + } + case 3169:{ + return Emoji_3169, nil + } + case 3170:{ + return Emoji_3170, nil + } + case 3171:{ + return Emoji_3171, nil + } + case 3172:{ + return Emoji_3172, nil + } + case 3173:{ + return Emoji_3173, nil + } + case 3174:{ + return Emoji_3174, nil + } + case 3175:{ + return Emoji_3175, nil + } + case 3176:{ + return Emoji_3176, nil + } + case 3177:{ + return Emoji_3177, nil + } + case 3178:{ + return Emoji_3178, nil + } + case 3179:{ + return Emoji_3179, nil + } + case 3180:{ + return Emoji_3180, nil + } + case 3181:{ + return Emoji_3181, nil + } + case 3182:{ + return Emoji_3182, nil + } + case 3183:{ + return Emoji_3183, nil + } + case 3184:{ + return Emoji_3184, nil + } + case 3185:{ + return Emoji_3185, nil + } + case 3186:{ + return Emoji_3186, nil + } + case 3187:{ + return Emoji_3187, nil + } + case 3188:{ + return Emoji_3188, nil + } + case 3189:{ + return Emoji_3189, nil + } + case 3190:{ + return Emoji_3190, nil + } + case 3191:{ + return Emoji_3191, nil + } + case 3192:{ + return Emoji_3192, nil + } + case 3193:{ + return Emoji_3193, nil + } + case 3194:{ + return Emoji_3194, nil + } + case 3195:{ + return Emoji_3195, nil + } + case 3196:{ + return Emoji_3196, nil + } + case 3197:{ + return Emoji_3197, nil + } + case 3198:{ + return Emoji_3198, nil + } + case 3199:{ + return Emoji_3199, nil + } + case 3200:{ + return Emoji_3200, nil + } + case 3201:{ + return Emoji_3201, nil + } + case 3202:{ + return Emoji_3202, nil + } + case 3203:{ + return Emoji_3203, nil + } + case 3204:{ + return Emoji_3204, nil + } + case 3205:{ + return Emoji_3205, nil + } + case 3206:{ + return Emoji_3206, nil + } + case 3207:{ + return Emoji_3207, nil + } + case 3208:{ + return Emoji_3208, nil + } + case 3209:{ + return Emoji_3209, nil + } + case 3210:{ + return Emoji_3210, nil + } + case 3211:{ + return Emoji_3211, nil + } + case 3212:{ + return Emoji_3212, nil + } + case 3213:{ + return Emoji_3213, nil + } + case 3214:{ + return Emoji_3214, nil + } + case 3215:{ + return Emoji_3215, nil + } + case 3216:{ + return Emoji_3216, nil + } + case 3217:{ + return Emoji_3217, nil + } + case 3218:{ + return Emoji_3218, nil + } + case 3219:{ + return Emoji_3219, nil + } + case 3220:{ + return Emoji_3220, nil + } + case 3221:{ + return Emoji_3221, nil + } + case 3222:{ + return Emoji_3222, nil + } + case 3223:{ + return Emoji_3223, nil + } + case 3224:{ + return Emoji_3224, nil + } + case 3225:{ + return Emoji_3225, nil + } + case 3226:{ + return Emoji_3226, nil + } + case 3227:{ + return Emoji_3227, nil + } + case 3228:{ + return Emoji_3228, nil + } + case 3229:{ + return Emoji_3229, nil + } + case 3230:{ + return Emoji_3230, nil + } + case 3231:{ + return Emoji_3231, nil + } + case 3232:{ + return Emoji_3232, nil + } + case 3233:{ + return Emoji_3233, nil + } + case 3234:{ + return Emoji_3234, nil + } + case 3235:{ + return Emoji_3235, nil + } + case 3236:{ + return Emoji_3236, nil + } + case 3237:{ + return Emoji_3237, nil + } + case 3238:{ + return Emoji_3238, nil + } + case 3239:{ + return Emoji_3239, nil + } + case 3240:{ + return Emoji_3240, nil + } + case 3241:{ + return Emoji_3241, nil + } + case 3242:{ + return Emoji_3242, nil + } + case 3243:{ + return Emoji_3243, nil + } + case 3244:{ + return Emoji_3244, nil + } + case 3245:{ + return Emoji_3245, nil + } + case 3246:{ + return Emoji_3246, nil + } + case 3247:{ + return Emoji_3247, nil + } + case 3248:{ + return Emoji_3248, nil + } + case 3249:{ + return Emoji_3249, nil + } + case 3250:{ + return Emoji_3250, nil + } + case 3251:{ + return Emoji_3251, nil + } + case 3252:{ + return Emoji_3252, nil + } + case 3253:{ + return Emoji_3253, nil + } + case 3254:{ + return Emoji_3254, nil + } + case 3255:{ + return Emoji_3255, nil + } + case 3256:{ + return Emoji_3256, nil + } + case 3257:{ + return Emoji_3257, nil + } + case 3258:{ + return Emoji_3258, nil + } + case 3259:{ + return Emoji_3259, nil + } + case 3260:{ + return Emoji_3260, nil + } + case 3261:{ + return Emoji_3261, nil + } + case 3262:{ + return Emoji_3262, nil + } + case 3263:{ + return Emoji_3263, nil + } + case 3264:{ + return Emoji_3264, nil + } + case 3265:{ + return Emoji_3265, nil + } + case 3266:{ + return Emoji_3266, nil + } + case 3267:{ + return Emoji_3267, nil + } + case 3268:{ + return Emoji_3268, nil + } + case 3269:{ + return Emoji_3269, nil + } + case 3270:{ + return Emoji_3270, nil + } + case 3271:{ + return Emoji_3271, nil + } + case 3272:{ + return Emoji_3272, nil + } + case 3273:{ + return Emoji_3273, nil + } + case 3274:{ + return Emoji_3274, nil + } + case 3275:{ + return Emoji_3275, nil + } + case 3276:{ + return Emoji_3276, nil + } + case 3277:{ + return Emoji_3277, nil + } + case 3278:{ + return Emoji_3278, nil + } + case 3279:{ + return Emoji_3279, nil + } + case 3280:{ + return Emoji_3280, nil + } + case 3281:{ + return Emoji_3281, nil + } + case 3282:{ + return Emoji_3282, nil + } + case 3283:{ + return Emoji_3283, nil + } + case 3284:{ + return Emoji_3284, nil + } + case 3285:{ + return Emoji_3285, nil + } + case 3286:{ + return Emoji_3286, nil + } + case 3287:{ + return Emoji_3287, nil + } + case 3288:{ + return Emoji_3288, nil + } + case 3289:{ + return Emoji_3289, nil + } + case 3290:{ + return Emoji_3290, nil + } + case 3291:{ + return Emoji_3291, nil + } + case 3292:{ + return Emoji_3292, nil + } + case 3293:{ + return Emoji_3293, nil + } + case 3294:{ + return Emoji_3294, nil + } + case 3295:{ + return Emoji_3295, nil + } + case 3296:{ + return Emoji_3296, nil + } + case 3297:{ + return Emoji_3297, nil + } + case 3298:{ + return Emoji_3298, nil + } + case 3299:{ + return Emoji_3299, nil + } + case 3300:{ + return Emoji_3300, nil + } + case 3301:{ + return Emoji_3301, nil + } + case 3302:{ + return Emoji_3302, nil + } + case 3303:{ + return Emoji_3303, nil + } + case 3304:{ + return Emoji_3304, nil + } + case 3305:{ + return Emoji_3305, nil + } + case 3306:{ + return Emoji_3306, nil + } + case 3307:{ + return Emoji_3307, nil + } + case 3308:{ + return Emoji_3308, nil + } + case 3309:{ + return Emoji_3309, nil + } + case 3310:{ + return Emoji_3310, nil + } + case 3311:{ + return Emoji_3311, nil + } + case 3312:{ + return Emoji_3312, nil + } + case 3313:{ + return Emoji_3313, nil + } + case 3314:{ + return Emoji_3314, nil + } + case 3315:{ + return Emoji_3315, nil + } + case 3316:{ + return Emoji_3316, nil + } + case 3317:{ + return Emoji_3317, nil + } + case 3318:{ + return Emoji_3318, nil + } + case 3319:{ + return Emoji_3319, nil + } + case 3320:{ + return Emoji_3320, nil + } + case 3321:{ + return Emoji_3321, nil + } + case 3322:{ + return Emoji_3322, nil + } + case 3323:{ + return Emoji_3323, nil + } + case 3324:{ + return Emoji_3324, nil + } + case 3325:{ + return Emoji_3325, nil + } + case 3326:{ + return Emoji_3326, nil + } + case 3327:{ + return Emoji_3327, nil + } + case 3328:{ + return Emoji_3328, nil + } + case 3329:{ + return Emoji_3329, nil + } + case 3330:{ + return Emoji_3330, nil + } + case 3331:{ + return Emoji_3331, nil + } + case 3332:{ + return Emoji_3332, nil + } + case 3333:{ + return Emoji_3333, nil + } + case 3334:{ + return Emoji_3334, nil + } + case 3335:{ + return Emoji_3335, nil + } + case 3336:{ + return Emoji_3336, nil + } + case 3337:{ + return Emoji_3337, nil + } + case 3338:{ + return Emoji_3338, nil + } + case 3339:{ + return Emoji_3339, nil + } + case 3340:{ + return Emoji_3340, nil + } + case 3341:{ + return Emoji_3341, nil + } + case 3342:{ + return Emoji_3342, nil + } + case 3343:{ + return Emoji_3343, nil + } + case 3344:{ + return Emoji_3344, nil + } + case 3345:{ + return Emoji_3345, nil + } + case 3346:{ + return Emoji_3346, nil + } + case 3347:{ + return Emoji_3347, nil + } + case 3348:{ + return Emoji_3348, nil + } + case 3349:{ + return Emoji_3349, nil + } + case 3350:{ + return Emoji_3350, nil + } + case 3351:{ + return Emoji_3351, nil + } + case 3352:{ + return Emoji_3352, nil + } + case 3353:{ + return Emoji_3353, nil + } + case 3354:{ + return Emoji_3354, nil + } + case 3355:{ + return Emoji_3355, nil + } + case 3356:{ + return Emoji_3356, nil + } + case 3357:{ + return Emoji_3357, nil + } + case 3358:{ + return Emoji_3358, nil + } + case 3359:{ + return Emoji_3359, nil + } + case 3360:{ + return Emoji_3360, nil + } + case 3361:{ + return Emoji_3361, nil + } + case 3362:{ + return Emoji_3362, nil + } + case 3363:{ + return Emoji_3363, nil + } + case 3364:{ + return Emoji_3364, nil + } + case 3365:{ + return Emoji_3365, nil + } + case 3366:{ + return Emoji_3366, nil + } + case 3367:{ + return Emoji_3367, nil + } + case 3368:{ + return Emoji_3368, nil + } + case 3369:{ + return Emoji_3369, nil + } + case 3370:{ + return Emoji_3370, nil + } + case 3371:{ + return Emoji_3371, nil + } + case 3372:{ + return Emoji_3372, nil + } + case 3373:{ + return Emoji_3373, nil + } + case 3374:{ + return Emoji_3374, nil + } + case 3375:{ + return Emoji_3375, nil + } + case 3376:{ + return Emoji_3376, nil + } + case 3377:{ + return Emoji_3377, nil + } + case 3378:{ + return Emoji_3378, nil + } + case 3379:{ + return Emoji_3379, nil + } + case 3380:{ + return Emoji_3380, nil + } + case 3381:{ + return Emoji_3381, nil + } + case 3382:{ + return Emoji_3382, nil + } + case 3383:{ + return Emoji_3383, nil + } + case 3384:{ + return Emoji_3384, nil + } + case 3385:{ + return Emoji_3385, nil + } + case 3386:{ + return Emoji_3386, nil + } + case 3387:{ + return Emoji_3387, nil + } + case 3388:{ + return Emoji_3388, nil + } + case 3389:{ + return Emoji_3389, nil + } + case 3390:{ + return Emoji_3390, nil + } + case 3391:{ + return Emoji_3391, nil + } + case 3392:{ + return Emoji_3392, nil + } + case 3393:{ + return Emoji_3393, nil + } + case 3394:{ + return Emoji_3394, nil + } + case 3395:{ + return Emoji_3395, nil + } + case 3396:{ + return Emoji_3396, nil + } + case 3397:{ + return Emoji_3397, nil + } + case 3398:{ + return Emoji_3398, nil + } + case 3399:{ + return Emoji_3399, nil + } + case 3400:{ + return Emoji_3400, nil + } + case 3401:{ + return Emoji_3401, nil + } + case 3402:{ + return Emoji_3402, nil + } + case 3403:{ + return Emoji_3403, nil + } + case 3404:{ + return Emoji_3404, nil + } + case 3405:{ + return Emoji_3405, nil + } + case 3406:{ + return Emoji_3406, nil + } + case 3407:{ + return Emoji_3407, nil + } + case 3408:{ + return Emoji_3408, nil + } + case 3409:{ + return Emoji_3409, nil + } + case 3410:{ + return Emoji_3410, nil + } + case 3411:{ + return Emoji_3411, nil + } + case 3412:{ + return Emoji_3412, nil + } + case 3413:{ + return Emoji_3413, nil + } + case 3414:{ + return Emoji_3414, nil + } + case 3415:{ + return Emoji_3415, nil + } + case 3416:{ + return Emoji_3416, nil + } + case 3417:{ + return Emoji_3417, nil + } + case 3418:{ + return Emoji_3418, nil + } + case 3419:{ + return Emoji_3419, nil + } + case 3420:{ + return Emoji_3420, nil + } + case 3421:{ + return Emoji_3421, nil + } + case 3422:{ + return Emoji_3422, nil + } + case 3423:{ + return Emoji_3423, nil + } + case 3424:{ + return Emoji_3424, nil + } + case 3425:{ + return Emoji_3425, nil + } + case 3426:{ + return Emoji_3426, nil + } + case 3427:{ + return Emoji_3427, nil + } + case 3428:{ + return Emoji_3428, nil + } + case 3429:{ + return Emoji_3429, nil + } + case 3430:{ + return Emoji_3430, nil + } + case 3431:{ + return Emoji_3431, nil + } + case 3432:{ + return Emoji_3432, nil + } + case 3433:{ + return Emoji_3433, nil + } + case 3434:{ + return Emoji_3434, nil + } + case 3435:{ + return Emoji_3435, nil + } + case 3436:{ + return Emoji_3436, nil + } + case 3437:{ + return Emoji_3437, nil + } + case 3438:{ + return Emoji_3438, nil + } + case 3439:{ + return Emoji_3439, nil + } + case 3440:{ + return Emoji_3440, nil + } + case 3441:{ + return Emoji_3441, nil + } + case 3442:{ + return Emoji_3442, nil + } + case 3443:{ + return Emoji_3443, nil + } + case 3444:{ + return Emoji_3444, nil + } + case 3445:{ + return Emoji_3445, nil + } + case 3446:{ + return Emoji_3446, nil + } + case 3447:{ + return Emoji_3447, nil + } + case 3448:{ + return Emoji_3448, nil + } + case 3449:{ + return Emoji_3449, nil + } + case 3450:{ + return Emoji_3450, nil + } + case 3451:{ + return Emoji_3451, nil + } + case 3452:{ + return Emoji_3452, nil + } + case 3453:{ + return Emoji_3453, nil + } + case 3454:{ + return Emoji_3454, nil + } + case 3455:{ + return Emoji_3455, nil + } + case 3456:{ + return Emoji_3456, nil + } + case 3457:{ + return Emoji_3457, nil + } + case 3458:{ + return Emoji_3458, nil + } + case 3459:{ + return Emoji_3459, nil + } + case 3460:{ + return Emoji_3460, nil + } + case 3461:{ + return Emoji_3461, nil + } + case 3462:{ + return Emoji_3462, nil + } + case 3463:{ + return Emoji_3463, nil + } + case 3464:{ + return Emoji_3464, nil + } + case 3465:{ + return Emoji_3465, nil + } + case 3466:{ + return Emoji_3466, nil + } + case 3467:{ + return Emoji_3467, nil + } + case 3468:{ + return Emoji_3468, nil + } + case 3469:{ + return Emoji_3469, nil + } + case 3470:{ + return Emoji_3470, nil + } + case 3471:{ + return Emoji_3471, nil + } + case 3472:{ + return Emoji_3472, nil + } + case 3473:{ + return Emoji_3473, nil + } + case 3474:{ + return Emoji_3474, nil + } + case 3475:{ + return Emoji_3475, nil + } + case 3476:{ + return Emoji_3476, nil + } + case 3477:{ + return Emoji_3477, nil + } + case 3478:{ + return Emoji_3478, nil + } + case 3479:{ + return Emoji_3479, nil + } + case 3480:{ + return Emoji_3480, nil + } + case 3481:{ + return Emoji_3481, nil + } + case 3482:{ + return Emoji_3482, nil + } + case 3483:{ + return Emoji_3483, nil + } + case 3484:{ + return Emoji_3484, nil + } + case 3485:{ + return Emoji_3485, nil + } + case 3486:{ + return Emoji_3486, nil + } + case 3487:{ + return Emoji_3487, nil + } + case 3488:{ + return Emoji_3488, nil + } + case 3489:{ + return Emoji_3489, nil + } + case 3490:{ + return Emoji_3490, nil + } + case 3491:{ + return Emoji_3491, nil + } + case 3492:{ + return Emoji_3492, nil + } + case 3493:{ + return Emoji_3493, nil + } + case 3494:{ + return Emoji_3494, nil + } + case 3495:{ + return Emoji_3495, nil + } + case 3496:{ + return Emoji_3496, nil + } + case 3497:{ + return Emoji_3497, nil + } + case 3498:{ + return Emoji_3498, nil + } + case 3499:{ + return Emoji_3499, nil + } + case 3500:{ + return Emoji_3500, nil + } + case 3501:{ + return Emoji_3501, nil + } + case 3502:{ + return Emoji_3502, nil + } + case 3503:{ + return Emoji_3503, nil + } + case 3504:{ + return Emoji_3504, nil + } + case 3505:{ + return Emoji_3505, nil + } + case 3506:{ + return Emoji_3506, nil + } + case 3507:{ + return Emoji_3507, nil + } + case 3508:{ + return Emoji_3508, nil + } + case 3509:{ + return Emoji_3509, nil + } + case 3510:{ + return Emoji_3510, nil + } + case 3511:{ + return Emoji_3511, nil + } + case 3512:{ + return Emoji_3512, nil + } + case 3513:{ + return Emoji_3513, nil + } + case 3514:{ + return Emoji_3514, nil + } + case 3515:{ + return Emoji_3515, nil + } + case 3516:{ + return Emoji_3516, nil + } + case 3517:{ + return Emoji_3517, nil + } + case 3518:{ + return Emoji_3518, nil + } + case 3519:{ + return Emoji_3519, nil + } + case 3520:{ + return Emoji_3520, nil + } + case 3521:{ + return Emoji_3521, nil + } + case 3522:{ + return Emoji_3522, nil + } + case 3523:{ + return Emoji_3523, nil + } + case 3524:{ + return Emoji_3524, nil + } + case 3525:{ + return Emoji_3525, nil + } + case 3526:{ + return Emoji_3526, nil + } + case 3527:{ + return Emoji_3527, nil + } + case 3528:{ + return Emoji_3528, nil + } + case 3529:{ + return Emoji_3529, nil + } + case 3530:{ + return Emoji_3530, nil + } + case 3531:{ + return Emoji_3531, nil + } + case 3532:{ + return Emoji_3532, nil + } + case 3533:{ + return Emoji_3533, nil + } + case 3534:{ + return Emoji_3534, nil + } + case 3535:{ + return Emoji_3535, nil + } + case 3536:{ + return Emoji_3536, nil + } + case 3537:{ + return Emoji_3537, nil + } + case 3538:{ + return Emoji_3538, nil + } + case 3539:{ + return Emoji_3539, nil + } + case 3540:{ + return Emoji_3540, nil + } + case 3541:{ + return Emoji_3541, nil + } + case 3542:{ + return Emoji_3542, nil + } + case 3543:{ + return Emoji_3543, nil + } + case 3544:{ + return Emoji_3544, nil + } + case 3545:{ + return Emoji_3545, nil + } + case 3546:{ + return Emoji_3546, nil + } + case 3547:{ + return Emoji_3547, nil + } + case 3548:{ + return Emoji_3548, nil + } + case 3549:{ + return Emoji_3549, nil + } + case 3550:{ + return Emoji_3550, nil + } + case 3551:{ + return Emoji_3551, nil + } + case 3552:{ + return Emoji_3552, nil + } + case 3553:{ + return Emoji_3553, nil + } + case 3554:{ + return Emoji_3554, nil + } + case 3555:{ + return Emoji_3555, nil + } + case 3556:{ + return Emoji_3556, nil + } + case 3557:{ + return Emoji_3557, nil + } + case 3558:{ + return Emoji_3558, nil + } + case 3559:{ + return Emoji_3559, nil + } + case 3560:{ + return Emoji_3560, nil + } + case 3561:{ + return Emoji_3561, nil + } + case 3562:{ + return Emoji_3562, nil + } + case 3563:{ + return Emoji_3563, nil + } + case 3564:{ + return Emoji_3564, nil + } + case 3565:{ + return Emoji_3565, nil + } + case 3566:{ + return Emoji_3566, nil + } + case 3567:{ + return Emoji_3567, nil + } + case 3568:{ + return Emoji_3568, nil + } + case 3569:{ + return Emoji_3569, nil + } + case 3570:{ + return Emoji_3570, nil + } + case 3571:{ + return Emoji_3571, nil + } + case 3572:{ + return Emoji_3572, nil + } + case 3573:{ + return Emoji_3573, nil + } + case 3574:{ + return Emoji_3574, nil + } + case 3575:{ + return Emoji_3575, nil + } + case 3576:{ + return Emoji_3576, nil + } + case 3577:{ + return Emoji_3577, nil + } + case 3578:{ + return Emoji_3578, nil + } + case 3579:{ + return Emoji_3579, nil + } + case 3580:{ + return Emoji_3580, nil + } + case 3581:{ + return Emoji_3581, nil + } + case 3582:{ + return Emoji_3582, nil + } + case 3583:{ + return Emoji_3583, nil + } + case 3584:{ + return Emoji_3584, nil + } + case 3585:{ + return Emoji_3585, nil + } + case 3586:{ + return Emoji_3586, nil + } + case 3587:{ + return Emoji_3587, nil + } + case 3588:{ + return Emoji_3588, nil + } + case 3589:{ + return Emoji_3589, nil + } + case 3590:{ + return Emoji_3590, nil + } + case 3591:{ + return Emoji_3591, nil + } + case 3592:{ + return Emoji_3592, nil + } + case 3593:{ + return Emoji_3593, nil + } + case 3594:{ + return Emoji_3594, nil + } + case 3595:{ + return Emoji_3595, nil + } + case 3596:{ + return Emoji_3596, nil + } + case 3597:{ + return Emoji_3597, nil + } + case 3598:{ + return Emoji_3598, nil + } + case 3599:{ + return Emoji_3599, nil + } + case 3600:{ + return Emoji_3600, nil + } + case 3601:{ + return Emoji_3601, nil + } + case 3602:{ + return Emoji_3602, nil + } + case 3603:{ + return Emoji_3603, nil + } + case 3604:{ + return Emoji_3604, nil + } + case 3605:{ + return Emoji_3605, nil + } + case 3606:{ + return Emoji_3606, nil + } + case 3607:{ + return Emoji_3607, nil + } + case 3608:{ + return Emoji_3608, nil + } + case 3609:{ + return Emoji_3609, nil + } + case 3610:{ + return Emoji_3610, nil + } + case 3611:{ + return Emoji_3611, nil + } + case 3612:{ + return Emoji_3612, nil + } + case 3613:{ + return Emoji_3613, nil + } + case 3614:{ + return Emoji_3614, nil + } + case 3615:{ + return Emoji_3615, nil + } + case 3616:{ + return Emoji_3616, nil + } + case 3617:{ + return Emoji_3617, nil + } + case 3618:{ + return Emoji_3618, nil + } + case 3619:{ + return Emoji_3619, nil + } + case 3620:{ + return Emoji_3620, nil + } + case 3621:{ + return Emoji_3621, nil + } + case 3622:{ + return Emoji_3622, nil + } + case 3623:{ + return Emoji_3623, nil + } + case 3624:{ + return Emoji_3624, nil + } + case 3625:{ + return Emoji_3625, nil + } + case 3626:{ + return Emoji_3626, nil + } + case 3627:{ + return Emoji_3627, nil + } + case 3628:{ + return Emoji_3628, nil + } + case 3629:{ + return Emoji_3629, nil + } + case 3630:{ + return Emoji_3630, nil + } + case 3631:{ + return Emoji_3631, nil + } + } + + identifierString := helpers.ConvertIntToString(inputIdentifier) + + return nil, errors.New("GetEmojiFileBytesFromIdentifier called with unknown emoji identifier: " + identifierString) +} + + diff --git a/resources/imageFiles/emojis/0001.svg b/resources/imageFiles/emojis/0001.svg new file mode 100644 index 0000000..92151d1 --- /dev/null +++ b/resources/imageFiles/emojis/0001.svg @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0002.svg b/resources/imageFiles/emojis/0002.svg new file mode 100644 index 0000000..d9de360 --- /dev/null +++ b/resources/imageFiles/emojis/0002.svg @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0003.svg b/resources/imageFiles/emojis/0003.svg new file mode 100644 index 0000000..594c524 --- /dev/null +++ b/resources/imageFiles/emojis/0003.svg @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0004.svg b/resources/imageFiles/emojis/0004.svg new file mode 100644 index 0000000..ee2a73c --- /dev/null +++ b/resources/imageFiles/emojis/0004.svg @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0005.svg b/resources/imageFiles/emojis/0005.svg new file mode 100644 index 0000000..5d5f654 --- /dev/null +++ b/resources/imageFiles/emojis/0005.svg @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0006.svg b/resources/imageFiles/emojis/0006.svg new file mode 100644 index 0000000..495149d --- /dev/null +++ b/resources/imageFiles/emojis/0006.svg @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0007.svg b/resources/imageFiles/emojis/0007.svg new file mode 100644 index 0000000..9bc08c6 --- /dev/null +++ b/resources/imageFiles/emojis/0007.svg @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0008.svg b/resources/imageFiles/emojis/0008.svg new file mode 100644 index 0000000..cccd227 --- /dev/null +++ b/resources/imageFiles/emojis/0008.svg @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0009.svg b/resources/imageFiles/emojis/0009.svg new file mode 100644 index 0000000..80548b5 --- /dev/null +++ b/resources/imageFiles/emojis/0009.svg @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0010.svg b/resources/imageFiles/emojis/0010.svg new file mode 100644 index 0000000..00caf16 --- /dev/null +++ b/resources/imageFiles/emojis/0010.svg @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0011.svg b/resources/imageFiles/emojis/0011.svg new file mode 100644 index 0000000..a100346 --- /dev/null +++ b/resources/imageFiles/emojis/0011.svg @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0012.svg b/resources/imageFiles/emojis/0012.svg new file mode 100644 index 0000000..3f2ac98 --- /dev/null +++ b/resources/imageFiles/emojis/0012.svg @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0013.svg b/resources/imageFiles/emojis/0013.svg new file mode 100644 index 0000000..9ab8894 --- /dev/null +++ b/resources/imageFiles/emojis/0013.svg @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0014.svg b/resources/imageFiles/emojis/0014.svg new file mode 100644 index 0000000..fc6c66a --- /dev/null +++ b/resources/imageFiles/emojis/0014.svg @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0015.svg b/resources/imageFiles/emojis/0015.svg new file mode 100644 index 0000000..ea603cd --- /dev/null +++ b/resources/imageFiles/emojis/0015.svg @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0016.svg b/resources/imageFiles/emojis/0016.svg new file mode 100644 index 0000000..45a31b8 --- /dev/null +++ b/resources/imageFiles/emojis/0016.svg @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0017.svg b/resources/imageFiles/emojis/0017.svg new file mode 100644 index 0000000..7756ce8 --- /dev/null +++ b/resources/imageFiles/emojis/0017.svg @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0018.svg b/resources/imageFiles/emojis/0018.svg new file mode 100644 index 0000000..58721c9 --- /dev/null +++ b/resources/imageFiles/emojis/0018.svg @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0019.svg b/resources/imageFiles/emojis/0019.svg new file mode 100644 index 0000000..08e1a1a --- /dev/null +++ b/resources/imageFiles/emojis/0019.svg @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0020.svg b/resources/imageFiles/emojis/0020.svg new file mode 100644 index 0000000..1e5b4d9 --- /dev/null +++ b/resources/imageFiles/emojis/0020.svg @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0021.svg b/resources/imageFiles/emojis/0021.svg new file mode 100644 index 0000000..04d8b9b --- /dev/null +++ b/resources/imageFiles/emojis/0021.svg @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0022.svg b/resources/imageFiles/emojis/0022.svg new file mode 100644 index 0000000..fe54cde --- /dev/null +++ b/resources/imageFiles/emojis/0022.svg @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0023.svg b/resources/imageFiles/emojis/0023.svg new file mode 100644 index 0000000..346317d --- /dev/null +++ b/resources/imageFiles/emojis/0023.svg @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0024.svg b/resources/imageFiles/emojis/0024.svg new file mode 100644 index 0000000..d9d9913 --- /dev/null +++ b/resources/imageFiles/emojis/0024.svg @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0025.svg b/resources/imageFiles/emojis/0025.svg new file mode 100644 index 0000000..fdefa66 --- /dev/null +++ b/resources/imageFiles/emojis/0025.svg @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0026.svg b/resources/imageFiles/emojis/0026.svg new file mode 100644 index 0000000..2d6bf58 --- /dev/null +++ b/resources/imageFiles/emojis/0026.svg @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0027.svg b/resources/imageFiles/emojis/0027.svg new file mode 100644 index 0000000..3874619 --- /dev/null +++ b/resources/imageFiles/emojis/0027.svg @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0028.svg b/resources/imageFiles/emojis/0028.svg new file mode 100644 index 0000000..604c071 --- /dev/null +++ b/resources/imageFiles/emojis/0028.svg @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0029.svg b/resources/imageFiles/emojis/0029.svg new file mode 100644 index 0000000..800668a --- /dev/null +++ b/resources/imageFiles/emojis/0029.svg @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0030.svg b/resources/imageFiles/emojis/0030.svg new file mode 100644 index 0000000..ca9169b --- /dev/null +++ b/resources/imageFiles/emojis/0030.svg @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0031.svg b/resources/imageFiles/emojis/0031.svg new file mode 100644 index 0000000..9bd30dd --- /dev/null +++ b/resources/imageFiles/emojis/0031.svg @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0032.svg b/resources/imageFiles/emojis/0032.svg new file mode 100644 index 0000000..382515b --- /dev/null +++ b/resources/imageFiles/emojis/0032.svg @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0033.svg b/resources/imageFiles/emojis/0033.svg new file mode 100644 index 0000000..f0bb273 --- /dev/null +++ b/resources/imageFiles/emojis/0033.svg @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0034.svg b/resources/imageFiles/emojis/0034.svg new file mode 100644 index 0000000..32ff2ac --- /dev/null +++ b/resources/imageFiles/emojis/0034.svg @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0035.svg b/resources/imageFiles/emojis/0035.svg new file mode 100644 index 0000000..d6e6bb1 --- /dev/null +++ b/resources/imageFiles/emojis/0035.svg @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0036.svg b/resources/imageFiles/emojis/0036.svg new file mode 100644 index 0000000..45b8111 --- /dev/null +++ b/resources/imageFiles/emojis/0036.svg @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0037.svg b/resources/imageFiles/emojis/0037.svg new file mode 100644 index 0000000..bee80fb --- /dev/null +++ b/resources/imageFiles/emojis/0037.svg @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0038.svg b/resources/imageFiles/emojis/0038.svg new file mode 100644 index 0000000..d3afacd --- /dev/null +++ b/resources/imageFiles/emojis/0038.svg @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0039.svg b/resources/imageFiles/emojis/0039.svg new file mode 100644 index 0000000..9c08f5b --- /dev/null +++ b/resources/imageFiles/emojis/0039.svg @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0040.svg b/resources/imageFiles/emojis/0040.svg new file mode 100644 index 0000000..22fd1d4 --- /dev/null +++ b/resources/imageFiles/emojis/0040.svg @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0041.svg b/resources/imageFiles/emojis/0041.svg new file mode 100644 index 0000000..7f73bb1 --- /dev/null +++ b/resources/imageFiles/emojis/0041.svg @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0042.svg b/resources/imageFiles/emojis/0042.svg new file mode 100644 index 0000000..1b67b5e --- /dev/null +++ b/resources/imageFiles/emojis/0042.svg @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0043.svg b/resources/imageFiles/emojis/0043.svg new file mode 100644 index 0000000..f2f8f83 --- /dev/null +++ b/resources/imageFiles/emojis/0043.svg @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0044.svg b/resources/imageFiles/emojis/0044.svg new file mode 100644 index 0000000..7ed062c --- /dev/null +++ b/resources/imageFiles/emojis/0044.svg @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0045.svg b/resources/imageFiles/emojis/0045.svg new file mode 100644 index 0000000..c51ab52 --- /dev/null +++ b/resources/imageFiles/emojis/0045.svg @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0046.svg b/resources/imageFiles/emojis/0046.svg new file mode 100644 index 0000000..2f2ae30 --- /dev/null +++ b/resources/imageFiles/emojis/0046.svg @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0047.svg b/resources/imageFiles/emojis/0047.svg new file mode 100644 index 0000000..8d4c535 --- /dev/null +++ b/resources/imageFiles/emojis/0047.svg @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0048.svg b/resources/imageFiles/emojis/0048.svg new file mode 100644 index 0000000..01fe801 --- /dev/null +++ b/resources/imageFiles/emojis/0048.svg @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0049.svg b/resources/imageFiles/emojis/0049.svg new file mode 100644 index 0000000..7991b64 --- /dev/null +++ b/resources/imageFiles/emojis/0049.svg @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0050.svg b/resources/imageFiles/emojis/0050.svg new file mode 100644 index 0000000..a9b84a2 --- /dev/null +++ b/resources/imageFiles/emojis/0050.svg @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0051.svg b/resources/imageFiles/emojis/0051.svg new file mode 100644 index 0000000..0427011 --- /dev/null +++ b/resources/imageFiles/emojis/0051.svg @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0052.svg b/resources/imageFiles/emojis/0052.svg new file mode 100644 index 0000000..d3dbf73 --- /dev/null +++ b/resources/imageFiles/emojis/0052.svg @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0053.svg b/resources/imageFiles/emojis/0053.svg new file mode 100644 index 0000000..e418d2d --- /dev/null +++ b/resources/imageFiles/emojis/0053.svg @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0054.svg b/resources/imageFiles/emojis/0054.svg new file mode 100644 index 0000000..1246ec3 --- /dev/null +++ b/resources/imageFiles/emojis/0054.svg @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0055.svg b/resources/imageFiles/emojis/0055.svg new file mode 100644 index 0000000..22f674d --- /dev/null +++ b/resources/imageFiles/emojis/0055.svg @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0056.svg b/resources/imageFiles/emojis/0056.svg new file mode 100644 index 0000000..57253d4 --- /dev/null +++ b/resources/imageFiles/emojis/0056.svg @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0057.svg b/resources/imageFiles/emojis/0057.svg new file mode 100644 index 0000000..e07a21f --- /dev/null +++ b/resources/imageFiles/emojis/0057.svg @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0058.svg b/resources/imageFiles/emojis/0058.svg new file mode 100644 index 0000000..b4f8556 --- /dev/null +++ b/resources/imageFiles/emojis/0058.svg @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0059.svg b/resources/imageFiles/emojis/0059.svg new file mode 100644 index 0000000..17e5756 --- /dev/null +++ b/resources/imageFiles/emojis/0059.svg @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0060.svg b/resources/imageFiles/emojis/0060.svg new file mode 100644 index 0000000..28c9344 --- /dev/null +++ b/resources/imageFiles/emojis/0060.svg @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0061.svg b/resources/imageFiles/emojis/0061.svg new file mode 100644 index 0000000..a434608 --- /dev/null +++ b/resources/imageFiles/emojis/0061.svg @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0062.svg b/resources/imageFiles/emojis/0062.svg new file mode 100644 index 0000000..8c25d76 --- /dev/null +++ b/resources/imageFiles/emojis/0062.svg @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0063.svg b/resources/imageFiles/emojis/0063.svg new file mode 100644 index 0000000..afca5f3 --- /dev/null +++ b/resources/imageFiles/emojis/0063.svg @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0064.svg b/resources/imageFiles/emojis/0064.svg new file mode 100644 index 0000000..9197df1 --- /dev/null +++ b/resources/imageFiles/emojis/0064.svg @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0065.svg b/resources/imageFiles/emojis/0065.svg new file mode 100644 index 0000000..ef6334c --- /dev/null +++ b/resources/imageFiles/emojis/0065.svg @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0066.svg b/resources/imageFiles/emojis/0066.svg new file mode 100644 index 0000000..4354824 --- /dev/null +++ b/resources/imageFiles/emojis/0066.svg @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0067.svg b/resources/imageFiles/emojis/0067.svg new file mode 100644 index 0000000..f2cbb6a --- /dev/null +++ b/resources/imageFiles/emojis/0067.svg @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0068.svg b/resources/imageFiles/emojis/0068.svg new file mode 100644 index 0000000..d6d4832 --- /dev/null +++ b/resources/imageFiles/emojis/0068.svg @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0069.svg b/resources/imageFiles/emojis/0069.svg new file mode 100644 index 0000000..3086c8d --- /dev/null +++ b/resources/imageFiles/emojis/0069.svg @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0070.svg b/resources/imageFiles/emojis/0070.svg new file mode 100644 index 0000000..7a52b19 --- /dev/null +++ b/resources/imageFiles/emojis/0070.svg @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0071.svg b/resources/imageFiles/emojis/0071.svg new file mode 100644 index 0000000..247e6a0 --- /dev/null +++ b/resources/imageFiles/emojis/0071.svg @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0072.svg b/resources/imageFiles/emojis/0072.svg new file mode 100644 index 0000000..cc5a6ff --- /dev/null +++ b/resources/imageFiles/emojis/0072.svg @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0073.svg b/resources/imageFiles/emojis/0073.svg new file mode 100644 index 0000000..3ff1f08 --- /dev/null +++ b/resources/imageFiles/emojis/0073.svg @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0074.svg b/resources/imageFiles/emojis/0074.svg new file mode 100644 index 0000000..f9ed5c8 --- /dev/null +++ b/resources/imageFiles/emojis/0074.svg @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0075.svg b/resources/imageFiles/emojis/0075.svg new file mode 100644 index 0000000..0d08f4c --- /dev/null +++ b/resources/imageFiles/emojis/0075.svg @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0076.svg b/resources/imageFiles/emojis/0076.svg new file mode 100644 index 0000000..104917d --- /dev/null +++ b/resources/imageFiles/emojis/0076.svg @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0077.svg b/resources/imageFiles/emojis/0077.svg new file mode 100644 index 0000000..2c6c374 --- /dev/null +++ b/resources/imageFiles/emojis/0077.svg @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0078.svg b/resources/imageFiles/emojis/0078.svg new file mode 100644 index 0000000..39b760c --- /dev/null +++ b/resources/imageFiles/emojis/0078.svg @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0079.svg b/resources/imageFiles/emojis/0079.svg new file mode 100644 index 0000000..25916bc --- /dev/null +++ b/resources/imageFiles/emojis/0079.svg @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0080.svg b/resources/imageFiles/emojis/0080.svg new file mode 100644 index 0000000..085b742 --- /dev/null +++ b/resources/imageFiles/emojis/0080.svg @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0081.svg b/resources/imageFiles/emojis/0081.svg new file mode 100644 index 0000000..6a4b5dc --- /dev/null +++ b/resources/imageFiles/emojis/0081.svg @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0082.svg b/resources/imageFiles/emojis/0082.svg new file mode 100644 index 0000000..163c0b6 --- /dev/null +++ b/resources/imageFiles/emojis/0082.svg @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0083.svg b/resources/imageFiles/emojis/0083.svg new file mode 100644 index 0000000..970b3f4 --- /dev/null +++ b/resources/imageFiles/emojis/0083.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0084.svg b/resources/imageFiles/emojis/0084.svg new file mode 100644 index 0000000..aada39f --- /dev/null +++ b/resources/imageFiles/emojis/0084.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0085.svg b/resources/imageFiles/emojis/0085.svg new file mode 100644 index 0000000..5266518 --- /dev/null +++ b/resources/imageFiles/emojis/0085.svg @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0086.svg b/resources/imageFiles/emojis/0086.svg new file mode 100644 index 0000000..79c6676 --- /dev/null +++ b/resources/imageFiles/emojis/0086.svg @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0087.svg b/resources/imageFiles/emojis/0087.svg new file mode 100644 index 0000000..59b1fab --- /dev/null +++ b/resources/imageFiles/emojis/0087.svg @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0088.svg b/resources/imageFiles/emojis/0088.svg new file mode 100644 index 0000000..2c52bdb --- /dev/null +++ b/resources/imageFiles/emojis/0088.svg @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0089.svg b/resources/imageFiles/emojis/0089.svg new file mode 100644 index 0000000..5026c8f --- /dev/null +++ b/resources/imageFiles/emojis/0089.svg @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0090.svg b/resources/imageFiles/emojis/0090.svg new file mode 100644 index 0000000..89cea0d --- /dev/null +++ b/resources/imageFiles/emojis/0090.svg @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0091.svg b/resources/imageFiles/emojis/0091.svg new file mode 100644 index 0000000..8728ae3 --- /dev/null +++ b/resources/imageFiles/emojis/0091.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0092.svg b/resources/imageFiles/emojis/0092.svg new file mode 100644 index 0000000..0f0da48 --- /dev/null +++ b/resources/imageFiles/emojis/0092.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0093.svg b/resources/imageFiles/emojis/0093.svg new file mode 100644 index 0000000..91acab6 --- /dev/null +++ b/resources/imageFiles/emojis/0093.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0094.svg b/resources/imageFiles/emojis/0094.svg new file mode 100644 index 0000000..5e3232a --- /dev/null +++ b/resources/imageFiles/emojis/0094.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0095.svg b/resources/imageFiles/emojis/0095.svg new file mode 100644 index 0000000..74e9af8 --- /dev/null +++ b/resources/imageFiles/emojis/0095.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0096.svg b/resources/imageFiles/emojis/0096.svg new file mode 100644 index 0000000..6f1cd27 --- /dev/null +++ b/resources/imageFiles/emojis/0096.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0097.svg b/resources/imageFiles/emojis/0097.svg new file mode 100644 index 0000000..6d5c46d --- /dev/null +++ b/resources/imageFiles/emojis/0097.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0098.svg b/resources/imageFiles/emojis/0098.svg new file mode 100644 index 0000000..2179257 --- /dev/null +++ b/resources/imageFiles/emojis/0098.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0099.svg b/resources/imageFiles/emojis/0099.svg new file mode 100644 index 0000000..429351a --- /dev/null +++ b/resources/imageFiles/emojis/0099.svg @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0100.svg b/resources/imageFiles/emojis/0100.svg new file mode 100644 index 0000000..476c479 --- /dev/null +++ b/resources/imageFiles/emojis/0100.svg @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0101.svg b/resources/imageFiles/emojis/0101.svg new file mode 100644 index 0000000..5d3dcfd --- /dev/null +++ b/resources/imageFiles/emojis/0101.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0102.svg b/resources/imageFiles/emojis/0102.svg new file mode 100644 index 0000000..895a6bb --- /dev/null +++ b/resources/imageFiles/emojis/0102.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0103.svg b/resources/imageFiles/emojis/0103.svg new file mode 100644 index 0000000..4fc5130 --- /dev/null +++ b/resources/imageFiles/emojis/0103.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0104.svg b/resources/imageFiles/emojis/0104.svg new file mode 100644 index 0000000..7d4ce06 --- /dev/null +++ b/resources/imageFiles/emojis/0104.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0105.svg b/resources/imageFiles/emojis/0105.svg new file mode 100644 index 0000000..6ade955 --- /dev/null +++ b/resources/imageFiles/emojis/0105.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0106.svg b/resources/imageFiles/emojis/0106.svg new file mode 100644 index 0000000..1bf2f79 --- /dev/null +++ b/resources/imageFiles/emojis/0106.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0107.svg b/resources/imageFiles/emojis/0107.svg new file mode 100644 index 0000000..4be44b2 --- /dev/null +++ b/resources/imageFiles/emojis/0107.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0108.svg b/resources/imageFiles/emojis/0108.svg new file mode 100644 index 0000000..1faaa62 --- /dev/null +++ b/resources/imageFiles/emojis/0108.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0109.svg b/resources/imageFiles/emojis/0109.svg new file mode 100644 index 0000000..e3a3916 --- /dev/null +++ b/resources/imageFiles/emojis/0109.svg @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0110.svg b/resources/imageFiles/emojis/0110.svg new file mode 100644 index 0000000..2ea5917 --- /dev/null +++ b/resources/imageFiles/emojis/0110.svg @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0111.svg b/resources/imageFiles/emojis/0111.svg new file mode 100644 index 0000000..a43a74f --- /dev/null +++ b/resources/imageFiles/emojis/0111.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0112.svg b/resources/imageFiles/emojis/0112.svg new file mode 100644 index 0000000..bd67598 --- /dev/null +++ b/resources/imageFiles/emojis/0112.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0113.svg b/resources/imageFiles/emojis/0113.svg new file mode 100644 index 0000000..7385cfd --- /dev/null +++ b/resources/imageFiles/emojis/0113.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0114.svg b/resources/imageFiles/emojis/0114.svg new file mode 100644 index 0000000..ced116e --- /dev/null +++ b/resources/imageFiles/emojis/0114.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0115.svg b/resources/imageFiles/emojis/0115.svg new file mode 100644 index 0000000..cbc3c37 --- /dev/null +++ b/resources/imageFiles/emojis/0115.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0116.svg b/resources/imageFiles/emojis/0116.svg new file mode 100644 index 0000000..d55cf2c --- /dev/null +++ b/resources/imageFiles/emojis/0116.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0117.svg b/resources/imageFiles/emojis/0117.svg new file mode 100644 index 0000000..24e8dcd --- /dev/null +++ b/resources/imageFiles/emojis/0117.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0118.svg b/resources/imageFiles/emojis/0118.svg new file mode 100644 index 0000000..859e759 --- /dev/null +++ b/resources/imageFiles/emojis/0118.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0119.svg b/resources/imageFiles/emojis/0119.svg new file mode 100644 index 0000000..584c548 --- /dev/null +++ b/resources/imageFiles/emojis/0119.svg @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0120.svg b/resources/imageFiles/emojis/0120.svg new file mode 100644 index 0000000..fabda9c --- /dev/null +++ b/resources/imageFiles/emojis/0120.svg @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0121.svg b/resources/imageFiles/emojis/0121.svg new file mode 100644 index 0000000..d9a0978 --- /dev/null +++ b/resources/imageFiles/emojis/0121.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0122.svg b/resources/imageFiles/emojis/0122.svg new file mode 100644 index 0000000..3c8d58e --- /dev/null +++ b/resources/imageFiles/emojis/0122.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0123.svg b/resources/imageFiles/emojis/0123.svg new file mode 100644 index 0000000..a920d4d --- /dev/null +++ b/resources/imageFiles/emojis/0123.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0124.svg b/resources/imageFiles/emojis/0124.svg new file mode 100644 index 0000000..0ef39e1 --- /dev/null +++ b/resources/imageFiles/emojis/0124.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0125.svg b/resources/imageFiles/emojis/0125.svg new file mode 100644 index 0000000..4562094 --- /dev/null +++ b/resources/imageFiles/emojis/0125.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0126.svg b/resources/imageFiles/emojis/0126.svg new file mode 100644 index 0000000..326e899 --- /dev/null +++ b/resources/imageFiles/emojis/0126.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0127.svg b/resources/imageFiles/emojis/0127.svg new file mode 100644 index 0000000..e5505cd --- /dev/null +++ b/resources/imageFiles/emojis/0127.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0128.svg b/resources/imageFiles/emojis/0128.svg new file mode 100644 index 0000000..2bc3182 --- /dev/null +++ b/resources/imageFiles/emojis/0128.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0129.svg b/resources/imageFiles/emojis/0129.svg new file mode 100644 index 0000000..7c01b0b --- /dev/null +++ b/resources/imageFiles/emojis/0129.svg @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0130.svg b/resources/imageFiles/emojis/0130.svg new file mode 100644 index 0000000..744779a --- /dev/null +++ b/resources/imageFiles/emojis/0130.svg @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0131.svg b/resources/imageFiles/emojis/0131.svg new file mode 100644 index 0000000..f5d3030 --- /dev/null +++ b/resources/imageFiles/emojis/0131.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0132.svg b/resources/imageFiles/emojis/0132.svg new file mode 100644 index 0000000..2f8298b --- /dev/null +++ b/resources/imageFiles/emojis/0132.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0133.svg b/resources/imageFiles/emojis/0133.svg new file mode 100644 index 0000000..8521520 --- /dev/null +++ b/resources/imageFiles/emojis/0133.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0134.svg b/resources/imageFiles/emojis/0134.svg new file mode 100644 index 0000000..b5de234 --- /dev/null +++ b/resources/imageFiles/emojis/0134.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0135.svg b/resources/imageFiles/emojis/0135.svg new file mode 100644 index 0000000..a8e1a24 --- /dev/null +++ b/resources/imageFiles/emojis/0135.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0136.svg b/resources/imageFiles/emojis/0136.svg new file mode 100644 index 0000000..38aec96 --- /dev/null +++ b/resources/imageFiles/emojis/0136.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0137.svg b/resources/imageFiles/emojis/0137.svg new file mode 100644 index 0000000..9126bf5 --- /dev/null +++ b/resources/imageFiles/emojis/0137.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0138.svg b/resources/imageFiles/emojis/0138.svg new file mode 100644 index 0000000..ea05150 --- /dev/null +++ b/resources/imageFiles/emojis/0138.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0139.svg b/resources/imageFiles/emojis/0139.svg new file mode 100644 index 0000000..ac98c0c --- /dev/null +++ b/resources/imageFiles/emojis/0139.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0140.svg b/resources/imageFiles/emojis/0140.svg new file mode 100644 index 0000000..830e4c8 --- /dev/null +++ b/resources/imageFiles/emojis/0140.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0141.svg b/resources/imageFiles/emojis/0141.svg new file mode 100644 index 0000000..466b156 --- /dev/null +++ b/resources/imageFiles/emojis/0141.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0142.svg b/resources/imageFiles/emojis/0142.svg new file mode 100644 index 0000000..5d73c0d --- /dev/null +++ b/resources/imageFiles/emojis/0142.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0143.svg b/resources/imageFiles/emojis/0143.svg new file mode 100644 index 0000000..330bfd2 --- /dev/null +++ b/resources/imageFiles/emojis/0143.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0144.svg b/resources/imageFiles/emojis/0144.svg new file mode 100644 index 0000000..6350b9b --- /dev/null +++ b/resources/imageFiles/emojis/0144.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0145.svg b/resources/imageFiles/emojis/0145.svg new file mode 100644 index 0000000..f79a5be --- /dev/null +++ b/resources/imageFiles/emojis/0145.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0146.svg b/resources/imageFiles/emojis/0146.svg new file mode 100644 index 0000000..a47669b --- /dev/null +++ b/resources/imageFiles/emojis/0146.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0147.svg b/resources/imageFiles/emojis/0147.svg new file mode 100644 index 0000000..8b61e46 --- /dev/null +++ b/resources/imageFiles/emojis/0147.svg @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0148.svg b/resources/imageFiles/emojis/0148.svg new file mode 100644 index 0000000..16fdff0 --- /dev/null +++ b/resources/imageFiles/emojis/0148.svg @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0149.svg b/resources/imageFiles/emojis/0149.svg new file mode 100644 index 0000000..10da0e8 --- /dev/null +++ b/resources/imageFiles/emojis/0149.svg @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0150.svg b/resources/imageFiles/emojis/0150.svg new file mode 100644 index 0000000..bc979bf --- /dev/null +++ b/resources/imageFiles/emojis/0150.svg @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0151.svg b/resources/imageFiles/emojis/0151.svg new file mode 100644 index 0000000..c894f03 --- /dev/null +++ b/resources/imageFiles/emojis/0151.svg @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0152.svg b/resources/imageFiles/emojis/0152.svg new file mode 100644 index 0000000..7f97ba7 --- /dev/null +++ b/resources/imageFiles/emojis/0152.svg @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0153.svg b/resources/imageFiles/emojis/0153.svg new file mode 100644 index 0000000..b8e37be --- /dev/null +++ b/resources/imageFiles/emojis/0153.svg @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0154.svg b/resources/imageFiles/emojis/0154.svg new file mode 100644 index 0000000..206ae71 --- /dev/null +++ b/resources/imageFiles/emojis/0154.svg @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0155.svg b/resources/imageFiles/emojis/0155.svg new file mode 100644 index 0000000..56798f9 --- /dev/null +++ b/resources/imageFiles/emojis/0155.svg @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0156.svg b/resources/imageFiles/emojis/0156.svg new file mode 100644 index 0000000..20b27b7 --- /dev/null +++ b/resources/imageFiles/emojis/0156.svg @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0157.svg b/resources/imageFiles/emojis/0157.svg new file mode 100644 index 0000000..7f41bde --- /dev/null +++ b/resources/imageFiles/emojis/0157.svg @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0158.svg b/resources/imageFiles/emojis/0158.svg new file mode 100644 index 0000000..f09801c --- /dev/null +++ b/resources/imageFiles/emojis/0158.svg @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0159.svg b/resources/imageFiles/emojis/0159.svg new file mode 100644 index 0000000..ccffef4 --- /dev/null +++ b/resources/imageFiles/emojis/0159.svg @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0160.svg b/resources/imageFiles/emojis/0160.svg new file mode 100644 index 0000000..6b8878b --- /dev/null +++ b/resources/imageFiles/emojis/0160.svg @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0161.svg b/resources/imageFiles/emojis/0161.svg new file mode 100644 index 0000000..86393f8 --- /dev/null +++ b/resources/imageFiles/emojis/0161.svg @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0162.svg b/resources/imageFiles/emojis/0162.svg new file mode 100644 index 0000000..de02fc1 --- /dev/null +++ b/resources/imageFiles/emojis/0162.svg @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0163.svg b/resources/imageFiles/emojis/0163.svg new file mode 100644 index 0000000..bbdcfda --- /dev/null +++ b/resources/imageFiles/emojis/0163.svg @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0164.svg b/resources/imageFiles/emojis/0164.svg new file mode 100644 index 0000000..82ac894 --- /dev/null +++ b/resources/imageFiles/emojis/0164.svg @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0165.svg b/resources/imageFiles/emojis/0165.svg new file mode 100644 index 0000000..e764266 --- /dev/null +++ b/resources/imageFiles/emojis/0165.svg @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0166.svg b/resources/imageFiles/emojis/0166.svg new file mode 100644 index 0000000..758344f --- /dev/null +++ b/resources/imageFiles/emojis/0166.svg @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0167.svg b/resources/imageFiles/emojis/0167.svg new file mode 100644 index 0000000..4b6b144 --- /dev/null +++ b/resources/imageFiles/emojis/0167.svg @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0168.svg b/resources/imageFiles/emojis/0168.svg new file mode 100644 index 0000000..e7cf03d --- /dev/null +++ b/resources/imageFiles/emojis/0168.svg @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0169.svg b/resources/imageFiles/emojis/0169.svg new file mode 100644 index 0000000..075eeb3 --- /dev/null +++ b/resources/imageFiles/emojis/0169.svg @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0170.svg b/resources/imageFiles/emojis/0170.svg new file mode 100644 index 0000000..d220458 --- /dev/null +++ b/resources/imageFiles/emojis/0170.svg @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0171.svg b/resources/imageFiles/emojis/0171.svg new file mode 100644 index 0000000..d471544 --- /dev/null +++ b/resources/imageFiles/emojis/0171.svg @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0172.svg b/resources/imageFiles/emojis/0172.svg new file mode 100644 index 0000000..33e4b89 --- /dev/null +++ b/resources/imageFiles/emojis/0172.svg @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0173.svg b/resources/imageFiles/emojis/0173.svg new file mode 100644 index 0000000..de97610 --- /dev/null +++ b/resources/imageFiles/emojis/0173.svg @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0174.svg b/resources/imageFiles/emojis/0174.svg new file mode 100644 index 0000000..775b7d7 --- /dev/null +++ b/resources/imageFiles/emojis/0174.svg @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0175.svg b/resources/imageFiles/emojis/0175.svg new file mode 100644 index 0000000..010c976 --- /dev/null +++ b/resources/imageFiles/emojis/0175.svg @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0176.svg b/resources/imageFiles/emojis/0176.svg new file mode 100644 index 0000000..79dbb11 --- /dev/null +++ b/resources/imageFiles/emojis/0176.svg @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0177.svg b/resources/imageFiles/emojis/0177.svg new file mode 100644 index 0000000..e196a84 --- /dev/null +++ b/resources/imageFiles/emojis/0177.svg @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0178.svg b/resources/imageFiles/emojis/0178.svg new file mode 100644 index 0000000..e9f764a --- /dev/null +++ b/resources/imageFiles/emojis/0178.svg @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0179.svg b/resources/imageFiles/emojis/0179.svg new file mode 100644 index 0000000..e1e969c --- /dev/null +++ b/resources/imageFiles/emojis/0179.svg @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0180.svg b/resources/imageFiles/emojis/0180.svg new file mode 100644 index 0000000..f3bdf17 --- /dev/null +++ b/resources/imageFiles/emojis/0180.svg @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0181.svg b/resources/imageFiles/emojis/0181.svg new file mode 100644 index 0000000..6a60f75 --- /dev/null +++ b/resources/imageFiles/emojis/0181.svg @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0182.svg b/resources/imageFiles/emojis/0182.svg new file mode 100644 index 0000000..7762441 --- /dev/null +++ b/resources/imageFiles/emojis/0182.svg @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0183.svg b/resources/imageFiles/emojis/0183.svg new file mode 100644 index 0000000..0e055da --- /dev/null +++ b/resources/imageFiles/emojis/0183.svg @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0184.svg b/resources/imageFiles/emojis/0184.svg new file mode 100644 index 0000000..7fa3bb9 --- /dev/null +++ b/resources/imageFiles/emojis/0184.svg @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0185.svg b/resources/imageFiles/emojis/0185.svg new file mode 100644 index 0000000..1e55cd0 --- /dev/null +++ b/resources/imageFiles/emojis/0185.svg @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0186.svg b/resources/imageFiles/emojis/0186.svg new file mode 100644 index 0000000..245202d --- /dev/null +++ b/resources/imageFiles/emojis/0186.svg @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0187.svg b/resources/imageFiles/emojis/0187.svg new file mode 100644 index 0000000..520e40d --- /dev/null +++ b/resources/imageFiles/emojis/0187.svg @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0188.svg b/resources/imageFiles/emojis/0188.svg new file mode 100644 index 0000000..35218be --- /dev/null +++ b/resources/imageFiles/emojis/0188.svg @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0189.svg b/resources/imageFiles/emojis/0189.svg new file mode 100644 index 0000000..5652220 --- /dev/null +++ b/resources/imageFiles/emojis/0189.svg @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0190.svg b/resources/imageFiles/emojis/0190.svg new file mode 100644 index 0000000..e8b0cec --- /dev/null +++ b/resources/imageFiles/emojis/0190.svg @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0191.svg b/resources/imageFiles/emojis/0191.svg new file mode 100644 index 0000000..6cec609 --- /dev/null +++ b/resources/imageFiles/emojis/0191.svg @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0192.svg b/resources/imageFiles/emojis/0192.svg new file mode 100644 index 0000000..a7a137c --- /dev/null +++ b/resources/imageFiles/emojis/0192.svg @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0193.svg b/resources/imageFiles/emojis/0193.svg new file mode 100644 index 0000000..70dbf8e --- /dev/null +++ b/resources/imageFiles/emojis/0193.svg @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0194.svg b/resources/imageFiles/emojis/0194.svg new file mode 100644 index 0000000..98e34a2 --- /dev/null +++ b/resources/imageFiles/emojis/0194.svg @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0195.svg b/resources/imageFiles/emojis/0195.svg new file mode 100644 index 0000000..e30c558 --- /dev/null +++ b/resources/imageFiles/emojis/0195.svg @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0196.svg b/resources/imageFiles/emojis/0196.svg new file mode 100644 index 0000000..556be51 --- /dev/null +++ b/resources/imageFiles/emojis/0196.svg @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0197.svg b/resources/imageFiles/emojis/0197.svg new file mode 100644 index 0000000..bf0982b --- /dev/null +++ b/resources/imageFiles/emojis/0197.svg @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0198.svg b/resources/imageFiles/emojis/0198.svg new file mode 100644 index 0000000..9976145 --- /dev/null +++ b/resources/imageFiles/emojis/0198.svg @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0199.svg b/resources/imageFiles/emojis/0199.svg new file mode 100644 index 0000000..12d424e --- /dev/null +++ b/resources/imageFiles/emojis/0199.svg @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0200.svg b/resources/imageFiles/emojis/0200.svg new file mode 100644 index 0000000..0329367 --- /dev/null +++ b/resources/imageFiles/emojis/0200.svg @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0201.svg b/resources/imageFiles/emojis/0201.svg new file mode 100644 index 0000000..8a3677f --- /dev/null +++ b/resources/imageFiles/emojis/0201.svg @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0202.svg b/resources/imageFiles/emojis/0202.svg new file mode 100644 index 0000000..f624e2f --- /dev/null +++ b/resources/imageFiles/emojis/0202.svg @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0203.svg b/resources/imageFiles/emojis/0203.svg new file mode 100644 index 0000000..b9e1993 --- /dev/null +++ b/resources/imageFiles/emojis/0203.svg @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0204.svg b/resources/imageFiles/emojis/0204.svg new file mode 100644 index 0000000..7f52480 --- /dev/null +++ b/resources/imageFiles/emojis/0204.svg @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0205.svg b/resources/imageFiles/emojis/0205.svg new file mode 100644 index 0000000..b821a68 --- /dev/null +++ b/resources/imageFiles/emojis/0205.svg @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0206.svg b/resources/imageFiles/emojis/0206.svg new file mode 100644 index 0000000..cd93808 --- /dev/null +++ b/resources/imageFiles/emojis/0206.svg @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0207.svg b/resources/imageFiles/emojis/0207.svg new file mode 100644 index 0000000..cff7c1c --- /dev/null +++ b/resources/imageFiles/emojis/0207.svg @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0208.svg b/resources/imageFiles/emojis/0208.svg new file mode 100644 index 0000000..5fd2ce7 --- /dev/null +++ b/resources/imageFiles/emojis/0208.svg @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0209.svg b/resources/imageFiles/emojis/0209.svg new file mode 100644 index 0000000..4bba5d4 --- /dev/null +++ b/resources/imageFiles/emojis/0209.svg @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0210.svg b/resources/imageFiles/emojis/0210.svg new file mode 100644 index 0000000..c964c3e --- /dev/null +++ b/resources/imageFiles/emojis/0210.svg @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0211.svg b/resources/imageFiles/emojis/0211.svg new file mode 100644 index 0000000..2ca45df --- /dev/null +++ b/resources/imageFiles/emojis/0211.svg @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0212.svg b/resources/imageFiles/emojis/0212.svg new file mode 100644 index 0000000..1d31555 --- /dev/null +++ b/resources/imageFiles/emojis/0212.svg @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0213.svg b/resources/imageFiles/emojis/0213.svg new file mode 100644 index 0000000..fdfad53 --- /dev/null +++ b/resources/imageFiles/emojis/0213.svg @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0214.svg b/resources/imageFiles/emojis/0214.svg new file mode 100644 index 0000000..cbe9ea1 --- /dev/null +++ b/resources/imageFiles/emojis/0214.svg @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0215.svg b/resources/imageFiles/emojis/0215.svg new file mode 100644 index 0000000..8516649 --- /dev/null +++ b/resources/imageFiles/emojis/0215.svg @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0216.svg b/resources/imageFiles/emojis/0216.svg new file mode 100644 index 0000000..f9bda89 --- /dev/null +++ b/resources/imageFiles/emojis/0216.svg @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0217.svg b/resources/imageFiles/emojis/0217.svg new file mode 100644 index 0000000..1e4b483 --- /dev/null +++ b/resources/imageFiles/emojis/0217.svg @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0218.svg b/resources/imageFiles/emojis/0218.svg new file mode 100644 index 0000000..a154bde --- /dev/null +++ b/resources/imageFiles/emojis/0218.svg @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0219.svg b/resources/imageFiles/emojis/0219.svg new file mode 100644 index 0000000..83a757b --- /dev/null +++ b/resources/imageFiles/emojis/0219.svg @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0220.svg b/resources/imageFiles/emojis/0220.svg new file mode 100644 index 0000000..5969fd1 --- /dev/null +++ b/resources/imageFiles/emojis/0220.svg @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0221.svg b/resources/imageFiles/emojis/0221.svg new file mode 100644 index 0000000..a1b6163 --- /dev/null +++ b/resources/imageFiles/emojis/0221.svg @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0222.svg b/resources/imageFiles/emojis/0222.svg new file mode 100644 index 0000000..0950515 --- /dev/null +++ b/resources/imageFiles/emojis/0222.svg @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0223.svg b/resources/imageFiles/emojis/0223.svg new file mode 100644 index 0000000..5734e4d --- /dev/null +++ b/resources/imageFiles/emojis/0223.svg @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0224.svg b/resources/imageFiles/emojis/0224.svg new file mode 100644 index 0000000..3a0b828 --- /dev/null +++ b/resources/imageFiles/emojis/0224.svg @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0225.svg b/resources/imageFiles/emojis/0225.svg new file mode 100644 index 0000000..197fe60 --- /dev/null +++ b/resources/imageFiles/emojis/0225.svg @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0226.svg b/resources/imageFiles/emojis/0226.svg new file mode 100644 index 0000000..17391fd --- /dev/null +++ b/resources/imageFiles/emojis/0226.svg @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0227.svg b/resources/imageFiles/emojis/0227.svg new file mode 100644 index 0000000..d0615d3 --- /dev/null +++ b/resources/imageFiles/emojis/0227.svg @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0228.svg b/resources/imageFiles/emojis/0228.svg new file mode 100644 index 0000000..b366f67 --- /dev/null +++ b/resources/imageFiles/emojis/0228.svg @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0229.svg b/resources/imageFiles/emojis/0229.svg new file mode 100644 index 0000000..3936b3f --- /dev/null +++ b/resources/imageFiles/emojis/0229.svg @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0230.svg b/resources/imageFiles/emojis/0230.svg new file mode 100644 index 0000000..8ba5ef4 --- /dev/null +++ b/resources/imageFiles/emojis/0230.svg @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0231.svg b/resources/imageFiles/emojis/0231.svg new file mode 100644 index 0000000..157c312 --- /dev/null +++ b/resources/imageFiles/emojis/0231.svg @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0232.svg b/resources/imageFiles/emojis/0232.svg new file mode 100644 index 0000000..7e4c37d --- /dev/null +++ b/resources/imageFiles/emojis/0232.svg @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0233.svg b/resources/imageFiles/emojis/0233.svg new file mode 100644 index 0000000..dbfbc69 --- /dev/null +++ b/resources/imageFiles/emojis/0233.svg @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0234.svg b/resources/imageFiles/emojis/0234.svg new file mode 100644 index 0000000..f37cd79 --- /dev/null +++ b/resources/imageFiles/emojis/0234.svg @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0235.svg b/resources/imageFiles/emojis/0235.svg new file mode 100644 index 0000000..782baa0 --- /dev/null +++ b/resources/imageFiles/emojis/0235.svg @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0236.svg b/resources/imageFiles/emojis/0236.svg new file mode 100644 index 0000000..035b0ab --- /dev/null +++ b/resources/imageFiles/emojis/0236.svg @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0237.svg b/resources/imageFiles/emojis/0237.svg new file mode 100644 index 0000000..3b68cf0 --- /dev/null +++ b/resources/imageFiles/emojis/0237.svg @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0238.svg b/resources/imageFiles/emojis/0238.svg new file mode 100644 index 0000000..d287228 --- /dev/null +++ b/resources/imageFiles/emojis/0238.svg @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0239.svg b/resources/imageFiles/emojis/0239.svg new file mode 100644 index 0000000..95ddfc4 --- /dev/null +++ b/resources/imageFiles/emojis/0239.svg @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0240.svg b/resources/imageFiles/emojis/0240.svg new file mode 100644 index 0000000..4531dbe --- /dev/null +++ b/resources/imageFiles/emojis/0240.svg @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0241.svg b/resources/imageFiles/emojis/0241.svg new file mode 100644 index 0000000..ccce4a1 --- /dev/null +++ b/resources/imageFiles/emojis/0241.svg @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0242.svg b/resources/imageFiles/emojis/0242.svg new file mode 100644 index 0000000..1008e08 --- /dev/null +++ b/resources/imageFiles/emojis/0242.svg @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0243.svg b/resources/imageFiles/emojis/0243.svg new file mode 100644 index 0000000..81ba86e --- /dev/null +++ b/resources/imageFiles/emojis/0243.svg @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0244.svg b/resources/imageFiles/emojis/0244.svg new file mode 100644 index 0000000..48ff4ba --- /dev/null +++ b/resources/imageFiles/emojis/0244.svg @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0245.svg b/resources/imageFiles/emojis/0245.svg new file mode 100644 index 0000000..5878ae6 --- /dev/null +++ b/resources/imageFiles/emojis/0245.svg @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0246.svg b/resources/imageFiles/emojis/0246.svg new file mode 100644 index 0000000..846f931 --- /dev/null +++ b/resources/imageFiles/emojis/0246.svg @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0247.svg b/resources/imageFiles/emojis/0247.svg new file mode 100644 index 0000000..c25054f --- /dev/null +++ b/resources/imageFiles/emojis/0247.svg @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0248.svg b/resources/imageFiles/emojis/0248.svg new file mode 100644 index 0000000..ee3a864 --- /dev/null +++ b/resources/imageFiles/emojis/0248.svg @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0249.svg b/resources/imageFiles/emojis/0249.svg new file mode 100644 index 0000000..92f467f --- /dev/null +++ b/resources/imageFiles/emojis/0249.svg @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0250.svg b/resources/imageFiles/emojis/0250.svg new file mode 100644 index 0000000..fa4ca7f --- /dev/null +++ b/resources/imageFiles/emojis/0250.svg @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0251.svg b/resources/imageFiles/emojis/0251.svg new file mode 100644 index 0000000..d8f5f26 --- /dev/null +++ b/resources/imageFiles/emojis/0251.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0252.svg b/resources/imageFiles/emojis/0252.svg new file mode 100644 index 0000000..3678a3a --- /dev/null +++ b/resources/imageFiles/emojis/0252.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0253.svg b/resources/imageFiles/emojis/0253.svg new file mode 100644 index 0000000..61c039b --- /dev/null +++ b/resources/imageFiles/emojis/0253.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0254.svg b/resources/imageFiles/emojis/0254.svg new file mode 100644 index 0000000..64d83d0 --- /dev/null +++ b/resources/imageFiles/emojis/0254.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0255.svg b/resources/imageFiles/emojis/0255.svg new file mode 100644 index 0000000..1e79d6b --- /dev/null +++ b/resources/imageFiles/emojis/0255.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0256.svg b/resources/imageFiles/emojis/0256.svg new file mode 100644 index 0000000..5bedf95 --- /dev/null +++ b/resources/imageFiles/emojis/0256.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0257.svg b/resources/imageFiles/emojis/0257.svg new file mode 100644 index 0000000..e3183d4 --- /dev/null +++ b/resources/imageFiles/emojis/0257.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0258.svg b/resources/imageFiles/emojis/0258.svg new file mode 100644 index 0000000..9b98303 --- /dev/null +++ b/resources/imageFiles/emojis/0258.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0259.svg b/resources/imageFiles/emojis/0259.svg new file mode 100644 index 0000000..169ec57 --- /dev/null +++ b/resources/imageFiles/emojis/0259.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0260.svg b/resources/imageFiles/emojis/0260.svg new file mode 100644 index 0000000..aa21210 --- /dev/null +++ b/resources/imageFiles/emojis/0260.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0261.svg b/resources/imageFiles/emojis/0261.svg new file mode 100644 index 0000000..ef0a446 --- /dev/null +++ b/resources/imageFiles/emojis/0261.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0262.svg b/resources/imageFiles/emojis/0262.svg new file mode 100644 index 0000000..45be29a --- /dev/null +++ b/resources/imageFiles/emojis/0262.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0263.svg b/resources/imageFiles/emojis/0263.svg new file mode 100644 index 0000000..879b9c6 --- /dev/null +++ b/resources/imageFiles/emojis/0263.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0264.svg b/resources/imageFiles/emojis/0264.svg new file mode 100644 index 0000000..289612d --- /dev/null +++ b/resources/imageFiles/emojis/0264.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0265.svg b/resources/imageFiles/emojis/0265.svg new file mode 100644 index 0000000..5e3804a --- /dev/null +++ b/resources/imageFiles/emojis/0265.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0266.svg b/resources/imageFiles/emojis/0266.svg new file mode 100644 index 0000000..5145a7a --- /dev/null +++ b/resources/imageFiles/emojis/0266.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0267.svg b/resources/imageFiles/emojis/0267.svg new file mode 100644 index 0000000..e63f2be --- /dev/null +++ b/resources/imageFiles/emojis/0267.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0268.svg b/resources/imageFiles/emojis/0268.svg new file mode 100644 index 0000000..63886c9 --- /dev/null +++ b/resources/imageFiles/emojis/0268.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0269.svg b/resources/imageFiles/emojis/0269.svg new file mode 100644 index 0000000..3e3dc01 --- /dev/null +++ b/resources/imageFiles/emojis/0269.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0270.svg b/resources/imageFiles/emojis/0270.svg new file mode 100644 index 0000000..f9bc6aa --- /dev/null +++ b/resources/imageFiles/emojis/0270.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0271.svg b/resources/imageFiles/emojis/0271.svg new file mode 100644 index 0000000..46c1e40 --- /dev/null +++ b/resources/imageFiles/emojis/0271.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0272.svg b/resources/imageFiles/emojis/0272.svg new file mode 100644 index 0000000..2664707 --- /dev/null +++ b/resources/imageFiles/emojis/0272.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0273.svg b/resources/imageFiles/emojis/0273.svg new file mode 100644 index 0000000..32b12ac --- /dev/null +++ b/resources/imageFiles/emojis/0273.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0274.svg b/resources/imageFiles/emojis/0274.svg new file mode 100644 index 0000000..e08b943 --- /dev/null +++ b/resources/imageFiles/emojis/0274.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0275.svg b/resources/imageFiles/emojis/0275.svg new file mode 100644 index 0000000..e28dbf5 --- /dev/null +++ b/resources/imageFiles/emojis/0275.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0276.svg b/resources/imageFiles/emojis/0276.svg new file mode 100644 index 0000000..14ee4da --- /dev/null +++ b/resources/imageFiles/emojis/0276.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0277.svg b/resources/imageFiles/emojis/0277.svg new file mode 100644 index 0000000..14c4863 --- /dev/null +++ b/resources/imageFiles/emojis/0277.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0278.svg b/resources/imageFiles/emojis/0278.svg new file mode 100644 index 0000000..77064a0 --- /dev/null +++ b/resources/imageFiles/emojis/0278.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0279.svg b/resources/imageFiles/emojis/0279.svg new file mode 100644 index 0000000..067fa24 --- /dev/null +++ b/resources/imageFiles/emojis/0279.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0280.svg b/resources/imageFiles/emojis/0280.svg new file mode 100644 index 0000000..63e4583 --- /dev/null +++ b/resources/imageFiles/emojis/0280.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0281.svg b/resources/imageFiles/emojis/0281.svg new file mode 100644 index 0000000..10ce6c4 --- /dev/null +++ b/resources/imageFiles/emojis/0281.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0282.svg b/resources/imageFiles/emojis/0282.svg new file mode 100644 index 0000000..bfef7b3 --- /dev/null +++ b/resources/imageFiles/emojis/0282.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0283.svg b/resources/imageFiles/emojis/0283.svg new file mode 100644 index 0000000..77bc21a --- /dev/null +++ b/resources/imageFiles/emojis/0283.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0284.svg b/resources/imageFiles/emojis/0284.svg new file mode 100644 index 0000000..e0c83c9 --- /dev/null +++ b/resources/imageFiles/emojis/0284.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0285.svg b/resources/imageFiles/emojis/0285.svg new file mode 100644 index 0000000..9c258cb --- /dev/null +++ b/resources/imageFiles/emojis/0285.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0286.svg b/resources/imageFiles/emojis/0286.svg new file mode 100644 index 0000000..3a8f8dd --- /dev/null +++ b/resources/imageFiles/emojis/0286.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0287.svg b/resources/imageFiles/emojis/0287.svg new file mode 100644 index 0000000..e3741c7 --- /dev/null +++ b/resources/imageFiles/emojis/0287.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0288.svg b/resources/imageFiles/emojis/0288.svg new file mode 100644 index 0000000..c335cae --- /dev/null +++ b/resources/imageFiles/emojis/0288.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0289.svg b/resources/imageFiles/emojis/0289.svg new file mode 100644 index 0000000..9f22b7f --- /dev/null +++ b/resources/imageFiles/emojis/0289.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0290.svg b/resources/imageFiles/emojis/0290.svg new file mode 100644 index 0000000..babb84f --- /dev/null +++ b/resources/imageFiles/emojis/0290.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0291.svg b/resources/imageFiles/emojis/0291.svg new file mode 100644 index 0000000..a67b8c4 --- /dev/null +++ b/resources/imageFiles/emojis/0291.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0292.svg b/resources/imageFiles/emojis/0292.svg new file mode 100644 index 0000000..0c75439 --- /dev/null +++ b/resources/imageFiles/emojis/0292.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0293.svg b/resources/imageFiles/emojis/0293.svg new file mode 100644 index 0000000..8aa2261 --- /dev/null +++ b/resources/imageFiles/emojis/0293.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0294.svg b/resources/imageFiles/emojis/0294.svg new file mode 100644 index 0000000..d2263b3 --- /dev/null +++ b/resources/imageFiles/emojis/0294.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0295.svg b/resources/imageFiles/emojis/0295.svg new file mode 100644 index 0000000..e099509 --- /dev/null +++ b/resources/imageFiles/emojis/0295.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0296.svg b/resources/imageFiles/emojis/0296.svg new file mode 100644 index 0000000..b2495d9 --- /dev/null +++ b/resources/imageFiles/emojis/0296.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0297.svg b/resources/imageFiles/emojis/0297.svg new file mode 100644 index 0000000..35859d5 --- /dev/null +++ b/resources/imageFiles/emojis/0297.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0298.svg b/resources/imageFiles/emojis/0298.svg new file mode 100644 index 0000000..4543d48 --- /dev/null +++ b/resources/imageFiles/emojis/0298.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0299.svg b/resources/imageFiles/emojis/0299.svg new file mode 100644 index 0000000..1e75e90 --- /dev/null +++ b/resources/imageFiles/emojis/0299.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0300.svg b/resources/imageFiles/emojis/0300.svg new file mode 100644 index 0000000..f749a66 --- /dev/null +++ b/resources/imageFiles/emojis/0300.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0301.svg b/resources/imageFiles/emojis/0301.svg new file mode 100644 index 0000000..8e4f1ea --- /dev/null +++ b/resources/imageFiles/emojis/0301.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0302.svg b/resources/imageFiles/emojis/0302.svg new file mode 100644 index 0000000..40976de --- /dev/null +++ b/resources/imageFiles/emojis/0302.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0303.svg b/resources/imageFiles/emojis/0303.svg new file mode 100644 index 0000000..2ea01e1 --- /dev/null +++ b/resources/imageFiles/emojis/0303.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0304.svg b/resources/imageFiles/emojis/0304.svg new file mode 100644 index 0000000..27685dc --- /dev/null +++ b/resources/imageFiles/emojis/0304.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0305.svg b/resources/imageFiles/emojis/0305.svg new file mode 100644 index 0000000..c1867b5 --- /dev/null +++ b/resources/imageFiles/emojis/0305.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0306.svg b/resources/imageFiles/emojis/0306.svg new file mode 100644 index 0000000..7c16926 --- /dev/null +++ b/resources/imageFiles/emojis/0306.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0307.svg b/resources/imageFiles/emojis/0307.svg new file mode 100644 index 0000000..29759d4 --- /dev/null +++ b/resources/imageFiles/emojis/0307.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0308.svg b/resources/imageFiles/emojis/0308.svg new file mode 100644 index 0000000..181cca1 --- /dev/null +++ b/resources/imageFiles/emojis/0308.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0309.svg b/resources/imageFiles/emojis/0309.svg new file mode 100644 index 0000000..b1eebdc --- /dev/null +++ b/resources/imageFiles/emojis/0309.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0310.svg b/resources/imageFiles/emojis/0310.svg new file mode 100644 index 0000000..5290051 --- /dev/null +++ b/resources/imageFiles/emojis/0310.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0311.svg b/resources/imageFiles/emojis/0311.svg new file mode 100644 index 0000000..bd0c086 --- /dev/null +++ b/resources/imageFiles/emojis/0311.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0312.svg b/resources/imageFiles/emojis/0312.svg new file mode 100644 index 0000000..486672a --- /dev/null +++ b/resources/imageFiles/emojis/0312.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0313.svg b/resources/imageFiles/emojis/0313.svg new file mode 100644 index 0000000..3feb3b5 --- /dev/null +++ b/resources/imageFiles/emojis/0313.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0314.svg b/resources/imageFiles/emojis/0314.svg new file mode 100644 index 0000000..0e57287 --- /dev/null +++ b/resources/imageFiles/emojis/0314.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0315.svg b/resources/imageFiles/emojis/0315.svg new file mode 100644 index 0000000..6e98828 --- /dev/null +++ b/resources/imageFiles/emojis/0315.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0316.svg b/resources/imageFiles/emojis/0316.svg new file mode 100644 index 0000000..59448b1 --- /dev/null +++ b/resources/imageFiles/emojis/0316.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0317.svg b/resources/imageFiles/emojis/0317.svg new file mode 100644 index 0000000..1405a27 --- /dev/null +++ b/resources/imageFiles/emojis/0317.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0318.svg b/resources/imageFiles/emojis/0318.svg new file mode 100644 index 0000000..054851d --- /dev/null +++ b/resources/imageFiles/emojis/0318.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0319.svg b/resources/imageFiles/emojis/0319.svg new file mode 100644 index 0000000..d99114f --- /dev/null +++ b/resources/imageFiles/emojis/0319.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0320.svg b/resources/imageFiles/emojis/0320.svg new file mode 100644 index 0000000..80f0b9c --- /dev/null +++ b/resources/imageFiles/emojis/0320.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0321.svg b/resources/imageFiles/emojis/0321.svg new file mode 100644 index 0000000..9ef0a1d --- /dev/null +++ b/resources/imageFiles/emojis/0321.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0322.svg b/resources/imageFiles/emojis/0322.svg new file mode 100644 index 0000000..62bbfa0 --- /dev/null +++ b/resources/imageFiles/emojis/0322.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0323.svg b/resources/imageFiles/emojis/0323.svg new file mode 100644 index 0000000..9a2344c --- /dev/null +++ b/resources/imageFiles/emojis/0323.svg @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0324.svg b/resources/imageFiles/emojis/0324.svg new file mode 100644 index 0000000..083f420 --- /dev/null +++ b/resources/imageFiles/emojis/0324.svg @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0325.svg b/resources/imageFiles/emojis/0325.svg new file mode 100644 index 0000000..b1f3829 --- /dev/null +++ b/resources/imageFiles/emojis/0325.svg @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0326.svg b/resources/imageFiles/emojis/0326.svg new file mode 100644 index 0000000..ca7abca --- /dev/null +++ b/resources/imageFiles/emojis/0326.svg @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0327.svg b/resources/imageFiles/emojis/0327.svg new file mode 100644 index 0000000..f9b5da6 --- /dev/null +++ b/resources/imageFiles/emojis/0327.svg @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0328.svg b/resources/imageFiles/emojis/0328.svg new file mode 100644 index 0000000..293e1a5 --- /dev/null +++ b/resources/imageFiles/emojis/0328.svg @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0329.svg b/resources/imageFiles/emojis/0329.svg new file mode 100644 index 0000000..c52a51e --- /dev/null +++ b/resources/imageFiles/emojis/0329.svg @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0330.svg b/resources/imageFiles/emojis/0330.svg new file mode 100644 index 0000000..25d22aa --- /dev/null +++ b/resources/imageFiles/emojis/0330.svg @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0331.svg b/resources/imageFiles/emojis/0331.svg new file mode 100644 index 0000000..1e986e8 --- /dev/null +++ b/resources/imageFiles/emojis/0331.svg @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0332.svg b/resources/imageFiles/emojis/0332.svg new file mode 100644 index 0000000..9bf1cb4 --- /dev/null +++ b/resources/imageFiles/emojis/0332.svg @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0333.svg b/resources/imageFiles/emojis/0333.svg new file mode 100644 index 0000000..04e0921 --- /dev/null +++ b/resources/imageFiles/emojis/0333.svg @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0334.svg b/resources/imageFiles/emojis/0334.svg new file mode 100644 index 0000000..9c8d460 --- /dev/null +++ b/resources/imageFiles/emojis/0334.svg @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0335.svg b/resources/imageFiles/emojis/0335.svg new file mode 100644 index 0000000..ca74a5a --- /dev/null +++ b/resources/imageFiles/emojis/0335.svg @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0336.svg b/resources/imageFiles/emojis/0336.svg new file mode 100644 index 0000000..ffe55c0 --- /dev/null +++ b/resources/imageFiles/emojis/0336.svg @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0337.svg b/resources/imageFiles/emojis/0337.svg new file mode 100644 index 0000000..1f70ae7 --- /dev/null +++ b/resources/imageFiles/emojis/0337.svg @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0338.svg b/resources/imageFiles/emojis/0338.svg new file mode 100644 index 0000000..7b105db --- /dev/null +++ b/resources/imageFiles/emojis/0338.svg @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0339.svg b/resources/imageFiles/emojis/0339.svg new file mode 100644 index 0000000..3010aa0 --- /dev/null +++ b/resources/imageFiles/emojis/0339.svg @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0340.svg b/resources/imageFiles/emojis/0340.svg new file mode 100644 index 0000000..af0d846 --- /dev/null +++ b/resources/imageFiles/emojis/0340.svg @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0341.svg b/resources/imageFiles/emojis/0341.svg new file mode 100644 index 0000000..45f317f --- /dev/null +++ b/resources/imageFiles/emojis/0341.svg @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0342.svg b/resources/imageFiles/emojis/0342.svg new file mode 100644 index 0000000..93b9b5f --- /dev/null +++ b/resources/imageFiles/emojis/0342.svg @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0343.svg b/resources/imageFiles/emojis/0343.svg new file mode 100644 index 0000000..feaa10a --- /dev/null +++ b/resources/imageFiles/emojis/0343.svg @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0344.svg b/resources/imageFiles/emojis/0344.svg new file mode 100644 index 0000000..881097f --- /dev/null +++ b/resources/imageFiles/emojis/0344.svg @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0345.svg b/resources/imageFiles/emojis/0345.svg new file mode 100644 index 0000000..9b5656a --- /dev/null +++ b/resources/imageFiles/emojis/0345.svg @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0346.svg b/resources/imageFiles/emojis/0346.svg new file mode 100644 index 0000000..521f20d --- /dev/null +++ b/resources/imageFiles/emojis/0346.svg @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0347.svg b/resources/imageFiles/emojis/0347.svg new file mode 100644 index 0000000..8087914 --- /dev/null +++ b/resources/imageFiles/emojis/0347.svg @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0348.svg b/resources/imageFiles/emojis/0348.svg new file mode 100644 index 0000000..817fdc9 --- /dev/null +++ b/resources/imageFiles/emojis/0348.svg @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0349.svg b/resources/imageFiles/emojis/0349.svg new file mode 100644 index 0000000..99658b8 --- /dev/null +++ b/resources/imageFiles/emojis/0349.svg @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0350.svg b/resources/imageFiles/emojis/0350.svg new file mode 100644 index 0000000..c6b61f1 --- /dev/null +++ b/resources/imageFiles/emojis/0350.svg @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0351.svg b/resources/imageFiles/emojis/0351.svg new file mode 100644 index 0000000..9fa67ee --- /dev/null +++ b/resources/imageFiles/emojis/0351.svg @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0352.svg b/resources/imageFiles/emojis/0352.svg new file mode 100644 index 0000000..6f60be5 --- /dev/null +++ b/resources/imageFiles/emojis/0352.svg @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0353.svg b/resources/imageFiles/emojis/0353.svg new file mode 100644 index 0000000..63b0bcc --- /dev/null +++ b/resources/imageFiles/emojis/0353.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0354.svg b/resources/imageFiles/emojis/0354.svg new file mode 100644 index 0000000..b972057 --- /dev/null +++ b/resources/imageFiles/emojis/0354.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0355.svg b/resources/imageFiles/emojis/0355.svg new file mode 100644 index 0000000..3fe63d6 --- /dev/null +++ b/resources/imageFiles/emojis/0355.svg @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0356.svg b/resources/imageFiles/emojis/0356.svg new file mode 100644 index 0000000..7ba1ee0 --- /dev/null +++ b/resources/imageFiles/emojis/0356.svg @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0357.svg b/resources/imageFiles/emojis/0357.svg new file mode 100644 index 0000000..98193ef --- /dev/null +++ b/resources/imageFiles/emojis/0357.svg @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0358.svg b/resources/imageFiles/emojis/0358.svg new file mode 100644 index 0000000..7ef409d --- /dev/null +++ b/resources/imageFiles/emojis/0358.svg @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0359.svg b/resources/imageFiles/emojis/0359.svg new file mode 100644 index 0000000..7a73411 --- /dev/null +++ b/resources/imageFiles/emojis/0359.svg @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0360.svg b/resources/imageFiles/emojis/0360.svg new file mode 100644 index 0000000..ec6b19b --- /dev/null +++ b/resources/imageFiles/emojis/0360.svg @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0361.svg b/resources/imageFiles/emojis/0361.svg new file mode 100644 index 0000000..52c957e --- /dev/null +++ b/resources/imageFiles/emojis/0361.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0362.svg b/resources/imageFiles/emojis/0362.svg new file mode 100644 index 0000000..f28cccd --- /dev/null +++ b/resources/imageFiles/emojis/0362.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0363.svg b/resources/imageFiles/emojis/0363.svg new file mode 100644 index 0000000..36c5aeb --- /dev/null +++ b/resources/imageFiles/emojis/0363.svg @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0364.svg b/resources/imageFiles/emojis/0364.svg new file mode 100644 index 0000000..dfa46b3 --- /dev/null +++ b/resources/imageFiles/emojis/0364.svg @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0365.svg b/resources/imageFiles/emojis/0365.svg new file mode 100644 index 0000000..c899de8 --- /dev/null +++ b/resources/imageFiles/emojis/0365.svg @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0366.svg b/resources/imageFiles/emojis/0366.svg new file mode 100644 index 0000000..7eb2518 --- /dev/null +++ b/resources/imageFiles/emojis/0366.svg @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0367.svg b/resources/imageFiles/emojis/0367.svg new file mode 100644 index 0000000..2d56c3c --- /dev/null +++ b/resources/imageFiles/emojis/0367.svg @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0368.svg b/resources/imageFiles/emojis/0368.svg new file mode 100644 index 0000000..88296cd --- /dev/null +++ b/resources/imageFiles/emojis/0368.svg @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0369.svg b/resources/imageFiles/emojis/0369.svg new file mode 100644 index 0000000..a88baab --- /dev/null +++ b/resources/imageFiles/emojis/0369.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0370.svg b/resources/imageFiles/emojis/0370.svg new file mode 100644 index 0000000..dca154c --- /dev/null +++ b/resources/imageFiles/emojis/0370.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0371.svg b/resources/imageFiles/emojis/0371.svg new file mode 100644 index 0000000..c4249cb --- /dev/null +++ b/resources/imageFiles/emojis/0371.svg @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0372.svg b/resources/imageFiles/emojis/0372.svg new file mode 100644 index 0000000..304c379 --- /dev/null +++ b/resources/imageFiles/emojis/0372.svg @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0373.svg b/resources/imageFiles/emojis/0373.svg new file mode 100644 index 0000000..6d65ea3 --- /dev/null +++ b/resources/imageFiles/emojis/0373.svg @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0374.svg b/resources/imageFiles/emojis/0374.svg new file mode 100644 index 0000000..6125104 --- /dev/null +++ b/resources/imageFiles/emojis/0374.svg @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0375.svg b/resources/imageFiles/emojis/0375.svg new file mode 100644 index 0000000..657cd1b --- /dev/null +++ b/resources/imageFiles/emojis/0375.svg @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0376.svg b/resources/imageFiles/emojis/0376.svg new file mode 100644 index 0000000..0fc2f8f --- /dev/null +++ b/resources/imageFiles/emojis/0376.svg @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0377.svg b/resources/imageFiles/emojis/0377.svg new file mode 100644 index 0000000..cd02396 --- /dev/null +++ b/resources/imageFiles/emojis/0377.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0378.svg b/resources/imageFiles/emojis/0378.svg new file mode 100644 index 0000000..e29f8bc --- /dev/null +++ b/resources/imageFiles/emojis/0378.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0379.svg b/resources/imageFiles/emojis/0379.svg new file mode 100644 index 0000000..4516f3e --- /dev/null +++ b/resources/imageFiles/emojis/0379.svg @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0380.svg b/resources/imageFiles/emojis/0380.svg new file mode 100644 index 0000000..34c76a4 --- /dev/null +++ b/resources/imageFiles/emojis/0380.svg @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0381.svg b/resources/imageFiles/emojis/0381.svg new file mode 100644 index 0000000..ecfbbaa --- /dev/null +++ b/resources/imageFiles/emojis/0381.svg @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0382.svg b/resources/imageFiles/emojis/0382.svg new file mode 100644 index 0000000..9c577ca --- /dev/null +++ b/resources/imageFiles/emojis/0382.svg @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0383.svg b/resources/imageFiles/emojis/0383.svg new file mode 100644 index 0000000..ef05bc7 --- /dev/null +++ b/resources/imageFiles/emojis/0383.svg @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0384.svg b/resources/imageFiles/emojis/0384.svg new file mode 100644 index 0000000..0cd2634 --- /dev/null +++ b/resources/imageFiles/emojis/0384.svg @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0385.svg b/resources/imageFiles/emojis/0385.svg new file mode 100644 index 0000000..2fe4d48 --- /dev/null +++ b/resources/imageFiles/emojis/0385.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0386.svg b/resources/imageFiles/emojis/0386.svg new file mode 100644 index 0000000..a991c40 --- /dev/null +++ b/resources/imageFiles/emojis/0386.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0387.svg b/resources/imageFiles/emojis/0387.svg new file mode 100644 index 0000000..3535748 --- /dev/null +++ b/resources/imageFiles/emojis/0387.svg @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0388.svg b/resources/imageFiles/emojis/0388.svg new file mode 100644 index 0000000..1e3ef8e --- /dev/null +++ b/resources/imageFiles/emojis/0388.svg @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0389.svg b/resources/imageFiles/emojis/0389.svg new file mode 100644 index 0000000..4a0ebef --- /dev/null +++ b/resources/imageFiles/emojis/0389.svg @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0390.svg b/resources/imageFiles/emojis/0390.svg new file mode 100644 index 0000000..6817b2f --- /dev/null +++ b/resources/imageFiles/emojis/0390.svg @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0391.svg b/resources/imageFiles/emojis/0391.svg new file mode 100644 index 0000000..d87b97c --- /dev/null +++ b/resources/imageFiles/emojis/0391.svg @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0392.svg b/resources/imageFiles/emojis/0392.svg new file mode 100644 index 0000000..597bd54 --- /dev/null +++ b/resources/imageFiles/emojis/0392.svg @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0393.svg b/resources/imageFiles/emojis/0393.svg new file mode 100644 index 0000000..fb5dcc7 --- /dev/null +++ b/resources/imageFiles/emojis/0393.svg @@ -0,0 +1,29 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0394.svg b/resources/imageFiles/emojis/0394.svg new file mode 100644 index 0000000..390bb35 --- /dev/null +++ b/resources/imageFiles/emojis/0394.svg @@ -0,0 +1,29 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0395.svg b/resources/imageFiles/emojis/0395.svg new file mode 100644 index 0000000..a2a882b --- /dev/null +++ b/resources/imageFiles/emojis/0395.svg @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0396.svg b/resources/imageFiles/emojis/0396.svg new file mode 100644 index 0000000..e8ba979 --- /dev/null +++ b/resources/imageFiles/emojis/0396.svg @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0397.svg b/resources/imageFiles/emojis/0397.svg new file mode 100644 index 0000000..ae02269 --- /dev/null +++ b/resources/imageFiles/emojis/0397.svg @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0398.svg b/resources/imageFiles/emojis/0398.svg new file mode 100644 index 0000000..45cb335 --- /dev/null +++ b/resources/imageFiles/emojis/0398.svg @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0399.svg b/resources/imageFiles/emojis/0399.svg new file mode 100644 index 0000000..a08d9c1 --- /dev/null +++ b/resources/imageFiles/emojis/0399.svg @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0400.svg b/resources/imageFiles/emojis/0400.svg new file mode 100644 index 0000000..19139a7 --- /dev/null +++ b/resources/imageFiles/emojis/0400.svg @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0401.svg b/resources/imageFiles/emojis/0401.svg new file mode 100644 index 0000000..d97220f --- /dev/null +++ b/resources/imageFiles/emojis/0401.svg @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0402.svg b/resources/imageFiles/emojis/0402.svg new file mode 100644 index 0000000..8c19483 --- /dev/null +++ b/resources/imageFiles/emojis/0402.svg @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0403.svg b/resources/imageFiles/emojis/0403.svg new file mode 100644 index 0000000..b468cc0 --- /dev/null +++ b/resources/imageFiles/emojis/0403.svg @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0404.svg b/resources/imageFiles/emojis/0404.svg new file mode 100644 index 0000000..e16230a --- /dev/null +++ b/resources/imageFiles/emojis/0404.svg @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0405.svg b/resources/imageFiles/emojis/0405.svg new file mode 100644 index 0000000..4bb0ef7 --- /dev/null +++ b/resources/imageFiles/emojis/0405.svg @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0406.svg b/resources/imageFiles/emojis/0406.svg new file mode 100644 index 0000000..d5d67ee --- /dev/null +++ b/resources/imageFiles/emojis/0406.svg @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0407.svg b/resources/imageFiles/emojis/0407.svg new file mode 100644 index 0000000..ad7412e --- /dev/null +++ b/resources/imageFiles/emojis/0407.svg @@ -0,0 +1,31 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0408.svg b/resources/imageFiles/emojis/0408.svg new file mode 100644 index 0000000..51ad2a5 --- /dev/null +++ b/resources/imageFiles/emojis/0408.svg @@ -0,0 +1,34 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0409.svg b/resources/imageFiles/emojis/0409.svg new file mode 100644 index 0000000..c240124 --- /dev/null +++ b/resources/imageFiles/emojis/0409.svg @@ -0,0 +1,32 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0410.svg b/resources/imageFiles/emojis/0410.svg new file mode 100644 index 0000000..0992fca --- /dev/null +++ b/resources/imageFiles/emojis/0410.svg @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0411.svg b/resources/imageFiles/emojis/0411.svg new file mode 100644 index 0000000..0a45b9c --- /dev/null +++ b/resources/imageFiles/emojis/0411.svg @@ -0,0 +1,40 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0412.svg b/resources/imageFiles/emojis/0412.svg new file mode 100644 index 0000000..581105b --- /dev/null +++ b/resources/imageFiles/emojis/0412.svg @@ -0,0 +1,29 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0413.svg b/resources/imageFiles/emojis/0413.svg new file mode 100644 index 0000000..140faec --- /dev/null +++ b/resources/imageFiles/emojis/0413.svg @@ -0,0 +1,29 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0414.svg b/resources/imageFiles/emojis/0414.svg new file mode 100644 index 0000000..13b025a --- /dev/null +++ b/resources/imageFiles/emojis/0414.svg @@ -0,0 +1,44 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0415.svg b/resources/imageFiles/emojis/0415.svg new file mode 100644 index 0000000..d3adec3 --- /dev/null +++ b/resources/imageFiles/emojis/0415.svg @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0416.svg b/resources/imageFiles/emojis/0416.svg new file mode 100644 index 0000000..1625b60 --- /dev/null +++ b/resources/imageFiles/emojis/0416.svg @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0417.svg b/resources/imageFiles/emojis/0417.svg new file mode 100644 index 0000000..7ac16ee --- /dev/null +++ b/resources/imageFiles/emojis/0417.svg @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0418.svg b/resources/imageFiles/emojis/0418.svg new file mode 100644 index 0000000..5f7c14e --- /dev/null +++ b/resources/imageFiles/emojis/0418.svg @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0419.svg b/resources/imageFiles/emojis/0419.svg new file mode 100644 index 0000000..f52d736 --- /dev/null +++ b/resources/imageFiles/emojis/0419.svg @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0420.svg b/resources/imageFiles/emojis/0420.svg new file mode 100644 index 0000000..e775fd0 --- /dev/null +++ b/resources/imageFiles/emojis/0420.svg @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0421.svg b/resources/imageFiles/emojis/0421.svg new file mode 100644 index 0000000..4a27e2b --- /dev/null +++ b/resources/imageFiles/emojis/0421.svg @@ -0,0 +1,29 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0422.svg b/resources/imageFiles/emojis/0422.svg new file mode 100644 index 0000000..5aa635f --- /dev/null +++ b/resources/imageFiles/emojis/0422.svg @@ -0,0 +1,29 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0423.svg b/resources/imageFiles/emojis/0423.svg new file mode 100644 index 0000000..223405a --- /dev/null +++ b/resources/imageFiles/emojis/0423.svg @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0424.svg b/resources/imageFiles/emojis/0424.svg new file mode 100644 index 0000000..3f6e56c --- /dev/null +++ b/resources/imageFiles/emojis/0424.svg @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0425.svg b/resources/imageFiles/emojis/0425.svg new file mode 100644 index 0000000..81157e5 --- /dev/null +++ b/resources/imageFiles/emojis/0425.svg @@ -0,0 +1,29 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0426.svg b/resources/imageFiles/emojis/0426.svg new file mode 100644 index 0000000..ac073d4 --- /dev/null +++ b/resources/imageFiles/emojis/0426.svg @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0427.svg b/resources/imageFiles/emojis/0427.svg new file mode 100644 index 0000000..046825c --- /dev/null +++ b/resources/imageFiles/emojis/0427.svg @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0428.svg b/resources/imageFiles/emojis/0428.svg new file mode 100644 index 0000000..9909edb --- /dev/null +++ b/resources/imageFiles/emojis/0428.svg @@ -0,0 +1,29 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0429.svg b/resources/imageFiles/emojis/0429.svg new file mode 100644 index 0000000..9df3ebd --- /dev/null +++ b/resources/imageFiles/emojis/0429.svg @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0430.svg b/resources/imageFiles/emojis/0430.svg new file mode 100644 index 0000000..a83e952 --- /dev/null +++ b/resources/imageFiles/emojis/0430.svg @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0431.svg b/resources/imageFiles/emojis/0431.svg new file mode 100644 index 0000000..2a47281 --- /dev/null +++ b/resources/imageFiles/emojis/0431.svg @@ -0,0 +1,29 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0432.svg b/resources/imageFiles/emojis/0432.svg new file mode 100644 index 0000000..8326e85 --- /dev/null +++ b/resources/imageFiles/emojis/0432.svg @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0433.svg b/resources/imageFiles/emojis/0433.svg new file mode 100644 index 0000000..35c5d27 --- /dev/null +++ b/resources/imageFiles/emojis/0433.svg @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0434.svg b/resources/imageFiles/emojis/0434.svg new file mode 100644 index 0000000..4003918 --- /dev/null +++ b/resources/imageFiles/emojis/0434.svg @@ -0,0 +1,29 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0435.svg b/resources/imageFiles/emojis/0435.svg new file mode 100644 index 0000000..1506f2b --- /dev/null +++ b/resources/imageFiles/emojis/0435.svg @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0436.svg b/resources/imageFiles/emojis/0436.svg new file mode 100644 index 0000000..eb696a5 --- /dev/null +++ b/resources/imageFiles/emojis/0436.svg @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0437.svg b/resources/imageFiles/emojis/0437.svg new file mode 100644 index 0000000..8ae5641 --- /dev/null +++ b/resources/imageFiles/emojis/0437.svg @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0438.svg b/resources/imageFiles/emojis/0438.svg new file mode 100644 index 0000000..6b50d39 --- /dev/null +++ b/resources/imageFiles/emojis/0438.svg @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0439.svg b/resources/imageFiles/emojis/0439.svg new file mode 100644 index 0000000..e13ed7a --- /dev/null +++ b/resources/imageFiles/emojis/0439.svg @@ -0,0 +1,32 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0440.svg b/resources/imageFiles/emojis/0440.svg new file mode 100644 index 0000000..62062b8 --- /dev/null +++ b/resources/imageFiles/emojis/0440.svg @@ -0,0 +1,32 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0441.svg b/resources/imageFiles/emojis/0441.svg new file mode 100644 index 0000000..7ce7a4d --- /dev/null +++ b/resources/imageFiles/emojis/0441.svg @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0442.svg b/resources/imageFiles/emojis/0442.svg new file mode 100644 index 0000000..f55f146 --- /dev/null +++ b/resources/imageFiles/emojis/0442.svg @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0443.svg b/resources/imageFiles/emojis/0443.svg new file mode 100644 index 0000000..7dd3cc5 --- /dev/null +++ b/resources/imageFiles/emojis/0443.svg @@ -0,0 +1,32 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0444.svg b/resources/imageFiles/emojis/0444.svg new file mode 100644 index 0000000..f98ee74 --- /dev/null +++ b/resources/imageFiles/emojis/0444.svg @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0445.svg b/resources/imageFiles/emojis/0445.svg new file mode 100644 index 0000000..0f11e6c --- /dev/null +++ b/resources/imageFiles/emojis/0445.svg @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0446.svg b/resources/imageFiles/emojis/0446.svg new file mode 100644 index 0000000..2590ecc --- /dev/null +++ b/resources/imageFiles/emojis/0446.svg @@ -0,0 +1,32 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0447.svg b/resources/imageFiles/emojis/0447.svg new file mode 100644 index 0000000..db6b042 --- /dev/null +++ b/resources/imageFiles/emojis/0447.svg @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0448.svg b/resources/imageFiles/emojis/0448.svg new file mode 100644 index 0000000..ff25d88 --- /dev/null +++ b/resources/imageFiles/emojis/0448.svg @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0449.svg b/resources/imageFiles/emojis/0449.svg new file mode 100644 index 0000000..2079b27 --- /dev/null +++ b/resources/imageFiles/emojis/0449.svg @@ -0,0 +1,32 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0450.svg b/resources/imageFiles/emojis/0450.svg new file mode 100644 index 0000000..0d13c2e --- /dev/null +++ b/resources/imageFiles/emojis/0450.svg @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0451.svg b/resources/imageFiles/emojis/0451.svg new file mode 100644 index 0000000..aec2223 --- /dev/null +++ b/resources/imageFiles/emojis/0451.svg @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0452.svg b/resources/imageFiles/emojis/0452.svg new file mode 100644 index 0000000..3e5428a --- /dev/null +++ b/resources/imageFiles/emojis/0452.svg @@ -0,0 +1,32 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0453.svg b/resources/imageFiles/emojis/0453.svg new file mode 100644 index 0000000..88c7c2a --- /dev/null +++ b/resources/imageFiles/emojis/0453.svg @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0454.svg b/resources/imageFiles/emojis/0454.svg new file mode 100644 index 0000000..2bb8b18 --- /dev/null +++ b/resources/imageFiles/emojis/0454.svg @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0455.svg b/resources/imageFiles/emojis/0455.svg new file mode 100644 index 0000000..3879d7f --- /dev/null +++ b/resources/imageFiles/emojis/0455.svg @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0456.svg b/resources/imageFiles/emojis/0456.svg new file mode 100644 index 0000000..9100ce9 --- /dev/null +++ b/resources/imageFiles/emojis/0456.svg @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0457.svg b/resources/imageFiles/emojis/0457.svg new file mode 100644 index 0000000..7db7325 --- /dev/null +++ b/resources/imageFiles/emojis/0457.svg @@ -0,0 +1,34 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0458.svg b/resources/imageFiles/emojis/0458.svg new file mode 100644 index 0000000..a4c4ac3 --- /dev/null +++ b/resources/imageFiles/emojis/0458.svg @@ -0,0 +1,34 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0459.svg b/resources/imageFiles/emojis/0459.svg new file mode 100644 index 0000000..d4911db --- /dev/null +++ b/resources/imageFiles/emojis/0459.svg @@ -0,0 +1,32 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0460.svg b/resources/imageFiles/emojis/0460.svg new file mode 100644 index 0000000..323d38e --- /dev/null +++ b/resources/imageFiles/emojis/0460.svg @@ -0,0 +1,34 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0461.svg b/resources/imageFiles/emojis/0461.svg new file mode 100644 index 0000000..75248fc --- /dev/null +++ b/resources/imageFiles/emojis/0461.svg @@ -0,0 +1,34 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0462.svg b/resources/imageFiles/emojis/0462.svg new file mode 100644 index 0000000..2f6dd82 --- /dev/null +++ b/resources/imageFiles/emojis/0462.svg @@ -0,0 +1,32 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0463.svg b/resources/imageFiles/emojis/0463.svg new file mode 100644 index 0000000..32da813 --- /dev/null +++ b/resources/imageFiles/emojis/0463.svg @@ -0,0 +1,34 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0464.svg b/resources/imageFiles/emojis/0464.svg new file mode 100644 index 0000000..8ecf617 --- /dev/null +++ b/resources/imageFiles/emojis/0464.svg @@ -0,0 +1,34 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0465.svg b/resources/imageFiles/emojis/0465.svg new file mode 100644 index 0000000..177755b --- /dev/null +++ b/resources/imageFiles/emojis/0465.svg @@ -0,0 +1,32 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0466.svg b/resources/imageFiles/emojis/0466.svg new file mode 100644 index 0000000..77a2184 --- /dev/null +++ b/resources/imageFiles/emojis/0466.svg @@ -0,0 +1,34 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0467.svg b/resources/imageFiles/emojis/0467.svg new file mode 100644 index 0000000..a11d2df --- /dev/null +++ b/resources/imageFiles/emojis/0467.svg @@ -0,0 +1,34 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0468.svg b/resources/imageFiles/emojis/0468.svg new file mode 100644 index 0000000..6557887 --- /dev/null +++ b/resources/imageFiles/emojis/0468.svg @@ -0,0 +1,32 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0469.svg b/resources/imageFiles/emojis/0469.svg new file mode 100644 index 0000000..b0ca0ca --- /dev/null +++ b/resources/imageFiles/emojis/0469.svg @@ -0,0 +1,34 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0470.svg b/resources/imageFiles/emojis/0470.svg new file mode 100644 index 0000000..f7bdcc9 --- /dev/null +++ b/resources/imageFiles/emojis/0470.svg @@ -0,0 +1,34 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0471.svg b/resources/imageFiles/emojis/0471.svg new file mode 100644 index 0000000..72db99a --- /dev/null +++ b/resources/imageFiles/emojis/0471.svg @@ -0,0 +1,32 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0472.svg b/resources/imageFiles/emojis/0472.svg new file mode 100644 index 0000000..b491eed --- /dev/null +++ b/resources/imageFiles/emojis/0472.svg @@ -0,0 +1,34 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0473.svg b/resources/imageFiles/emojis/0473.svg new file mode 100644 index 0000000..d406dab --- /dev/null +++ b/resources/imageFiles/emojis/0473.svg @@ -0,0 +1,32 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0474.svg b/resources/imageFiles/emojis/0474.svg new file mode 100644 index 0000000..f28d895 --- /dev/null +++ b/resources/imageFiles/emojis/0474.svg @@ -0,0 +1,34 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0475.svg b/resources/imageFiles/emojis/0475.svg new file mode 100644 index 0000000..e313994 --- /dev/null +++ b/resources/imageFiles/emojis/0475.svg @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0476.svg b/resources/imageFiles/emojis/0476.svg new file mode 100644 index 0000000..43970e4 --- /dev/null +++ b/resources/imageFiles/emojis/0476.svg @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0477.svg b/resources/imageFiles/emojis/0477.svg new file mode 100644 index 0000000..09d8f3c --- /dev/null +++ b/resources/imageFiles/emojis/0477.svg @@ -0,0 +1,31 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0478.svg b/resources/imageFiles/emojis/0478.svg new file mode 100644 index 0000000..01f5ae8 --- /dev/null +++ b/resources/imageFiles/emojis/0478.svg @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0479.svg b/resources/imageFiles/emojis/0479.svg new file mode 100644 index 0000000..497ef7f --- /dev/null +++ b/resources/imageFiles/emojis/0479.svg @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0480.svg b/resources/imageFiles/emojis/0480.svg new file mode 100644 index 0000000..ef9331b --- /dev/null +++ b/resources/imageFiles/emojis/0480.svg @@ -0,0 +1,31 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0481.svg b/resources/imageFiles/emojis/0481.svg new file mode 100644 index 0000000..b251223 --- /dev/null +++ b/resources/imageFiles/emojis/0481.svg @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0482.svg b/resources/imageFiles/emojis/0482.svg new file mode 100644 index 0000000..2c6a2c5 --- /dev/null +++ b/resources/imageFiles/emojis/0482.svg @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0483.svg b/resources/imageFiles/emojis/0483.svg new file mode 100644 index 0000000..dfa7b92 --- /dev/null +++ b/resources/imageFiles/emojis/0483.svg @@ -0,0 +1,31 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0484.svg b/resources/imageFiles/emojis/0484.svg new file mode 100644 index 0000000..9eacfee --- /dev/null +++ b/resources/imageFiles/emojis/0484.svg @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0485.svg b/resources/imageFiles/emojis/0485.svg new file mode 100644 index 0000000..82ecad4 --- /dev/null +++ b/resources/imageFiles/emojis/0485.svg @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0486.svg b/resources/imageFiles/emojis/0486.svg new file mode 100644 index 0000000..3dc2501 --- /dev/null +++ b/resources/imageFiles/emojis/0486.svg @@ -0,0 +1,31 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0487.svg b/resources/imageFiles/emojis/0487.svg new file mode 100644 index 0000000..8246b73 --- /dev/null +++ b/resources/imageFiles/emojis/0487.svg @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0488.svg b/resources/imageFiles/emojis/0488.svg new file mode 100644 index 0000000..0aa7ddc --- /dev/null +++ b/resources/imageFiles/emojis/0488.svg @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0489.svg b/resources/imageFiles/emojis/0489.svg new file mode 100644 index 0000000..2307d53 --- /dev/null +++ b/resources/imageFiles/emojis/0489.svg @@ -0,0 +1,31 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0490.svg b/resources/imageFiles/emojis/0490.svg new file mode 100644 index 0000000..90051eb --- /dev/null +++ b/resources/imageFiles/emojis/0490.svg @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0491.svg b/resources/imageFiles/emojis/0491.svg new file mode 100644 index 0000000..f8fd56b --- /dev/null +++ b/resources/imageFiles/emojis/0491.svg @@ -0,0 +1,31 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0492.svg b/resources/imageFiles/emojis/0492.svg new file mode 100644 index 0000000..f8b2c50 --- /dev/null +++ b/resources/imageFiles/emojis/0492.svg @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0493.svg b/resources/imageFiles/emojis/0493.svg new file mode 100644 index 0000000..f117912 --- /dev/null +++ b/resources/imageFiles/emojis/0493.svg @@ -0,0 +1,35 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0494.svg b/resources/imageFiles/emojis/0494.svg new file mode 100644 index 0000000..bd59d77 --- /dev/null +++ b/resources/imageFiles/emojis/0494.svg @@ -0,0 +1,44 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0495.svg b/resources/imageFiles/emojis/0495.svg new file mode 100644 index 0000000..6cc3e69 --- /dev/null +++ b/resources/imageFiles/emojis/0495.svg @@ -0,0 +1,31 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0496.svg b/resources/imageFiles/emojis/0496.svg new file mode 100644 index 0000000..a400ea3 --- /dev/null +++ b/resources/imageFiles/emojis/0496.svg @@ -0,0 +1,40 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0497.svg b/resources/imageFiles/emojis/0497.svg new file mode 100644 index 0000000..d484ff8 --- /dev/null +++ b/resources/imageFiles/emojis/0497.svg @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0498.svg b/resources/imageFiles/emojis/0498.svg new file mode 100644 index 0000000..a7e1fb7 --- /dev/null +++ b/resources/imageFiles/emojis/0498.svg @@ -0,0 +1,43 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0499.svg b/resources/imageFiles/emojis/0499.svg new file mode 100644 index 0000000..64c3e5a --- /dev/null +++ b/resources/imageFiles/emojis/0499.svg @@ -0,0 +1,31 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0500.svg b/resources/imageFiles/emojis/0500.svg new file mode 100644 index 0000000..01612d5 --- /dev/null +++ b/resources/imageFiles/emojis/0500.svg @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0501.svg b/resources/imageFiles/emojis/0501.svg new file mode 100644 index 0000000..479411b --- /dev/null +++ b/resources/imageFiles/emojis/0501.svg @@ -0,0 +1,34 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0502.svg b/resources/imageFiles/emojis/0502.svg new file mode 100644 index 0000000..a87bca4 --- /dev/null +++ b/resources/imageFiles/emojis/0502.svg @@ -0,0 +1,32 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0503.svg b/resources/imageFiles/emojis/0503.svg new file mode 100644 index 0000000..0227a2e --- /dev/null +++ b/resources/imageFiles/emojis/0503.svg @@ -0,0 +1,32 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0504.svg b/resources/imageFiles/emojis/0504.svg new file mode 100644 index 0000000..85459c4 --- /dev/null +++ b/resources/imageFiles/emojis/0504.svg @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0505.svg b/resources/imageFiles/emojis/0505.svg new file mode 100644 index 0000000..e8298f5 --- /dev/null +++ b/resources/imageFiles/emojis/0505.svg @@ -0,0 +1,40 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0506.svg b/resources/imageFiles/emojis/0506.svg new file mode 100644 index 0000000..746d8d6 --- /dev/null +++ b/resources/imageFiles/emojis/0506.svg @@ -0,0 +1,29 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0507.svg b/resources/imageFiles/emojis/0507.svg new file mode 100644 index 0000000..037675c --- /dev/null +++ b/resources/imageFiles/emojis/0507.svg @@ -0,0 +1,29 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0508.svg b/resources/imageFiles/emojis/0508.svg new file mode 100644 index 0000000..13f1455 --- /dev/null +++ b/resources/imageFiles/emojis/0508.svg @@ -0,0 +1,44 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0509.svg b/resources/imageFiles/emojis/0509.svg new file mode 100644 index 0000000..4bfa0d6 --- /dev/null +++ b/resources/imageFiles/emojis/0509.svg @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0510.svg b/resources/imageFiles/emojis/0510.svg new file mode 100644 index 0000000..583d6f7 --- /dev/null +++ b/resources/imageFiles/emojis/0510.svg @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0511.svg b/resources/imageFiles/emojis/0511.svg new file mode 100644 index 0000000..4669e24 --- /dev/null +++ b/resources/imageFiles/emojis/0511.svg @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0512.svg b/resources/imageFiles/emojis/0512.svg new file mode 100644 index 0000000..c7642e4 --- /dev/null +++ b/resources/imageFiles/emojis/0512.svg @@ -0,0 +1,29 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0513.svg b/resources/imageFiles/emojis/0513.svg new file mode 100644 index 0000000..8379a03 --- /dev/null +++ b/resources/imageFiles/emojis/0513.svg @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0514.svg b/resources/imageFiles/emojis/0514.svg new file mode 100644 index 0000000..cd53e9c --- /dev/null +++ b/resources/imageFiles/emojis/0514.svg @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0515.svg b/resources/imageFiles/emojis/0515.svg new file mode 100644 index 0000000..00e538d --- /dev/null +++ b/resources/imageFiles/emojis/0515.svg @@ -0,0 +1,29 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0516.svg b/resources/imageFiles/emojis/0516.svg new file mode 100644 index 0000000..a90c38b --- /dev/null +++ b/resources/imageFiles/emojis/0516.svg @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0517.svg b/resources/imageFiles/emojis/0517.svg new file mode 100644 index 0000000..9832334 --- /dev/null +++ b/resources/imageFiles/emojis/0517.svg @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0518.svg b/resources/imageFiles/emojis/0518.svg new file mode 100644 index 0000000..580c921 --- /dev/null +++ b/resources/imageFiles/emojis/0518.svg @@ -0,0 +1,29 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0519.svg b/resources/imageFiles/emojis/0519.svg new file mode 100644 index 0000000..2910428 --- /dev/null +++ b/resources/imageFiles/emojis/0519.svg @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0520.svg b/resources/imageFiles/emojis/0520.svg new file mode 100644 index 0000000..4df3975 --- /dev/null +++ b/resources/imageFiles/emojis/0520.svg @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0521.svg b/resources/imageFiles/emojis/0521.svg new file mode 100644 index 0000000..dc293da --- /dev/null +++ b/resources/imageFiles/emojis/0521.svg @@ -0,0 +1,29 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0522.svg b/resources/imageFiles/emojis/0522.svg new file mode 100644 index 0000000..ec1f4b7 --- /dev/null +++ b/resources/imageFiles/emojis/0522.svg @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0523.svg b/resources/imageFiles/emojis/0523.svg new file mode 100644 index 0000000..7bbfe0b --- /dev/null +++ b/resources/imageFiles/emojis/0523.svg @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0524.svg b/resources/imageFiles/emojis/0524.svg new file mode 100644 index 0000000..7f4f246 --- /dev/null +++ b/resources/imageFiles/emojis/0524.svg @@ -0,0 +1,29 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0525.svg b/resources/imageFiles/emojis/0525.svg new file mode 100644 index 0000000..b1795a8 --- /dev/null +++ b/resources/imageFiles/emojis/0525.svg @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0526.svg b/resources/imageFiles/emojis/0526.svg new file mode 100644 index 0000000..4d915f0 --- /dev/null +++ b/resources/imageFiles/emojis/0526.svg @@ -0,0 +1,29 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0527.svg b/resources/imageFiles/emojis/0527.svg new file mode 100644 index 0000000..b5b2499 --- /dev/null +++ b/resources/imageFiles/emojis/0527.svg @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0528.svg b/resources/imageFiles/emojis/0528.svg new file mode 100644 index 0000000..6feb16d --- /dev/null +++ b/resources/imageFiles/emojis/0528.svg @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0529.svg b/resources/imageFiles/emojis/0529.svg new file mode 100644 index 0000000..7f1fd92 --- /dev/null +++ b/resources/imageFiles/emojis/0529.svg @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0530.svg b/resources/imageFiles/emojis/0530.svg new file mode 100644 index 0000000..a4d40e7 --- /dev/null +++ b/resources/imageFiles/emojis/0530.svg @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0531.svg b/resources/imageFiles/emojis/0531.svg new file mode 100644 index 0000000..b8f6370 --- /dev/null +++ b/resources/imageFiles/emojis/0531.svg @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0532.svg b/resources/imageFiles/emojis/0532.svg new file mode 100644 index 0000000..270f750 --- /dev/null +++ b/resources/imageFiles/emojis/0532.svg @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0533.svg b/resources/imageFiles/emojis/0533.svg new file mode 100644 index 0000000..314e647 --- /dev/null +++ b/resources/imageFiles/emojis/0533.svg @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0534.svg b/resources/imageFiles/emojis/0534.svg new file mode 100644 index 0000000..a32a4b6 --- /dev/null +++ b/resources/imageFiles/emojis/0534.svg @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0535.svg b/resources/imageFiles/emojis/0535.svg new file mode 100644 index 0000000..dbaf46f --- /dev/null +++ b/resources/imageFiles/emojis/0535.svg @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0536.svg b/resources/imageFiles/emojis/0536.svg new file mode 100644 index 0000000..afcccd5 --- /dev/null +++ b/resources/imageFiles/emojis/0536.svg @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0537.svg b/resources/imageFiles/emojis/0537.svg new file mode 100644 index 0000000..98bda5f --- /dev/null +++ b/resources/imageFiles/emojis/0537.svg @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0538.svg b/resources/imageFiles/emojis/0538.svg new file mode 100644 index 0000000..3bc4aca --- /dev/null +++ b/resources/imageFiles/emojis/0538.svg @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0539.svg b/resources/imageFiles/emojis/0539.svg new file mode 100644 index 0000000..0dcb092 --- /dev/null +++ b/resources/imageFiles/emojis/0539.svg @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0540.svg b/resources/imageFiles/emojis/0540.svg new file mode 100644 index 0000000..82d9449 --- /dev/null +++ b/resources/imageFiles/emojis/0540.svg @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0541.svg b/resources/imageFiles/emojis/0541.svg new file mode 100644 index 0000000..71c5181 --- /dev/null +++ b/resources/imageFiles/emojis/0541.svg @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0542.svg b/resources/imageFiles/emojis/0542.svg new file mode 100644 index 0000000..38d430a --- /dev/null +++ b/resources/imageFiles/emojis/0542.svg @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0543.svg b/resources/imageFiles/emojis/0543.svg new file mode 100644 index 0000000..6545324 --- /dev/null +++ b/resources/imageFiles/emojis/0543.svg @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0544.svg b/resources/imageFiles/emojis/0544.svg new file mode 100644 index 0000000..b9a9a4b --- /dev/null +++ b/resources/imageFiles/emojis/0544.svg @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0545.svg b/resources/imageFiles/emojis/0545.svg new file mode 100644 index 0000000..590829d --- /dev/null +++ b/resources/imageFiles/emojis/0545.svg @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0546.svg b/resources/imageFiles/emojis/0546.svg new file mode 100644 index 0000000..a02e755 --- /dev/null +++ b/resources/imageFiles/emojis/0546.svg @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0547.svg b/resources/imageFiles/emojis/0547.svg new file mode 100644 index 0000000..859fad4 --- /dev/null +++ b/resources/imageFiles/emojis/0547.svg @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0548.svg b/resources/imageFiles/emojis/0548.svg new file mode 100644 index 0000000..3060722 --- /dev/null +++ b/resources/imageFiles/emojis/0548.svg @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0549.svg b/resources/imageFiles/emojis/0549.svg new file mode 100644 index 0000000..d9ed067 --- /dev/null +++ b/resources/imageFiles/emojis/0549.svg @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0550.svg b/resources/imageFiles/emojis/0550.svg new file mode 100644 index 0000000..cdf9d7b --- /dev/null +++ b/resources/imageFiles/emojis/0550.svg @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0551.svg b/resources/imageFiles/emojis/0551.svg new file mode 100644 index 0000000..06127eb --- /dev/null +++ b/resources/imageFiles/emojis/0551.svg @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0552.svg b/resources/imageFiles/emojis/0552.svg new file mode 100644 index 0000000..9f16e13 --- /dev/null +++ b/resources/imageFiles/emojis/0552.svg @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0553.svg b/resources/imageFiles/emojis/0553.svg new file mode 100644 index 0000000..1809267 --- /dev/null +++ b/resources/imageFiles/emojis/0553.svg @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0554.svg b/resources/imageFiles/emojis/0554.svg new file mode 100644 index 0000000..095a0d4 --- /dev/null +++ b/resources/imageFiles/emojis/0554.svg @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0555.svg b/resources/imageFiles/emojis/0555.svg new file mode 100644 index 0000000..6d14942 --- /dev/null +++ b/resources/imageFiles/emojis/0555.svg @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0556.svg b/resources/imageFiles/emojis/0556.svg new file mode 100644 index 0000000..e93e332 --- /dev/null +++ b/resources/imageFiles/emojis/0556.svg @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0557.svg b/resources/imageFiles/emojis/0557.svg new file mode 100644 index 0000000..0fc0364 --- /dev/null +++ b/resources/imageFiles/emojis/0557.svg @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0558.svg b/resources/imageFiles/emojis/0558.svg new file mode 100644 index 0000000..e94094d --- /dev/null +++ b/resources/imageFiles/emojis/0558.svg @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0559.svg b/resources/imageFiles/emojis/0559.svg new file mode 100644 index 0000000..35fbcdd --- /dev/null +++ b/resources/imageFiles/emojis/0559.svg @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0560.svg b/resources/imageFiles/emojis/0560.svg new file mode 100644 index 0000000..2905e9b --- /dev/null +++ b/resources/imageFiles/emojis/0560.svg @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0561.svg b/resources/imageFiles/emojis/0561.svg new file mode 100644 index 0000000..6e95a55 --- /dev/null +++ b/resources/imageFiles/emojis/0561.svg @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0562.svg b/resources/imageFiles/emojis/0562.svg new file mode 100644 index 0000000..b6c2967 --- /dev/null +++ b/resources/imageFiles/emojis/0562.svg @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0563.svg b/resources/imageFiles/emojis/0563.svg new file mode 100644 index 0000000..6fc0056 --- /dev/null +++ b/resources/imageFiles/emojis/0563.svg @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0564.svg b/resources/imageFiles/emojis/0564.svg new file mode 100644 index 0000000..9611c84 --- /dev/null +++ b/resources/imageFiles/emojis/0564.svg @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0565.svg b/resources/imageFiles/emojis/0565.svg new file mode 100644 index 0000000..96da0d6 --- /dev/null +++ b/resources/imageFiles/emojis/0565.svg @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0566.svg b/resources/imageFiles/emojis/0566.svg new file mode 100644 index 0000000..4efe42c --- /dev/null +++ b/resources/imageFiles/emojis/0566.svg @@ -0,0 +1,39 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0567.svg b/resources/imageFiles/emojis/0567.svg new file mode 100644 index 0000000..d910d50 --- /dev/null +++ b/resources/imageFiles/emojis/0567.svg @@ -0,0 +1,35 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0568.svg b/resources/imageFiles/emojis/0568.svg new file mode 100644 index 0000000..05965a3 --- /dev/null +++ b/resources/imageFiles/emojis/0568.svg @@ -0,0 +1,36 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0569.svg b/resources/imageFiles/emojis/0569.svg new file mode 100644 index 0000000..8834f81 --- /dev/null +++ b/resources/imageFiles/emojis/0569.svg @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0570.svg b/resources/imageFiles/emojis/0570.svg new file mode 100644 index 0000000..17d31dc --- /dev/null +++ b/resources/imageFiles/emojis/0570.svg @@ -0,0 +1,43 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0571.svg b/resources/imageFiles/emojis/0571.svg new file mode 100644 index 0000000..ee2f434 --- /dev/null +++ b/resources/imageFiles/emojis/0571.svg @@ -0,0 +1,49 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0572.svg b/resources/imageFiles/emojis/0572.svg new file mode 100644 index 0000000..8e01cb0 --- /dev/null +++ b/resources/imageFiles/emojis/0572.svg @@ -0,0 +1,39 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0573.svg b/resources/imageFiles/emojis/0573.svg new file mode 100644 index 0000000..516d52b --- /dev/null +++ b/resources/imageFiles/emojis/0573.svg @@ -0,0 +1,39 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0574.svg b/resources/imageFiles/emojis/0574.svg new file mode 100644 index 0000000..ad6b1f1 --- /dev/null +++ b/resources/imageFiles/emojis/0574.svg @@ -0,0 +1,39 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0575.svg b/resources/imageFiles/emojis/0575.svg new file mode 100644 index 0000000..754ef9a --- /dev/null +++ b/resources/imageFiles/emojis/0575.svg @@ -0,0 +1,39 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0576.svg b/resources/imageFiles/emojis/0576.svg new file mode 100644 index 0000000..73eff64 --- /dev/null +++ b/resources/imageFiles/emojis/0576.svg @@ -0,0 +1,39 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0577.svg b/resources/imageFiles/emojis/0577.svg new file mode 100644 index 0000000..86b0c21 --- /dev/null +++ b/resources/imageFiles/emojis/0577.svg @@ -0,0 +1,39 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0578.svg b/resources/imageFiles/emojis/0578.svg new file mode 100644 index 0000000..6e3755b --- /dev/null +++ b/resources/imageFiles/emojis/0578.svg @@ -0,0 +1,39 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0579.svg b/resources/imageFiles/emojis/0579.svg new file mode 100644 index 0000000..1b7d8df --- /dev/null +++ b/resources/imageFiles/emojis/0579.svg @@ -0,0 +1,39 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0580.svg b/resources/imageFiles/emojis/0580.svg new file mode 100644 index 0000000..44a762e --- /dev/null +++ b/resources/imageFiles/emojis/0580.svg @@ -0,0 +1,39 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0581.svg b/resources/imageFiles/emojis/0581.svg new file mode 100644 index 0000000..5d33073 --- /dev/null +++ b/resources/imageFiles/emojis/0581.svg @@ -0,0 +1,39 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0582.svg b/resources/imageFiles/emojis/0582.svg new file mode 100644 index 0000000..70e7dbc --- /dev/null +++ b/resources/imageFiles/emojis/0582.svg @@ -0,0 +1,39 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0583.svg b/resources/imageFiles/emojis/0583.svg new file mode 100644 index 0000000..0a94deb --- /dev/null +++ b/resources/imageFiles/emojis/0583.svg @@ -0,0 +1,39 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0584.svg b/resources/imageFiles/emojis/0584.svg new file mode 100644 index 0000000..7ccbaf4 --- /dev/null +++ b/resources/imageFiles/emojis/0584.svg @@ -0,0 +1,39 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0585.svg b/resources/imageFiles/emojis/0585.svg new file mode 100644 index 0000000..0e62004 --- /dev/null +++ b/resources/imageFiles/emojis/0585.svg @@ -0,0 +1,39 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0586.svg b/resources/imageFiles/emojis/0586.svg new file mode 100644 index 0000000..754919b --- /dev/null +++ b/resources/imageFiles/emojis/0586.svg @@ -0,0 +1,39 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0587.svg b/resources/imageFiles/emojis/0587.svg new file mode 100644 index 0000000..9a381e1 --- /dev/null +++ b/resources/imageFiles/emojis/0587.svg @@ -0,0 +1,39 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0588.svg b/resources/imageFiles/emojis/0588.svg new file mode 100644 index 0000000..80a1ddc --- /dev/null +++ b/resources/imageFiles/emojis/0588.svg @@ -0,0 +1,39 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0589.svg b/resources/imageFiles/emojis/0589.svg new file mode 100644 index 0000000..62e5101 --- /dev/null +++ b/resources/imageFiles/emojis/0589.svg @@ -0,0 +1,39 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0590.svg b/resources/imageFiles/emojis/0590.svg new file mode 100644 index 0000000..85cc264 --- /dev/null +++ b/resources/imageFiles/emojis/0590.svg @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0591.svg b/resources/imageFiles/emojis/0591.svg new file mode 100644 index 0000000..ac03aab --- /dev/null +++ b/resources/imageFiles/emojis/0591.svg @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0592.svg b/resources/imageFiles/emojis/0592.svg new file mode 100644 index 0000000..e662de7 --- /dev/null +++ b/resources/imageFiles/emojis/0592.svg @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0593.svg b/resources/imageFiles/emojis/0593.svg new file mode 100644 index 0000000..fb44ef8 --- /dev/null +++ b/resources/imageFiles/emojis/0593.svg @@ -0,0 +1,42 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0594.svg b/resources/imageFiles/emojis/0594.svg new file mode 100644 index 0000000..e5c795a --- /dev/null +++ b/resources/imageFiles/emojis/0594.svg @@ -0,0 +1,42 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0595.svg b/resources/imageFiles/emojis/0595.svg new file mode 100644 index 0000000..a886a8e --- /dev/null +++ b/resources/imageFiles/emojis/0595.svg @@ -0,0 +1,45 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0596.svg b/resources/imageFiles/emojis/0596.svg new file mode 100644 index 0000000..956b1a1 --- /dev/null +++ b/resources/imageFiles/emojis/0596.svg @@ -0,0 +1,45 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0597.svg b/resources/imageFiles/emojis/0597.svg new file mode 100644 index 0000000..d0ec34e --- /dev/null +++ b/resources/imageFiles/emojis/0597.svg @@ -0,0 +1,42 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0598.svg b/resources/imageFiles/emojis/0598.svg new file mode 100644 index 0000000..6666671 --- /dev/null +++ b/resources/imageFiles/emojis/0598.svg @@ -0,0 +1,45 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0599.svg b/resources/imageFiles/emojis/0599.svg new file mode 100644 index 0000000..54f70af --- /dev/null +++ b/resources/imageFiles/emojis/0599.svg @@ -0,0 +1,45 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0600.svg b/resources/imageFiles/emojis/0600.svg new file mode 100644 index 0000000..69103a8 --- /dev/null +++ b/resources/imageFiles/emojis/0600.svg @@ -0,0 +1,42 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0601.svg b/resources/imageFiles/emojis/0601.svg new file mode 100644 index 0000000..76bb3f9 --- /dev/null +++ b/resources/imageFiles/emojis/0601.svg @@ -0,0 +1,45 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0602.svg b/resources/imageFiles/emojis/0602.svg new file mode 100644 index 0000000..bbeeacd --- /dev/null +++ b/resources/imageFiles/emojis/0602.svg @@ -0,0 +1,45 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0603.svg b/resources/imageFiles/emojis/0603.svg new file mode 100644 index 0000000..1b04929 --- /dev/null +++ b/resources/imageFiles/emojis/0603.svg @@ -0,0 +1,42 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0604.svg b/resources/imageFiles/emojis/0604.svg new file mode 100644 index 0000000..dd30f56 --- /dev/null +++ b/resources/imageFiles/emojis/0604.svg @@ -0,0 +1,45 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0605.svg b/resources/imageFiles/emojis/0605.svg new file mode 100644 index 0000000..4ae4bca --- /dev/null +++ b/resources/imageFiles/emojis/0605.svg @@ -0,0 +1,45 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0606.svg b/resources/imageFiles/emojis/0606.svg new file mode 100644 index 0000000..367bc9a --- /dev/null +++ b/resources/imageFiles/emojis/0606.svg @@ -0,0 +1,42 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0607.svg b/resources/imageFiles/emojis/0607.svg new file mode 100644 index 0000000..04fc32d --- /dev/null +++ b/resources/imageFiles/emojis/0607.svg @@ -0,0 +1,45 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0608.svg b/resources/imageFiles/emojis/0608.svg new file mode 100644 index 0000000..3838ccc --- /dev/null +++ b/resources/imageFiles/emojis/0608.svg @@ -0,0 +1,45 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0609.svg b/resources/imageFiles/emojis/0609.svg new file mode 100644 index 0000000..e8e8157 --- /dev/null +++ b/resources/imageFiles/emojis/0609.svg @@ -0,0 +1,45 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0610.svg b/resources/imageFiles/emojis/0610.svg new file mode 100644 index 0000000..a0b97f5 --- /dev/null +++ b/resources/imageFiles/emojis/0610.svg @@ -0,0 +1,45 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0611.svg b/resources/imageFiles/emojis/0611.svg new file mode 100644 index 0000000..4235bcd --- /dev/null +++ b/resources/imageFiles/emojis/0611.svg @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0612.svg b/resources/imageFiles/emojis/0612.svg new file mode 100644 index 0000000..17830c8 --- /dev/null +++ b/resources/imageFiles/emojis/0612.svg @@ -0,0 +1,33 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0613.svg b/resources/imageFiles/emojis/0613.svg new file mode 100644 index 0000000..33d1821 --- /dev/null +++ b/resources/imageFiles/emojis/0613.svg @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0614.svg b/resources/imageFiles/emojis/0614.svg new file mode 100644 index 0000000..3495a4f --- /dev/null +++ b/resources/imageFiles/emojis/0614.svg @@ -0,0 +1,40 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0615.svg b/resources/imageFiles/emojis/0615.svg new file mode 100644 index 0000000..9777c1a --- /dev/null +++ b/resources/imageFiles/emojis/0615.svg @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0616.svg b/resources/imageFiles/emojis/0616.svg new file mode 100644 index 0000000..18b689f --- /dev/null +++ b/resources/imageFiles/emojis/0616.svg @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0617.svg b/resources/imageFiles/emojis/0617.svg new file mode 100644 index 0000000..a3b66b7 --- /dev/null +++ b/resources/imageFiles/emojis/0617.svg @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0618.svg b/resources/imageFiles/emojis/0618.svg new file mode 100644 index 0000000..c14d2f5 --- /dev/null +++ b/resources/imageFiles/emojis/0618.svg @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0619.svg b/resources/imageFiles/emojis/0619.svg new file mode 100644 index 0000000..fa7bb75 --- /dev/null +++ b/resources/imageFiles/emojis/0619.svg @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0620.svg b/resources/imageFiles/emojis/0620.svg new file mode 100644 index 0000000..dde4ac6 --- /dev/null +++ b/resources/imageFiles/emojis/0620.svg @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0621.svg b/resources/imageFiles/emojis/0621.svg new file mode 100644 index 0000000..a943dff --- /dev/null +++ b/resources/imageFiles/emojis/0621.svg @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0622.svg b/resources/imageFiles/emojis/0622.svg new file mode 100644 index 0000000..e81f2eb --- /dev/null +++ b/resources/imageFiles/emojis/0622.svg @@ -0,0 +1,52 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0623.svg b/resources/imageFiles/emojis/0623.svg new file mode 100644 index 0000000..79865b8 --- /dev/null +++ b/resources/imageFiles/emojis/0623.svg @@ -0,0 +1,63 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0624.svg b/resources/imageFiles/emojis/0624.svg new file mode 100644 index 0000000..b343bcd --- /dev/null +++ b/resources/imageFiles/emojis/0624.svg @@ -0,0 +1,33 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0625.svg b/resources/imageFiles/emojis/0625.svg new file mode 100644 index 0000000..038f668 --- /dev/null +++ b/resources/imageFiles/emojis/0625.svg @@ -0,0 +1,33 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0626.svg b/resources/imageFiles/emojis/0626.svg new file mode 100644 index 0000000..616ba64 --- /dev/null +++ b/resources/imageFiles/emojis/0626.svg @@ -0,0 +1,33 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0627.svg b/resources/imageFiles/emojis/0627.svg new file mode 100644 index 0000000..5eb79fd --- /dev/null +++ b/resources/imageFiles/emojis/0627.svg @@ -0,0 +1,33 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0628.svg b/resources/imageFiles/emojis/0628.svg new file mode 100644 index 0000000..8536e74 --- /dev/null +++ b/resources/imageFiles/emojis/0628.svg @@ -0,0 +1,33 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0629.svg b/resources/imageFiles/emojis/0629.svg new file mode 100644 index 0000000..6b09f7c --- /dev/null +++ b/resources/imageFiles/emojis/0629.svg @@ -0,0 +1,33 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0630.svg b/resources/imageFiles/emojis/0630.svg new file mode 100644 index 0000000..442b0fa --- /dev/null +++ b/resources/imageFiles/emojis/0630.svg @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0631.svg b/resources/imageFiles/emojis/0631.svg new file mode 100644 index 0000000..a5af21e --- /dev/null +++ b/resources/imageFiles/emojis/0631.svg @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0632.svg b/resources/imageFiles/emojis/0632.svg new file mode 100644 index 0000000..3bb142f --- /dev/null +++ b/resources/imageFiles/emojis/0632.svg @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0633.svg b/resources/imageFiles/emojis/0633.svg new file mode 100644 index 0000000..0587192 --- /dev/null +++ b/resources/imageFiles/emojis/0633.svg @@ -0,0 +1,47 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0634.svg b/resources/imageFiles/emojis/0634.svg new file mode 100644 index 0000000..863db16 --- /dev/null +++ b/resources/imageFiles/emojis/0634.svg @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0635.svg b/resources/imageFiles/emojis/0635.svg new file mode 100644 index 0000000..d6f9afb --- /dev/null +++ b/resources/imageFiles/emojis/0635.svg @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0636.svg b/resources/imageFiles/emojis/0636.svg new file mode 100644 index 0000000..b44a642 --- /dev/null +++ b/resources/imageFiles/emojis/0636.svg @@ -0,0 +1,47 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0637.svg b/resources/imageFiles/emojis/0637.svg new file mode 100644 index 0000000..2754d33 --- /dev/null +++ b/resources/imageFiles/emojis/0637.svg @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0638.svg b/resources/imageFiles/emojis/0638.svg new file mode 100644 index 0000000..fc98fe4 --- /dev/null +++ b/resources/imageFiles/emojis/0638.svg @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0639.svg b/resources/imageFiles/emojis/0639.svg new file mode 100644 index 0000000..4a71c43 --- /dev/null +++ b/resources/imageFiles/emojis/0639.svg @@ -0,0 +1,47 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0640.svg b/resources/imageFiles/emojis/0640.svg new file mode 100644 index 0000000..f0f1356 --- /dev/null +++ b/resources/imageFiles/emojis/0640.svg @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0641.svg b/resources/imageFiles/emojis/0641.svg new file mode 100644 index 0000000..1540fe0 --- /dev/null +++ b/resources/imageFiles/emojis/0641.svg @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0642.svg b/resources/imageFiles/emojis/0642.svg new file mode 100644 index 0000000..8744969 --- /dev/null +++ b/resources/imageFiles/emojis/0642.svg @@ -0,0 +1,47 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0643.svg b/resources/imageFiles/emojis/0643.svg new file mode 100644 index 0000000..1e0edfc --- /dev/null +++ b/resources/imageFiles/emojis/0643.svg @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0644.svg b/resources/imageFiles/emojis/0644.svg new file mode 100644 index 0000000..fd4a3f2 --- /dev/null +++ b/resources/imageFiles/emojis/0644.svg @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0645.svg b/resources/imageFiles/emojis/0645.svg new file mode 100644 index 0000000..ab5e1b0 --- /dev/null +++ b/resources/imageFiles/emojis/0645.svg @@ -0,0 +1,47 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0646.svg b/resources/imageFiles/emojis/0646.svg new file mode 100644 index 0000000..2db0008 --- /dev/null +++ b/resources/imageFiles/emojis/0646.svg @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0647.svg b/resources/imageFiles/emojis/0647.svg new file mode 100644 index 0000000..21b7588 --- /dev/null +++ b/resources/imageFiles/emojis/0647.svg @@ -0,0 +1,47 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0648.svg b/resources/imageFiles/emojis/0648.svg new file mode 100644 index 0000000..b51b23d --- /dev/null +++ b/resources/imageFiles/emojis/0648.svg @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0649.svg b/resources/imageFiles/emojis/0649.svg new file mode 100644 index 0000000..b4f2287 --- /dev/null +++ b/resources/imageFiles/emojis/0649.svg @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0650.svg b/resources/imageFiles/emojis/0650.svg new file mode 100644 index 0000000..d7a1d45 --- /dev/null +++ b/resources/imageFiles/emojis/0650.svg @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0651.svg b/resources/imageFiles/emojis/0651.svg new file mode 100644 index 0000000..1804b93 --- /dev/null +++ b/resources/imageFiles/emojis/0651.svg @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0652.svg b/resources/imageFiles/emojis/0652.svg new file mode 100644 index 0000000..63c6548 --- /dev/null +++ b/resources/imageFiles/emojis/0652.svg @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0653.svg b/resources/imageFiles/emojis/0653.svg new file mode 100644 index 0000000..7adba54 --- /dev/null +++ b/resources/imageFiles/emojis/0653.svg @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0654.svg b/resources/imageFiles/emojis/0654.svg new file mode 100644 index 0000000..10185b8 --- /dev/null +++ b/resources/imageFiles/emojis/0654.svg @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0655.svg b/resources/imageFiles/emojis/0655.svg new file mode 100644 index 0000000..acaa981 --- /dev/null +++ b/resources/imageFiles/emojis/0655.svg @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0656.svg b/resources/imageFiles/emojis/0656.svg new file mode 100644 index 0000000..557d209 --- /dev/null +++ b/resources/imageFiles/emojis/0656.svg @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0657.svg b/resources/imageFiles/emojis/0657.svg new file mode 100644 index 0000000..c0e8cc0 --- /dev/null +++ b/resources/imageFiles/emojis/0657.svg @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0658.svg b/resources/imageFiles/emojis/0658.svg new file mode 100644 index 0000000..e954376 --- /dev/null +++ b/resources/imageFiles/emojis/0658.svg @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0659.svg b/resources/imageFiles/emojis/0659.svg new file mode 100644 index 0000000..23ee6c2 --- /dev/null +++ b/resources/imageFiles/emojis/0659.svg @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0660.svg b/resources/imageFiles/emojis/0660.svg new file mode 100644 index 0000000..6b590fd --- /dev/null +++ b/resources/imageFiles/emojis/0660.svg @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0661.svg b/resources/imageFiles/emojis/0661.svg new file mode 100644 index 0000000..52ac47d --- /dev/null +++ b/resources/imageFiles/emojis/0661.svg @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0662.svg b/resources/imageFiles/emojis/0662.svg new file mode 100644 index 0000000..6603910 --- /dev/null +++ b/resources/imageFiles/emojis/0662.svg @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0663.svg b/resources/imageFiles/emojis/0663.svg new file mode 100644 index 0000000..1d79916 --- /dev/null +++ b/resources/imageFiles/emojis/0663.svg @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0664.svg b/resources/imageFiles/emojis/0664.svg new file mode 100644 index 0000000..04f8f70 --- /dev/null +++ b/resources/imageFiles/emojis/0664.svg @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0665.svg b/resources/imageFiles/emojis/0665.svg new file mode 100644 index 0000000..c71afa1 --- /dev/null +++ b/resources/imageFiles/emojis/0665.svg @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0666.svg b/resources/imageFiles/emojis/0666.svg new file mode 100644 index 0000000..a1b08d1 --- /dev/null +++ b/resources/imageFiles/emojis/0666.svg @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0667.svg b/resources/imageFiles/emojis/0667.svg new file mode 100644 index 0000000..9a76079 --- /dev/null +++ b/resources/imageFiles/emojis/0667.svg @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0668.svg b/resources/imageFiles/emojis/0668.svg new file mode 100644 index 0000000..f0091da --- /dev/null +++ b/resources/imageFiles/emojis/0668.svg @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0669.svg b/resources/imageFiles/emojis/0669.svg new file mode 100644 index 0000000..7be24aa --- /dev/null +++ b/resources/imageFiles/emojis/0669.svg @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0670.svg b/resources/imageFiles/emojis/0670.svg new file mode 100644 index 0000000..1d99b22 --- /dev/null +++ b/resources/imageFiles/emojis/0670.svg @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0671.svg b/resources/imageFiles/emojis/0671.svg new file mode 100644 index 0000000..646e441 --- /dev/null +++ b/resources/imageFiles/emojis/0671.svg @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0672.svg b/resources/imageFiles/emojis/0672.svg new file mode 100644 index 0000000..8122404 --- /dev/null +++ b/resources/imageFiles/emojis/0672.svg @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0673.svg b/resources/imageFiles/emojis/0673.svg new file mode 100644 index 0000000..8383e38 --- /dev/null +++ b/resources/imageFiles/emojis/0673.svg @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0674.svg b/resources/imageFiles/emojis/0674.svg new file mode 100644 index 0000000..2aa705b --- /dev/null +++ b/resources/imageFiles/emojis/0674.svg @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0675.svg b/resources/imageFiles/emojis/0675.svg new file mode 100644 index 0000000..4496d33 --- /dev/null +++ b/resources/imageFiles/emojis/0675.svg @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0676.svg b/resources/imageFiles/emojis/0676.svg new file mode 100644 index 0000000..9ed2bde --- /dev/null +++ b/resources/imageFiles/emojis/0676.svg @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0677.svg b/resources/imageFiles/emojis/0677.svg new file mode 100644 index 0000000..9fac5a2 --- /dev/null +++ b/resources/imageFiles/emojis/0677.svg @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0678.svg b/resources/imageFiles/emojis/0678.svg new file mode 100644 index 0000000..4b560e9 --- /dev/null +++ b/resources/imageFiles/emojis/0678.svg @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0679.svg b/resources/imageFiles/emojis/0679.svg new file mode 100644 index 0000000..f05fa65 --- /dev/null +++ b/resources/imageFiles/emojis/0679.svg @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0680.svg b/resources/imageFiles/emojis/0680.svg new file mode 100644 index 0000000..152ab20 --- /dev/null +++ b/resources/imageFiles/emojis/0680.svg @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0681.svg b/resources/imageFiles/emojis/0681.svg new file mode 100644 index 0000000..9f5b077 --- /dev/null +++ b/resources/imageFiles/emojis/0681.svg @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0682.svg b/resources/imageFiles/emojis/0682.svg new file mode 100644 index 0000000..b4ca165 --- /dev/null +++ b/resources/imageFiles/emojis/0682.svg @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0683.svg b/resources/imageFiles/emojis/0683.svg new file mode 100644 index 0000000..72875b6 --- /dev/null +++ b/resources/imageFiles/emojis/0683.svg @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0684.svg b/resources/imageFiles/emojis/0684.svg new file mode 100644 index 0000000..c2ef216 --- /dev/null +++ b/resources/imageFiles/emojis/0684.svg @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0685.svg b/resources/imageFiles/emojis/0685.svg new file mode 100644 index 0000000..384c4aa --- /dev/null +++ b/resources/imageFiles/emojis/0685.svg @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0686.svg b/resources/imageFiles/emojis/0686.svg new file mode 100644 index 0000000..9a7e7a2 --- /dev/null +++ b/resources/imageFiles/emojis/0686.svg @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0687.svg b/resources/imageFiles/emojis/0687.svg new file mode 100644 index 0000000..4c49c53 --- /dev/null +++ b/resources/imageFiles/emojis/0687.svg @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0688.svg b/resources/imageFiles/emojis/0688.svg new file mode 100644 index 0000000..5058702 --- /dev/null +++ b/resources/imageFiles/emojis/0688.svg @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0689.svg b/resources/imageFiles/emojis/0689.svg new file mode 100644 index 0000000..c7784af --- /dev/null +++ b/resources/imageFiles/emojis/0689.svg @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0690.svg b/resources/imageFiles/emojis/0690.svg new file mode 100644 index 0000000..280a85a --- /dev/null +++ b/resources/imageFiles/emojis/0690.svg @@ -0,0 +1,39 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0691.svg b/resources/imageFiles/emojis/0691.svg new file mode 100644 index 0000000..934e9a2 --- /dev/null +++ b/resources/imageFiles/emojis/0691.svg @@ -0,0 +1,39 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0692.svg b/resources/imageFiles/emojis/0692.svg new file mode 100644 index 0000000..4a754e0 --- /dev/null +++ b/resources/imageFiles/emojis/0692.svg @@ -0,0 +1,49 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0693.svg b/resources/imageFiles/emojis/0693.svg new file mode 100644 index 0000000..e4cdbc6 --- /dev/null +++ b/resources/imageFiles/emojis/0693.svg @@ -0,0 +1,43 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0694.svg b/resources/imageFiles/emojis/0694.svg new file mode 100644 index 0000000..29822f7 --- /dev/null +++ b/resources/imageFiles/emojis/0694.svg @@ -0,0 +1,39 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0695.svg b/resources/imageFiles/emojis/0695.svg new file mode 100644 index 0000000..273c074 --- /dev/null +++ b/resources/imageFiles/emojis/0695.svg @@ -0,0 +1,49 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0696.svg b/resources/imageFiles/emojis/0696.svg new file mode 100644 index 0000000..cfe801f --- /dev/null +++ b/resources/imageFiles/emojis/0696.svg @@ -0,0 +1,43 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0697.svg b/resources/imageFiles/emojis/0697.svg new file mode 100644 index 0000000..253b9b9 --- /dev/null +++ b/resources/imageFiles/emojis/0697.svg @@ -0,0 +1,39 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0698.svg b/resources/imageFiles/emojis/0698.svg new file mode 100644 index 0000000..4ab87f2 --- /dev/null +++ b/resources/imageFiles/emojis/0698.svg @@ -0,0 +1,49 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0699.svg b/resources/imageFiles/emojis/0699.svg new file mode 100644 index 0000000..e17f6cc --- /dev/null +++ b/resources/imageFiles/emojis/0699.svg @@ -0,0 +1,43 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0700.svg b/resources/imageFiles/emojis/0700.svg new file mode 100644 index 0000000..ccde7e6 --- /dev/null +++ b/resources/imageFiles/emojis/0700.svg @@ -0,0 +1,39 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0701.svg b/resources/imageFiles/emojis/0701.svg new file mode 100644 index 0000000..34ab758 --- /dev/null +++ b/resources/imageFiles/emojis/0701.svg @@ -0,0 +1,49 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0702.svg b/resources/imageFiles/emojis/0702.svg new file mode 100644 index 0000000..6f6552c --- /dev/null +++ b/resources/imageFiles/emojis/0702.svg @@ -0,0 +1,43 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0703.svg b/resources/imageFiles/emojis/0703.svg new file mode 100644 index 0000000..6c0732f --- /dev/null +++ b/resources/imageFiles/emojis/0703.svg @@ -0,0 +1,39 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0704.svg b/resources/imageFiles/emojis/0704.svg new file mode 100644 index 0000000..064da65 --- /dev/null +++ b/resources/imageFiles/emojis/0704.svg @@ -0,0 +1,49 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0705.svg b/resources/imageFiles/emojis/0705.svg new file mode 100644 index 0000000..80997b4 --- /dev/null +++ b/resources/imageFiles/emojis/0705.svg @@ -0,0 +1,43 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0706.svg b/resources/imageFiles/emojis/0706.svg new file mode 100644 index 0000000..917254f --- /dev/null +++ b/resources/imageFiles/emojis/0706.svg @@ -0,0 +1,49 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0707.svg b/resources/imageFiles/emojis/0707.svg new file mode 100644 index 0000000..067e2f4 --- /dev/null +++ b/resources/imageFiles/emojis/0707.svg @@ -0,0 +1,43 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0708.svg b/resources/imageFiles/emojis/0708.svg new file mode 100644 index 0000000..c459a27 --- /dev/null +++ b/resources/imageFiles/emojis/0708.svg @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0709.svg b/resources/imageFiles/emojis/0709.svg new file mode 100644 index 0000000..b97ecf2 --- /dev/null +++ b/resources/imageFiles/emojis/0709.svg @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0710.svg b/resources/imageFiles/emojis/0710.svg new file mode 100644 index 0000000..15b2a45 --- /dev/null +++ b/resources/imageFiles/emojis/0710.svg @@ -0,0 +1,32 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0711.svg b/resources/imageFiles/emojis/0711.svg new file mode 100644 index 0000000..3636f14 --- /dev/null +++ b/resources/imageFiles/emojis/0711.svg @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0712.svg b/resources/imageFiles/emojis/0712.svg new file mode 100644 index 0000000..acd6de1 --- /dev/null +++ b/resources/imageFiles/emojis/0712.svg @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0713.svg b/resources/imageFiles/emojis/0713.svg new file mode 100644 index 0000000..750329c --- /dev/null +++ b/resources/imageFiles/emojis/0713.svg @@ -0,0 +1,32 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0714.svg b/resources/imageFiles/emojis/0714.svg new file mode 100644 index 0000000..8f781b6 --- /dev/null +++ b/resources/imageFiles/emojis/0714.svg @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0715.svg b/resources/imageFiles/emojis/0715.svg new file mode 100644 index 0000000..30bb52f --- /dev/null +++ b/resources/imageFiles/emojis/0715.svg @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0716.svg b/resources/imageFiles/emojis/0716.svg new file mode 100644 index 0000000..2cfc5ef --- /dev/null +++ b/resources/imageFiles/emojis/0716.svg @@ -0,0 +1,32 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0717.svg b/resources/imageFiles/emojis/0717.svg new file mode 100644 index 0000000..f970b7c --- /dev/null +++ b/resources/imageFiles/emojis/0717.svg @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0718.svg b/resources/imageFiles/emojis/0718.svg new file mode 100644 index 0000000..2dedcce --- /dev/null +++ b/resources/imageFiles/emojis/0718.svg @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0719.svg b/resources/imageFiles/emojis/0719.svg new file mode 100644 index 0000000..e587b84 --- /dev/null +++ b/resources/imageFiles/emojis/0719.svg @@ -0,0 +1,32 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0720.svg b/resources/imageFiles/emojis/0720.svg new file mode 100644 index 0000000..08ab05f --- /dev/null +++ b/resources/imageFiles/emojis/0720.svg @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0721.svg b/resources/imageFiles/emojis/0721.svg new file mode 100644 index 0000000..17665ce --- /dev/null +++ b/resources/imageFiles/emojis/0721.svg @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0722.svg b/resources/imageFiles/emojis/0722.svg new file mode 100644 index 0000000..cf8f0cd --- /dev/null +++ b/resources/imageFiles/emojis/0722.svg @@ -0,0 +1,32 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0723.svg b/resources/imageFiles/emojis/0723.svg new file mode 100644 index 0000000..d74d600 --- /dev/null +++ b/resources/imageFiles/emojis/0723.svg @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0724.svg b/resources/imageFiles/emojis/0724.svg new file mode 100644 index 0000000..01d0be5 --- /dev/null +++ b/resources/imageFiles/emojis/0724.svg @@ -0,0 +1,32 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0725.svg b/resources/imageFiles/emojis/0725.svg new file mode 100644 index 0000000..16b996a --- /dev/null +++ b/resources/imageFiles/emojis/0725.svg @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0726.svg b/resources/imageFiles/emojis/0726.svg new file mode 100644 index 0000000..19567b4 --- /dev/null +++ b/resources/imageFiles/emojis/0726.svg @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0727.svg b/resources/imageFiles/emojis/0727.svg new file mode 100644 index 0000000..7648878 --- /dev/null +++ b/resources/imageFiles/emojis/0727.svg @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0728.svg b/resources/imageFiles/emojis/0728.svg new file mode 100644 index 0000000..fa2537c --- /dev/null +++ b/resources/imageFiles/emojis/0728.svg @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0729.svg b/resources/imageFiles/emojis/0729.svg new file mode 100644 index 0000000..dac65b4 --- /dev/null +++ b/resources/imageFiles/emojis/0729.svg @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0730.svg b/resources/imageFiles/emojis/0730.svg new file mode 100644 index 0000000..cf17de5 --- /dev/null +++ b/resources/imageFiles/emojis/0730.svg @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0731.svg b/resources/imageFiles/emojis/0731.svg new file mode 100644 index 0000000..7d47612 --- /dev/null +++ b/resources/imageFiles/emojis/0731.svg @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0732.svg b/resources/imageFiles/emojis/0732.svg new file mode 100644 index 0000000..4a3f15b --- /dev/null +++ b/resources/imageFiles/emojis/0732.svg @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0733.svg b/resources/imageFiles/emojis/0733.svg new file mode 100644 index 0000000..09bb592 --- /dev/null +++ b/resources/imageFiles/emojis/0733.svg @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0734.svg b/resources/imageFiles/emojis/0734.svg new file mode 100644 index 0000000..cd5be15 --- /dev/null +++ b/resources/imageFiles/emojis/0734.svg @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0735.svg b/resources/imageFiles/emojis/0735.svg new file mode 100644 index 0000000..99561c3 --- /dev/null +++ b/resources/imageFiles/emojis/0735.svg @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0736.svg b/resources/imageFiles/emojis/0736.svg new file mode 100644 index 0000000..3adb635 --- /dev/null +++ b/resources/imageFiles/emojis/0736.svg @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0737.svg b/resources/imageFiles/emojis/0737.svg new file mode 100644 index 0000000..9acaca8 --- /dev/null +++ b/resources/imageFiles/emojis/0737.svg @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0738.svg b/resources/imageFiles/emojis/0738.svg new file mode 100644 index 0000000..19bcab6 --- /dev/null +++ b/resources/imageFiles/emojis/0738.svg @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0739.svg b/resources/imageFiles/emojis/0739.svg new file mode 100644 index 0000000..c5e0a20 --- /dev/null +++ b/resources/imageFiles/emojis/0739.svg @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0740.svg b/resources/imageFiles/emojis/0740.svg new file mode 100644 index 0000000..b8f0390 --- /dev/null +++ b/resources/imageFiles/emojis/0740.svg @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0741.svg b/resources/imageFiles/emojis/0741.svg new file mode 100644 index 0000000..db146bf --- /dev/null +++ b/resources/imageFiles/emojis/0741.svg @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0742.svg b/resources/imageFiles/emojis/0742.svg new file mode 100644 index 0000000..c82bf33 --- /dev/null +++ b/resources/imageFiles/emojis/0742.svg @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0743.svg b/resources/imageFiles/emojis/0743.svg new file mode 100644 index 0000000..a7f7e91 --- /dev/null +++ b/resources/imageFiles/emojis/0743.svg @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0744.svg b/resources/imageFiles/emojis/0744.svg new file mode 100644 index 0000000..1101a0d --- /dev/null +++ b/resources/imageFiles/emojis/0744.svg @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0745.svg b/resources/imageFiles/emojis/0745.svg new file mode 100644 index 0000000..2a706a9 --- /dev/null +++ b/resources/imageFiles/emojis/0745.svg @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0746.svg b/resources/imageFiles/emojis/0746.svg new file mode 100644 index 0000000..d395ca4 --- /dev/null +++ b/resources/imageFiles/emojis/0746.svg @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0747.svg b/resources/imageFiles/emojis/0747.svg new file mode 100644 index 0000000..4d104fa --- /dev/null +++ b/resources/imageFiles/emojis/0747.svg @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0748.svg b/resources/imageFiles/emojis/0748.svg new file mode 100644 index 0000000..ded4e79 --- /dev/null +++ b/resources/imageFiles/emojis/0748.svg @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0749.svg b/resources/imageFiles/emojis/0749.svg new file mode 100644 index 0000000..781e15e --- /dev/null +++ b/resources/imageFiles/emojis/0749.svg @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0750.svg b/resources/imageFiles/emojis/0750.svg new file mode 100644 index 0000000..09f415e --- /dev/null +++ b/resources/imageFiles/emojis/0750.svg @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0751.svg b/resources/imageFiles/emojis/0751.svg new file mode 100644 index 0000000..2e5f283 --- /dev/null +++ b/resources/imageFiles/emojis/0751.svg @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0752.svg b/resources/imageFiles/emojis/0752.svg new file mode 100644 index 0000000..86468f1 --- /dev/null +++ b/resources/imageFiles/emojis/0752.svg @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0753.svg b/resources/imageFiles/emojis/0753.svg new file mode 100644 index 0000000..80c9ea9 --- /dev/null +++ b/resources/imageFiles/emojis/0753.svg @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0754.svg b/resources/imageFiles/emojis/0754.svg new file mode 100644 index 0000000..23bca51 --- /dev/null +++ b/resources/imageFiles/emojis/0754.svg @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0755.svg b/resources/imageFiles/emojis/0755.svg new file mode 100644 index 0000000..1c8a192 --- /dev/null +++ b/resources/imageFiles/emojis/0755.svg @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0756.svg b/resources/imageFiles/emojis/0756.svg new file mode 100644 index 0000000..c68662c --- /dev/null +++ b/resources/imageFiles/emojis/0756.svg @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0757.svg b/resources/imageFiles/emojis/0757.svg new file mode 100644 index 0000000..15fe86a --- /dev/null +++ b/resources/imageFiles/emojis/0757.svg @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0758.svg b/resources/imageFiles/emojis/0758.svg new file mode 100644 index 0000000..71198cb --- /dev/null +++ b/resources/imageFiles/emojis/0758.svg @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0759.svg b/resources/imageFiles/emojis/0759.svg new file mode 100644 index 0000000..009b15e --- /dev/null +++ b/resources/imageFiles/emojis/0759.svg @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0760.svg b/resources/imageFiles/emojis/0760.svg new file mode 100644 index 0000000..4eab306 --- /dev/null +++ b/resources/imageFiles/emojis/0760.svg @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0761.svg b/resources/imageFiles/emojis/0761.svg new file mode 100644 index 0000000..1b26a18 --- /dev/null +++ b/resources/imageFiles/emojis/0761.svg @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0762.svg b/resources/imageFiles/emojis/0762.svg new file mode 100644 index 0000000..b6e2262 --- /dev/null +++ b/resources/imageFiles/emojis/0762.svg @@ -0,0 +1,32 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0763.svg b/resources/imageFiles/emojis/0763.svg new file mode 100644 index 0000000..2a71da7 --- /dev/null +++ b/resources/imageFiles/emojis/0763.svg @@ -0,0 +1,32 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0764.svg b/resources/imageFiles/emojis/0764.svg new file mode 100644 index 0000000..d6500f8 --- /dev/null +++ b/resources/imageFiles/emojis/0764.svg @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0765.svg b/resources/imageFiles/emojis/0765.svg new file mode 100644 index 0000000..d8979d2 --- /dev/null +++ b/resources/imageFiles/emojis/0765.svg @@ -0,0 +1,40 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0766.svg b/resources/imageFiles/emojis/0766.svg new file mode 100644 index 0000000..b37c340 --- /dev/null +++ b/resources/imageFiles/emojis/0766.svg @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0767.svg b/resources/imageFiles/emojis/0767.svg new file mode 100644 index 0000000..7300c84 --- /dev/null +++ b/resources/imageFiles/emojis/0767.svg @@ -0,0 +1,38 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0768.svg b/resources/imageFiles/emojis/0768.svg new file mode 100644 index 0000000..d65ebae --- /dev/null +++ b/resources/imageFiles/emojis/0768.svg @@ -0,0 +1,44 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0769.svg b/resources/imageFiles/emojis/0769.svg new file mode 100644 index 0000000..b9217f6 --- /dev/null +++ b/resources/imageFiles/emojis/0769.svg @@ -0,0 +1,32 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0770.svg b/resources/imageFiles/emojis/0770.svg new file mode 100644 index 0000000..a3e7587 --- /dev/null +++ b/resources/imageFiles/emojis/0770.svg @@ -0,0 +1,40 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0771.svg b/resources/imageFiles/emojis/0771.svg new file mode 100644 index 0000000..9261211 --- /dev/null +++ b/resources/imageFiles/emojis/0771.svg @@ -0,0 +1,31 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0772.svg b/resources/imageFiles/emojis/0772.svg new file mode 100644 index 0000000..72b7613 --- /dev/null +++ b/resources/imageFiles/emojis/0772.svg @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0773.svg b/resources/imageFiles/emojis/0773.svg new file mode 100644 index 0000000..61c76bb --- /dev/null +++ b/resources/imageFiles/emojis/0773.svg @@ -0,0 +1,43 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0774.svg b/resources/imageFiles/emojis/0774.svg new file mode 100644 index 0000000..fb40271 --- /dev/null +++ b/resources/imageFiles/emojis/0774.svg @@ -0,0 +1,32 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0775.svg b/resources/imageFiles/emojis/0775.svg new file mode 100644 index 0000000..0d68b6a --- /dev/null +++ b/resources/imageFiles/emojis/0775.svg @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0776.svg b/resources/imageFiles/emojis/0776.svg new file mode 100644 index 0000000..bcd1612 --- /dev/null +++ b/resources/imageFiles/emojis/0776.svg @@ -0,0 +1,32 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0777.svg b/resources/imageFiles/emojis/0777.svg new file mode 100644 index 0000000..d8c2944 --- /dev/null +++ b/resources/imageFiles/emojis/0777.svg @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0778.svg b/resources/imageFiles/emojis/0778.svg new file mode 100644 index 0000000..c52179c --- /dev/null +++ b/resources/imageFiles/emojis/0778.svg @@ -0,0 +1,31 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0779.svg b/resources/imageFiles/emojis/0779.svg new file mode 100644 index 0000000..d4679ae --- /dev/null +++ b/resources/imageFiles/emojis/0779.svg @@ -0,0 +1,32 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0780.svg b/resources/imageFiles/emojis/0780.svg new file mode 100644 index 0000000..6e40e1c --- /dev/null +++ b/resources/imageFiles/emojis/0780.svg @@ -0,0 +1,46 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0781.svg b/resources/imageFiles/emojis/0781.svg new file mode 100644 index 0000000..57fa2e4 --- /dev/null +++ b/resources/imageFiles/emojis/0781.svg @@ -0,0 +1,33 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0782.svg b/resources/imageFiles/emojis/0782.svg new file mode 100644 index 0000000..e75d19a --- /dev/null +++ b/resources/imageFiles/emojis/0782.svg @@ -0,0 +1,42 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0783.svg b/resources/imageFiles/emojis/0783.svg new file mode 100644 index 0000000..6b28501 --- /dev/null +++ b/resources/imageFiles/emojis/0783.svg @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0784.svg b/resources/imageFiles/emojis/0784.svg new file mode 100644 index 0000000..ca5234f --- /dev/null +++ b/resources/imageFiles/emojis/0784.svg @@ -0,0 +1,33 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0785.svg b/resources/imageFiles/emojis/0785.svg new file mode 100644 index 0000000..67374e0 --- /dev/null +++ b/resources/imageFiles/emojis/0785.svg @@ -0,0 +1,31 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0786.svg b/resources/imageFiles/emojis/0786.svg new file mode 100644 index 0000000..6963112 --- /dev/null +++ b/resources/imageFiles/emojis/0786.svg @@ -0,0 +1,33 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0787.svg b/resources/imageFiles/emojis/0787.svg new file mode 100644 index 0000000..065cf4f --- /dev/null +++ b/resources/imageFiles/emojis/0787.svg @@ -0,0 +1,34 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0788.svg b/resources/imageFiles/emojis/0788.svg new file mode 100644 index 0000000..9d0bea3 --- /dev/null +++ b/resources/imageFiles/emojis/0788.svg @@ -0,0 +1,41 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0789.svg b/resources/imageFiles/emojis/0789.svg new file mode 100644 index 0000000..07f898e --- /dev/null +++ b/resources/imageFiles/emojis/0789.svg @@ -0,0 +1,43 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0790.svg b/resources/imageFiles/emojis/0790.svg new file mode 100644 index 0000000..c48e79e --- /dev/null +++ b/resources/imageFiles/emojis/0790.svg @@ -0,0 +1,31 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0791.svg b/resources/imageFiles/emojis/0791.svg new file mode 100644 index 0000000..b147fd0 --- /dev/null +++ b/resources/imageFiles/emojis/0791.svg @@ -0,0 +1,32 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0792.svg b/resources/imageFiles/emojis/0792.svg new file mode 100644 index 0000000..ec2adc3 --- /dev/null +++ b/resources/imageFiles/emojis/0792.svg @@ -0,0 +1,45 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0793.svg b/resources/imageFiles/emojis/0793.svg new file mode 100644 index 0000000..4578ccb --- /dev/null +++ b/resources/imageFiles/emojis/0793.svg @@ -0,0 +1,32 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0794.svg b/resources/imageFiles/emojis/0794.svg new file mode 100644 index 0000000..d9352b0 --- /dev/null +++ b/resources/imageFiles/emojis/0794.svg @@ -0,0 +1,46 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0795.svg b/resources/imageFiles/emojis/0795.svg new file mode 100644 index 0000000..36df705 --- /dev/null +++ b/resources/imageFiles/emojis/0795.svg @@ -0,0 +1,33 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0796.svg b/resources/imageFiles/emojis/0796.svg new file mode 100644 index 0000000..80e7de1 --- /dev/null +++ b/resources/imageFiles/emojis/0796.svg @@ -0,0 +1,42 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0797.svg b/resources/imageFiles/emojis/0797.svg new file mode 100644 index 0000000..c669949 --- /dev/null +++ b/resources/imageFiles/emojis/0797.svg @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0798.svg b/resources/imageFiles/emojis/0798.svg new file mode 100644 index 0000000..cfebca8 --- /dev/null +++ b/resources/imageFiles/emojis/0798.svg @@ -0,0 +1,49 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0799.svg b/resources/imageFiles/emojis/0799.svg new file mode 100644 index 0000000..e15f670 --- /dev/null +++ b/resources/imageFiles/emojis/0799.svg @@ -0,0 +1,33 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0800.svg b/resources/imageFiles/emojis/0800.svg new file mode 100644 index 0000000..f9cb11d --- /dev/null +++ b/resources/imageFiles/emojis/0800.svg @@ -0,0 +1,31 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0801.svg b/resources/imageFiles/emojis/0801.svg new file mode 100644 index 0000000..d268e45 --- /dev/null +++ b/resources/imageFiles/emojis/0801.svg @@ -0,0 +1,33 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0802.svg b/resources/imageFiles/emojis/0802.svg new file mode 100644 index 0000000..eef8b20 --- /dev/null +++ b/resources/imageFiles/emojis/0802.svg @@ -0,0 +1,34 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0803.svg b/resources/imageFiles/emojis/0803.svg new file mode 100644 index 0000000..cd05dbf --- /dev/null +++ b/resources/imageFiles/emojis/0803.svg @@ -0,0 +1,41 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0804.svg b/resources/imageFiles/emojis/0804.svg new file mode 100644 index 0000000..7f410ec --- /dev/null +++ b/resources/imageFiles/emojis/0804.svg @@ -0,0 +1,43 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0805.svg b/resources/imageFiles/emojis/0805.svg new file mode 100644 index 0000000..b95fa35 --- /dev/null +++ b/resources/imageFiles/emojis/0805.svg @@ -0,0 +1,31 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0806.svg b/resources/imageFiles/emojis/0806.svg new file mode 100644 index 0000000..af2fb9c --- /dev/null +++ b/resources/imageFiles/emojis/0806.svg @@ -0,0 +1,32 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0807.svg b/resources/imageFiles/emojis/0807.svg new file mode 100644 index 0000000..f1b2eb8 --- /dev/null +++ b/resources/imageFiles/emojis/0807.svg @@ -0,0 +1,45 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0808.svg b/resources/imageFiles/emojis/0808.svg new file mode 100644 index 0000000..0230b4b --- /dev/null +++ b/resources/imageFiles/emojis/0808.svg @@ -0,0 +1,32 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0809.svg b/resources/imageFiles/emojis/0809.svg new file mode 100644 index 0000000..c97ca51 --- /dev/null +++ b/resources/imageFiles/emojis/0809.svg @@ -0,0 +1,46 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0810.svg b/resources/imageFiles/emojis/0810.svg new file mode 100644 index 0000000..1acc09e --- /dev/null +++ b/resources/imageFiles/emojis/0810.svg @@ -0,0 +1,33 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0811.svg b/resources/imageFiles/emojis/0811.svg new file mode 100644 index 0000000..19333cc --- /dev/null +++ b/resources/imageFiles/emojis/0811.svg @@ -0,0 +1,42 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0812.svg b/resources/imageFiles/emojis/0812.svg new file mode 100644 index 0000000..599c1b9 --- /dev/null +++ b/resources/imageFiles/emojis/0812.svg @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0813.svg b/resources/imageFiles/emojis/0813.svg new file mode 100644 index 0000000..6daeb7a --- /dev/null +++ b/resources/imageFiles/emojis/0813.svg @@ -0,0 +1,49 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0814.svg b/resources/imageFiles/emojis/0814.svg new file mode 100644 index 0000000..2114dff --- /dev/null +++ b/resources/imageFiles/emojis/0814.svg @@ -0,0 +1,33 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0815.svg b/resources/imageFiles/emojis/0815.svg new file mode 100644 index 0000000..f7cc1c8 --- /dev/null +++ b/resources/imageFiles/emojis/0815.svg @@ -0,0 +1,31 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0816.svg b/resources/imageFiles/emojis/0816.svg new file mode 100644 index 0000000..ed84368 --- /dev/null +++ b/resources/imageFiles/emojis/0816.svg @@ -0,0 +1,33 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0817.svg b/resources/imageFiles/emojis/0817.svg new file mode 100644 index 0000000..d3ee4f3 --- /dev/null +++ b/resources/imageFiles/emojis/0817.svg @@ -0,0 +1,34 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0818.svg b/resources/imageFiles/emojis/0818.svg new file mode 100644 index 0000000..14a7764 --- /dev/null +++ b/resources/imageFiles/emojis/0818.svg @@ -0,0 +1,41 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0819.svg b/resources/imageFiles/emojis/0819.svg new file mode 100644 index 0000000..ccc12dd --- /dev/null +++ b/resources/imageFiles/emojis/0819.svg @@ -0,0 +1,43 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0820.svg b/resources/imageFiles/emojis/0820.svg new file mode 100644 index 0000000..4d10f2f --- /dev/null +++ b/resources/imageFiles/emojis/0820.svg @@ -0,0 +1,31 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0821.svg b/resources/imageFiles/emojis/0821.svg new file mode 100644 index 0000000..8524e53 --- /dev/null +++ b/resources/imageFiles/emojis/0821.svg @@ -0,0 +1,32 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0822.svg b/resources/imageFiles/emojis/0822.svg new file mode 100644 index 0000000..4aa1527 --- /dev/null +++ b/resources/imageFiles/emojis/0822.svg @@ -0,0 +1,45 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0823.svg b/resources/imageFiles/emojis/0823.svg new file mode 100644 index 0000000..9f81ae6 --- /dev/null +++ b/resources/imageFiles/emojis/0823.svg @@ -0,0 +1,32 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0824.svg b/resources/imageFiles/emojis/0824.svg new file mode 100644 index 0000000..85b6c3a --- /dev/null +++ b/resources/imageFiles/emojis/0824.svg @@ -0,0 +1,46 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0825.svg b/resources/imageFiles/emojis/0825.svg new file mode 100644 index 0000000..9202774 --- /dev/null +++ b/resources/imageFiles/emojis/0825.svg @@ -0,0 +1,33 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0826.svg b/resources/imageFiles/emojis/0826.svg new file mode 100644 index 0000000..783e2bb --- /dev/null +++ b/resources/imageFiles/emojis/0826.svg @@ -0,0 +1,42 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0827.svg b/resources/imageFiles/emojis/0827.svg new file mode 100644 index 0000000..cf53e36 --- /dev/null +++ b/resources/imageFiles/emojis/0827.svg @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0828.svg b/resources/imageFiles/emojis/0828.svg new file mode 100644 index 0000000..dd5c588 --- /dev/null +++ b/resources/imageFiles/emojis/0828.svg @@ -0,0 +1,49 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0829.svg b/resources/imageFiles/emojis/0829.svg new file mode 100644 index 0000000..b2bf5e4 --- /dev/null +++ b/resources/imageFiles/emojis/0829.svg @@ -0,0 +1,33 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0830.svg b/resources/imageFiles/emojis/0830.svg new file mode 100644 index 0000000..711524a --- /dev/null +++ b/resources/imageFiles/emojis/0830.svg @@ -0,0 +1,31 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0831.svg b/resources/imageFiles/emojis/0831.svg new file mode 100644 index 0000000..cfebe7b --- /dev/null +++ b/resources/imageFiles/emojis/0831.svg @@ -0,0 +1,33 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0832.svg b/resources/imageFiles/emojis/0832.svg new file mode 100644 index 0000000..2717199 --- /dev/null +++ b/resources/imageFiles/emojis/0832.svg @@ -0,0 +1,34 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0833.svg b/resources/imageFiles/emojis/0833.svg new file mode 100644 index 0000000..5458a66 --- /dev/null +++ b/resources/imageFiles/emojis/0833.svg @@ -0,0 +1,41 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0834.svg b/resources/imageFiles/emojis/0834.svg new file mode 100644 index 0000000..04f3938 --- /dev/null +++ b/resources/imageFiles/emojis/0834.svg @@ -0,0 +1,43 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0835.svg b/resources/imageFiles/emojis/0835.svg new file mode 100644 index 0000000..ed18f48 --- /dev/null +++ b/resources/imageFiles/emojis/0835.svg @@ -0,0 +1,31 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0836.svg b/resources/imageFiles/emojis/0836.svg new file mode 100644 index 0000000..3706dcd --- /dev/null +++ b/resources/imageFiles/emojis/0836.svg @@ -0,0 +1,32 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0837.svg b/resources/imageFiles/emojis/0837.svg new file mode 100644 index 0000000..181d28e --- /dev/null +++ b/resources/imageFiles/emojis/0837.svg @@ -0,0 +1,45 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0838.svg b/resources/imageFiles/emojis/0838.svg new file mode 100644 index 0000000..61e2489 --- /dev/null +++ b/resources/imageFiles/emojis/0838.svg @@ -0,0 +1,32 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0839.svg b/resources/imageFiles/emojis/0839.svg new file mode 100644 index 0000000..4e4d744 --- /dev/null +++ b/resources/imageFiles/emojis/0839.svg @@ -0,0 +1,46 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0840.svg b/resources/imageFiles/emojis/0840.svg new file mode 100644 index 0000000..5ae0667 --- /dev/null +++ b/resources/imageFiles/emojis/0840.svg @@ -0,0 +1,33 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0841.svg b/resources/imageFiles/emojis/0841.svg new file mode 100644 index 0000000..1c4d667 --- /dev/null +++ b/resources/imageFiles/emojis/0841.svg @@ -0,0 +1,42 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0842.svg b/resources/imageFiles/emojis/0842.svg new file mode 100644 index 0000000..fccdff7 --- /dev/null +++ b/resources/imageFiles/emojis/0842.svg @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0843.svg b/resources/imageFiles/emojis/0843.svg new file mode 100644 index 0000000..b14db2f --- /dev/null +++ b/resources/imageFiles/emojis/0843.svg @@ -0,0 +1,49 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0844.svg b/resources/imageFiles/emojis/0844.svg new file mode 100644 index 0000000..ab574bc --- /dev/null +++ b/resources/imageFiles/emojis/0844.svg @@ -0,0 +1,33 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0845.svg b/resources/imageFiles/emojis/0845.svg new file mode 100644 index 0000000..565952c --- /dev/null +++ b/resources/imageFiles/emojis/0845.svg @@ -0,0 +1,31 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0846.svg b/resources/imageFiles/emojis/0846.svg new file mode 100644 index 0000000..35c7697 --- /dev/null +++ b/resources/imageFiles/emojis/0846.svg @@ -0,0 +1,33 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0847.svg b/resources/imageFiles/emojis/0847.svg new file mode 100644 index 0000000..0962eeb --- /dev/null +++ b/resources/imageFiles/emojis/0847.svg @@ -0,0 +1,34 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0848.svg b/resources/imageFiles/emojis/0848.svg new file mode 100644 index 0000000..1261051 --- /dev/null +++ b/resources/imageFiles/emojis/0848.svg @@ -0,0 +1,41 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0849.svg b/resources/imageFiles/emojis/0849.svg new file mode 100644 index 0000000..98b8f31 --- /dev/null +++ b/resources/imageFiles/emojis/0849.svg @@ -0,0 +1,43 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0850.svg b/resources/imageFiles/emojis/0850.svg new file mode 100644 index 0000000..2353b09 --- /dev/null +++ b/resources/imageFiles/emojis/0850.svg @@ -0,0 +1,31 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0851.svg b/resources/imageFiles/emojis/0851.svg new file mode 100644 index 0000000..6ead80e --- /dev/null +++ b/resources/imageFiles/emojis/0851.svg @@ -0,0 +1,32 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0852.svg b/resources/imageFiles/emojis/0852.svg new file mode 100644 index 0000000..af3ad5c --- /dev/null +++ b/resources/imageFiles/emojis/0852.svg @@ -0,0 +1,45 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0853.svg b/resources/imageFiles/emojis/0853.svg new file mode 100644 index 0000000..2212256 --- /dev/null +++ b/resources/imageFiles/emojis/0853.svg @@ -0,0 +1,32 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0854.svg b/resources/imageFiles/emojis/0854.svg new file mode 100644 index 0000000..ba2d76e --- /dev/null +++ b/resources/imageFiles/emojis/0854.svg @@ -0,0 +1,46 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0855.svg b/resources/imageFiles/emojis/0855.svg new file mode 100644 index 0000000..b4ceb6c --- /dev/null +++ b/resources/imageFiles/emojis/0855.svg @@ -0,0 +1,33 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0856.svg b/resources/imageFiles/emojis/0856.svg new file mode 100644 index 0000000..cc66475 --- /dev/null +++ b/resources/imageFiles/emojis/0856.svg @@ -0,0 +1,42 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0857.svg b/resources/imageFiles/emojis/0857.svg new file mode 100644 index 0000000..d8d96c1 --- /dev/null +++ b/resources/imageFiles/emojis/0857.svg @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0858.svg b/resources/imageFiles/emojis/0858.svg new file mode 100644 index 0000000..8ea0a2e --- /dev/null +++ b/resources/imageFiles/emojis/0858.svg @@ -0,0 +1,33 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0859.svg b/resources/imageFiles/emojis/0859.svg new file mode 100644 index 0000000..310d455 --- /dev/null +++ b/resources/imageFiles/emojis/0859.svg @@ -0,0 +1,31 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0860.svg b/resources/imageFiles/emojis/0860.svg new file mode 100644 index 0000000..3b7757e --- /dev/null +++ b/resources/imageFiles/emojis/0860.svg @@ -0,0 +1,33 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0861.svg b/resources/imageFiles/emojis/0861.svg new file mode 100644 index 0000000..b26acf9 --- /dev/null +++ b/resources/imageFiles/emojis/0861.svg @@ -0,0 +1,34 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0862.svg b/resources/imageFiles/emojis/0862.svg new file mode 100644 index 0000000..49bbeca --- /dev/null +++ b/resources/imageFiles/emojis/0862.svg @@ -0,0 +1,41 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0863.svg b/resources/imageFiles/emojis/0863.svg new file mode 100644 index 0000000..37903f5 --- /dev/null +++ b/resources/imageFiles/emojis/0863.svg @@ -0,0 +1,43 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0864.svg b/resources/imageFiles/emojis/0864.svg new file mode 100644 index 0000000..c21a709 --- /dev/null +++ b/resources/imageFiles/emojis/0864.svg @@ -0,0 +1,31 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0865.svg b/resources/imageFiles/emojis/0865.svg new file mode 100644 index 0000000..82022b3 --- /dev/null +++ b/resources/imageFiles/emojis/0865.svg @@ -0,0 +1,32 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0866.svg b/resources/imageFiles/emojis/0866.svg new file mode 100644 index 0000000..dcbe7e1 --- /dev/null +++ b/resources/imageFiles/emojis/0866.svg @@ -0,0 +1,45 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0867.svg b/resources/imageFiles/emojis/0867.svg new file mode 100644 index 0000000..61d1603 --- /dev/null +++ b/resources/imageFiles/emojis/0867.svg @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0868.svg b/resources/imageFiles/emojis/0868.svg new file mode 100644 index 0000000..4abed53 --- /dev/null +++ b/resources/imageFiles/emojis/0868.svg @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0869.svg b/resources/imageFiles/emojis/0869.svg new file mode 100644 index 0000000..8557bfc --- /dev/null +++ b/resources/imageFiles/emojis/0869.svg @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0870.svg b/resources/imageFiles/emojis/0870.svg new file mode 100644 index 0000000..fc090b8 --- /dev/null +++ b/resources/imageFiles/emojis/0870.svg @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0871.svg b/resources/imageFiles/emojis/0871.svg new file mode 100644 index 0000000..05fa331 --- /dev/null +++ b/resources/imageFiles/emojis/0871.svg @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0872.svg b/resources/imageFiles/emojis/0872.svg new file mode 100644 index 0000000..7e59000 --- /dev/null +++ b/resources/imageFiles/emojis/0872.svg @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0873.svg b/resources/imageFiles/emojis/0873.svg new file mode 100644 index 0000000..5401e9b --- /dev/null +++ b/resources/imageFiles/emojis/0873.svg @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0874.svg b/resources/imageFiles/emojis/0874.svg new file mode 100644 index 0000000..99405d8 --- /dev/null +++ b/resources/imageFiles/emojis/0874.svg @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0875.svg b/resources/imageFiles/emojis/0875.svg new file mode 100644 index 0000000..ca4c0f3 --- /dev/null +++ b/resources/imageFiles/emojis/0875.svg @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0876.svg b/resources/imageFiles/emojis/0876.svg new file mode 100644 index 0000000..b8a11fb --- /dev/null +++ b/resources/imageFiles/emojis/0876.svg @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0877.svg b/resources/imageFiles/emojis/0877.svg new file mode 100644 index 0000000..1d37a32 --- /dev/null +++ b/resources/imageFiles/emojis/0877.svg @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0878.svg b/resources/imageFiles/emojis/0878.svg new file mode 100644 index 0000000..95d2601 --- /dev/null +++ b/resources/imageFiles/emojis/0878.svg @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0879.svg b/resources/imageFiles/emojis/0879.svg new file mode 100644 index 0000000..03432b8 --- /dev/null +++ b/resources/imageFiles/emojis/0879.svg @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0880.svg b/resources/imageFiles/emojis/0880.svg new file mode 100644 index 0000000..dea9117 --- /dev/null +++ b/resources/imageFiles/emojis/0880.svg @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0881.svg b/resources/imageFiles/emojis/0881.svg new file mode 100644 index 0000000..295d099 --- /dev/null +++ b/resources/imageFiles/emojis/0881.svg @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0882.svg b/resources/imageFiles/emojis/0882.svg new file mode 100644 index 0000000..ea8d5a8 --- /dev/null +++ b/resources/imageFiles/emojis/0882.svg @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0883.svg b/resources/imageFiles/emojis/0883.svg new file mode 100644 index 0000000..674b436 --- /dev/null +++ b/resources/imageFiles/emojis/0883.svg @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0884.svg b/resources/imageFiles/emojis/0884.svg new file mode 100644 index 0000000..72a1df1 --- /dev/null +++ b/resources/imageFiles/emojis/0884.svg @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0885.svg b/resources/imageFiles/emojis/0885.svg new file mode 100644 index 0000000..65f24ca --- /dev/null +++ b/resources/imageFiles/emojis/0885.svg @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0886.svg b/resources/imageFiles/emojis/0886.svg new file mode 100644 index 0000000..a710cfd --- /dev/null +++ b/resources/imageFiles/emojis/0886.svg @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0887.svg b/resources/imageFiles/emojis/0887.svg new file mode 100644 index 0000000..e4af860 --- /dev/null +++ b/resources/imageFiles/emojis/0887.svg @@ -0,0 +1,38 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0888.svg b/resources/imageFiles/emojis/0888.svg new file mode 100644 index 0000000..51fae60 --- /dev/null +++ b/resources/imageFiles/emojis/0888.svg @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0889.svg b/resources/imageFiles/emojis/0889.svg new file mode 100644 index 0000000..245a84b --- /dev/null +++ b/resources/imageFiles/emojis/0889.svg @@ -0,0 +1,38 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0890.svg b/resources/imageFiles/emojis/0890.svg new file mode 100644 index 0000000..8646994 --- /dev/null +++ b/resources/imageFiles/emojis/0890.svg @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0891.svg b/resources/imageFiles/emojis/0891.svg new file mode 100644 index 0000000..b694523 --- /dev/null +++ b/resources/imageFiles/emojis/0891.svg @@ -0,0 +1,38 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0892.svg b/resources/imageFiles/emojis/0892.svg new file mode 100644 index 0000000..1a223c9 --- /dev/null +++ b/resources/imageFiles/emojis/0892.svg @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0893.svg b/resources/imageFiles/emojis/0893.svg new file mode 100644 index 0000000..116adfa --- /dev/null +++ b/resources/imageFiles/emojis/0893.svg @@ -0,0 +1,38 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0894.svg b/resources/imageFiles/emojis/0894.svg new file mode 100644 index 0000000..a07cd14 --- /dev/null +++ b/resources/imageFiles/emojis/0894.svg @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0895.svg b/resources/imageFiles/emojis/0895.svg new file mode 100644 index 0000000..064494b --- /dev/null +++ b/resources/imageFiles/emojis/0895.svg @@ -0,0 +1,38 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0896.svg b/resources/imageFiles/emojis/0896.svg new file mode 100644 index 0000000..d6fb3ca --- /dev/null +++ b/resources/imageFiles/emojis/0896.svg @@ -0,0 +1,38 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0897.svg b/resources/imageFiles/emojis/0897.svg new file mode 100644 index 0000000..1c42b0c --- /dev/null +++ b/resources/imageFiles/emojis/0897.svg @@ -0,0 +1,41 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0898.svg b/resources/imageFiles/emojis/0898.svg new file mode 100644 index 0000000..96480e2 --- /dev/null +++ b/resources/imageFiles/emojis/0898.svg @@ -0,0 +1,41 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0899.svg b/resources/imageFiles/emojis/0899.svg new file mode 100644 index 0000000..5c02660 --- /dev/null +++ b/resources/imageFiles/emojis/0899.svg @@ -0,0 +1,51 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0900.svg b/resources/imageFiles/emojis/0900.svg new file mode 100644 index 0000000..2a05186 --- /dev/null +++ b/resources/imageFiles/emojis/0900.svg @@ -0,0 +1,47 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0901.svg b/resources/imageFiles/emojis/0901.svg new file mode 100644 index 0000000..efb1e07 --- /dev/null +++ b/resources/imageFiles/emojis/0901.svg @@ -0,0 +1,41 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0902.svg b/resources/imageFiles/emojis/0902.svg new file mode 100644 index 0000000..5e3be54 --- /dev/null +++ b/resources/imageFiles/emojis/0902.svg @@ -0,0 +1,51 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0903.svg b/resources/imageFiles/emojis/0903.svg new file mode 100644 index 0000000..dded46e --- /dev/null +++ b/resources/imageFiles/emojis/0903.svg @@ -0,0 +1,47 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0904.svg b/resources/imageFiles/emojis/0904.svg new file mode 100644 index 0000000..2bea6ad --- /dev/null +++ b/resources/imageFiles/emojis/0904.svg @@ -0,0 +1,41 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0905.svg b/resources/imageFiles/emojis/0905.svg new file mode 100644 index 0000000..478e143 --- /dev/null +++ b/resources/imageFiles/emojis/0905.svg @@ -0,0 +1,51 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0906.svg b/resources/imageFiles/emojis/0906.svg new file mode 100644 index 0000000..5dfb315 --- /dev/null +++ b/resources/imageFiles/emojis/0906.svg @@ -0,0 +1,47 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0907.svg b/resources/imageFiles/emojis/0907.svg new file mode 100644 index 0000000..f5424cf --- /dev/null +++ b/resources/imageFiles/emojis/0907.svg @@ -0,0 +1,41 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0908.svg b/resources/imageFiles/emojis/0908.svg new file mode 100644 index 0000000..84c335f --- /dev/null +++ b/resources/imageFiles/emojis/0908.svg @@ -0,0 +1,51 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0909.svg b/resources/imageFiles/emojis/0909.svg new file mode 100644 index 0000000..0c56459 --- /dev/null +++ b/resources/imageFiles/emojis/0909.svg @@ -0,0 +1,47 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0910.svg b/resources/imageFiles/emojis/0910.svg new file mode 100644 index 0000000..9503b7c --- /dev/null +++ b/resources/imageFiles/emojis/0910.svg @@ -0,0 +1,41 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0911.svg b/resources/imageFiles/emojis/0911.svg new file mode 100644 index 0000000..036e48b --- /dev/null +++ b/resources/imageFiles/emojis/0911.svg @@ -0,0 +1,51 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0912.svg b/resources/imageFiles/emojis/0912.svg new file mode 100644 index 0000000..d8ed277 --- /dev/null +++ b/resources/imageFiles/emojis/0912.svg @@ -0,0 +1,47 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0913.svg b/resources/imageFiles/emojis/0913.svg new file mode 100644 index 0000000..89da984 --- /dev/null +++ b/resources/imageFiles/emojis/0913.svg @@ -0,0 +1,51 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0914.svg b/resources/imageFiles/emojis/0914.svg new file mode 100644 index 0000000..0045127 --- /dev/null +++ b/resources/imageFiles/emojis/0914.svg @@ -0,0 +1,47 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0915.svg b/resources/imageFiles/emojis/0915.svg new file mode 100644 index 0000000..2e6c909 --- /dev/null +++ b/resources/imageFiles/emojis/0915.svg @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0916.svg b/resources/imageFiles/emojis/0916.svg new file mode 100644 index 0000000..703934a --- /dev/null +++ b/resources/imageFiles/emojis/0916.svg @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0917.svg b/resources/imageFiles/emojis/0917.svg new file mode 100644 index 0000000..95b3891 --- /dev/null +++ b/resources/imageFiles/emojis/0917.svg @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0918.svg b/resources/imageFiles/emojis/0918.svg new file mode 100644 index 0000000..08d0be0 --- /dev/null +++ b/resources/imageFiles/emojis/0918.svg @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0919.svg b/resources/imageFiles/emojis/0919.svg new file mode 100644 index 0000000..f2dc16d --- /dev/null +++ b/resources/imageFiles/emojis/0919.svg @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0920.svg b/resources/imageFiles/emojis/0920.svg new file mode 100644 index 0000000..82028fc --- /dev/null +++ b/resources/imageFiles/emojis/0920.svg @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0921.svg b/resources/imageFiles/emojis/0921.svg new file mode 100644 index 0000000..84bf5ac --- /dev/null +++ b/resources/imageFiles/emojis/0921.svg @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0922.svg b/resources/imageFiles/emojis/0922.svg new file mode 100644 index 0000000..6a444a0 --- /dev/null +++ b/resources/imageFiles/emojis/0922.svg @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0923.svg b/resources/imageFiles/emojis/0923.svg new file mode 100644 index 0000000..22ac482 --- /dev/null +++ b/resources/imageFiles/emojis/0923.svg @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0924.svg b/resources/imageFiles/emojis/0924.svg new file mode 100644 index 0000000..d8f1665 --- /dev/null +++ b/resources/imageFiles/emojis/0924.svg @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0925.svg b/resources/imageFiles/emojis/0925.svg new file mode 100644 index 0000000..a48133f --- /dev/null +++ b/resources/imageFiles/emojis/0925.svg @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0926.svg b/resources/imageFiles/emojis/0926.svg new file mode 100644 index 0000000..b83811f --- /dev/null +++ b/resources/imageFiles/emojis/0926.svg @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0927.svg b/resources/imageFiles/emojis/0927.svg new file mode 100644 index 0000000..f681df0 --- /dev/null +++ b/resources/imageFiles/emojis/0927.svg @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0928.svg b/resources/imageFiles/emojis/0928.svg new file mode 100644 index 0000000..f675efd --- /dev/null +++ b/resources/imageFiles/emojis/0928.svg @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0929.svg b/resources/imageFiles/emojis/0929.svg new file mode 100644 index 0000000..adf36ec --- /dev/null +++ b/resources/imageFiles/emojis/0929.svg @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0930.svg b/resources/imageFiles/emojis/0930.svg new file mode 100644 index 0000000..1a0b579 --- /dev/null +++ b/resources/imageFiles/emojis/0930.svg @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0931.svg b/resources/imageFiles/emojis/0931.svg new file mode 100644 index 0000000..9ba6de6 --- /dev/null +++ b/resources/imageFiles/emojis/0931.svg @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0932.svg b/resources/imageFiles/emojis/0932.svg new file mode 100644 index 0000000..43e3f34 --- /dev/null +++ b/resources/imageFiles/emojis/0932.svg @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0933.svg b/resources/imageFiles/emojis/0933.svg new file mode 100644 index 0000000..b931093 --- /dev/null +++ b/resources/imageFiles/emojis/0933.svg @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0934.svg b/resources/imageFiles/emojis/0934.svg new file mode 100644 index 0000000..ca3b6cd --- /dev/null +++ b/resources/imageFiles/emojis/0934.svg @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0935.svg b/resources/imageFiles/emojis/0935.svg new file mode 100644 index 0000000..3209a96 --- /dev/null +++ b/resources/imageFiles/emojis/0935.svg @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0936.svg b/resources/imageFiles/emojis/0936.svg new file mode 100644 index 0000000..ca07710 --- /dev/null +++ b/resources/imageFiles/emojis/0936.svg @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0937.svg b/resources/imageFiles/emojis/0937.svg new file mode 100644 index 0000000..ccbf965 --- /dev/null +++ b/resources/imageFiles/emojis/0937.svg @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0938.svg b/resources/imageFiles/emojis/0938.svg new file mode 100644 index 0000000..f10dafb --- /dev/null +++ b/resources/imageFiles/emojis/0938.svg @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0939.svg b/resources/imageFiles/emojis/0939.svg new file mode 100644 index 0000000..0b3c341 --- /dev/null +++ b/resources/imageFiles/emojis/0939.svg @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0940.svg b/resources/imageFiles/emojis/0940.svg new file mode 100644 index 0000000..01166b7 --- /dev/null +++ b/resources/imageFiles/emojis/0940.svg @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0941.svg b/resources/imageFiles/emojis/0941.svg new file mode 100644 index 0000000..cb11d2d --- /dev/null +++ b/resources/imageFiles/emojis/0941.svg @@ -0,0 +1,40 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0942.svg b/resources/imageFiles/emojis/0942.svg new file mode 100644 index 0000000..90ac51d --- /dev/null +++ b/resources/imageFiles/emojis/0942.svg @@ -0,0 +1,32 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0943.svg b/resources/imageFiles/emojis/0943.svg new file mode 100644 index 0000000..27dc9f3 --- /dev/null +++ b/resources/imageFiles/emojis/0943.svg @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0944.svg b/resources/imageFiles/emojis/0944.svg new file mode 100644 index 0000000..8b8b8d5 --- /dev/null +++ b/resources/imageFiles/emojis/0944.svg @@ -0,0 +1,40 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0945.svg b/resources/imageFiles/emojis/0945.svg new file mode 100644 index 0000000..56f9dfd --- /dev/null +++ b/resources/imageFiles/emojis/0945.svg @@ -0,0 +1,32 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0946.svg b/resources/imageFiles/emojis/0946.svg new file mode 100644 index 0000000..8a8191a --- /dev/null +++ b/resources/imageFiles/emojis/0946.svg @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0947.svg b/resources/imageFiles/emojis/0947.svg new file mode 100644 index 0000000..00f81aa --- /dev/null +++ b/resources/imageFiles/emojis/0947.svg @@ -0,0 +1,40 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0948.svg b/resources/imageFiles/emojis/0948.svg new file mode 100644 index 0000000..0087097 --- /dev/null +++ b/resources/imageFiles/emojis/0948.svg @@ -0,0 +1,32 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0949.svg b/resources/imageFiles/emojis/0949.svg new file mode 100644 index 0000000..3693c42 --- /dev/null +++ b/resources/imageFiles/emojis/0949.svg @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0950.svg b/resources/imageFiles/emojis/0950.svg new file mode 100644 index 0000000..594c0ec --- /dev/null +++ b/resources/imageFiles/emojis/0950.svg @@ -0,0 +1,40 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0951.svg b/resources/imageFiles/emojis/0951.svg new file mode 100644 index 0000000..15800af --- /dev/null +++ b/resources/imageFiles/emojis/0951.svg @@ -0,0 +1,32 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0952.svg b/resources/imageFiles/emojis/0952.svg new file mode 100644 index 0000000..99ac762 --- /dev/null +++ b/resources/imageFiles/emojis/0952.svg @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0953.svg b/resources/imageFiles/emojis/0953.svg new file mode 100644 index 0000000..f77ef88 --- /dev/null +++ b/resources/imageFiles/emojis/0953.svg @@ -0,0 +1,40 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0954.svg b/resources/imageFiles/emojis/0954.svg new file mode 100644 index 0000000..ecb6eda --- /dev/null +++ b/resources/imageFiles/emojis/0954.svg @@ -0,0 +1,32 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0955.svg b/resources/imageFiles/emojis/0955.svg new file mode 100644 index 0000000..7cd99da --- /dev/null +++ b/resources/imageFiles/emojis/0955.svg @@ -0,0 +1,40 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0956.svg b/resources/imageFiles/emojis/0956.svg new file mode 100644 index 0000000..fce3a4c --- /dev/null +++ b/resources/imageFiles/emojis/0956.svg @@ -0,0 +1,32 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0957.svg b/resources/imageFiles/emojis/0957.svg new file mode 100644 index 0000000..c8afa6b --- /dev/null +++ b/resources/imageFiles/emojis/0957.svg @@ -0,0 +1,35 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0958.svg b/resources/imageFiles/emojis/0958.svg new file mode 100644 index 0000000..2f10d73 --- /dev/null +++ b/resources/imageFiles/emojis/0958.svg @@ -0,0 +1,44 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0959.svg b/resources/imageFiles/emojis/0959.svg new file mode 100644 index 0000000..fe050e4 --- /dev/null +++ b/resources/imageFiles/emojis/0959.svg @@ -0,0 +1,31 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0960.svg b/resources/imageFiles/emojis/0960.svg new file mode 100644 index 0000000..9a58796 --- /dev/null +++ b/resources/imageFiles/emojis/0960.svg @@ -0,0 +1,40 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0961.svg b/resources/imageFiles/emojis/0961.svg new file mode 100644 index 0000000..b3275d9 --- /dev/null +++ b/resources/imageFiles/emojis/0961.svg @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0962.svg b/resources/imageFiles/emojis/0962.svg new file mode 100644 index 0000000..a322682 --- /dev/null +++ b/resources/imageFiles/emojis/0962.svg @@ -0,0 +1,43 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0963.svg b/resources/imageFiles/emojis/0963.svg new file mode 100644 index 0000000..3efc536 --- /dev/null +++ b/resources/imageFiles/emojis/0963.svg @@ -0,0 +1,31 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0964.svg b/resources/imageFiles/emojis/0964.svg new file mode 100644 index 0000000..3e4c639 --- /dev/null +++ b/resources/imageFiles/emojis/0964.svg @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0965.svg b/resources/imageFiles/emojis/0965.svg new file mode 100644 index 0000000..5112a19 --- /dev/null +++ b/resources/imageFiles/emojis/0965.svg @@ -0,0 +1,34 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0966.svg b/resources/imageFiles/emojis/0966.svg new file mode 100644 index 0000000..e6ba2d0 --- /dev/null +++ b/resources/imageFiles/emojis/0966.svg @@ -0,0 +1,32 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0967.svg b/resources/imageFiles/emojis/0967.svg new file mode 100644 index 0000000..fe9e069 --- /dev/null +++ b/resources/imageFiles/emojis/0967.svg @@ -0,0 +1,32 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0968.svg b/resources/imageFiles/emojis/0968.svg new file mode 100644 index 0000000..2e51991 --- /dev/null +++ b/resources/imageFiles/emojis/0968.svg @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0969.svg b/resources/imageFiles/emojis/0969.svg new file mode 100644 index 0000000..ce3ad1e --- /dev/null +++ b/resources/imageFiles/emojis/0969.svg @@ -0,0 +1,40 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0970.svg b/resources/imageFiles/emojis/0970.svg new file mode 100644 index 0000000..e91c67d --- /dev/null +++ b/resources/imageFiles/emojis/0970.svg @@ -0,0 +1,29 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0971.svg b/resources/imageFiles/emojis/0971.svg new file mode 100644 index 0000000..7c8434d --- /dev/null +++ b/resources/imageFiles/emojis/0971.svg @@ -0,0 +1,29 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0972.svg b/resources/imageFiles/emojis/0972.svg new file mode 100644 index 0000000..389202e --- /dev/null +++ b/resources/imageFiles/emojis/0972.svg @@ -0,0 +1,44 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0973.svg b/resources/imageFiles/emojis/0973.svg new file mode 100644 index 0000000..2b38b90 --- /dev/null +++ b/resources/imageFiles/emojis/0973.svg @@ -0,0 +1,35 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0974.svg b/resources/imageFiles/emojis/0974.svg new file mode 100644 index 0000000..bbabca8 --- /dev/null +++ b/resources/imageFiles/emojis/0974.svg @@ -0,0 +1,44 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0975.svg b/resources/imageFiles/emojis/0975.svg new file mode 100644 index 0000000..e69dca5 --- /dev/null +++ b/resources/imageFiles/emojis/0975.svg @@ -0,0 +1,31 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0976.svg b/resources/imageFiles/emojis/0976.svg new file mode 100644 index 0000000..e87b05b --- /dev/null +++ b/resources/imageFiles/emojis/0976.svg @@ -0,0 +1,40 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0977.svg b/resources/imageFiles/emojis/0977.svg new file mode 100644 index 0000000..b7253e8 --- /dev/null +++ b/resources/imageFiles/emojis/0977.svg @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0978.svg b/resources/imageFiles/emojis/0978.svg new file mode 100644 index 0000000..020166e --- /dev/null +++ b/resources/imageFiles/emojis/0978.svg @@ -0,0 +1,43 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0979.svg b/resources/imageFiles/emojis/0979.svg new file mode 100644 index 0000000..606f848 --- /dev/null +++ b/resources/imageFiles/emojis/0979.svg @@ -0,0 +1,31 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0980.svg b/resources/imageFiles/emojis/0980.svg new file mode 100644 index 0000000..1e27ee1 --- /dev/null +++ b/resources/imageFiles/emojis/0980.svg @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0981.svg b/resources/imageFiles/emojis/0981.svg new file mode 100644 index 0000000..5cfe7ba --- /dev/null +++ b/resources/imageFiles/emojis/0981.svg @@ -0,0 +1,34 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0982.svg b/resources/imageFiles/emojis/0982.svg new file mode 100644 index 0000000..33c2823 --- /dev/null +++ b/resources/imageFiles/emojis/0982.svg @@ -0,0 +1,32 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0983.svg b/resources/imageFiles/emojis/0983.svg new file mode 100644 index 0000000..1073b78 --- /dev/null +++ b/resources/imageFiles/emojis/0983.svg @@ -0,0 +1,32 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0984.svg b/resources/imageFiles/emojis/0984.svg new file mode 100644 index 0000000..261dde2 --- /dev/null +++ b/resources/imageFiles/emojis/0984.svg @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0985.svg b/resources/imageFiles/emojis/0985.svg new file mode 100644 index 0000000..7fd7444 --- /dev/null +++ b/resources/imageFiles/emojis/0985.svg @@ -0,0 +1,40 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0986.svg b/resources/imageFiles/emojis/0986.svg new file mode 100644 index 0000000..5ffcdde --- /dev/null +++ b/resources/imageFiles/emojis/0986.svg @@ -0,0 +1,29 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0987.svg b/resources/imageFiles/emojis/0987.svg new file mode 100644 index 0000000..9e8609b --- /dev/null +++ b/resources/imageFiles/emojis/0987.svg @@ -0,0 +1,29 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0988.svg b/resources/imageFiles/emojis/0988.svg new file mode 100644 index 0000000..40abe6d --- /dev/null +++ b/resources/imageFiles/emojis/0988.svg @@ -0,0 +1,44 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0989.svg b/resources/imageFiles/emojis/0989.svg new file mode 100644 index 0000000..4391b19 --- /dev/null +++ b/resources/imageFiles/emojis/0989.svg @@ -0,0 +1,35 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0990.svg b/resources/imageFiles/emojis/0990.svg new file mode 100644 index 0000000..1086c17 --- /dev/null +++ b/resources/imageFiles/emojis/0990.svg @@ -0,0 +1,44 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0991.svg b/resources/imageFiles/emojis/0991.svg new file mode 100644 index 0000000..350ec1f --- /dev/null +++ b/resources/imageFiles/emojis/0991.svg @@ -0,0 +1,31 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0992.svg b/resources/imageFiles/emojis/0992.svg new file mode 100644 index 0000000..cdb5991 --- /dev/null +++ b/resources/imageFiles/emojis/0992.svg @@ -0,0 +1,40 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0993.svg b/resources/imageFiles/emojis/0993.svg new file mode 100644 index 0000000..cc94bd4 --- /dev/null +++ b/resources/imageFiles/emojis/0993.svg @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0994.svg b/resources/imageFiles/emojis/0994.svg new file mode 100644 index 0000000..5d7369a --- /dev/null +++ b/resources/imageFiles/emojis/0994.svg @@ -0,0 +1,43 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0995.svg b/resources/imageFiles/emojis/0995.svg new file mode 100644 index 0000000..792768e --- /dev/null +++ b/resources/imageFiles/emojis/0995.svg @@ -0,0 +1,31 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0996.svg b/resources/imageFiles/emojis/0996.svg new file mode 100644 index 0000000..f787488 --- /dev/null +++ b/resources/imageFiles/emojis/0996.svg @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0997.svg b/resources/imageFiles/emojis/0997.svg new file mode 100644 index 0000000..c1ee1f2 --- /dev/null +++ b/resources/imageFiles/emojis/0997.svg @@ -0,0 +1,34 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0998.svg b/resources/imageFiles/emojis/0998.svg new file mode 100644 index 0000000..769abfd --- /dev/null +++ b/resources/imageFiles/emojis/0998.svg @@ -0,0 +1,32 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/0999.svg b/resources/imageFiles/emojis/0999.svg new file mode 100644 index 0000000..45ae70c --- /dev/null +++ b/resources/imageFiles/emojis/0999.svg @@ -0,0 +1,32 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1000.svg b/resources/imageFiles/emojis/1000.svg new file mode 100644 index 0000000..cae5058 --- /dev/null +++ b/resources/imageFiles/emojis/1000.svg @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1001.svg b/resources/imageFiles/emojis/1001.svg new file mode 100644 index 0000000..eaf412d --- /dev/null +++ b/resources/imageFiles/emojis/1001.svg @@ -0,0 +1,40 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1002.svg b/resources/imageFiles/emojis/1002.svg new file mode 100644 index 0000000..c7895f9 --- /dev/null +++ b/resources/imageFiles/emojis/1002.svg @@ -0,0 +1,29 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1003.svg b/resources/imageFiles/emojis/1003.svg new file mode 100644 index 0000000..e3b98ad --- /dev/null +++ b/resources/imageFiles/emojis/1003.svg @@ -0,0 +1,29 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1004.svg b/resources/imageFiles/emojis/1004.svg new file mode 100644 index 0000000..1055123 --- /dev/null +++ b/resources/imageFiles/emojis/1004.svg @@ -0,0 +1,44 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1005.svg b/resources/imageFiles/emojis/1005.svg new file mode 100644 index 0000000..c17c4ae --- /dev/null +++ b/resources/imageFiles/emojis/1005.svg @@ -0,0 +1,35 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1006.svg b/resources/imageFiles/emojis/1006.svg new file mode 100644 index 0000000..460a7d4 --- /dev/null +++ b/resources/imageFiles/emojis/1006.svg @@ -0,0 +1,44 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1007.svg b/resources/imageFiles/emojis/1007.svg new file mode 100644 index 0000000..8188834 --- /dev/null +++ b/resources/imageFiles/emojis/1007.svg @@ -0,0 +1,31 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1008.svg b/resources/imageFiles/emojis/1008.svg new file mode 100644 index 0000000..5386e2f --- /dev/null +++ b/resources/imageFiles/emojis/1008.svg @@ -0,0 +1,40 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1009.svg b/resources/imageFiles/emojis/1009.svg new file mode 100644 index 0000000..d32cc8e --- /dev/null +++ b/resources/imageFiles/emojis/1009.svg @@ -0,0 +1,29 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1010.svg b/resources/imageFiles/emojis/1010.svg new file mode 100644 index 0000000..067bd85 --- /dev/null +++ b/resources/imageFiles/emojis/1010.svg @@ -0,0 +1,29 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1011.svg b/resources/imageFiles/emojis/1011.svg new file mode 100644 index 0000000..9d99043 --- /dev/null +++ b/resources/imageFiles/emojis/1011.svg @@ -0,0 +1,29 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1012.svg b/resources/imageFiles/emojis/1012.svg new file mode 100644 index 0000000..7cf69c4 --- /dev/null +++ b/resources/imageFiles/emojis/1012.svg @@ -0,0 +1,29 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1013.svg b/resources/imageFiles/emojis/1013.svg new file mode 100644 index 0000000..af2859c --- /dev/null +++ b/resources/imageFiles/emojis/1013.svg @@ -0,0 +1,29 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1014.svg b/resources/imageFiles/emojis/1014.svg new file mode 100644 index 0000000..1cc3f99 --- /dev/null +++ b/resources/imageFiles/emojis/1014.svg @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1015.svg b/resources/imageFiles/emojis/1015.svg new file mode 100644 index 0000000..1ad5208 --- /dev/null +++ b/resources/imageFiles/emojis/1015.svg @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1016.svg b/resources/imageFiles/emojis/1016.svg new file mode 100644 index 0000000..608bf63 --- /dev/null +++ b/resources/imageFiles/emojis/1016.svg @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1017.svg b/resources/imageFiles/emojis/1017.svg new file mode 100644 index 0000000..004e246 --- /dev/null +++ b/resources/imageFiles/emojis/1017.svg @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1018.svg b/resources/imageFiles/emojis/1018.svg new file mode 100644 index 0000000..cac12c1 --- /dev/null +++ b/resources/imageFiles/emojis/1018.svg @@ -0,0 +1,29 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1019.svg b/resources/imageFiles/emojis/1019.svg new file mode 100644 index 0000000..190cad9 --- /dev/null +++ b/resources/imageFiles/emojis/1019.svg @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1020.svg b/resources/imageFiles/emojis/1020.svg new file mode 100644 index 0000000..a52ce86 --- /dev/null +++ b/resources/imageFiles/emojis/1020.svg @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1021.svg b/resources/imageFiles/emojis/1021.svg new file mode 100644 index 0000000..de61885 --- /dev/null +++ b/resources/imageFiles/emojis/1021.svg @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1022.svg b/resources/imageFiles/emojis/1022.svg new file mode 100644 index 0000000..6af35e0 --- /dev/null +++ b/resources/imageFiles/emojis/1022.svg @@ -0,0 +1,43 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1023.svg b/resources/imageFiles/emojis/1023.svg new file mode 100644 index 0000000..67d5ae5 --- /dev/null +++ b/resources/imageFiles/emojis/1023.svg @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1024.svg b/resources/imageFiles/emojis/1024.svg new file mode 100644 index 0000000..60b00d8 --- /dev/null +++ b/resources/imageFiles/emojis/1024.svg @@ -0,0 +1,31 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1025.svg b/resources/imageFiles/emojis/1025.svg new file mode 100644 index 0000000..f13d7f1 --- /dev/null +++ b/resources/imageFiles/emojis/1025.svg @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1026.svg b/resources/imageFiles/emojis/1026.svg new file mode 100644 index 0000000..d41c61d --- /dev/null +++ b/resources/imageFiles/emojis/1026.svg @@ -0,0 +1,34 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1027.svg b/resources/imageFiles/emojis/1027.svg new file mode 100644 index 0000000..0ffb316 --- /dev/null +++ b/resources/imageFiles/emojis/1027.svg @@ -0,0 +1,29 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1028.svg b/resources/imageFiles/emojis/1028.svg new file mode 100644 index 0000000..8706064 --- /dev/null +++ b/resources/imageFiles/emojis/1028.svg @@ -0,0 +1,29 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1029.svg b/resources/imageFiles/emojis/1029.svg new file mode 100644 index 0000000..4ac7ad2 --- /dev/null +++ b/resources/imageFiles/emojis/1029.svg @@ -0,0 +1,44 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1030.svg b/resources/imageFiles/emojis/1030.svg new file mode 100644 index 0000000..eb7161e --- /dev/null +++ b/resources/imageFiles/emojis/1030.svg @@ -0,0 +1,35 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1031.svg b/resources/imageFiles/emojis/1031.svg new file mode 100644 index 0000000..6865550 --- /dev/null +++ b/resources/imageFiles/emojis/1031.svg @@ -0,0 +1,31 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1032.svg b/resources/imageFiles/emojis/1032.svg new file mode 100644 index 0000000..6e9670e --- /dev/null +++ b/resources/imageFiles/emojis/1032.svg @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1033.svg b/resources/imageFiles/emojis/1033.svg new file mode 100644 index 0000000..2535cfe --- /dev/null +++ b/resources/imageFiles/emojis/1033.svg @@ -0,0 +1,49 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1034.svg b/resources/imageFiles/emojis/1034.svg new file mode 100644 index 0000000..221b44a --- /dev/null +++ b/resources/imageFiles/emojis/1034.svg @@ -0,0 +1,49 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1035.svg b/resources/imageFiles/emojis/1035.svg new file mode 100644 index 0000000..e2ea11f --- /dev/null +++ b/resources/imageFiles/emojis/1035.svg @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1036.svg b/resources/imageFiles/emojis/1036.svg new file mode 100644 index 0000000..38bcba1 --- /dev/null +++ b/resources/imageFiles/emojis/1036.svg @@ -0,0 +1,31 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1037.svg b/resources/imageFiles/emojis/1037.svg new file mode 100644 index 0000000..a166b8e --- /dev/null +++ b/resources/imageFiles/emojis/1037.svg @@ -0,0 +1,33 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1038.svg b/resources/imageFiles/emojis/1038.svg new file mode 100644 index 0000000..95fe90e --- /dev/null +++ b/resources/imageFiles/emojis/1038.svg @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1039.svg b/resources/imageFiles/emojis/1039.svg new file mode 100644 index 0000000..e48c0a9 --- /dev/null +++ b/resources/imageFiles/emojis/1039.svg @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1040.svg b/resources/imageFiles/emojis/1040.svg new file mode 100644 index 0000000..8a947ba --- /dev/null +++ b/resources/imageFiles/emojis/1040.svg @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1041.svg b/resources/imageFiles/emojis/1041.svg new file mode 100644 index 0000000..de7b792 --- /dev/null +++ b/resources/imageFiles/emojis/1041.svg @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1042.svg b/resources/imageFiles/emojis/1042.svg new file mode 100644 index 0000000..fe424b1 --- /dev/null +++ b/resources/imageFiles/emojis/1042.svg @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1043.svg b/resources/imageFiles/emojis/1043.svg new file mode 100644 index 0000000..7ef19a2 --- /dev/null +++ b/resources/imageFiles/emojis/1043.svg @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1044.svg b/resources/imageFiles/emojis/1044.svg new file mode 100644 index 0000000..4c6b201 --- /dev/null +++ b/resources/imageFiles/emojis/1044.svg @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1045.svg b/resources/imageFiles/emojis/1045.svg new file mode 100644 index 0000000..9fe333b --- /dev/null +++ b/resources/imageFiles/emojis/1045.svg @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1046.svg b/resources/imageFiles/emojis/1046.svg new file mode 100644 index 0000000..721c8b8 --- /dev/null +++ b/resources/imageFiles/emojis/1046.svg @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1047.svg b/resources/imageFiles/emojis/1047.svg new file mode 100644 index 0000000..baaa304 --- /dev/null +++ b/resources/imageFiles/emojis/1047.svg @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1048.svg b/resources/imageFiles/emojis/1048.svg new file mode 100644 index 0000000..972ac9a --- /dev/null +++ b/resources/imageFiles/emojis/1048.svg @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1049.svg b/resources/imageFiles/emojis/1049.svg new file mode 100644 index 0000000..740c81b --- /dev/null +++ b/resources/imageFiles/emojis/1049.svg @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1050.svg b/resources/imageFiles/emojis/1050.svg new file mode 100644 index 0000000..34b1a9c --- /dev/null +++ b/resources/imageFiles/emojis/1050.svg @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1051.svg b/resources/imageFiles/emojis/1051.svg new file mode 100644 index 0000000..4b57cf5 --- /dev/null +++ b/resources/imageFiles/emojis/1051.svg @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1052.svg b/resources/imageFiles/emojis/1052.svg new file mode 100644 index 0000000..ebd2a56 --- /dev/null +++ b/resources/imageFiles/emojis/1052.svg @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1053.svg b/resources/imageFiles/emojis/1053.svg new file mode 100644 index 0000000..d5a33f4 --- /dev/null +++ b/resources/imageFiles/emojis/1053.svg @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1054.svg b/resources/imageFiles/emojis/1054.svg new file mode 100644 index 0000000..7193a45 --- /dev/null +++ b/resources/imageFiles/emojis/1054.svg @@ -0,0 +1,39 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1055.svg b/resources/imageFiles/emojis/1055.svg new file mode 100644 index 0000000..7b75380 --- /dev/null +++ b/resources/imageFiles/emojis/1055.svg @@ -0,0 +1,39 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1056.svg b/resources/imageFiles/emojis/1056.svg new file mode 100644 index 0000000..2ee9f4a --- /dev/null +++ b/resources/imageFiles/emojis/1056.svg @@ -0,0 +1,39 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1057.svg b/resources/imageFiles/emojis/1057.svg new file mode 100644 index 0000000..5317e13 --- /dev/null +++ b/resources/imageFiles/emojis/1057.svg @@ -0,0 +1,39 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1058.svg b/resources/imageFiles/emojis/1058.svg new file mode 100644 index 0000000..1e38078 --- /dev/null +++ b/resources/imageFiles/emojis/1058.svg @@ -0,0 +1,39 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1059.svg b/resources/imageFiles/emojis/1059.svg new file mode 100644 index 0000000..e3f56fd --- /dev/null +++ b/resources/imageFiles/emojis/1059.svg @@ -0,0 +1,39 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1060.svg b/resources/imageFiles/emojis/1060.svg new file mode 100644 index 0000000..19796ad --- /dev/null +++ b/resources/imageFiles/emojis/1060.svg @@ -0,0 +1,38 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1061.svg b/resources/imageFiles/emojis/1061.svg new file mode 100644 index 0000000..5f41701 --- /dev/null +++ b/resources/imageFiles/emojis/1061.svg @@ -0,0 +1,32 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1062.svg b/resources/imageFiles/emojis/1062.svg new file mode 100644 index 0000000..46ee371 --- /dev/null +++ b/resources/imageFiles/emojis/1062.svg @@ -0,0 +1,31 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1063.svg b/resources/imageFiles/emojis/1063.svg new file mode 100644 index 0000000..4df4d98 --- /dev/null +++ b/resources/imageFiles/emojis/1063.svg @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1064.svg b/resources/imageFiles/emojis/1064.svg new file mode 100644 index 0000000..855ed54 --- /dev/null +++ b/resources/imageFiles/emojis/1064.svg @@ -0,0 +1,32 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1065.svg b/resources/imageFiles/emojis/1065.svg new file mode 100644 index 0000000..7489a15 --- /dev/null +++ b/resources/imageFiles/emojis/1065.svg @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1066.svg b/resources/imageFiles/emojis/1066.svg new file mode 100644 index 0000000..2f76f23 --- /dev/null +++ b/resources/imageFiles/emojis/1066.svg @@ -0,0 +1,31 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1067.svg b/resources/imageFiles/emojis/1067.svg new file mode 100644 index 0000000..80f22e9 --- /dev/null +++ b/resources/imageFiles/emojis/1067.svg @@ -0,0 +1,38 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1068.svg b/resources/imageFiles/emojis/1068.svg new file mode 100644 index 0000000..3d81a09 --- /dev/null +++ b/resources/imageFiles/emojis/1068.svg @@ -0,0 +1,32 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1069.svg b/resources/imageFiles/emojis/1069.svg new file mode 100644 index 0000000..ea3ecdb --- /dev/null +++ b/resources/imageFiles/emojis/1069.svg @@ -0,0 +1,31 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1070.svg b/resources/imageFiles/emojis/1070.svg new file mode 100644 index 0000000..da33a7d --- /dev/null +++ b/resources/imageFiles/emojis/1070.svg @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1071.svg b/resources/imageFiles/emojis/1071.svg new file mode 100644 index 0000000..21c6765 --- /dev/null +++ b/resources/imageFiles/emojis/1071.svg @@ -0,0 +1,32 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1072.svg b/resources/imageFiles/emojis/1072.svg new file mode 100644 index 0000000..1a6aa7a --- /dev/null +++ b/resources/imageFiles/emojis/1072.svg @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1073.svg b/resources/imageFiles/emojis/1073.svg new file mode 100644 index 0000000..209f4a6 --- /dev/null +++ b/resources/imageFiles/emojis/1073.svg @@ -0,0 +1,31 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1074.svg b/resources/imageFiles/emojis/1074.svg new file mode 100644 index 0000000..6f52be8 --- /dev/null +++ b/resources/imageFiles/emojis/1074.svg @@ -0,0 +1,38 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1075.svg b/resources/imageFiles/emojis/1075.svg new file mode 100644 index 0000000..ae02318 --- /dev/null +++ b/resources/imageFiles/emojis/1075.svg @@ -0,0 +1,32 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1076.svg b/resources/imageFiles/emojis/1076.svg new file mode 100644 index 0000000..7b600b8 --- /dev/null +++ b/resources/imageFiles/emojis/1076.svg @@ -0,0 +1,31 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1077.svg b/resources/imageFiles/emojis/1077.svg new file mode 100644 index 0000000..73f1eff --- /dev/null +++ b/resources/imageFiles/emojis/1077.svg @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1078.svg b/resources/imageFiles/emojis/1078.svg new file mode 100644 index 0000000..2da6c47 --- /dev/null +++ b/resources/imageFiles/emojis/1078.svg @@ -0,0 +1,32 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1079.svg b/resources/imageFiles/emojis/1079.svg new file mode 100644 index 0000000..3791806 --- /dev/null +++ b/resources/imageFiles/emojis/1079.svg @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1080.svg b/resources/imageFiles/emojis/1080.svg new file mode 100644 index 0000000..b375fb2 --- /dev/null +++ b/resources/imageFiles/emojis/1080.svg @@ -0,0 +1,31 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1081.svg b/resources/imageFiles/emojis/1081.svg new file mode 100644 index 0000000..63f15f8 --- /dev/null +++ b/resources/imageFiles/emojis/1081.svg @@ -0,0 +1,38 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1082.svg b/resources/imageFiles/emojis/1082.svg new file mode 100644 index 0000000..202ada9 --- /dev/null +++ b/resources/imageFiles/emojis/1082.svg @@ -0,0 +1,32 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1083.svg b/resources/imageFiles/emojis/1083.svg new file mode 100644 index 0000000..d008664 --- /dev/null +++ b/resources/imageFiles/emojis/1083.svg @@ -0,0 +1,31 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1084.svg b/resources/imageFiles/emojis/1084.svg new file mode 100644 index 0000000..97eaa71 --- /dev/null +++ b/resources/imageFiles/emojis/1084.svg @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1085.svg b/resources/imageFiles/emojis/1085.svg new file mode 100644 index 0000000..855abdb --- /dev/null +++ b/resources/imageFiles/emojis/1085.svg @@ -0,0 +1,32 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1086.svg b/resources/imageFiles/emojis/1086.svg new file mode 100644 index 0000000..fa2ce8b --- /dev/null +++ b/resources/imageFiles/emojis/1086.svg @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1087.svg b/resources/imageFiles/emojis/1087.svg new file mode 100644 index 0000000..82d192f --- /dev/null +++ b/resources/imageFiles/emojis/1087.svg @@ -0,0 +1,31 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1088.svg b/resources/imageFiles/emojis/1088.svg new file mode 100644 index 0000000..e0cad48 --- /dev/null +++ b/resources/imageFiles/emojis/1088.svg @@ -0,0 +1,38 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1089.svg b/resources/imageFiles/emojis/1089.svg new file mode 100644 index 0000000..3ddaa06 --- /dev/null +++ b/resources/imageFiles/emojis/1089.svg @@ -0,0 +1,32 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1090.svg b/resources/imageFiles/emojis/1090.svg new file mode 100644 index 0000000..a950c57 --- /dev/null +++ b/resources/imageFiles/emojis/1090.svg @@ -0,0 +1,31 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1091.svg b/resources/imageFiles/emojis/1091.svg new file mode 100644 index 0000000..fec8231 --- /dev/null +++ b/resources/imageFiles/emojis/1091.svg @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1092.svg b/resources/imageFiles/emojis/1092.svg new file mode 100644 index 0000000..6b84e33 --- /dev/null +++ b/resources/imageFiles/emojis/1092.svg @@ -0,0 +1,32 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1093.svg b/resources/imageFiles/emojis/1093.svg new file mode 100644 index 0000000..adefa19 --- /dev/null +++ b/resources/imageFiles/emojis/1093.svg @@ -0,0 +1,31 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1094.svg b/resources/imageFiles/emojis/1094.svg new file mode 100644 index 0000000..78d0698 --- /dev/null +++ b/resources/imageFiles/emojis/1094.svg @@ -0,0 +1,29 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1095.svg b/resources/imageFiles/emojis/1095.svg new file mode 100644 index 0000000..62d5869 --- /dev/null +++ b/resources/imageFiles/emojis/1095.svg @@ -0,0 +1,36 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1096.svg b/resources/imageFiles/emojis/1096.svg new file mode 100644 index 0000000..ee5b89e --- /dev/null +++ b/resources/imageFiles/emojis/1096.svg @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1097.svg b/resources/imageFiles/emojis/1097.svg new file mode 100644 index 0000000..0eb216c --- /dev/null +++ b/resources/imageFiles/emojis/1097.svg @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1098.svg b/resources/imageFiles/emojis/1098.svg new file mode 100644 index 0000000..1b47280 --- /dev/null +++ b/resources/imageFiles/emojis/1098.svg @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1099.svg b/resources/imageFiles/emojis/1099.svg new file mode 100644 index 0000000..95694da --- /dev/null +++ b/resources/imageFiles/emojis/1099.svg @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1100.svg b/resources/imageFiles/emojis/1100.svg new file mode 100644 index 0000000..f66ba5f --- /dev/null +++ b/resources/imageFiles/emojis/1100.svg @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1101.svg b/resources/imageFiles/emojis/1101.svg new file mode 100644 index 0000000..f09af8c --- /dev/null +++ b/resources/imageFiles/emojis/1101.svg @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1102.svg b/resources/imageFiles/emojis/1102.svg new file mode 100644 index 0000000..e9e1d0e --- /dev/null +++ b/resources/imageFiles/emojis/1102.svg @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1103.svg b/resources/imageFiles/emojis/1103.svg new file mode 100644 index 0000000..ebc4f14 --- /dev/null +++ b/resources/imageFiles/emojis/1103.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1104.svg b/resources/imageFiles/emojis/1104.svg new file mode 100644 index 0000000..74c8037 --- /dev/null +++ b/resources/imageFiles/emojis/1104.svg @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1105.svg b/resources/imageFiles/emojis/1105.svg new file mode 100644 index 0000000..7e69673 --- /dev/null +++ b/resources/imageFiles/emojis/1105.svg @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1106.svg b/resources/imageFiles/emojis/1106.svg new file mode 100644 index 0000000..9692c20 --- /dev/null +++ b/resources/imageFiles/emojis/1106.svg @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1107.svg b/resources/imageFiles/emojis/1107.svg new file mode 100644 index 0000000..4863e22 --- /dev/null +++ b/resources/imageFiles/emojis/1107.svg @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1108.svg b/resources/imageFiles/emojis/1108.svg new file mode 100644 index 0000000..8b8f9d0 --- /dev/null +++ b/resources/imageFiles/emojis/1108.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1109.svg b/resources/imageFiles/emojis/1109.svg new file mode 100644 index 0000000..0f76d28 --- /dev/null +++ b/resources/imageFiles/emojis/1109.svg @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1110.svg b/resources/imageFiles/emojis/1110.svg new file mode 100644 index 0000000..27f9807 --- /dev/null +++ b/resources/imageFiles/emojis/1110.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1111.svg b/resources/imageFiles/emojis/1111.svg new file mode 100644 index 0000000..d38215e --- /dev/null +++ b/resources/imageFiles/emojis/1111.svg @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1112.svg b/resources/imageFiles/emojis/1112.svg new file mode 100644 index 0000000..07b3f04 --- /dev/null +++ b/resources/imageFiles/emojis/1112.svg @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1113.svg b/resources/imageFiles/emojis/1113.svg new file mode 100644 index 0000000..3f4153d --- /dev/null +++ b/resources/imageFiles/emojis/1113.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1114.svg b/resources/imageFiles/emojis/1114.svg new file mode 100644 index 0000000..bee4f62 --- /dev/null +++ b/resources/imageFiles/emojis/1114.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1115.svg b/resources/imageFiles/emojis/1115.svg new file mode 100644 index 0000000..e295f25 --- /dev/null +++ b/resources/imageFiles/emojis/1115.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1116.svg b/resources/imageFiles/emojis/1116.svg new file mode 100644 index 0000000..c2f29e7 --- /dev/null +++ b/resources/imageFiles/emojis/1116.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1117.svg b/resources/imageFiles/emojis/1117.svg new file mode 100644 index 0000000..75ace4e --- /dev/null +++ b/resources/imageFiles/emojis/1117.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1118.svg b/resources/imageFiles/emojis/1118.svg new file mode 100644 index 0000000..f999b25 --- /dev/null +++ b/resources/imageFiles/emojis/1118.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1119.svg b/resources/imageFiles/emojis/1119.svg new file mode 100644 index 0000000..c4bb47c --- /dev/null +++ b/resources/imageFiles/emojis/1119.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1120.svg b/resources/imageFiles/emojis/1120.svg new file mode 100644 index 0000000..aabf626 --- /dev/null +++ b/resources/imageFiles/emojis/1120.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1121.svg b/resources/imageFiles/emojis/1121.svg new file mode 100644 index 0000000..7c55db5 --- /dev/null +++ b/resources/imageFiles/emojis/1121.svg @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1122.svg b/resources/imageFiles/emojis/1122.svg new file mode 100644 index 0000000..b24aa36 --- /dev/null +++ b/resources/imageFiles/emojis/1122.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1123.svg b/resources/imageFiles/emojis/1123.svg new file mode 100644 index 0000000..010006b --- /dev/null +++ b/resources/imageFiles/emojis/1123.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1124.svg b/resources/imageFiles/emojis/1124.svg new file mode 100644 index 0000000..68c4633 --- /dev/null +++ b/resources/imageFiles/emojis/1124.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1125.svg b/resources/imageFiles/emojis/1125.svg new file mode 100644 index 0000000..135a848 --- /dev/null +++ b/resources/imageFiles/emojis/1125.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1126.svg b/resources/imageFiles/emojis/1126.svg new file mode 100644 index 0000000..4e6ae00 --- /dev/null +++ b/resources/imageFiles/emojis/1126.svg @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1127.svg b/resources/imageFiles/emojis/1127.svg new file mode 100644 index 0000000..425b1c5 --- /dev/null +++ b/resources/imageFiles/emojis/1127.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1128.svg b/resources/imageFiles/emojis/1128.svg new file mode 100644 index 0000000..3341c0e --- /dev/null +++ b/resources/imageFiles/emojis/1128.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1129.svg b/resources/imageFiles/emojis/1129.svg new file mode 100644 index 0000000..a23a767 --- /dev/null +++ b/resources/imageFiles/emojis/1129.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1130.svg b/resources/imageFiles/emojis/1130.svg new file mode 100644 index 0000000..fa08373 --- /dev/null +++ b/resources/imageFiles/emojis/1130.svg @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1131.svg b/resources/imageFiles/emojis/1131.svg new file mode 100644 index 0000000..9979355 --- /dev/null +++ b/resources/imageFiles/emojis/1131.svg @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1132.svg b/resources/imageFiles/emojis/1132.svg new file mode 100644 index 0000000..d2b2f8f --- /dev/null +++ b/resources/imageFiles/emojis/1132.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1133.svg b/resources/imageFiles/emojis/1133.svg new file mode 100644 index 0000000..993cd3f --- /dev/null +++ b/resources/imageFiles/emojis/1133.svg @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1134.svg b/resources/imageFiles/emojis/1134.svg new file mode 100644 index 0000000..09c8c07 --- /dev/null +++ b/resources/imageFiles/emojis/1134.svg @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1135.svg b/resources/imageFiles/emojis/1135.svg new file mode 100644 index 0000000..9bb7015 --- /dev/null +++ b/resources/imageFiles/emojis/1135.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1136.svg b/resources/imageFiles/emojis/1136.svg new file mode 100644 index 0000000..0dbfdbd --- /dev/null +++ b/resources/imageFiles/emojis/1136.svg @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1137.svg b/resources/imageFiles/emojis/1137.svg new file mode 100644 index 0000000..6801c7b --- /dev/null +++ b/resources/imageFiles/emojis/1137.svg @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1138.svg b/resources/imageFiles/emojis/1138.svg new file mode 100644 index 0000000..c5f9f69 --- /dev/null +++ b/resources/imageFiles/emojis/1138.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1139.svg b/resources/imageFiles/emojis/1139.svg new file mode 100644 index 0000000..32161f0 --- /dev/null +++ b/resources/imageFiles/emojis/1139.svg @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1140.svg b/resources/imageFiles/emojis/1140.svg new file mode 100644 index 0000000..fc967be --- /dev/null +++ b/resources/imageFiles/emojis/1140.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1141.svg b/resources/imageFiles/emojis/1141.svg new file mode 100644 index 0000000..d0a3cd8 --- /dev/null +++ b/resources/imageFiles/emojis/1141.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1142.svg b/resources/imageFiles/emojis/1142.svg new file mode 100644 index 0000000..cdd546b --- /dev/null +++ b/resources/imageFiles/emojis/1142.svg @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1143.svg b/resources/imageFiles/emojis/1143.svg new file mode 100644 index 0000000..70b1988 --- /dev/null +++ b/resources/imageFiles/emojis/1143.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1144.svg b/resources/imageFiles/emojis/1144.svg new file mode 100644 index 0000000..ccc5423 --- /dev/null +++ b/resources/imageFiles/emojis/1144.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1145.svg b/resources/imageFiles/emojis/1145.svg new file mode 100644 index 0000000..d6c7ef7 --- /dev/null +++ b/resources/imageFiles/emojis/1145.svg @@ -0,0 +1,29 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1146.svg b/resources/imageFiles/emojis/1146.svg new file mode 100644 index 0000000..a79c65a --- /dev/null +++ b/resources/imageFiles/emojis/1146.svg @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1147.svg b/resources/imageFiles/emojis/1147.svg new file mode 100644 index 0000000..5845be1 --- /dev/null +++ b/resources/imageFiles/emojis/1147.svg @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1148.svg b/resources/imageFiles/emojis/1148.svg new file mode 100644 index 0000000..79ead35 --- /dev/null +++ b/resources/imageFiles/emojis/1148.svg @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1149.svg b/resources/imageFiles/emojis/1149.svg new file mode 100644 index 0000000..1c366f0 --- /dev/null +++ b/resources/imageFiles/emojis/1149.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1150.svg b/resources/imageFiles/emojis/1150.svg new file mode 100644 index 0000000..92de9ce --- /dev/null +++ b/resources/imageFiles/emojis/1150.svg @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1151.svg b/resources/imageFiles/emojis/1151.svg new file mode 100644 index 0000000..9e6c0d0 --- /dev/null +++ b/resources/imageFiles/emojis/1151.svg @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1152.svg b/resources/imageFiles/emojis/1152.svg new file mode 100644 index 0000000..8dddf3b --- /dev/null +++ b/resources/imageFiles/emojis/1152.svg @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1153.svg b/resources/imageFiles/emojis/1153.svg new file mode 100644 index 0000000..0c7192e --- /dev/null +++ b/resources/imageFiles/emojis/1153.svg @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1154.svg b/resources/imageFiles/emojis/1154.svg new file mode 100644 index 0000000..42d0d27 --- /dev/null +++ b/resources/imageFiles/emojis/1154.svg @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1155.svg b/resources/imageFiles/emojis/1155.svg new file mode 100644 index 0000000..fd7e8bb --- /dev/null +++ b/resources/imageFiles/emojis/1155.svg @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1156.svg b/resources/imageFiles/emojis/1156.svg new file mode 100644 index 0000000..47b5988 --- /dev/null +++ b/resources/imageFiles/emojis/1156.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1157.svg b/resources/imageFiles/emojis/1157.svg new file mode 100644 index 0000000..76c7c6b --- /dev/null +++ b/resources/imageFiles/emojis/1157.svg @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1158.svg b/resources/imageFiles/emojis/1158.svg new file mode 100644 index 0000000..013e385 --- /dev/null +++ b/resources/imageFiles/emojis/1158.svg @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1159.svg b/resources/imageFiles/emojis/1159.svg new file mode 100644 index 0000000..ab2a4ca --- /dev/null +++ b/resources/imageFiles/emojis/1159.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1160.svg b/resources/imageFiles/emojis/1160.svg new file mode 100644 index 0000000..384e481 --- /dev/null +++ b/resources/imageFiles/emojis/1160.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1161.svg b/resources/imageFiles/emojis/1161.svg new file mode 100644 index 0000000..9408108 --- /dev/null +++ b/resources/imageFiles/emojis/1161.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1162.svg b/resources/imageFiles/emojis/1162.svg new file mode 100644 index 0000000..fd1b175 --- /dev/null +++ b/resources/imageFiles/emojis/1162.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1163.svg b/resources/imageFiles/emojis/1163.svg new file mode 100644 index 0000000..66af9d2 --- /dev/null +++ b/resources/imageFiles/emojis/1163.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1164.svg b/resources/imageFiles/emojis/1164.svg new file mode 100644 index 0000000..abc498b --- /dev/null +++ b/resources/imageFiles/emojis/1164.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1165.svg b/resources/imageFiles/emojis/1165.svg new file mode 100644 index 0000000..37f7954 --- /dev/null +++ b/resources/imageFiles/emojis/1165.svg @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1166.svg b/resources/imageFiles/emojis/1166.svg new file mode 100644 index 0000000..226bcd8 --- /dev/null +++ b/resources/imageFiles/emojis/1166.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1167.svg b/resources/imageFiles/emojis/1167.svg new file mode 100644 index 0000000..958e99a --- /dev/null +++ b/resources/imageFiles/emojis/1167.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1168.svg b/resources/imageFiles/emojis/1168.svg new file mode 100644 index 0000000..a117e1c --- /dev/null +++ b/resources/imageFiles/emojis/1168.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1169.svg b/resources/imageFiles/emojis/1169.svg new file mode 100644 index 0000000..fda938b --- /dev/null +++ b/resources/imageFiles/emojis/1169.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1170.svg b/resources/imageFiles/emojis/1170.svg new file mode 100644 index 0000000..3d54c3a --- /dev/null +++ b/resources/imageFiles/emojis/1170.svg @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1171.svg b/resources/imageFiles/emojis/1171.svg new file mode 100644 index 0000000..f0cac21 --- /dev/null +++ b/resources/imageFiles/emojis/1171.svg @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1172.svg b/resources/imageFiles/emojis/1172.svg new file mode 100644 index 0000000..0beed8a --- /dev/null +++ b/resources/imageFiles/emojis/1172.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1173.svg b/resources/imageFiles/emojis/1173.svg new file mode 100644 index 0000000..e5ec17c --- /dev/null +++ b/resources/imageFiles/emojis/1173.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1174.svg b/resources/imageFiles/emojis/1174.svg new file mode 100644 index 0000000..fff4d79 --- /dev/null +++ b/resources/imageFiles/emojis/1174.svg @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1175.svg b/resources/imageFiles/emojis/1175.svg new file mode 100644 index 0000000..79ae1fb --- /dev/null +++ b/resources/imageFiles/emojis/1175.svg @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1176.svg b/resources/imageFiles/emojis/1176.svg new file mode 100644 index 0000000..fde6ec0 --- /dev/null +++ b/resources/imageFiles/emojis/1176.svg @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1177.svg b/resources/imageFiles/emojis/1177.svg new file mode 100644 index 0000000..b8de804 --- /dev/null +++ b/resources/imageFiles/emojis/1177.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1178.svg b/resources/imageFiles/emojis/1178.svg new file mode 100644 index 0000000..b7ce61e --- /dev/null +++ b/resources/imageFiles/emojis/1178.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1179.svg b/resources/imageFiles/emojis/1179.svg new file mode 100644 index 0000000..bdebbf1 --- /dev/null +++ b/resources/imageFiles/emojis/1179.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1180.svg b/resources/imageFiles/emojis/1180.svg new file mode 100644 index 0000000..8760825 --- /dev/null +++ b/resources/imageFiles/emojis/1180.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1181.svg b/resources/imageFiles/emojis/1181.svg new file mode 100644 index 0000000..2d1bd06 --- /dev/null +++ b/resources/imageFiles/emojis/1181.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1182.svg b/resources/imageFiles/emojis/1182.svg new file mode 100644 index 0000000..c9a3ffb --- /dev/null +++ b/resources/imageFiles/emojis/1182.svg @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1183.svg b/resources/imageFiles/emojis/1183.svg new file mode 100644 index 0000000..d987f8c --- /dev/null +++ b/resources/imageFiles/emojis/1183.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1184.svg b/resources/imageFiles/emojis/1184.svg new file mode 100644 index 0000000..4369d35 --- /dev/null +++ b/resources/imageFiles/emojis/1184.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1185.svg b/resources/imageFiles/emojis/1185.svg new file mode 100644 index 0000000..6519b29 --- /dev/null +++ b/resources/imageFiles/emojis/1185.svg @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1186.svg b/resources/imageFiles/emojis/1186.svg new file mode 100644 index 0000000..d5469bb --- /dev/null +++ b/resources/imageFiles/emojis/1186.svg @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1187.svg b/resources/imageFiles/emojis/1187.svg new file mode 100644 index 0000000..f534752 --- /dev/null +++ b/resources/imageFiles/emojis/1187.svg @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1188.svg b/resources/imageFiles/emojis/1188.svg new file mode 100644 index 0000000..da7338c --- /dev/null +++ b/resources/imageFiles/emojis/1188.svg @@ -0,0 +1,34 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1189.svg b/resources/imageFiles/emojis/1189.svg new file mode 100644 index 0000000..d8f9c67 --- /dev/null +++ b/resources/imageFiles/emojis/1189.svg @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1190.svg b/resources/imageFiles/emojis/1190.svg new file mode 100644 index 0000000..e2cbc66 --- /dev/null +++ b/resources/imageFiles/emojis/1190.svg @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1191.svg b/resources/imageFiles/emojis/1191.svg new file mode 100644 index 0000000..8cef72d --- /dev/null +++ b/resources/imageFiles/emojis/1191.svg @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1192.svg b/resources/imageFiles/emojis/1192.svg new file mode 100644 index 0000000..6091b54 --- /dev/null +++ b/resources/imageFiles/emojis/1192.svg @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1193.svg b/resources/imageFiles/emojis/1193.svg new file mode 100644 index 0000000..f8ee1f0 --- /dev/null +++ b/resources/imageFiles/emojis/1193.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1194.svg b/resources/imageFiles/emojis/1194.svg new file mode 100644 index 0000000..6c9aa4d --- /dev/null +++ b/resources/imageFiles/emojis/1194.svg @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1195.svg b/resources/imageFiles/emojis/1195.svg new file mode 100644 index 0000000..be79795 --- /dev/null +++ b/resources/imageFiles/emojis/1195.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1196.svg b/resources/imageFiles/emojis/1196.svg new file mode 100644 index 0000000..3c875a0 --- /dev/null +++ b/resources/imageFiles/emojis/1196.svg @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1197.svg b/resources/imageFiles/emojis/1197.svg new file mode 100644 index 0000000..20ca88b --- /dev/null +++ b/resources/imageFiles/emojis/1197.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1198.svg b/resources/imageFiles/emojis/1198.svg new file mode 100644 index 0000000..62bd5c0 --- /dev/null +++ b/resources/imageFiles/emojis/1198.svg @@ -0,0 +1,45 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1199.svg b/resources/imageFiles/emojis/1199.svg new file mode 100644 index 0000000..0f12d60 --- /dev/null +++ b/resources/imageFiles/emojis/1199.svg @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1200.svg b/resources/imageFiles/emojis/1200.svg new file mode 100644 index 0000000..df3df46 --- /dev/null +++ b/resources/imageFiles/emojis/1200.svg @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1201.svg b/resources/imageFiles/emojis/1201.svg new file mode 100644 index 0000000..7a48281 --- /dev/null +++ b/resources/imageFiles/emojis/1201.svg @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1202.svg b/resources/imageFiles/emojis/1202.svg new file mode 100644 index 0000000..40c8d81 --- /dev/null +++ b/resources/imageFiles/emojis/1202.svg @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1203.svg b/resources/imageFiles/emojis/1203.svg new file mode 100644 index 0000000..dcf0c97 --- /dev/null +++ b/resources/imageFiles/emojis/1203.svg @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1204.svg b/resources/imageFiles/emojis/1204.svg new file mode 100644 index 0000000..15ee694 --- /dev/null +++ b/resources/imageFiles/emojis/1204.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1205.svg b/resources/imageFiles/emojis/1205.svg new file mode 100644 index 0000000..b4d67e9 --- /dev/null +++ b/resources/imageFiles/emojis/1205.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1206.svg b/resources/imageFiles/emojis/1206.svg new file mode 100644 index 0000000..2e3f32f --- /dev/null +++ b/resources/imageFiles/emojis/1206.svg @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1207.svg b/resources/imageFiles/emojis/1207.svg new file mode 100644 index 0000000..a7f28fc --- /dev/null +++ b/resources/imageFiles/emojis/1207.svg @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1208.svg b/resources/imageFiles/emojis/1208.svg new file mode 100644 index 0000000..508d98e --- /dev/null +++ b/resources/imageFiles/emojis/1208.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1209.svg b/resources/imageFiles/emojis/1209.svg new file mode 100644 index 0000000..d6fe64f --- /dev/null +++ b/resources/imageFiles/emojis/1209.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1210.svg b/resources/imageFiles/emojis/1210.svg new file mode 100644 index 0000000..61f2098 --- /dev/null +++ b/resources/imageFiles/emojis/1210.svg @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1211.svg b/resources/imageFiles/emojis/1211.svg new file mode 100644 index 0000000..2c55a6d --- /dev/null +++ b/resources/imageFiles/emojis/1211.svg @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1212.svg b/resources/imageFiles/emojis/1212.svg new file mode 100644 index 0000000..85455e6 --- /dev/null +++ b/resources/imageFiles/emojis/1212.svg @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1213.svg b/resources/imageFiles/emojis/1213.svg new file mode 100644 index 0000000..b7c88aa --- /dev/null +++ b/resources/imageFiles/emojis/1213.svg @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1214.svg b/resources/imageFiles/emojis/1214.svg new file mode 100644 index 0000000..b217abb --- /dev/null +++ b/resources/imageFiles/emojis/1214.svg @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1215.svg b/resources/imageFiles/emojis/1215.svg new file mode 100644 index 0000000..c2be975 --- /dev/null +++ b/resources/imageFiles/emojis/1215.svg @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1216.svg b/resources/imageFiles/emojis/1216.svg new file mode 100644 index 0000000..d785694 --- /dev/null +++ b/resources/imageFiles/emojis/1216.svg @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1217.svg b/resources/imageFiles/emojis/1217.svg new file mode 100644 index 0000000..ce56a7f --- /dev/null +++ b/resources/imageFiles/emojis/1217.svg @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1218.svg b/resources/imageFiles/emojis/1218.svg new file mode 100644 index 0000000..96fccc0 --- /dev/null +++ b/resources/imageFiles/emojis/1218.svg @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1219.svg b/resources/imageFiles/emojis/1219.svg new file mode 100644 index 0000000..bf6fc67 --- /dev/null +++ b/resources/imageFiles/emojis/1219.svg @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1220.svg b/resources/imageFiles/emojis/1220.svg new file mode 100644 index 0000000..63fbba1 --- /dev/null +++ b/resources/imageFiles/emojis/1220.svg @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1221.svg b/resources/imageFiles/emojis/1221.svg new file mode 100644 index 0000000..6a700eb --- /dev/null +++ b/resources/imageFiles/emojis/1221.svg @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1222.svg b/resources/imageFiles/emojis/1222.svg new file mode 100644 index 0000000..da01966 --- /dev/null +++ b/resources/imageFiles/emojis/1222.svg @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1223.svg b/resources/imageFiles/emojis/1223.svg new file mode 100644 index 0000000..695acb6 --- /dev/null +++ b/resources/imageFiles/emojis/1223.svg @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1224.svg b/resources/imageFiles/emojis/1224.svg new file mode 100644 index 0000000..825bd1a --- /dev/null +++ b/resources/imageFiles/emojis/1224.svg @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1225.svg b/resources/imageFiles/emojis/1225.svg new file mode 100644 index 0000000..f16681f --- /dev/null +++ b/resources/imageFiles/emojis/1225.svg @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1226.svg b/resources/imageFiles/emojis/1226.svg new file mode 100644 index 0000000..b510a50 --- /dev/null +++ b/resources/imageFiles/emojis/1226.svg @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1227.svg b/resources/imageFiles/emojis/1227.svg new file mode 100644 index 0000000..0c972c3 --- /dev/null +++ b/resources/imageFiles/emojis/1227.svg @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1228.svg b/resources/imageFiles/emojis/1228.svg new file mode 100644 index 0000000..ada8ec4 --- /dev/null +++ b/resources/imageFiles/emojis/1228.svg @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1229.svg b/resources/imageFiles/emojis/1229.svg new file mode 100644 index 0000000..086f879 --- /dev/null +++ b/resources/imageFiles/emojis/1229.svg @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1230.svg b/resources/imageFiles/emojis/1230.svg new file mode 100644 index 0000000..6fef698 --- /dev/null +++ b/resources/imageFiles/emojis/1230.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1231.svg b/resources/imageFiles/emojis/1231.svg new file mode 100644 index 0000000..bef11c3 --- /dev/null +++ b/resources/imageFiles/emojis/1231.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1232.svg b/resources/imageFiles/emojis/1232.svg new file mode 100644 index 0000000..5497f0f --- /dev/null +++ b/resources/imageFiles/emojis/1232.svg @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1233.svg b/resources/imageFiles/emojis/1233.svg new file mode 100644 index 0000000..6c3ee41 --- /dev/null +++ b/resources/imageFiles/emojis/1233.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1234.svg b/resources/imageFiles/emojis/1234.svg new file mode 100644 index 0000000..2f21edf --- /dev/null +++ b/resources/imageFiles/emojis/1234.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1235.svg b/resources/imageFiles/emojis/1235.svg new file mode 100644 index 0000000..9cc1ae0 --- /dev/null +++ b/resources/imageFiles/emojis/1235.svg @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1236.svg b/resources/imageFiles/emojis/1236.svg new file mode 100644 index 0000000..62c0e56 --- /dev/null +++ b/resources/imageFiles/emojis/1236.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1237.svg b/resources/imageFiles/emojis/1237.svg new file mode 100644 index 0000000..69cb631 --- /dev/null +++ b/resources/imageFiles/emojis/1237.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1238.svg b/resources/imageFiles/emojis/1238.svg new file mode 100644 index 0000000..01f9016 --- /dev/null +++ b/resources/imageFiles/emojis/1238.svg @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1239.svg b/resources/imageFiles/emojis/1239.svg new file mode 100644 index 0000000..f73b582 --- /dev/null +++ b/resources/imageFiles/emojis/1239.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1240.svg b/resources/imageFiles/emojis/1240.svg new file mode 100644 index 0000000..09ac372 --- /dev/null +++ b/resources/imageFiles/emojis/1240.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1241.svg b/resources/imageFiles/emojis/1241.svg new file mode 100644 index 0000000..df913e4 --- /dev/null +++ b/resources/imageFiles/emojis/1241.svg @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1242.svg b/resources/imageFiles/emojis/1242.svg new file mode 100644 index 0000000..d3cf6ad --- /dev/null +++ b/resources/imageFiles/emojis/1242.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1243.svg b/resources/imageFiles/emojis/1243.svg new file mode 100644 index 0000000..da633d3 --- /dev/null +++ b/resources/imageFiles/emojis/1243.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1244.svg b/resources/imageFiles/emojis/1244.svg new file mode 100644 index 0000000..3527f91 --- /dev/null +++ b/resources/imageFiles/emojis/1244.svg @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1245.svg b/resources/imageFiles/emojis/1245.svg new file mode 100644 index 0000000..13ab39e --- /dev/null +++ b/resources/imageFiles/emojis/1245.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1246.svg b/resources/imageFiles/emojis/1246.svg new file mode 100644 index 0000000..310629a --- /dev/null +++ b/resources/imageFiles/emojis/1246.svg @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1247.svg b/resources/imageFiles/emojis/1247.svg new file mode 100644 index 0000000..b0ac3ba --- /dev/null +++ b/resources/imageFiles/emojis/1247.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1248.svg b/resources/imageFiles/emojis/1248.svg new file mode 100644 index 0000000..0ae925c --- /dev/null +++ b/resources/imageFiles/emojis/1248.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1249.svg b/resources/imageFiles/emojis/1249.svg new file mode 100644 index 0000000..2d64659 --- /dev/null +++ b/resources/imageFiles/emojis/1249.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1250.svg b/resources/imageFiles/emojis/1250.svg new file mode 100644 index 0000000..71ae237 --- /dev/null +++ b/resources/imageFiles/emojis/1250.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1251.svg b/resources/imageFiles/emojis/1251.svg new file mode 100644 index 0000000..c049d74 --- /dev/null +++ b/resources/imageFiles/emojis/1251.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1252.svg b/resources/imageFiles/emojis/1252.svg new file mode 100644 index 0000000..00dd6ed --- /dev/null +++ b/resources/imageFiles/emojis/1252.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1253.svg b/resources/imageFiles/emojis/1253.svg new file mode 100644 index 0000000..c102340 --- /dev/null +++ b/resources/imageFiles/emojis/1253.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1254.svg b/resources/imageFiles/emojis/1254.svg new file mode 100644 index 0000000..ca0f206 --- /dev/null +++ b/resources/imageFiles/emojis/1254.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1255.svg b/resources/imageFiles/emojis/1255.svg new file mode 100644 index 0000000..abaedb0 --- /dev/null +++ b/resources/imageFiles/emojis/1255.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1256.svg b/resources/imageFiles/emojis/1256.svg new file mode 100644 index 0000000..ec2e734 --- /dev/null +++ b/resources/imageFiles/emojis/1256.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1257.svg b/resources/imageFiles/emojis/1257.svg new file mode 100644 index 0000000..55ad73c --- /dev/null +++ b/resources/imageFiles/emojis/1257.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1258.svg b/resources/imageFiles/emojis/1258.svg new file mode 100644 index 0000000..d7af60a --- /dev/null +++ b/resources/imageFiles/emojis/1258.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1259.svg b/resources/imageFiles/emojis/1259.svg new file mode 100644 index 0000000..378f3e0 --- /dev/null +++ b/resources/imageFiles/emojis/1259.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1260.svg b/resources/imageFiles/emojis/1260.svg new file mode 100644 index 0000000..003809d --- /dev/null +++ b/resources/imageFiles/emojis/1260.svg @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1261.svg b/resources/imageFiles/emojis/1261.svg new file mode 100644 index 0000000..9ba2c4b --- /dev/null +++ b/resources/imageFiles/emojis/1261.svg @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1262.svg b/resources/imageFiles/emojis/1262.svg new file mode 100644 index 0000000..95ff35b --- /dev/null +++ b/resources/imageFiles/emojis/1262.svg @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1263.svg b/resources/imageFiles/emojis/1263.svg new file mode 100644 index 0000000..f2e17ce --- /dev/null +++ b/resources/imageFiles/emojis/1263.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1264.svg b/resources/imageFiles/emojis/1264.svg new file mode 100644 index 0000000..eb572a5 --- /dev/null +++ b/resources/imageFiles/emojis/1264.svg @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1265.svg b/resources/imageFiles/emojis/1265.svg new file mode 100644 index 0000000..b1eecdb --- /dev/null +++ b/resources/imageFiles/emojis/1265.svg @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1266.svg b/resources/imageFiles/emojis/1266.svg new file mode 100644 index 0000000..8e9a48a --- /dev/null +++ b/resources/imageFiles/emojis/1266.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1267.svg b/resources/imageFiles/emojis/1267.svg new file mode 100644 index 0000000..1236525 --- /dev/null +++ b/resources/imageFiles/emojis/1267.svg @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1268.svg b/resources/imageFiles/emojis/1268.svg new file mode 100644 index 0000000..9f925a3 --- /dev/null +++ b/resources/imageFiles/emojis/1268.svg @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1269.svg b/resources/imageFiles/emojis/1269.svg new file mode 100644 index 0000000..0280423 --- /dev/null +++ b/resources/imageFiles/emojis/1269.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1270.svg b/resources/imageFiles/emojis/1270.svg new file mode 100644 index 0000000..541f702 --- /dev/null +++ b/resources/imageFiles/emojis/1270.svg @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1271.svg b/resources/imageFiles/emojis/1271.svg new file mode 100644 index 0000000..730392e --- /dev/null +++ b/resources/imageFiles/emojis/1271.svg @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1272.svg b/resources/imageFiles/emojis/1272.svg new file mode 100644 index 0000000..3e8b9e6 --- /dev/null +++ b/resources/imageFiles/emojis/1272.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1273.svg b/resources/imageFiles/emojis/1273.svg new file mode 100644 index 0000000..2bf4a76 --- /dev/null +++ b/resources/imageFiles/emojis/1273.svg @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1274.svg b/resources/imageFiles/emojis/1274.svg new file mode 100644 index 0000000..247a225 --- /dev/null +++ b/resources/imageFiles/emojis/1274.svg @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1275.svg b/resources/imageFiles/emojis/1275.svg new file mode 100644 index 0000000..ba86894 --- /dev/null +++ b/resources/imageFiles/emojis/1275.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1276.svg b/resources/imageFiles/emojis/1276.svg new file mode 100644 index 0000000..a70b3ad --- /dev/null +++ b/resources/imageFiles/emojis/1276.svg @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1277.svg b/resources/imageFiles/emojis/1277.svg new file mode 100644 index 0000000..9156740 --- /dev/null +++ b/resources/imageFiles/emojis/1277.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1278.svg b/resources/imageFiles/emojis/1278.svg new file mode 100644 index 0000000..6233478 --- /dev/null +++ b/resources/imageFiles/emojis/1278.svg @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1279.svg b/resources/imageFiles/emojis/1279.svg new file mode 100644 index 0000000..bb7cd3c --- /dev/null +++ b/resources/imageFiles/emojis/1279.svg @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1280.svg b/resources/imageFiles/emojis/1280.svg new file mode 100644 index 0000000..62fbdd4 --- /dev/null +++ b/resources/imageFiles/emojis/1280.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1281.svg b/resources/imageFiles/emojis/1281.svg new file mode 100644 index 0000000..8426f73 --- /dev/null +++ b/resources/imageFiles/emojis/1281.svg @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1282.svg b/resources/imageFiles/emojis/1282.svg new file mode 100644 index 0000000..b73de77 --- /dev/null +++ b/resources/imageFiles/emojis/1282.svg @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1283.svg b/resources/imageFiles/emojis/1283.svg new file mode 100644 index 0000000..fa1e7bd --- /dev/null +++ b/resources/imageFiles/emojis/1283.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1284.svg b/resources/imageFiles/emojis/1284.svg new file mode 100644 index 0000000..0c7e467 --- /dev/null +++ b/resources/imageFiles/emojis/1284.svg @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1285.svg b/resources/imageFiles/emojis/1285.svg new file mode 100644 index 0000000..9cce4fc --- /dev/null +++ b/resources/imageFiles/emojis/1285.svg @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1286.svg b/resources/imageFiles/emojis/1286.svg new file mode 100644 index 0000000..08a310c --- /dev/null +++ b/resources/imageFiles/emojis/1286.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1287.svg b/resources/imageFiles/emojis/1287.svg new file mode 100644 index 0000000..80720f9 --- /dev/null +++ b/resources/imageFiles/emojis/1287.svg @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1288.svg b/resources/imageFiles/emojis/1288.svg new file mode 100644 index 0000000..194ef49 --- /dev/null +++ b/resources/imageFiles/emojis/1288.svg @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1289.svg b/resources/imageFiles/emojis/1289.svg new file mode 100644 index 0000000..eb1906d --- /dev/null +++ b/resources/imageFiles/emojis/1289.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1290.svg b/resources/imageFiles/emojis/1290.svg new file mode 100644 index 0000000..07751f4 --- /dev/null +++ b/resources/imageFiles/emojis/1290.svg @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1291.svg b/resources/imageFiles/emojis/1291.svg new file mode 100644 index 0000000..332a399 --- /dev/null +++ b/resources/imageFiles/emojis/1291.svg @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1292.svg b/resources/imageFiles/emojis/1292.svg new file mode 100644 index 0000000..441198e --- /dev/null +++ b/resources/imageFiles/emojis/1292.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1293.svg b/resources/imageFiles/emojis/1293.svg new file mode 100644 index 0000000..fe14995 --- /dev/null +++ b/resources/imageFiles/emojis/1293.svg @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1294.svg b/resources/imageFiles/emojis/1294.svg new file mode 100644 index 0000000..72ff3ac --- /dev/null +++ b/resources/imageFiles/emojis/1294.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1295.svg b/resources/imageFiles/emojis/1295.svg new file mode 100644 index 0000000..ec7576c --- /dev/null +++ b/resources/imageFiles/emojis/1295.svg @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1296.svg b/resources/imageFiles/emojis/1296.svg new file mode 100644 index 0000000..164eb8b --- /dev/null +++ b/resources/imageFiles/emojis/1296.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1297.svg b/resources/imageFiles/emojis/1297.svg new file mode 100644 index 0000000..d6f2f9a --- /dev/null +++ b/resources/imageFiles/emojis/1297.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1298.svg b/resources/imageFiles/emojis/1298.svg new file mode 100644 index 0000000..cc92cda --- /dev/null +++ b/resources/imageFiles/emojis/1298.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1299.svg b/resources/imageFiles/emojis/1299.svg new file mode 100644 index 0000000..3c83b7a --- /dev/null +++ b/resources/imageFiles/emojis/1299.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1300.svg b/resources/imageFiles/emojis/1300.svg new file mode 100644 index 0000000..808b3b5 --- /dev/null +++ b/resources/imageFiles/emojis/1300.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1301.svg b/resources/imageFiles/emojis/1301.svg new file mode 100644 index 0000000..489cee5 --- /dev/null +++ b/resources/imageFiles/emojis/1301.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1302.svg b/resources/imageFiles/emojis/1302.svg new file mode 100644 index 0000000..8bd475f --- /dev/null +++ b/resources/imageFiles/emojis/1302.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1303.svg b/resources/imageFiles/emojis/1303.svg new file mode 100644 index 0000000..b671698 --- /dev/null +++ b/resources/imageFiles/emojis/1303.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1304.svg b/resources/imageFiles/emojis/1304.svg new file mode 100644 index 0000000..2e9c55a --- /dev/null +++ b/resources/imageFiles/emojis/1304.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1305.svg b/resources/imageFiles/emojis/1305.svg new file mode 100644 index 0000000..06a4fa0 --- /dev/null +++ b/resources/imageFiles/emojis/1305.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1306.svg b/resources/imageFiles/emojis/1306.svg new file mode 100644 index 0000000..17e387e --- /dev/null +++ b/resources/imageFiles/emojis/1306.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1307.svg b/resources/imageFiles/emojis/1307.svg new file mode 100644 index 0000000..b4dbd76 --- /dev/null +++ b/resources/imageFiles/emojis/1307.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1308.svg b/resources/imageFiles/emojis/1308.svg new file mode 100644 index 0000000..101de2b --- /dev/null +++ b/resources/imageFiles/emojis/1308.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1309.svg b/resources/imageFiles/emojis/1309.svg new file mode 100644 index 0000000..258ca75 --- /dev/null +++ b/resources/imageFiles/emojis/1309.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1310.svg b/resources/imageFiles/emojis/1310.svg new file mode 100644 index 0000000..e1a8670 --- /dev/null +++ b/resources/imageFiles/emojis/1310.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1311.svg b/resources/imageFiles/emojis/1311.svg new file mode 100644 index 0000000..a22fc5f --- /dev/null +++ b/resources/imageFiles/emojis/1311.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1312.svg b/resources/imageFiles/emojis/1312.svg new file mode 100644 index 0000000..4987f9f --- /dev/null +++ b/resources/imageFiles/emojis/1312.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1313.svg b/resources/imageFiles/emojis/1313.svg new file mode 100644 index 0000000..58c6cad --- /dev/null +++ b/resources/imageFiles/emojis/1313.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1314.svg b/resources/imageFiles/emojis/1314.svg new file mode 100644 index 0000000..2624619 --- /dev/null +++ b/resources/imageFiles/emojis/1314.svg @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1315.svg b/resources/imageFiles/emojis/1315.svg new file mode 100644 index 0000000..95add4e --- /dev/null +++ b/resources/imageFiles/emojis/1315.svg @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1316.svg b/resources/imageFiles/emojis/1316.svg new file mode 100644 index 0000000..3279a88 --- /dev/null +++ b/resources/imageFiles/emojis/1316.svg @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1317.svg b/resources/imageFiles/emojis/1317.svg new file mode 100644 index 0000000..c1d39b6 --- /dev/null +++ b/resources/imageFiles/emojis/1317.svg @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1318.svg b/resources/imageFiles/emojis/1318.svg new file mode 100644 index 0000000..cd94be9 --- /dev/null +++ b/resources/imageFiles/emojis/1318.svg @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1319.svg b/resources/imageFiles/emojis/1319.svg new file mode 100644 index 0000000..55c9fd3 --- /dev/null +++ b/resources/imageFiles/emojis/1319.svg @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1320.svg b/resources/imageFiles/emojis/1320.svg new file mode 100644 index 0000000..b7efea6 --- /dev/null +++ b/resources/imageFiles/emojis/1320.svg @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1321.svg b/resources/imageFiles/emojis/1321.svg new file mode 100644 index 0000000..b95d67b --- /dev/null +++ b/resources/imageFiles/emojis/1321.svg @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1322.svg b/resources/imageFiles/emojis/1322.svg new file mode 100644 index 0000000..9a8dfb7 --- /dev/null +++ b/resources/imageFiles/emojis/1322.svg @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1323.svg b/resources/imageFiles/emojis/1323.svg new file mode 100644 index 0000000..7c91a76 --- /dev/null +++ b/resources/imageFiles/emojis/1323.svg @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1324.svg b/resources/imageFiles/emojis/1324.svg new file mode 100644 index 0000000..0ef9f93 --- /dev/null +++ b/resources/imageFiles/emojis/1324.svg @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1325.svg b/resources/imageFiles/emojis/1325.svg new file mode 100644 index 0000000..8d2e0e8 --- /dev/null +++ b/resources/imageFiles/emojis/1325.svg @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1326.svg b/resources/imageFiles/emojis/1326.svg new file mode 100644 index 0000000..68a0c0b --- /dev/null +++ b/resources/imageFiles/emojis/1326.svg @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1327.svg b/resources/imageFiles/emojis/1327.svg new file mode 100644 index 0000000..b36acb7 --- /dev/null +++ b/resources/imageFiles/emojis/1327.svg @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1328.svg b/resources/imageFiles/emojis/1328.svg new file mode 100644 index 0000000..625dfdf --- /dev/null +++ b/resources/imageFiles/emojis/1328.svg @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1329.svg b/resources/imageFiles/emojis/1329.svg new file mode 100644 index 0000000..da3673b --- /dev/null +++ b/resources/imageFiles/emojis/1329.svg @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1330.svg b/resources/imageFiles/emojis/1330.svg new file mode 100644 index 0000000..f14b08d --- /dev/null +++ b/resources/imageFiles/emojis/1330.svg @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1331.svg b/resources/imageFiles/emojis/1331.svg new file mode 100644 index 0000000..2663bf8 --- /dev/null +++ b/resources/imageFiles/emojis/1331.svg @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1332.svg b/resources/imageFiles/emojis/1332.svg new file mode 100644 index 0000000..617ef34 --- /dev/null +++ b/resources/imageFiles/emojis/1332.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1333.svg b/resources/imageFiles/emojis/1333.svg new file mode 100644 index 0000000..936609e --- /dev/null +++ b/resources/imageFiles/emojis/1333.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1334.svg b/resources/imageFiles/emojis/1334.svg new file mode 100644 index 0000000..244a91f --- /dev/null +++ b/resources/imageFiles/emojis/1334.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1335.svg b/resources/imageFiles/emojis/1335.svg new file mode 100644 index 0000000..4fd070a --- /dev/null +++ b/resources/imageFiles/emojis/1335.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1336.svg b/resources/imageFiles/emojis/1336.svg new file mode 100644 index 0000000..56acc6f --- /dev/null +++ b/resources/imageFiles/emojis/1336.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1337.svg b/resources/imageFiles/emojis/1337.svg new file mode 100644 index 0000000..7c9a02d --- /dev/null +++ b/resources/imageFiles/emojis/1337.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1338.svg b/resources/imageFiles/emojis/1338.svg new file mode 100644 index 0000000..f53ff44 --- /dev/null +++ b/resources/imageFiles/emojis/1338.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1339.svg b/resources/imageFiles/emojis/1339.svg new file mode 100644 index 0000000..8747576 --- /dev/null +++ b/resources/imageFiles/emojis/1339.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1340.svg b/resources/imageFiles/emojis/1340.svg new file mode 100644 index 0000000..8ce7a31 --- /dev/null +++ b/resources/imageFiles/emojis/1340.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1341.svg b/resources/imageFiles/emojis/1341.svg new file mode 100644 index 0000000..46a0cb1 --- /dev/null +++ b/resources/imageFiles/emojis/1341.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1342.svg b/resources/imageFiles/emojis/1342.svg new file mode 100644 index 0000000..5065be7 --- /dev/null +++ b/resources/imageFiles/emojis/1342.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1343.svg b/resources/imageFiles/emojis/1343.svg new file mode 100644 index 0000000..7cb8a17 --- /dev/null +++ b/resources/imageFiles/emojis/1343.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1344.svg b/resources/imageFiles/emojis/1344.svg new file mode 100644 index 0000000..4d40620 --- /dev/null +++ b/resources/imageFiles/emojis/1344.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1345.svg b/resources/imageFiles/emojis/1345.svg new file mode 100644 index 0000000..ff17382 --- /dev/null +++ b/resources/imageFiles/emojis/1345.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1346.svg b/resources/imageFiles/emojis/1346.svg new file mode 100644 index 0000000..1b95498 --- /dev/null +++ b/resources/imageFiles/emojis/1346.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1347.svg b/resources/imageFiles/emojis/1347.svg new file mode 100644 index 0000000..944abc1 --- /dev/null +++ b/resources/imageFiles/emojis/1347.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1348.svg b/resources/imageFiles/emojis/1348.svg new file mode 100644 index 0000000..7ddf6d3 --- /dev/null +++ b/resources/imageFiles/emojis/1348.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1349.svg b/resources/imageFiles/emojis/1349.svg new file mode 100644 index 0000000..7f7eaab --- /dev/null +++ b/resources/imageFiles/emojis/1349.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1350.svg b/resources/imageFiles/emojis/1350.svg new file mode 100644 index 0000000..ad9dec4 --- /dev/null +++ b/resources/imageFiles/emojis/1350.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1351.svg b/resources/imageFiles/emojis/1351.svg new file mode 100644 index 0000000..8eba11b --- /dev/null +++ b/resources/imageFiles/emojis/1351.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1352.svg b/resources/imageFiles/emojis/1352.svg new file mode 100644 index 0000000..f489e93 --- /dev/null +++ b/resources/imageFiles/emojis/1352.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1353.svg b/resources/imageFiles/emojis/1353.svg new file mode 100644 index 0000000..185b8f4 --- /dev/null +++ b/resources/imageFiles/emojis/1353.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1354.svg b/resources/imageFiles/emojis/1354.svg new file mode 100644 index 0000000..201e296 --- /dev/null +++ b/resources/imageFiles/emojis/1354.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1355.svg b/resources/imageFiles/emojis/1355.svg new file mode 100644 index 0000000..1f64b10 --- /dev/null +++ b/resources/imageFiles/emojis/1355.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1356.svg b/resources/imageFiles/emojis/1356.svg new file mode 100644 index 0000000..77abbf7 --- /dev/null +++ b/resources/imageFiles/emojis/1356.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1357.svg b/resources/imageFiles/emojis/1357.svg new file mode 100644 index 0000000..37dcc76 --- /dev/null +++ b/resources/imageFiles/emojis/1357.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1358.svg b/resources/imageFiles/emojis/1358.svg new file mode 100644 index 0000000..0a941b6 --- /dev/null +++ b/resources/imageFiles/emojis/1358.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1359.svg b/resources/imageFiles/emojis/1359.svg new file mode 100644 index 0000000..dd8ce06 --- /dev/null +++ b/resources/imageFiles/emojis/1359.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1360.svg b/resources/imageFiles/emojis/1360.svg new file mode 100644 index 0000000..305a06b --- /dev/null +++ b/resources/imageFiles/emojis/1360.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1361.svg b/resources/imageFiles/emojis/1361.svg new file mode 100644 index 0000000..7450180 --- /dev/null +++ b/resources/imageFiles/emojis/1361.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1362.svg b/resources/imageFiles/emojis/1362.svg new file mode 100644 index 0000000..9415d2e --- /dev/null +++ b/resources/imageFiles/emojis/1362.svg @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1363.svg b/resources/imageFiles/emojis/1363.svg new file mode 100644 index 0000000..726eca9 --- /dev/null +++ b/resources/imageFiles/emojis/1363.svg @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1364.svg b/resources/imageFiles/emojis/1364.svg new file mode 100644 index 0000000..57f9400 --- /dev/null +++ b/resources/imageFiles/emojis/1364.svg @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1365.svg b/resources/imageFiles/emojis/1365.svg new file mode 100644 index 0000000..6bba5f9 --- /dev/null +++ b/resources/imageFiles/emojis/1365.svg @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1366.svg b/resources/imageFiles/emojis/1366.svg new file mode 100644 index 0000000..588ed2c --- /dev/null +++ b/resources/imageFiles/emojis/1366.svg @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1367.svg b/resources/imageFiles/emojis/1367.svg new file mode 100644 index 0000000..2bf62ca --- /dev/null +++ b/resources/imageFiles/emojis/1367.svg @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1368.svg b/resources/imageFiles/emojis/1368.svg new file mode 100644 index 0000000..e32efb4 --- /dev/null +++ b/resources/imageFiles/emojis/1368.svg @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1369.svg b/resources/imageFiles/emojis/1369.svg new file mode 100644 index 0000000..4f17da7 --- /dev/null +++ b/resources/imageFiles/emojis/1369.svg @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1370.svg b/resources/imageFiles/emojis/1370.svg new file mode 100644 index 0000000..45b074b --- /dev/null +++ b/resources/imageFiles/emojis/1370.svg @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1371.svg b/resources/imageFiles/emojis/1371.svg new file mode 100644 index 0000000..de8a7f7 --- /dev/null +++ b/resources/imageFiles/emojis/1371.svg @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1372.svg b/resources/imageFiles/emojis/1372.svg new file mode 100644 index 0000000..699dc9c --- /dev/null +++ b/resources/imageFiles/emojis/1372.svg @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1373.svg b/resources/imageFiles/emojis/1373.svg new file mode 100644 index 0000000..019d0e3 --- /dev/null +++ b/resources/imageFiles/emojis/1373.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1374.svg b/resources/imageFiles/emojis/1374.svg new file mode 100644 index 0000000..e44989f --- /dev/null +++ b/resources/imageFiles/emojis/1374.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1375.svg b/resources/imageFiles/emojis/1375.svg new file mode 100644 index 0000000..302ce4e --- /dev/null +++ b/resources/imageFiles/emojis/1375.svg @@ -0,0 +1,31 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1376.svg b/resources/imageFiles/emojis/1376.svg new file mode 100644 index 0000000..57a3a41 --- /dev/null +++ b/resources/imageFiles/emojis/1376.svg @@ -0,0 +1,31 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1377.svg b/resources/imageFiles/emojis/1377.svg new file mode 100644 index 0000000..50b502e --- /dev/null +++ b/resources/imageFiles/emojis/1377.svg @@ -0,0 +1,31 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1378.svg b/resources/imageFiles/emojis/1378.svg new file mode 100644 index 0000000..4284090 --- /dev/null +++ b/resources/imageFiles/emojis/1378.svg @@ -0,0 +1,31 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1379.svg b/resources/imageFiles/emojis/1379.svg new file mode 100644 index 0000000..ab7dbc9 --- /dev/null +++ b/resources/imageFiles/emojis/1379.svg @@ -0,0 +1,31 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1380.svg b/resources/imageFiles/emojis/1380.svg new file mode 100644 index 0000000..55aa925 --- /dev/null +++ b/resources/imageFiles/emojis/1380.svg @@ -0,0 +1,31 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1381.svg b/resources/imageFiles/emojis/1381.svg new file mode 100644 index 0000000..0063919 --- /dev/null +++ b/resources/imageFiles/emojis/1381.svg @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1382.svg b/resources/imageFiles/emojis/1382.svg new file mode 100644 index 0000000..5df8f18 --- /dev/null +++ b/resources/imageFiles/emojis/1382.svg @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1383.svg b/resources/imageFiles/emojis/1383.svg new file mode 100644 index 0000000..3ecc560 --- /dev/null +++ b/resources/imageFiles/emojis/1383.svg @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1384.svg b/resources/imageFiles/emojis/1384.svg new file mode 100644 index 0000000..e39bd4e --- /dev/null +++ b/resources/imageFiles/emojis/1384.svg @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1385.svg b/resources/imageFiles/emojis/1385.svg new file mode 100644 index 0000000..c52ff32 --- /dev/null +++ b/resources/imageFiles/emojis/1385.svg @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1386.svg b/resources/imageFiles/emojis/1386.svg new file mode 100644 index 0000000..0bb1743 --- /dev/null +++ b/resources/imageFiles/emojis/1386.svg @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1387.svg b/resources/imageFiles/emojis/1387.svg new file mode 100644 index 0000000..4f2eede --- /dev/null +++ b/resources/imageFiles/emojis/1387.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1388.svg b/resources/imageFiles/emojis/1388.svg new file mode 100644 index 0000000..ebcbc3b --- /dev/null +++ b/resources/imageFiles/emojis/1388.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1389.svg b/resources/imageFiles/emojis/1389.svg new file mode 100644 index 0000000..1c3ebe9 --- /dev/null +++ b/resources/imageFiles/emojis/1389.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1390.svg b/resources/imageFiles/emojis/1390.svg new file mode 100644 index 0000000..453c961 --- /dev/null +++ b/resources/imageFiles/emojis/1390.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1391.svg b/resources/imageFiles/emojis/1391.svg new file mode 100644 index 0000000..59c6d73 --- /dev/null +++ b/resources/imageFiles/emojis/1391.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1392.svg b/resources/imageFiles/emojis/1392.svg new file mode 100644 index 0000000..fa9b99b --- /dev/null +++ b/resources/imageFiles/emojis/1392.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1393.svg b/resources/imageFiles/emojis/1393.svg new file mode 100644 index 0000000..92a9237 --- /dev/null +++ b/resources/imageFiles/emojis/1393.svg @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1394.svg b/resources/imageFiles/emojis/1394.svg new file mode 100644 index 0000000..6d913de --- /dev/null +++ b/resources/imageFiles/emojis/1394.svg @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1395.svg b/resources/imageFiles/emojis/1395.svg new file mode 100644 index 0000000..4383ef9 --- /dev/null +++ b/resources/imageFiles/emojis/1395.svg @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1396.svg b/resources/imageFiles/emojis/1396.svg new file mode 100644 index 0000000..9ed0c39 --- /dev/null +++ b/resources/imageFiles/emojis/1396.svg @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1397.svg b/resources/imageFiles/emojis/1397.svg new file mode 100644 index 0000000..b7dacba --- /dev/null +++ b/resources/imageFiles/emojis/1397.svg @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1398.svg b/resources/imageFiles/emojis/1398.svg new file mode 100644 index 0000000..887f3a5 --- /dev/null +++ b/resources/imageFiles/emojis/1398.svg @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1399.svg b/resources/imageFiles/emojis/1399.svg new file mode 100644 index 0000000..a68b843 --- /dev/null +++ b/resources/imageFiles/emojis/1399.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1400.svg b/resources/imageFiles/emojis/1400.svg new file mode 100644 index 0000000..19eeb84 --- /dev/null +++ b/resources/imageFiles/emojis/1400.svg @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1401.svg b/resources/imageFiles/emojis/1401.svg new file mode 100644 index 0000000..2d7efd6 --- /dev/null +++ b/resources/imageFiles/emojis/1401.svg @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1402.svg b/resources/imageFiles/emojis/1402.svg new file mode 100644 index 0000000..fd8aa52 --- /dev/null +++ b/resources/imageFiles/emojis/1402.svg @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1403.svg b/resources/imageFiles/emojis/1403.svg new file mode 100644 index 0000000..f979c60 --- /dev/null +++ b/resources/imageFiles/emojis/1403.svg @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1404.svg b/resources/imageFiles/emojis/1404.svg new file mode 100644 index 0000000..4add4ad --- /dev/null +++ b/resources/imageFiles/emojis/1404.svg @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1405.svg b/resources/imageFiles/emojis/1405.svg new file mode 100644 index 0000000..32f93f6 --- /dev/null +++ b/resources/imageFiles/emojis/1405.svg @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1406.svg b/resources/imageFiles/emojis/1406.svg new file mode 100644 index 0000000..c8ba440 --- /dev/null +++ b/resources/imageFiles/emojis/1406.svg @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1407.svg b/resources/imageFiles/emojis/1407.svg new file mode 100644 index 0000000..7e3d6b9 --- /dev/null +++ b/resources/imageFiles/emojis/1407.svg @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1408.svg b/resources/imageFiles/emojis/1408.svg new file mode 100644 index 0000000..b0cfab6 --- /dev/null +++ b/resources/imageFiles/emojis/1408.svg @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1409.svg b/resources/imageFiles/emojis/1409.svg new file mode 100644 index 0000000..6a66bf5 --- /dev/null +++ b/resources/imageFiles/emojis/1409.svg @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1410.svg b/resources/imageFiles/emojis/1410.svg new file mode 100644 index 0000000..01b1460 --- /dev/null +++ b/resources/imageFiles/emojis/1410.svg @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1411.svg b/resources/imageFiles/emojis/1411.svg new file mode 100644 index 0000000..ec54077 --- /dev/null +++ b/resources/imageFiles/emojis/1411.svg @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1412.svg b/resources/imageFiles/emojis/1412.svg new file mode 100644 index 0000000..6ef73c1 --- /dev/null +++ b/resources/imageFiles/emojis/1412.svg @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1413.svg b/resources/imageFiles/emojis/1413.svg new file mode 100644 index 0000000..3226365 --- /dev/null +++ b/resources/imageFiles/emojis/1413.svg @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1414.svg b/resources/imageFiles/emojis/1414.svg new file mode 100644 index 0000000..a85fc94 --- /dev/null +++ b/resources/imageFiles/emojis/1414.svg @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1415.svg b/resources/imageFiles/emojis/1415.svg new file mode 100644 index 0000000..cb70d0a --- /dev/null +++ b/resources/imageFiles/emojis/1415.svg @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1416.svg b/resources/imageFiles/emojis/1416.svg new file mode 100644 index 0000000..9905a6c --- /dev/null +++ b/resources/imageFiles/emojis/1416.svg @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1417.svg b/resources/imageFiles/emojis/1417.svg new file mode 100644 index 0000000..085bf3c --- /dev/null +++ b/resources/imageFiles/emojis/1417.svg @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1418.svg b/resources/imageFiles/emojis/1418.svg new file mode 100644 index 0000000..a1b1975 --- /dev/null +++ b/resources/imageFiles/emojis/1418.svg @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1419.svg b/resources/imageFiles/emojis/1419.svg new file mode 100644 index 0000000..560a902 --- /dev/null +++ b/resources/imageFiles/emojis/1419.svg @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1420.svg b/resources/imageFiles/emojis/1420.svg new file mode 100644 index 0000000..9c6ffa2 --- /dev/null +++ b/resources/imageFiles/emojis/1420.svg @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1421.svg b/resources/imageFiles/emojis/1421.svg new file mode 100644 index 0000000..831c5de --- /dev/null +++ b/resources/imageFiles/emojis/1421.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1422.svg b/resources/imageFiles/emojis/1422.svg new file mode 100644 index 0000000..84b5233 --- /dev/null +++ b/resources/imageFiles/emojis/1422.svg @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1423.svg b/resources/imageFiles/emojis/1423.svg new file mode 100644 index 0000000..46c674a --- /dev/null +++ b/resources/imageFiles/emojis/1423.svg @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1424.svg b/resources/imageFiles/emojis/1424.svg new file mode 100644 index 0000000..6032e3a --- /dev/null +++ b/resources/imageFiles/emojis/1424.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1425.svg b/resources/imageFiles/emojis/1425.svg new file mode 100644 index 0000000..c9362fd --- /dev/null +++ b/resources/imageFiles/emojis/1425.svg @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1426.svg b/resources/imageFiles/emojis/1426.svg new file mode 100644 index 0000000..59b5d16 --- /dev/null +++ b/resources/imageFiles/emojis/1426.svg @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1427.svg b/resources/imageFiles/emojis/1427.svg new file mode 100644 index 0000000..ce0becd --- /dev/null +++ b/resources/imageFiles/emojis/1427.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1428.svg b/resources/imageFiles/emojis/1428.svg new file mode 100644 index 0000000..c627757 --- /dev/null +++ b/resources/imageFiles/emojis/1428.svg @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1429.svg b/resources/imageFiles/emojis/1429.svg new file mode 100644 index 0000000..0d3d6f9 --- /dev/null +++ b/resources/imageFiles/emojis/1429.svg @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1430.svg b/resources/imageFiles/emojis/1430.svg new file mode 100644 index 0000000..204b04a --- /dev/null +++ b/resources/imageFiles/emojis/1430.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1431.svg b/resources/imageFiles/emojis/1431.svg new file mode 100644 index 0000000..6983aab --- /dev/null +++ b/resources/imageFiles/emojis/1431.svg @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1432.svg b/resources/imageFiles/emojis/1432.svg new file mode 100644 index 0000000..2421721 --- /dev/null +++ b/resources/imageFiles/emojis/1432.svg @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1433.svg b/resources/imageFiles/emojis/1433.svg new file mode 100644 index 0000000..c3ad396 --- /dev/null +++ b/resources/imageFiles/emojis/1433.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1434.svg b/resources/imageFiles/emojis/1434.svg new file mode 100644 index 0000000..95b629f --- /dev/null +++ b/resources/imageFiles/emojis/1434.svg @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1435.svg b/resources/imageFiles/emojis/1435.svg new file mode 100644 index 0000000..3198627 --- /dev/null +++ b/resources/imageFiles/emojis/1435.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1436.svg b/resources/imageFiles/emojis/1436.svg new file mode 100644 index 0000000..eb44c53 --- /dev/null +++ b/resources/imageFiles/emojis/1436.svg @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1437.svg b/resources/imageFiles/emojis/1437.svg new file mode 100644 index 0000000..cbb3ce2 --- /dev/null +++ b/resources/imageFiles/emojis/1437.svg @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1438.svg b/resources/imageFiles/emojis/1438.svg new file mode 100644 index 0000000..2d13e2f --- /dev/null +++ b/resources/imageFiles/emojis/1438.svg @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1439.svg b/resources/imageFiles/emojis/1439.svg new file mode 100644 index 0000000..df75390 --- /dev/null +++ b/resources/imageFiles/emojis/1439.svg @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1440.svg b/resources/imageFiles/emojis/1440.svg new file mode 100644 index 0000000..1fa4c65 --- /dev/null +++ b/resources/imageFiles/emojis/1440.svg @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1441.svg b/resources/imageFiles/emojis/1441.svg new file mode 100644 index 0000000..93311fc --- /dev/null +++ b/resources/imageFiles/emojis/1441.svg @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1442.svg b/resources/imageFiles/emojis/1442.svg new file mode 100644 index 0000000..0f9e7ae --- /dev/null +++ b/resources/imageFiles/emojis/1442.svg @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1443.svg b/resources/imageFiles/emojis/1443.svg new file mode 100644 index 0000000..9887ff0 --- /dev/null +++ b/resources/imageFiles/emojis/1443.svg @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1444.svg b/resources/imageFiles/emojis/1444.svg new file mode 100644 index 0000000..5b32eaf --- /dev/null +++ b/resources/imageFiles/emojis/1444.svg @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1445.svg b/resources/imageFiles/emojis/1445.svg new file mode 100644 index 0000000..351a2f6 --- /dev/null +++ b/resources/imageFiles/emojis/1445.svg @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1446.svg b/resources/imageFiles/emojis/1446.svg new file mode 100644 index 0000000..a34da5a --- /dev/null +++ b/resources/imageFiles/emojis/1446.svg @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1447.svg b/resources/imageFiles/emojis/1447.svg new file mode 100644 index 0000000..e9abfbb --- /dev/null +++ b/resources/imageFiles/emojis/1447.svg @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1448.svg b/resources/imageFiles/emojis/1448.svg new file mode 100644 index 0000000..cf61317 --- /dev/null +++ b/resources/imageFiles/emojis/1448.svg @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1449.svg b/resources/imageFiles/emojis/1449.svg new file mode 100644 index 0000000..e4dc460 --- /dev/null +++ b/resources/imageFiles/emojis/1449.svg @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1450.svg b/resources/imageFiles/emojis/1450.svg new file mode 100644 index 0000000..cbfa20d --- /dev/null +++ b/resources/imageFiles/emojis/1450.svg @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1451.svg b/resources/imageFiles/emojis/1451.svg new file mode 100644 index 0000000..5951073 --- /dev/null +++ b/resources/imageFiles/emojis/1451.svg @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1452.svg b/resources/imageFiles/emojis/1452.svg new file mode 100644 index 0000000..7bfc896 --- /dev/null +++ b/resources/imageFiles/emojis/1452.svg @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1453.svg b/resources/imageFiles/emojis/1453.svg new file mode 100644 index 0000000..9d6d9f7 --- /dev/null +++ b/resources/imageFiles/emojis/1453.svg @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1454.svg b/resources/imageFiles/emojis/1454.svg new file mode 100644 index 0000000..5f21b4a --- /dev/null +++ b/resources/imageFiles/emojis/1454.svg @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1455.svg b/resources/imageFiles/emojis/1455.svg new file mode 100644 index 0000000..8035bd4 --- /dev/null +++ b/resources/imageFiles/emojis/1455.svg @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1456.svg b/resources/imageFiles/emojis/1456.svg new file mode 100644 index 0000000..9621208 --- /dev/null +++ b/resources/imageFiles/emojis/1456.svg @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1457.svg b/resources/imageFiles/emojis/1457.svg new file mode 100644 index 0000000..4fa4cbe --- /dev/null +++ b/resources/imageFiles/emojis/1457.svg @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1458.svg b/resources/imageFiles/emojis/1458.svg new file mode 100644 index 0000000..97bd5c9 --- /dev/null +++ b/resources/imageFiles/emojis/1458.svg @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1459.svg b/resources/imageFiles/emojis/1459.svg new file mode 100644 index 0000000..48d67da --- /dev/null +++ b/resources/imageFiles/emojis/1459.svg @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1460.svg b/resources/imageFiles/emojis/1460.svg new file mode 100644 index 0000000..6052b44 --- /dev/null +++ b/resources/imageFiles/emojis/1460.svg @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1461.svg b/resources/imageFiles/emojis/1461.svg new file mode 100644 index 0000000..7226382 --- /dev/null +++ b/resources/imageFiles/emojis/1461.svg @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1462.svg b/resources/imageFiles/emojis/1462.svg new file mode 100644 index 0000000..4cfef8a --- /dev/null +++ b/resources/imageFiles/emojis/1462.svg @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1463.svg b/resources/imageFiles/emojis/1463.svg new file mode 100644 index 0000000..9df15af --- /dev/null +++ b/resources/imageFiles/emojis/1463.svg @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1464.svg b/resources/imageFiles/emojis/1464.svg new file mode 100644 index 0000000..5cbbc02 --- /dev/null +++ b/resources/imageFiles/emojis/1464.svg @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1465.svg b/resources/imageFiles/emojis/1465.svg new file mode 100644 index 0000000..b97dd38 --- /dev/null +++ b/resources/imageFiles/emojis/1465.svg @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1466.svg b/resources/imageFiles/emojis/1466.svg new file mode 100644 index 0000000..d0eeb0a --- /dev/null +++ b/resources/imageFiles/emojis/1466.svg @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1467.svg b/resources/imageFiles/emojis/1467.svg new file mode 100644 index 0000000..707548b --- /dev/null +++ b/resources/imageFiles/emojis/1467.svg @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1468.svg b/resources/imageFiles/emojis/1468.svg new file mode 100644 index 0000000..5b216d5 --- /dev/null +++ b/resources/imageFiles/emojis/1468.svg @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1469.svg b/resources/imageFiles/emojis/1469.svg new file mode 100644 index 0000000..2ad3cfb --- /dev/null +++ b/resources/imageFiles/emojis/1469.svg @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1470.svg b/resources/imageFiles/emojis/1470.svg new file mode 100644 index 0000000..7d0d62a --- /dev/null +++ b/resources/imageFiles/emojis/1470.svg @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1471.svg b/resources/imageFiles/emojis/1471.svg new file mode 100644 index 0000000..51a7eed --- /dev/null +++ b/resources/imageFiles/emojis/1471.svg @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1472.svg b/resources/imageFiles/emojis/1472.svg new file mode 100644 index 0000000..18a2fe1 --- /dev/null +++ b/resources/imageFiles/emojis/1472.svg @@ -0,0 +1,32 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1473.svg b/resources/imageFiles/emojis/1473.svg new file mode 100644 index 0000000..0ec227c --- /dev/null +++ b/resources/imageFiles/emojis/1473.svg @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1474.svg b/resources/imageFiles/emojis/1474.svg new file mode 100644 index 0000000..a46deba --- /dev/null +++ b/resources/imageFiles/emojis/1474.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1475.svg b/resources/imageFiles/emojis/1475.svg new file mode 100644 index 0000000..2843576 --- /dev/null +++ b/resources/imageFiles/emojis/1475.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1476.svg b/resources/imageFiles/emojis/1476.svg new file mode 100644 index 0000000..fa3f413 --- /dev/null +++ b/resources/imageFiles/emojis/1476.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1477.svg b/resources/imageFiles/emojis/1477.svg new file mode 100644 index 0000000..c0c195b --- /dev/null +++ b/resources/imageFiles/emojis/1477.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1478.svg b/resources/imageFiles/emojis/1478.svg new file mode 100644 index 0000000..313fe42 --- /dev/null +++ b/resources/imageFiles/emojis/1478.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1479.svg b/resources/imageFiles/emojis/1479.svg new file mode 100644 index 0000000..c7f326e --- /dev/null +++ b/resources/imageFiles/emojis/1479.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1480.svg b/resources/imageFiles/emojis/1480.svg new file mode 100644 index 0000000..75b61ec --- /dev/null +++ b/resources/imageFiles/emojis/1480.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1481.svg b/resources/imageFiles/emojis/1481.svg new file mode 100644 index 0000000..2fa17ff --- /dev/null +++ b/resources/imageFiles/emojis/1481.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1482.svg b/resources/imageFiles/emojis/1482.svg new file mode 100644 index 0000000..b485844 --- /dev/null +++ b/resources/imageFiles/emojis/1482.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1483.svg b/resources/imageFiles/emojis/1483.svg new file mode 100644 index 0000000..9be2729 --- /dev/null +++ b/resources/imageFiles/emojis/1483.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1484.svg b/resources/imageFiles/emojis/1484.svg new file mode 100644 index 0000000..ba30def --- /dev/null +++ b/resources/imageFiles/emojis/1484.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1485.svg b/resources/imageFiles/emojis/1485.svg new file mode 100644 index 0000000..21d9478 --- /dev/null +++ b/resources/imageFiles/emojis/1485.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1486.svg b/resources/imageFiles/emojis/1486.svg new file mode 100644 index 0000000..0f254de --- /dev/null +++ b/resources/imageFiles/emojis/1486.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1487.svg b/resources/imageFiles/emojis/1487.svg new file mode 100644 index 0000000..35f2a7f --- /dev/null +++ b/resources/imageFiles/emojis/1487.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1488.svg b/resources/imageFiles/emojis/1488.svg new file mode 100644 index 0000000..77bc7b6 --- /dev/null +++ b/resources/imageFiles/emojis/1488.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1489.svg b/resources/imageFiles/emojis/1489.svg new file mode 100644 index 0000000..9cfe67b --- /dev/null +++ b/resources/imageFiles/emojis/1489.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1490.svg b/resources/imageFiles/emojis/1490.svg new file mode 100644 index 0000000..444c3cd --- /dev/null +++ b/resources/imageFiles/emojis/1490.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1491.svg b/resources/imageFiles/emojis/1491.svg new file mode 100644 index 0000000..52223b7 --- /dev/null +++ b/resources/imageFiles/emojis/1491.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1492.svg b/resources/imageFiles/emojis/1492.svg new file mode 100644 index 0000000..a49429d --- /dev/null +++ b/resources/imageFiles/emojis/1492.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1493.svg b/resources/imageFiles/emojis/1493.svg new file mode 100644 index 0000000..0d5efbe --- /dev/null +++ b/resources/imageFiles/emojis/1493.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1494.svg b/resources/imageFiles/emojis/1494.svg new file mode 100644 index 0000000..d3d9166 --- /dev/null +++ b/resources/imageFiles/emojis/1494.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1495.svg b/resources/imageFiles/emojis/1495.svg new file mode 100644 index 0000000..d3ba4dc --- /dev/null +++ b/resources/imageFiles/emojis/1495.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1496.svg b/resources/imageFiles/emojis/1496.svg new file mode 100644 index 0000000..c3b8014 --- /dev/null +++ b/resources/imageFiles/emojis/1496.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1497.svg b/resources/imageFiles/emojis/1497.svg new file mode 100644 index 0000000..a968f87 --- /dev/null +++ b/resources/imageFiles/emojis/1497.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1498.svg b/resources/imageFiles/emojis/1498.svg new file mode 100644 index 0000000..0ba19d6 --- /dev/null +++ b/resources/imageFiles/emojis/1498.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1499.svg b/resources/imageFiles/emojis/1499.svg new file mode 100644 index 0000000..d81077c --- /dev/null +++ b/resources/imageFiles/emojis/1499.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1500.svg b/resources/imageFiles/emojis/1500.svg new file mode 100644 index 0000000..6a7f913 --- /dev/null +++ b/resources/imageFiles/emojis/1500.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1501.svg b/resources/imageFiles/emojis/1501.svg new file mode 100644 index 0000000..d5d9f21 --- /dev/null +++ b/resources/imageFiles/emojis/1501.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1502.svg b/resources/imageFiles/emojis/1502.svg new file mode 100644 index 0000000..20ebccd --- /dev/null +++ b/resources/imageFiles/emojis/1502.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1503.svg b/resources/imageFiles/emojis/1503.svg new file mode 100644 index 0000000..2def4d9 --- /dev/null +++ b/resources/imageFiles/emojis/1503.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1504.svg b/resources/imageFiles/emojis/1504.svg new file mode 100644 index 0000000..ef401ae --- /dev/null +++ b/resources/imageFiles/emojis/1504.svg @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1505.svg b/resources/imageFiles/emojis/1505.svg new file mode 100644 index 0000000..6bbbb60 --- /dev/null +++ b/resources/imageFiles/emojis/1505.svg @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1506.svg b/resources/imageFiles/emojis/1506.svg new file mode 100644 index 0000000..8215be9 --- /dev/null +++ b/resources/imageFiles/emojis/1506.svg @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1507.svg b/resources/imageFiles/emojis/1507.svg new file mode 100644 index 0000000..6cfbedf --- /dev/null +++ b/resources/imageFiles/emojis/1507.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1508.svg b/resources/imageFiles/emojis/1508.svg new file mode 100644 index 0000000..44ef053 --- /dev/null +++ b/resources/imageFiles/emojis/1508.svg @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1509.svg b/resources/imageFiles/emojis/1509.svg new file mode 100644 index 0000000..b8f8492 --- /dev/null +++ b/resources/imageFiles/emojis/1509.svg @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1510.svg b/resources/imageFiles/emojis/1510.svg new file mode 100644 index 0000000..ca4e80b --- /dev/null +++ b/resources/imageFiles/emojis/1510.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1511.svg b/resources/imageFiles/emojis/1511.svg new file mode 100644 index 0000000..a8b46b1 --- /dev/null +++ b/resources/imageFiles/emojis/1511.svg @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1512.svg b/resources/imageFiles/emojis/1512.svg new file mode 100644 index 0000000..eda29ad --- /dev/null +++ b/resources/imageFiles/emojis/1512.svg @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1513.svg b/resources/imageFiles/emojis/1513.svg new file mode 100644 index 0000000..f96787c --- /dev/null +++ b/resources/imageFiles/emojis/1513.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1514.svg b/resources/imageFiles/emojis/1514.svg new file mode 100644 index 0000000..2d7987a --- /dev/null +++ b/resources/imageFiles/emojis/1514.svg @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1515.svg b/resources/imageFiles/emojis/1515.svg new file mode 100644 index 0000000..d69cfdd --- /dev/null +++ b/resources/imageFiles/emojis/1515.svg @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1516.svg b/resources/imageFiles/emojis/1516.svg new file mode 100644 index 0000000..ddb54ad --- /dev/null +++ b/resources/imageFiles/emojis/1516.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1517.svg b/resources/imageFiles/emojis/1517.svg new file mode 100644 index 0000000..3dd2d48 --- /dev/null +++ b/resources/imageFiles/emojis/1517.svg @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1518.svg b/resources/imageFiles/emojis/1518.svg new file mode 100644 index 0000000..76445ce --- /dev/null +++ b/resources/imageFiles/emojis/1518.svg @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1519.svg b/resources/imageFiles/emojis/1519.svg new file mode 100644 index 0000000..31963d2 --- /dev/null +++ b/resources/imageFiles/emojis/1519.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1520.svg b/resources/imageFiles/emojis/1520.svg new file mode 100644 index 0000000..54592dd --- /dev/null +++ b/resources/imageFiles/emojis/1520.svg @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1521.svg b/resources/imageFiles/emojis/1521.svg new file mode 100644 index 0000000..28de2ba --- /dev/null +++ b/resources/imageFiles/emojis/1521.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1522.svg b/resources/imageFiles/emojis/1522.svg new file mode 100644 index 0000000..2091c2c --- /dev/null +++ b/resources/imageFiles/emojis/1522.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1523.svg b/resources/imageFiles/emojis/1523.svg new file mode 100644 index 0000000..1da1f53 --- /dev/null +++ b/resources/imageFiles/emojis/1523.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1524.svg b/resources/imageFiles/emojis/1524.svg new file mode 100644 index 0000000..29bd159 --- /dev/null +++ b/resources/imageFiles/emojis/1524.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1525.svg b/resources/imageFiles/emojis/1525.svg new file mode 100644 index 0000000..9eff241 --- /dev/null +++ b/resources/imageFiles/emojis/1525.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1526.svg b/resources/imageFiles/emojis/1526.svg new file mode 100644 index 0000000..4a01fec --- /dev/null +++ b/resources/imageFiles/emojis/1526.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1527.svg b/resources/imageFiles/emojis/1527.svg new file mode 100644 index 0000000..e6c7138 --- /dev/null +++ b/resources/imageFiles/emojis/1527.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1528.svg b/resources/imageFiles/emojis/1528.svg new file mode 100644 index 0000000..96e08e2 --- /dev/null +++ b/resources/imageFiles/emojis/1528.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1529.svg b/resources/imageFiles/emojis/1529.svg new file mode 100644 index 0000000..ce414c5 --- /dev/null +++ b/resources/imageFiles/emojis/1529.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1530.svg b/resources/imageFiles/emojis/1530.svg new file mode 100644 index 0000000..3ca6604 --- /dev/null +++ b/resources/imageFiles/emojis/1530.svg @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1531.svg b/resources/imageFiles/emojis/1531.svg new file mode 100644 index 0000000..03708cf --- /dev/null +++ b/resources/imageFiles/emojis/1531.svg @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1532.svg b/resources/imageFiles/emojis/1532.svg new file mode 100644 index 0000000..d5ad995 --- /dev/null +++ b/resources/imageFiles/emojis/1532.svg @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1533.svg b/resources/imageFiles/emojis/1533.svg new file mode 100644 index 0000000..1bd67c4 --- /dev/null +++ b/resources/imageFiles/emojis/1533.svg @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1534.svg b/resources/imageFiles/emojis/1534.svg new file mode 100644 index 0000000..556b1ed --- /dev/null +++ b/resources/imageFiles/emojis/1534.svg @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1535.svg b/resources/imageFiles/emojis/1535.svg new file mode 100644 index 0000000..1c3232a --- /dev/null +++ b/resources/imageFiles/emojis/1535.svg @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1536.svg b/resources/imageFiles/emojis/1536.svg new file mode 100644 index 0000000..1c37f43 --- /dev/null +++ b/resources/imageFiles/emojis/1536.svg @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1537.svg b/resources/imageFiles/emojis/1537.svg new file mode 100644 index 0000000..fd3a1a4 --- /dev/null +++ b/resources/imageFiles/emojis/1537.svg @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1538.svg b/resources/imageFiles/emojis/1538.svg new file mode 100644 index 0000000..c97ddcc --- /dev/null +++ b/resources/imageFiles/emojis/1538.svg @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1539.svg b/resources/imageFiles/emojis/1539.svg new file mode 100644 index 0000000..2fe2bdb --- /dev/null +++ b/resources/imageFiles/emojis/1539.svg @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1540.svg b/resources/imageFiles/emojis/1540.svg new file mode 100644 index 0000000..18858aa --- /dev/null +++ b/resources/imageFiles/emojis/1540.svg @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1541.svg b/resources/imageFiles/emojis/1541.svg new file mode 100644 index 0000000..7af0d66 --- /dev/null +++ b/resources/imageFiles/emojis/1541.svg @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1542.svg b/resources/imageFiles/emojis/1542.svg new file mode 100644 index 0000000..cd82429 --- /dev/null +++ b/resources/imageFiles/emojis/1542.svg @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1543.svg b/resources/imageFiles/emojis/1543.svg new file mode 100644 index 0000000..39133a8 --- /dev/null +++ b/resources/imageFiles/emojis/1543.svg @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1544.svg b/resources/imageFiles/emojis/1544.svg new file mode 100644 index 0000000..d960bae --- /dev/null +++ b/resources/imageFiles/emojis/1544.svg @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1545.svg b/resources/imageFiles/emojis/1545.svg new file mode 100644 index 0000000..7ffb3e3 --- /dev/null +++ b/resources/imageFiles/emojis/1545.svg @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1546.svg b/resources/imageFiles/emojis/1546.svg new file mode 100644 index 0000000..7ff6e5e --- /dev/null +++ b/resources/imageFiles/emojis/1546.svg @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1547.svg b/resources/imageFiles/emojis/1547.svg new file mode 100644 index 0000000..bed90f1 --- /dev/null +++ b/resources/imageFiles/emojis/1547.svg @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1548.svg b/resources/imageFiles/emojis/1548.svg new file mode 100644 index 0000000..fb47cb3 --- /dev/null +++ b/resources/imageFiles/emojis/1548.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1549.svg b/resources/imageFiles/emojis/1549.svg new file mode 100644 index 0000000..0d3d74c --- /dev/null +++ b/resources/imageFiles/emojis/1549.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1550.svg b/resources/imageFiles/emojis/1550.svg new file mode 100644 index 0000000..eb7ade3 --- /dev/null +++ b/resources/imageFiles/emojis/1550.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1551.svg b/resources/imageFiles/emojis/1551.svg new file mode 100644 index 0000000..32e252f --- /dev/null +++ b/resources/imageFiles/emojis/1551.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1552.svg b/resources/imageFiles/emojis/1552.svg new file mode 100644 index 0000000..9cd5326 --- /dev/null +++ b/resources/imageFiles/emojis/1552.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1553.svg b/resources/imageFiles/emojis/1553.svg new file mode 100644 index 0000000..e152314 --- /dev/null +++ b/resources/imageFiles/emojis/1553.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1554.svg b/resources/imageFiles/emojis/1554.svg new file mode 100644 index 0000000..e3d1b06 --- /dev/null +++ b/resources/imageFiles/emojis/1554.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1555.svg b/resources/imageFiles/emojis/1555.svg new file mode 100644 index 0000000..e8a9976 --- /dev/null +++ b/resources/imageFiles/emojis/1555.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1556.svg b/resources/imageFiles/emojis/1556.svg new file mode 100644 index 0000000..986016a --- /dev/null +++ b/resources/imageFiles/emojis/1556.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1557.svg b/resources/imageFiles/emojis/1557.svg new file mode 100644 index 0000000..c3ca193 --- /dev/null +++ b/resources/imageFiles/emojis/1557.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1558.svg b/resources/imageFiles/emojis/1558.svg new file mode 100644 index 0000000..304e351 --- /dev/null +++ b/resources/imageFiles/emojis/1558.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1559.svg b/resources/imageFiles/emojis/1559.svg new file mode 100644 index 0000000..6fe3c05 --- /dev/null +++ b/resources/imageFiles/emojis/1559.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1560.svg b/resources/imageFiles/emojis/1560.svg new file mode 100644 index 0000000..f216125 --- /dev/null +++ b/resources/imageFiles/emojis/1560.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1561.svg b/resources/imageFiles/emojis/1561.svg new file mode 100644 index 0000000..e4060c5 --- /dev/null +++ b/resources/imageFiles/emojis/1561.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1562.svg b/resources/imageFiles/emojis/1562.svg new file mode 100644 index 0000000..394ef4a --- /dev/null +++ b/resources/imageFiles/emojis/1562.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1563.svg b/resources/imageFiles/emojis/1563.svg new file mode 100644 index 0000000..fc0bbcc --- /dev/null +++ b/resources/imageFiles/emojis/1563.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1564.svg b/resources/imageFiles/emojis/1564.svg new file mode 100644 index 0000000..c0542e4 --- /dev/null +++ b/resources/imageFiles/emojis/1564.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1565.svg b/resources/imageFiles/emojis/1565.svg new file mode 100644 index 0000000..14db7f3 --- /dev/null +++ b/resources/imageFiles/emojis/1565.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1566.svg b/resources/imageFiles/emojis/1566.svg new file mode 100644 index 0000000..6ef7d43 --- /dev/null +++ b/resources/imageFiles/emojis/1566.svg @@ -0,0 +1,38 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1567.svg b/resources/imageFiles/emojis/1567.svg new file mode 100644 index 0000000..188eb2e --- /dev/null +++ b/resources/imageFiles/emojis/1567.svg @@ -0,0 +1,38 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1568.svg b/resources/imageFiles/emojis/1568.svg new file mode 100644 index 0000000..96d186e --- /dev/null +++ b/resources/imageFiles/emojis/1568.svg @@ -0,0 +1,41 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1569.svg b/resources/imageFiles/emojis/1569.svg new file mode 100644 index 0000000..242d47c --- /dev/null +++ b/resources/imageFiles/emojis/1569.svg @@ -0,0 +1,38 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1570.svg b/resources/imageFiles/emojis/1570.svg new file mode 100644 index 0000000..0d98306 --- /dev/null +++ b/resources/imageFiles/emojis/1570.svg @@ -0,0 +1,33 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1571.svg b/resources/imageFiles/emojis/1571.svg new file mode 100644 index 0000000..3c98141 --- /dev/null +++ b/resources/imageFiles/emojis/1571.svg @@ -0,0 +1,38 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1572.svg b/resources/imageFiles/emojis/1572.svg new file mode 100644 index 0000000..381f2ae --- /dev/null +++ b/resources/imageFiles/emojis/1572.svg @@ -0,0 +1,41 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1573.svg b/resources/imageFiles/emojis/1573.svg new file mode 100644 index 0000000..10df824 --- /dev/null +++ b/resources/imageFiles/emojis/1573.svg @@ -0,0 +1,33 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1574.svg b/resources/imageFiles/emojis/1574.svg new file mode 100644 index 0000000..1cf2123 --- /dev/null +++ b/resources/imageFiles/emojis/1574.svg @@ -0,0 +1,38 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1575.svg b/resources/imageFiles/emojis/1575.svg new file mode 100644 index 0000000..c51ea18 --- /dev/null +++ b/resources/imageFiles/emojis/1575.svg @@ -0,0 +1,41 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1576.svg b/resources/imageFiles/emojis/1576.svg new file mode 100644 index 0000000..7902e31 --- /dev/null +++ b/resources/imageFiles/emojis/1576.svg @@ -0,0 +1,33 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1577.svg b/resources/imageFiles/emojis/1577.svg new file mode 100644 index 0000000..b5927ed --- /dev/null +++ b/resources/imageFiles/emojis/1577.svg @@ -0,0 +1,38 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1578.svg b/resources/imageFiles/emojis/1578.svg new file mode 100644 index 0000000..7471c3b --- /dev/null +++ b/resources/imageFiles/emojis/1578.svg @@ -0,0 +1,41 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1579.svg b/resources/imageFiles/emojis/1579.svg new file mode 100644 index 0000000..5b83a53 --- /dev/null +++ b/resources/imageFiles/emojis/1579.svg @@ -0,0 +1,33 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1580.svg b/resources/imageFiles/emojis/1580.svg new file mode 100644 index 0000000..3c0eb87 --- /dev/null +++ b/resources/imageFiles/emojis/1580.svg @@ -0,0 +1,41 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1581.svg b/resources/imageFiles/emojis/1581.svg new file mode 100644 index 0000000..bfab716 --- /dev/null +++ b/resources/imageFiles/emojis/1581.svg @@ -0,0 +1,33 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1582.svg b/resources/imageFiles/emojis/1582.svg new file mode 100644 index 0000000..eac0a90 --- /dev/null +++ b/resources/imageFiles/emojis/1582.svg @@ -0,0 +1,41 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1583.svg b/resources/imageFiles/emojis/1583.svg new file mode 100644 index 0000000..7c06512 --- /dev/null +++ b/resources/imageFiles/emojis/1583.svg @@ -0,0 +1,33 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1584.svg b/resources/imageFiles/emojis/1584.svg new file mode 100644 index 0000000..24e573a --- /dev/null +++ b/resources/imageFiles/emojis/1584.svg @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1585.svg b/resources/imageFiles/emojis/1585.svg new file mode 100644 index 0000000..0fdefeb --- /dev/null +++ b/resources/imageFiles/emojis/1585.svg @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1586.svg b/resources/imageFiles/emojis/1586.svg new file mode 100644 index 0000000..c1b0278 --- /dev/null +++ b/resources/imageFiles/emojis/1586.svg @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1587.svg b/resources/imageFiles/emojis/1587.svg new file mode 100644 index 0000000..ab4038a --- /dev/null +++ b/resources/imageFiles/emojis/1587.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1588.svg b/resources/imageFiles/emojis/1588.svg new file mode 100644 index 0000000..d9e75e8 --- /dev/null +++ b/resources/imageFiles/emojis/1588.svg @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1589.svg b/resources/imageFiles/emojis/1589.svg new file mode 100644 index 0000000..63c60c6 --- /dev/null +++ b/resources/imageFiles/emojis/1589.svg @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1590.svg b/resources/imageFiles/emojis/1590.svg new file mode 100644 index 0000000..4b9d674 --- /dev/null +++ b/resources/imageFiles/emojis/1590.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1591.svg b/resources/imageFiles/emojis/1591.svg new file mode 100644 index 0000000..6428abe --- /dev/null +++ b/resources/imageFiles/emojis/1591.svg @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1592.svg b/resources/imageFiles/emojis/1592.svg new file mode 100644 index 0000000..867d38a --- /dev/null +++ b/resources/imageFiles/emojis/1592.svg @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1593.svg b/resources/imageFiles/emojis/1593.svg new file mode 100644 index 0000000..f8dc3f1 --- /dev/null +++ b/resources/imageFiles/emojis/1593.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1594.svg b/resources/imageFiles/emojis/1594.svg new file mode 100644 index 0000000..b7c8b5a --- /dev/null +++ b/resources/imageFiles/emojis/1594.svg @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1595.svg b/resources/imageFiles/emojis/1595.svg new file mode 100644 index 0000000..668c9af --- /dev/null +++ b/resources/imageFiles/emojis/1595.svg @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1596.svg b/resources/imageFiles/emojis/1596.svg new file mode 100644 index 0000000..8e20e59 --- /dev/null +++ b/resources/imageFiles/emojis/1596.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1597.svg b/resources/imageFiles/emojis/1597.svg new file mode 100644 index 0000000..5e57fe3 --- /dev/null +++ b/resources/imageFiles/emojis/1597.svg @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1598.svg b/resources/imageFiles/emojis/1598.svg new file mode 100644 index 0000000..a37331b --- /dev/null +++ b/resources/imageFiles/emojis/1598.svg @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1599.svg b/resources/imageFiles/emojis/1599.svg new file mode 100644 index 0000000..f67f93f --- /dev/null +++ b/resources/imageFiles/emojis/1599.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1600.svg b/resources/imageFiles/emojis/1600.svg new file mode 100644 index 0000000..36e46a0 --- /dev/null +++ b/resources/imageFiles/emojis/1600.svg @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1601.svg b/resources/imageFiles/emojis/1601.svg new file mode 100644 index 0000000..4d0186d --- /dev/null +++ b/resources/imageFiles/emojis/1601.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1602.svg b/resources/imageFiles/emojis/1602.svg new file mode 100644 index 0000000..af46f5f --- /dev/null +++ b/resources/imageFiles/emojis/1602.svg @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1603.svg b/resources/imageFiles/emojis/1603.svg new file mode 100644 index 0000000..36d4ef9 --- /dev/null +++ b/resources/imageFiles/emojis/1603.svg @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1604.svg b/resources/imageFiles/emojis/1604.svg new file mode 100644 index 0000000..5969226 --- /dev/null +++ b/resources/imageFiles/emojis/1604.svg @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1605.svg b/resources/imageFiles/emojis/1605.svg new file mode 100644 index 0000000..ba7066b --- /dev/null +++ b/resources/imageFiles/emojis/1605.svg @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1606.svg b/resources/imageFiles/emojis/1606.svg new file mode 100644 index 0000000..45089a1 --- /dev/null +++ b/resources/imageFiles/emojis/1606.svg @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1607.svg b/resources/imageFiles/emojis/1607.svg new file mode 100644 index 0000000..f583304 --- /dev/null +++ b/resources/imageFiles/emojis/1607.svg @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1608.svg b/resources/imageFiles/emojis/1608.svg new file mode 100644 index 0000000..d415116 --- /dev/null +++ b/resources/imageFiles/emojis/1608.svg @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1609.svg b/resources/imageFiles/emojis/1609.svg new file mode 100644 index 0000000..5abb4e8 --- /dev/null +++ b/resources/imageFiles/emojis/1609.svg @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1610.svg b/resources/imageFiles/emojis/1610.svg new file mode 100644 index 0000000..673eeef --- /dev/null +++ b/resources/imageFiles/emojis/1610.svg @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1611.svg b/resources/imageFiles/emojis/1611.svg new file mode 100644 index 0000000..3afe0ff --- /dev/null +++ b/resources/imageFiles/emojis/1611.svg @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1612.svg b/resources/imageFiles/emojis/1612.svg new file mode 100644 index 0000000..f7a88b5 --- /dev/null +++ b/resources/imageFiles/emojis/1612.svg @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1613.svg b/resources/imageFiles/emojis/1613.svg new file mode 100644 index 0000000..5675e2c --- /dev/null +++ b/resources/imageFiles/emojis/1613.svg @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1614.svg b/resources/imageFiles/emojis/1614.svg new file mode 100644 index 0000000..34ec5b2 --- /dev/null +++ b/resources/imageFiles/emojis/1614.svg @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1615.svg b/resources/imageFiles/emojis/1615.svg new file mode 100644 index 0000000..ed16e6e --- /dev/null +++ b/resources/imageFiles/emojis/1615.svg @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1616.svg b/resources/imageFiles/emojis/1616.svg new file mode 100644 index 0000000..99ccd53 --- /dev/null +++ b/resources/imageFiles/emojis/1616.svg @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1617.svg b/resources/imageFiles/emojis/1617.svg new file mode 100644 index 0000000..67c73a8 --- /dev/null +++ b/resources/imageFiles/emojis/1617.svg @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1618.svg b/resources/imageFiles/emojis/1618.svg new file mode 100644 index 0000000..30a1208 --- /dev/null +++ b/resources/imageFiles/emojis/1618.svg @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1619.svg b/resources/imageFiles/emojis/1619.svg new file mode 100644 index 0000000..c1abd71 --- /dev/null +++ b/resources/imageFiles/emojis/1619.svg @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1620.svg b/resources/imageFiles/emojis/1620.svg new file mode 100644 index 0000000..45f1d54 --- /dev/null +++ b/resources/imageFiles/emojis/1620.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1621.svg b/resources/imageFiles/emojis/1621.svg new file mode 100644 index 0000000..8d02937 --- /dev/null +++ b/resources/imageFiles/emojis/1621.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1622.svg b/resources/imageFiles/emojis/1622.svg new file mode 100644 index 0000000..f7cf9ff --- /dev/null +++ b/resources/imageFiles/emojis/1622.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1623.svg b/resources/imageFiles/emojis/1623.svg new file mode 100644 index 0000000..cfb8d01 --- /dev/null +++ b/resources/imageFiles/emojis/1623.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1624.svg b/resources/imageFiles/emojis/1624.svg new file mode 100644 index 0000000..c5e3c68 --- /dev/null +++ b/resources/imageFiles/emojis/1624.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1625.svg b/resources/imageFiles/emojis/1625.svg new file mode 100644 index 0000000..42dcb17 --- /dev/null +++ b/resources/imageFiles/emojis/1625.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1626.svg b/resources/imageFiles/emojis/1626.svg new file mode 100644 index 0000000..7436fee --- /dev/null +++ b/resources/imageFiles/emojis/1626.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1627.svg b/resources/imageFiles/emojis/1627.svg new file mode 100644 index 0000000..1387ccb --- /dev/null +++ b/resources/imageFiles/emojis/1627.svg @@ -0,0 +1,32 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1628.svg b/resources/imageFiles/emojis/1628.svg new file mode 100644 index 0000000..1e24629 --- /dev/null +++ b/resources/imageFiles/emojis/1628.svg @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1629.svg b/resources/imageFiles/emojis/1629.svg new file mode 100644 index 0000000..19c0b3b --- /dev/null +++ b/resources/imageFiles/emojis/1629.svg @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1630.svg b/resources/imageFiles/emojis/1630.svg new file mode 100644 index 0000000..553fa30 --- /dev/null +++ b/resources/imageFiles/emojis/1630.svg @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1631.svg b/resources/imageFiles/emojis/1631.svg new file mode 100644 index 0000000..f5c6ee8 --- /dev/null +++ b/resources/imageFiles/emojis/1631.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1632.svg b/resources/imageFiles/emojis/1632.svg new file mode 100644 index 0000000..eb8b573 --- /dev/null +++ b/resources/imageFiles/emojis/1632.svg @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1633.svg b/resources/imageFiles/emojis/1633.svg new file mode 100644 index 0000000..07f95ba --- /dev/null +++ b/resources/imageFiles/emojis/1633.svg @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1634.svg b/resources/imageFiles/emojis/1634.svg new file mode 100644 index 0000000..8fb9b59 --- /dev/null +++ b/resources/imageFiles/emojis/1634.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1635.svg b/resources/imageFiles/emojis/1635.svg new file mode 100644 index 0000000..11f1371 --- /dev/null +++ b/resources/imageFiles/emojis/1635.svg @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1636.svg b/resources/imageFiles/emojis/1636.svg new file mode 100644 index 0000000..97e7228 --- /dev/null +++ b/resources/imageFiles/emojis/1636.svg @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1637.svg b/resources/imageFiles/emojis/1637.svg new file mode 100644 index 0000000..8b52441 --- /dev/null +++ b/resources/imageFiles/emojis/1637.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1638.svg b/resources/imageFiles/emojis/1638.svg new file mode 100644 index 0000000..0b381c8 --- /dev/null +++ b/resources/imageFiles/emojis/1638.svg @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1639.svg b/resources/imageFiles/emojis/1639.svg new file mode 100644 index 0000000..f191a61 --- /dev/null +++ b/resources/imageFiles/emojis/1639.svg @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1640.svg b/resources/imageFiles/emojis/1640.svg new file mode 100644 index 0000000..1fb6d16 --- /dev/null +++ b/resources/imageFiles/emojis/1640.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1641.svg b/resources/imageFiles/emojis/1641.svg new file mode 100644 index 0000000..c965398 --- /dev/null +++ b/resources/imageFiles/emojis/1641.svg @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1642.svg b/resources/imageFiles/emojis/1642.svg new file mode 100644 index 0000000..9b5da31 --- /dev/null +++ b/resources/imageFiles/emojis/1642.svg @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1643.svg b/resources/imageFiles/emojis/1643.svg new file mode 100644 index 0000000..8aba6b0 --- /dev/null +++ b/resources/imageFiles/emojis/1643.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1644.svg b/resources/imageFiles/emojis/1644.svg new file mode 100644 index 0000000..4a8f856 --- /dev/null +++ b/resources/imageFiles/emojis/1644.svg @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1645.svg b/resources/imageFiles/emojis/1645.svg new file mode 100644 index 0000000..6a84e2c --- /dev/null +++ b/resources/imageFiles/emojis/1645.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1646.svg b/resources/imageFiles/emojis/1646.svg new file mode 100644 index 0000000..7c9175d --- /dev/null +++ b/resources/imageFiles/emojis/1646.svg @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1647.svg b/resources/imageFiles/emojis/1647.svg new file mode 100644 index 0000000..764ce81 --- /dev/null +++ b/resources/imageFiles/emojis/1647.svg @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1648.svg b/resources/imageFiles/emojis/1648.svg new file mode 100644 index 0000000..6db5f8f --- /dev/null +++ b/resources/imageFiles/emojis/1648.svg @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1649.svg b/resources/imageFiles/emojis/1649.svg new file mode 100644 index 0000000..e0c0d6b --- /dev/null +++ b/resources/imageFiles/emojis/1649.svg @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1650.svg b/resources/imageFiles/emojis/1650.svg new file mode 100644 index 0000000..a7e7ca4 --- /dev/null +++ b/resources/imageFiles/emojis/1650.svg @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1651.svg b/resources/imageFiles/emojis/1651.svg new file mode 100644 index 0000000..27f9597 --- /dev/null +++ b/resources/imageFiles/emojis/1651.svg @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1652.svg b/resources/imageFiles/emojis/1652.svg new file mode 100644 index 0000000..cfccc4c --- /dev/null +++ b/resources/imageFiles/emojis/1652.svg @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1653.svg b/resources/imageFiles/emojis/1653.svg new file mode 100644 index 0000000..f931cbc --- /dev/null +++ b/resources/imageFiles/emojis/1653.svg @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1654.svg b/resources/imageFiles/emojis/1654.svg new file mode 100644 index 0000000..ba2a9fd --- /dev/null +++ b/resources/imageFiles/emojis/1654.svg @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1655.svg b/resources/imageFiles/emojis/1655.svg new file mode 100644 index 0000000..0f7fe80 --- /dev/null +++ b/resources/imageFiles/emojis/1655.svg @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1656.svg b/resources/imageFiles/emojis/1656.svg new file mode 100644 index 0000000..0390763 --- /dev/null +++ b/resources/imageFiles/emojis/1656.svg @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1657.svg b/resources/imageFiles/emojis/1657.svg new file mode 100644 index 0000000..de79dd4 --- /dev/null +++ b/resources/imageFiles/emojis/1657.svg @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1658.svg b/resources/imageFiles/emojis/1658.svg new file mode 100644 index 0000000..ca1dde6 --- /dev/null +++ b/resources/imageFiles/emojis/1658.svg @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1659.svg b/resources/imageFiles/emojis/1659.svg new file mode 100644 index 0000000..6090605 --- /dev/null +++ b/resources/imageFiles/emojis/1659.svg @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1660.svg b/resources/imageFiles/emojis/1660.svg new file mode 100644 index 0000000..1ac419b --- /dev/null +++ b/resources/imageFiles/emojis/1660.svg @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1661.svg b/resources/imageFiles/emojis/1661.svg new file mode 100644 index 0000000..3a3018d --- /dev/null +++ b/resources/imageFiles/emojis/1661.svg @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1662.svg b/resources/imageFiles/emojis/1662.svg new file mode 100644 index 0000000..7eb3db1 --- /dev/null +++ b/resources/imageFiles/emojis/1662.svg @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1663.svg b/resources/imageFiles/emojis/1663.svg new file mode 100644 index 0000000..91f4b49 --- /dev/null +++ b/resources/imageFiles/emojis/1663.svg @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1664.svg b/resources/imageFiles/emojis/1664.svg new file mode 100644 index 0000000..1015fc8 --- /dev/null +++ b/resources/imageFiles/emojis/1664.svg @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1665.svg b/resources/imageFiles/emojis/1665.svg new file mode 100644 index 0000000..cae471c --- /dev/null +++ b/resources/imageFiles/emojis/1665.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1666.svg b/resources/imageFiles/emojis/1666.svg new file mode 100644 index 0000000..8e91978 --- /dev/null +++ b/resources/imageFiles/emojis/1666.svg @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1667.svg b/resources/imageFiles/emojis/1667.svg new file mode 100644 index 0000000..de1efcd --- /dev/null +++ b/resources/imageFiles/emojis/1667.svg @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1668.svg b/resources/imageFiles/emojis/1668.svg new file mode 100644 index 0000000..c2a5cbc --- /dev/null +++ b/resources/imageFiles/emojis/1668.svg @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1669.svg b/resources/imageFiles/emojis/1669.svg new file mode 100644 index 0000000..dc26b9f --- /dev/null +++ b/resources/imageFiles/emojis/1669.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1670.svg b/resources/imageFiles/emojis/1670.svg new file mode 100644 index 0000000..1030cbb --- /dev/null +++ b/resources/imageFiles/emojis/1670.svg @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1671.svg b/resources/imageFiles/emojis/1671.svg new file mode 100644 index 0000000..02f7a56 --- /dev/null +++ b/resources/imageFiles/emojis/1671.svg @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1672.svg b/resources/imageFiles/emojis/1672.svg new file mode 100644 index 0000000..ded3485 --- /dev/null +++ b/resources/imageFiles/emojis/1672.svg @@ -0,0 +1,55 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1673.svg b/resources/imageFiles/emojis/1673.svg new file mode 100644 index 0000000..d598d43 --- /dev/null +++ b/resources/imageFiles/emojis/1673.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1674.svg b/resources/imageFiles/emojis/1674.svg new file mode 100644 index 0000000..7fdd421 --- /dev/null +++ b/resources/imageFiles/emojis/1674.svg @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1675.svg b/resources/imageFiles/emojis/1675.svg new file mode 100644 index 0000000..1292bd7 --- /dev/null +++ b/resources/imageFiles/emojis/1675.svg @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1676.svg b/resources/imageFiles/emojis/1676.svg new file mode 100644 index 0000000..58bb2fe --- /dev/null +++ b/resources/imageFiles/emojis/1676.svg @@ -0,0 +1,36 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1677.svg b/resources/imageFiles/emojis/1677.svg new file mode 100644 index 0000000..1d1a324 --- /dev/null +++ b/resources/imageFiles/emojis/1677.svg @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1678.svg b/resources/imageFiles/emojis/1678.svg new file mode 100644 index 0000000..292e961 --- /dev/null +++ b/resources/imageFiles/emojis/1678.svg @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1679.svg b/resources/imageFiles/emojis/1679.svg new file mode 100644 index 0000000..ba0e436 --- /dev/null +++ b/resources/imageFiles/emojis/1679.svg @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1680.svg b/resources/imageFiles/emojis/1680.svg new file mode 100644 index 0000000..c1b338c --- /dev/null +++ b/resources/imageFiles/emojis/1680.svg @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1681.svg b/resources/imageFiles/emojis/1681.svg new file mode 100644 index 0000000..0ca5054 --- /dev/null +++ b/resources/imageFiles/emojis/1681.svg @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1682.svg b/resources/imageFiles/emojis/1682.svg new file mode 100644 index 0000000..04a7852 --- /dev/null +++ b/resources/imageFiles/emojis/1682.svg @@ -0,0 +1,32 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1683.svg b/resources/imageFiles/emojis/1683.svg new file mode 100644 index 0000000..0650601 --- /dev/null +++ b/resources/imageFiles/emojis/1683.svg @@ -0,0 +1,36 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1684.svg b/resources/imageFiles/emojis/1684.svg new file mode 100644 index 0000000..c6fee19 --- /dev/null +++ b/resources/imageFiles/emojis/1684.svg @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1685.svg b/resources/imageFiles/emojis/1685.svg new file mode 100644 index 0000000..f3b35cf --- /dev/null +++ b/resources/imageFiles/emojis/1685.svg @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1686.svg b/resources/imageFiles/emojis/1686.svg new file mode 100644 index 0000000..6c054a8 --- /dev/null +++ b/resources/imageFiles/emojis/1686.svg @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1687.svg b/resources/imageFiles/emojis/1687.svg new file mode 100644 index 0000000..d7e78bf --- /dev/null +++ b/resources/imageFiles/emojis/1687.svg @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1688.svg b/resources/imageFiles/emojis/1688.svg new file mode 100644 index 0000000..f9720a8 --- /dev/null +++ b/resources/imageFiles/emojis/1688.svg @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1689.svg b/resources/imageFiles/emojis/1689.svg new file mode 100644 index 0000000..6fa8dc1 --- /dev/null +++ b/resources/imageFiles/emojis/1689.svg @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1690.svg b/resources/imageFiles/emojis/1690.svg new file mode 100644 index 0000000..e6c6e10 --- /dev/null +++ b/resources/imageFiles/emojis/1690.svg @@ -0,0 +1,34 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1691.svg b/resources/imageFiles/emojis/1691.svg new file mode 100644 index 0000000..eacc3fe --- /dev/null +++ b/resources/imageFiles/emojis/1691.svg @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1692.svg b/resources/imageFiles/emojis/1692.svg new file mode 100644 index 0000000..e006c8a --- /dev/null +++ b/resources/imageFiles/emojis/1692.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1693.svg b/resources/imageFiles/emojis/1693.svg new file mode 100644 index 0000000..cc8c05a --- /dev/null +++ b/resources/imageFiles/emojis/1693.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1694.svg b/resources/imageFiles/emojis/1694.svg new file mode 100644 index 0000000..31be1b0 --- /dev/null +++ b/resources/imageFiles/emojis/1694.svg @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1695.svg b/resources/imageFiles/emojis/1695.svg new file mode 100644 index 0000000..fef7dca --- /dev/null +++ b/resources/imageFiles/emojis/1695.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1696.svg b/resources/imageFiles/emojis/1696.svg new file mode 100644 index 0000000..738e897 --- /dev/null +++ b/resources/imageFiles/emojis/1696.svg @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1697.svg b/resources/imageFiles/emojis/1697.svg new file mode 100644 index 0000000..7e0b29d --- /dev/null +++ b/resources/imageFiles/emojis/1697.svg @@ -0,0 +1,59 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1698.svg b/resources/imageFiles/emojis/1698.svg new file mode 100644 index 0000000..bb4a4f8 --- /dev/null +++ b/resources/imageFiles/emojis/1698.svg @@ -0,0 +1,33 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1699.svg b/resources/imageFiles/emojis/1699.svg new file mode 100644 index 0000000..7ab7886 --- /dev/null +++ b/resources/imageFiles/emojis/1699.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1700.svg b/resources/imageFiles/emojis/1700.svg new file mode 100644 index 0000000..a280807 --- /dev/null +++ b/resources/imageFiles/emojis/1700.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/resources/imageFiles/emojis/1701.svg b/resources/imageFiles/emojis/1701.svg new file mode 100644 index 0000000..67ac055 --- /dev/null +++ b/resources/imageFiles/emojis/1701.svg @@ -0,0 +1,29 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1702.svg b/resources/imageFiles/emojis/1702.svg new file mode 100644 index 0000000..d64fd35 --- /dev/null +++ b/resources/imageFiles/emojis/1702.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1703.svg b/resources/imageFiles/emojis/1703.svg new file mode 100644 index 0000000..7e29e25 --- /dev/null +++ b/resources/imageFiles/emojis/1703.svg @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1704.svg b/resources/imageFiles/emojis/1704.svg new file mode 100644 index 0000000..2c6f442 --- /dev/null +++ b/resources/imageFiles/emojis/1704.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1705.svg b/resources/imageFiles/emojis/1705.svg new file mode 100644 index 0000000..b8406a3 --- /dev/null +++ b/resources/imageFiles/emojis/1705.svg @@ -0,0 +1,36 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1706.svg b/resources/imageFiles/emojis/1706.svg new file mode 100644 index 0000000..4ff5ae6 --- /dev/null +++ b/resources/imageFiles/emojis/1706.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1707.svg b/resources/imageFiles/emojis/1707.svg new file mode 100644 index 0000000..1d1d9c8 --- /dev/null +++ b/resources/imageFiles/emojis/1707.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1708.svg b/resources/imageFiles/emojis/1708.svg new file mode 100644 index 0000000..0d7734e --- /dev/null +++ b/resources/imageFiles/emojis/1708.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1709.svg b/resources/imageFiles/emojis/1709.svg new file mode 100644 index 0000000..cefc00b --- /dev/null +++ b/resources/imageFiles/emojis/1709.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1710.svg b/resources/imageFiles/emojis/1710.svg new file mode 100644 index 0000000..4be46c0 --- /dev/null +++ b/resources/imageFiles/emojis/1710.svg @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1711.svg b/resources/imageFiles/emojis/1711.svg new file mode 100644 index 0000000..f80129e --- /dev/null +++ b/resources/imageFiles/emojis/1711.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1712.svg b/resources/imageFiles/emojis/1712.svg new file mode 100644 index 0000000..c3968cf --- /dev/null +++ b/resources/imageFiles/emojis/1712.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1713.svg b/resources/imageFiles/emojis/1713.svg new file mode 100644 index 0000000..7d397ad --- /dev/null +++ b/resources/imageFiles/emojis/1713.svg @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1714.svg b/resources/imageFiles/emojis/1714.svg new file mode 100644 index 0000000..47252bc --- /dev/null +++ b/resources/imageFiles/emojis/1714.svg @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1715.svg b/resources/imageFiles/emojis/1715.svg new file mode 100644 index 0000000..143d765 --- /dev/null +++ b/resources/imageFiles/emojis/1715.svg @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1716.svg b/resources/imageFiles/emojis/1716.svg new file mode 100644 index 0000000..67da3e1 --- /dev/null +++ b/resources/imageFiles/emojis/1716.svg @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1717.svg b/resources/imageFiles/emojis/1717.svg new file mode 100644 index 0000000..5298c51 --- /dev/null +++ b/resources/imageFiles/emojis/1717.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1718.svg b/resources/imageFiles/emojis/1718.svg new file mode 100644 index 0000000..3629fdf --- /dev/null +++ b/resources/imageFiles/emojis/1718.svg @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1719.svg b/resources/imageFiles/emojis/1719.svg new file mode 100644 index 0000000..8cf0189 --- /dev/null +++ b/resources/imageFiles/emojis/1719.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1720.svg b/resources/imageFiles/emojis/1720.svg new file mode 100644 index 0000000..f4bd618 --- /dev/null +++ b/resources/imageFiles/emojis/1720.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1721.svg b/resources/imageFiles/emojis/1721.svg new file mode 100644 index 0000000..3ad9b2e --- /dev/null +++ b/resources/imageFiles/emojis/1721.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1722.svg b/resources/imageFiles/emojis/1722.svg new file mode 100644 index 0000000..53d3161 --- /dev/null +++ b/resources/imageFiles/emojis/1722.svg @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1723.svg b/resources/imageFiles/emojis/1723.svg new file mode 100644 index 0000000..a6e8e3e --- /dev/null +++ b/resources/imageFiles/emojis/1723.svg @@ -0,0 +1,32 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1724.svg b/resources/imageFiles/emojis/1724.svg new file mode 100644 index 0000000..3e29b3a --- /dev/null +++ b/resources/imageFiles/emojis/1724.svg @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1725.svg b/resources/imageFiles/emojis/1725.svg new file mode 100644 index 0000000..14538af --- /dev/null +++ b/resources/imageFiles/emojis/1725.svg @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1726.svg b/resources/imageFiles/emojis/1726.svg new file mode 100644 index 0000000..7f7d133 --- /dev/null +++ b/resources/imageFiles/emojis/1726.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1727.svg b/resources/imageFiles/emojis/1727.svg new file mode 100644 index 0000000..edf90af --- /dev/null +++ b/resources/imageFiles/emojis/1727.svg @@ -0,0 +1,29 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1728.svg b/resources/imageFiles/emojis/1728.svg new file mode 100644 index 0000000..586c77f --- /dev/null +++ b/resources/imageFiles/emojis/1728.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1729.svg b/resources/imageFiles/emojis/1729.svg new file mode 100644 index 0000000..1480cd4 --- /dev/null +++ b/resources/imageFiles/emojis/1729.svg @@ -0,0 +1,29 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1730.svg b/resources/imageFiles/emojis/1730.svg new file mode 100644 index 0000000..c3507e6 --- /dev/null +++ b/resources/imageFiles/emojis/1730.svg @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1731.svg b/resources/imageFiles/emojis/1731.svg new file mode 100644 index 0000000..2fdc30a --- /dev/null +++ b/resources/imageFiles/emojis/1731.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1732.svg b/resources/imageFiles/emojis/1732.svg new file mode 100644 index 0000000..0cfe5c6 --- /dev/null +++ b/resources/imageFiles/emojis/1732.svg @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1733.svg b/resources/imageFiles/emojis/1733.svg new file mode 100644 index 0000000..3b3f280 --- /dev/null +++ b/resources/imageFiles/emojis/1733.svg @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1734.svg b/resources/imageFiles/emojis/1734.svg new file mode 100644 index 0000000..6e68fe0 --- /dev/null +++ b/resources/imageFiles/emojis/1734.svg @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1735.svg b/resources/imageFiles/emojis/1735.svg new file mode 100644 index 0000000..e18aa94 --- /dev/null +++ b/resources/imageFiles/emojis/1735.svg @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1736.svg b/resources/imageFiles/emojis/1736.svg new file mode 100644 index 0000000..05f3200 --- /dev/null +++ b/resources/imageFiles/emojis/1736.svg @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1737.svg b/resources/imageFiles/emojis/1737.svg new file mode 100644 index 0000000..5728b16 --- /dev/null +++ b/resources/imageFiles/emojis/1737.svg @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1738.svg b/resources/imageFiles/emojis/1738.svg new file mode 100644 index 0000000..0ff8503 --- /dev/null +++ b/resources/imageFiles/emojis/1738.svg @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1739.svg b/resources/imageFiles/emojis/1739.svg new file mode 100644 index 0000000..d717ab9 --- /dev/null +++ b/resources/imageFiles/emojis/1739.svg @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1740.svg b/resources/imageFiles/emojis/1740.svg new file mode 100644 index 0000000..187ca13 --- /dev/null +++ b/resources/imageFiles/emojis/1740.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1741.svg b/resources/imageFiles/emojis/1741.svg new file mode 100644 index 0000000..59bdfa1 --- /dev/null +++ b/resources/imageFiles/emojis/1741.svg @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1742.svg b/resources/imageFiles/emojis/1742.svg new file mode 100644 index 0000000..baa872e --- /dev/null +++ b/resources/imageFiles/emojis/1742.svg @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1743.svg b/resources/imageFiles/emojis/1743.svg new file mode 100644 index 0000000..0595e21 --- /dev/null +++ b/resources/imageFiles/emojis/1743.svg @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1744.svg b/resources/imageFiles/emojis/1744.svg new file mode 100644 index 0000000..1a31528 --- /dev/null +++ b/resources/imageFiles/emojis/1744.svg @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1745.svg b/resources/imageFiles/emojis/1745.svg new file mode 100644 index 0000000..273dfc7 --- /dev/null +++ b/resources/imageFiles/emojis/1745.svg @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1746.svg b/resources/imageFiles/emojis/1746.svg new file mode 100644 index 0000000..c4336b8 --- /dev/null +++ b/resources/imageFiles/emojis/1746.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1747.svg b/resources/imageFiles/emojis/1747.svg new file mode 100644 index 0000000..1e279ab --- /dev/null +++ b/resources/imageFiles/emojis/1747.svg @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1748.svg b/resources/imageFiles/emojis/1748.svg new file mode 100644 index 0000000..090cd96 --- /dev/null +++ b/resources/imageFiles/emojis/1748.svg @@ -0,0 +1,35 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1749.svg b/resources/imageFiles/emojis/1749.svg new file mode 100644 index 0000000..f7a2f89 --- /dev/null +++ b/resources/imageFiles/emojis/1749.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1750.svg b/resources/imageFiles/emojis/1750.svg new file mode 100644 index 0000000..959660b --- /dev/null +++ b/resources/imageFiles/emojis/1750.svg @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1751.svg b/resources/imageFiles/emojis/1751.svg new file mode 100644 index 0000000..28174aa --- /dev/null +++ b/resources/imageFiles/emojis/1751.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1752.svg b/resources/imageFiles/emojis/1752.svg new file mode 100644 index 0000000..96e9d98 --- /dev/null +++ b/resources/imageFiles/emojis/1752.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1753.svg b/resources/imageFiles/emojis/1753.svg new file mode 100644 index 0000000..7f3c892 --- /dev/null +++ b/resources/imageFiles/emojis/1753.svg @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1754.svg b/resources/imageFiles/emojis/1754.svg new file mode 100644 index 0000000..2c69b85 --- /dev/null +++ b/resources/imageFiles/emojis/1754.svg @@ -0,0 +1,39 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1755.svg b/resources/imageFiles/emojis/1755.svg new file mode 100644 index 0000000..63e2903 --- /dev/null +++ b/resources/imageFiles/emojis/1755.svg @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1756.svg b/resources/imageFiles/emojis/1756.svg new file mode 100644 index 0000000..c4191b2 --- /dev/null +++ b/resources/imageFiles/emojis/1756.svg @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1757.svg b/resources/imageFiles/emojis/1757.svg new file mode 100644 index 0000000..75ca3dd --- /dev/null +++ b/resources/imageFiles/emojis/1757.svg @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1758.svg b/resources/imageFiles/emojis/1758.svg new file mode 100644 index 0000000..c9983bd --- /dev/null +++ b/resources/imageFiles/emojis/1758.svg @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1759.svg b/resources/imageFiles/emojis/1759.svg new file mode 100644 index 0000000..f528283 --- /dev/null +++ b/resources/imageFiles/emojis/1759.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1760.svg b/resources/imageFiles/emojis/1760.svg new file mode 100644 index 0000000..6f8313d --- /dev/null +++ b/resources/imageFiles/emojis/1760.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1761.svg b/resources/imageFiles/emojis/1761.svg new file mode 100644 index 0000000..c5b57b5 --- /dev/null +++ b/resources/imageFiles/emojis/1761.svg @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1762.svg b/resources/imageFiles/emojis/1762.svg new file mode 100644 index 0000000..41cf70b --- /dev/null +++ b/resources/imageFiles/emojis/1762.svg @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1763.svg b/resources/imageFiles/emojis/1763.svg new file mode 100644 index 0000000..40612c6 --- /dev/null +++ b/resources/imageFiles/emojis/1763.svg @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1764.svg b/resources/imageFiles/emojis/1764.svg new file mode 100644 index 0000000..26aa279 --- /dev/null +++ b/resources/imageFiles/emojis/1764.svg @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1765.svg b/resources/imageFiles/emojis/1765.svg new file mode 100644 index 0000000..5ba5dee --- /dev/null +++ b/resources/imageFiles/emojis/1765.svg @@ -0,0 +1,38 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1766.svg b/resources/imageFiles/emojis/1766.svg new file mode 100644 index 0000000..16ae271 --- /dev/null +++ b/resources/imageFiles/emojis/1766.svg @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1767.svg b/resources/imageFiles/emojis/1767.svg new file mode 100644 index 0000000..5625cd9 --- /dev/null +++ b/resources/imageFiles/emojis/1767.svg @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1768.svg b/resources/imageFiles/emojis/1768.svg new file mode 100644 index 0000000..9ac3cce --- /dev/null +++ b/resources/imageFiles/emojis/1768.svg @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1769.svg b/resources/imageFiles/emojis/1769.svg new file mode 100644 index 0000000..0220a6e --- /dev/null +++ b/resources/imageFiles/emojis/1769.svg @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1770.svg b/resources/imageFiles/emojis/1770.svg new file mode 100644 index 0000000..824b58a --- /dev/null +++ b/resources/imageFiles/emojis/1770.svg @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1771.svg b/resources/imageFiles/emojis/1771.svg new file mode 100644 index 0000000..2ab7d67 --- /dev/null +++ b/resources/imageFiles/emojis/1771.svg @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1772.svg b/resources/imageFiles/emojis/1772.svg new file mode 100644 index 0000000..f7aa2e7 --- /dev/null +++ b/resources/imageFiles/emojis/1772.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1773.svg b/resources/imageFiles/emojis/1773.svg new file mode 100644 index 0000000..8f8db55 --- /dev/null +++ b/resources/imageFiles/emojis/1773.svg @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1774.svg b/resources/imageFiles/emojis/1774.svg new file mode 100644 index 0000000..eb30038 --- /dev/null +++ b/resources/imageFiles/emojis/1774.svg @@ -0,0 +1,33 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1775.svg b/resources/imageFiles/emojis/1775.svg new file mode 100644 index 0000000..546c9a1 --- /dev/null +++ b/resources/imageFiles/emojis/1775.svg @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1776.svg b/resources/imageFiles/emojis/1776.svg new file mode 100644 index 0000000..b9e21cc --- /dev/null +++ b/resources/imageFiles/emojis/1776.svg @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1777.svg b/resources/imageFiles/emojis/1777.svg new file mode 100644 index 0000000..b0654e4 --- /dev/null +++ b/resources/imageFiles/emojis/1777.svg @@ -0,0 +1,31 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1778.svg b/resources/imageFiles/emojis/1778.svg new file mode 100644 index 0000000..aeda876 --- /dev/null +++ b/resources/imageFiles/emojis/1778.svg @@ -0,0 +1,29 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1779.svg b/resources/imageFiles/emojis/1779.svg new file mode 100644 index 0000000..e9e3913 --- /dev/null +++ b/resources/imageFiles/emojis/1779.svg @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1780.svg b/resources/imageFiles/emojis/1780.svg new file mode 100644 index 0000000..a5f8206 --- /dev/null +++ b/resources/imageFiles/emojis/1780.svg @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1781.svg b/resources/imageFiles/emojis/1781.svg new file mode 100644 index 0000000..b21586d --- /dev/null +++ b/resources/imageFiles/emojis/1781.svg @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1782.svg b/resources/imageFiles/emojis/1782.svg new file mode 100644 index 0000000..67e7534 --- /dev/null +++ b/resources/imageFiles/emojis/1782.svg @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1783.svg b/resources/imageFiles/emojis/1783.svg new file mode 100644 index 0000000..f1e36a4 --- /dev/null +++ b/resources/imageFiles/emojis/1783.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1784.svg b/resources/imageFiles/emojis/1784.svg new file mode 100644 index 0000000..03d4fca --- /dev/null +++ b/resources/imageFiles/emojis/1784.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1785.svg b/resources/imageFiles/emojis/1785.svg new file mode 100644 index 0000000..fa0201b --- /dev/null +++ b/resources/imageFiles/emojis/1785.svg @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1786.svg b/resources/imageFiles/emojis/1786.svg new file mode 100644 index 0000000..2f50df5 --- /dev/null +++ b/resources/imageFiles/emojis/1786.svg @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1787.svg b/resources/imageFiles/emojis/1787.svg new file mode 100644 index 0000000..236a1ec --- /dev/null +++ b/resources/imageFiles/emojis/1787.svg @@ -0,0 +1,31 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1788.svg b/resources/imageFiles/emojis/1788.svg new file mode 100644 index 0000000..e58fc53 --- /dev/null +++ b/resources/imageFiles/emojis/1788.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1789.svg b/resources/imageFiles/emojis/1789.svg new file mode 100644 index 0000000..585a06c --- /dev/null +++ b/resources/imageFiles/emojis/1789.svg @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1790.svg b/resources/imageFiles/emojis/1790.svg new file mode 100644 index 0000000..966e549 --- /dev/null +++ b/resources/imageFiles/emojis/1790.svg @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1791.svg b/resources/imageFiles/emojis/1791.svg new file mode 100644 index 0000000..fa21f66 --- /dev/null +++ b/resources/imageFiles/emojis/1791.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1792.svg b/resources/imageFiles/emojis/1792.svg new file mode 100644 index 0000000..a7dfdee --- /dev/null +++ b/resources/imageFiles/emojis/1792.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1793.svg b/resources/imageFiles/emojis/1793.svg new file mode 100644 index 0000000..1880c41 --- /dev/null +++ b/resources/imageFiles/emojis/1793.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1794.svg b/resources/imageFiles/emojis/1794.svg new file mode 100644 index 0000000..f23ddec --- /dev/null +++ b/resources/imageFiles/emojis/1794.svg @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1795.svg b/resources/imageFiles/emojis/1795.svg new file mode 100644 index 0000000..2893bb7 --- /dev/null +++ b/resources/imageFiles/emojis/1795.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1796.svg b/resources/imageFiles/emojis/1796.svg new file mode 100644 index 0000000..f85de66 --- /dev/null +++ b/resources/imageFiles/emojis/1796.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1797.svg b/resources/imageFiles/emojis/1797.svg new file mode 100644 index 0000000..4f250c5 --- /dev/null +++ b/resources/imageFiles/emojis/1797.svg @@ -0,0 +1,40 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1798.svg b/resources/imageFiles/emojis/1798.svg new file mode 100644 index 0000000..3d4cbd4 --- /dev/null +++ b/resources/imageFiles/emojis/1798.svg @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1799.svg b/resources/imageFiles/emojis/1799.svg new file mode 100644 index 0000000..8c909bf --- /dev/null +++ b/resources/imageFiles/emojis/1799.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1800.svg b/resources/imageFiles/emojis/1800.svg new file mode 100644 index 0000000..d037859 --- /dev/null +++ b/resources/imageFiles/emojis/1800.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1801.svg b/resources/imageFiles/emojis/1801.svg new file mode 100644 index 0000000..bea9495 --- /dev/null +++ b/resources/imageFiles/emojis/1801.svg @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1802.svg b/resources/imageFiles/emojis/1802.svg new file mode 100644 index 0000000..e07444d --- /dev/null +++ b/resources/imageFiles/emojis/1802.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1803.svg b/resources/imageFiles/emojis/1803.svg new file mode 100644 index 0000000..8063b9e --- /dev/null +++ b/resources/imageFiles/emojis/1803.svg @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1804.svg b/resources/imageFiles/emojis/1804.svg new file mode 100644 index 0000000..45b2aed --- /dev/null +++ b/resources/imageFiles/emojis/1804.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1805.svg b/resources/imageFiles/emojis/1805.svg new file mode 100644 index 0000000..c10ab45 --- /dev/null +++ b/resources/imageFiles/emojis/1805.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1806.svg b/resources/imageFiles/emojis/1806.svg new file mode 100644 index 0000000..fc7fe94 --- /dev/null +++ b/resources/imageFiles/emojis/1806.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1807.svg b/resources/imageFiles/emojis/1807.svg new file mode 100644 index 0000000..215312f --- /dev/null +++ b/resources/imageFiles/emojis/1807.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1808.svg b/resources/imageFiles/emojis/1808.svg new file mode 100644 index 0000000..b5b6c8a --- /dev/null +++ b/resources/imageFiles/emojis/1808.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1809.svg b/resources/imageFiles/emojis/1809.svg new file mode 100644 index 0000000..7d00db9 --- /dev/null +++ b/resources/imageFiles/emojis/1809.svg @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1810.svg b/resources/imageFiles/emojis/1810.svg new file mode 100644 index 0000000..af49750 --- /dev/null +++ b/resources/imageFiles/emojis/1810.svg @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1811.svg b/resources/imageFiles/emojis/1811.svg new file mode 100644 index 0000000..ad1a8eb --- /dev/null +++ b/resources/imageFiles/emojis/1811.svg @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1812.svg b/resources/imageFiles/emojis/1812.svg new file mode 100644 index 0000000..ed85617 --- /dev/null +++ b/resources/imageFiles/emojis/1812.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1813.svg b/resources/imageFiles/emojis/1813.svg new file mode 100644 index 0000000..2d88d80 --- /dev/null +++ b/resources/imageFiles/emojis/1813.svg @@ -0,0 +1,55 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1814.svg b/resources/imageFiles/emojis/1814.svg new file mode 100644 index 0000000..aa793ad --- /dev/null +++ b/resources/imageFiles/emojis/1814.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1815.svg b/resources/imageFiles/emojis/1815.svg new file mode 100644 index 0000000..317f720 --- /dev/null +++ b/resources/imageFiles/emojis/1815.svg @@ -0,0 +1,50 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1816.svg b/resources/imageFiles/emojis/1816.svg new file mode 100644 index 0000000..e5e1d4c --- /dev/null +++ b/resources/imageFiles/emojis/1816.svg @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1817.svg b/resources/imageFiles/emojis/1817.svg new file mode 100644 index 0000000..3c0e0f7 --- /dev/null +++ b/resources/imageFiles/emojis/1817.svg @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1818.svg b/resources/imageFiles/emojis/1818.svg new file mode 100644 index 0000000..2d15b92 --- /dev/null +++ b/resources/imageFiles/emojis/1818.svg @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1819.svg b/resources/imageFiles/emojis/1819.svg new file mode 100644 index 0000000..d22f7d7 --- /dev/null +++ b/resources/imageFiles/emojis/1819.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1820.svg b/resources/imageFiles/emojis/1820.svg new file mode 100644 index 0000000..4ed5a89 --- /dev/null +++ b/resources/imageFiles/emojis/1820.svg @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1821.svg b/resources/imageFiles/emojis/1821.svg new file mode 100644 index 0000000..63a4796 --- /dev/null +++ b/resources/imageFiles/emojis/1821.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1822.svg b/resources/imageFiles/emojis/1822.svg new file mode 100644 index 0000000..78ef8ee --- /dev/null +++ b/resources/imageFiles/emojis/1822.svg @@ -0,0 +1,33 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1823.svg b/resources/imageFiles/emojis/1823.svg new file mode 100644 index 0000000..67ec69f --- /dev/null +++ b/resources/imageFiles/emojis/1823.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1824.svg b/resources/imageFiles/emojis/1824.svg new file mode 100644 index 0000000..8f1aca7 --- /dev/null +++ b/resources/imageFiles/emojis/1824.svg @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1825.svg b/resources/imageFiles/emojis/1825.svg new file mode 100644 index 0000000..67e55cf --- /dev/null +++ b/resources/imageFiles/emojis/1825.svg @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1826.svg b/resources/imageFiles/emojis/1826.svg new file mode 100644 index 0000000..b3ba6af --- /dev/null +++ b/resources/imageFiles/emojis/1826.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1827.svg b/resources/imageFiles/emojis/1827.svg new file mode 100644 index 0000000..cf687b8 --- /dev/null +++ b/resources/imageFiles/emojis/1827.svg @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1828.svg b/resources/imageFiles/emojis/1828.svg new file mode 100644 index 0000000..ce35b0f --- /dev/null +++ b/resources/imageFiles/emojis/1828.svg @@ -0,0 +1,38 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1829.svg b/resources/imageFiles/emojis/1829.svg new file mode 100644 index 0000000..e12dcaf --- /dev/null +++ b/resources/imageFiles/emojis/1829.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1830.svg b/resources/imageFiles/emojis/1830.svg new file mode 100644 index 0000000..a3a8794 --- /dev/null +++ b/resources/imageFiles/emojis/1830.svg @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1831.svg b/resources/imageFiles/emojis/1831.svg new file mode 100644 index 0000000..d8b9de3 --- /dev/null +++ b/resources/imageFiles/emojis/1831.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1832.svg b/resources/imageFiles/emojis/1832.svg new file mode 100644 index 0000000..649e112 --- /dev/null +++ b/resources/imageFiles/emojis/1832.svg @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1833.svg b/resources/imageFiles/emojis/1833.svg new file mode 100644 index 0000000..41f354e --- /dev/null +++ b/resources/imageFiles/emojis/1833.svg @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1834.svg b/resources/imageFiles/emojis/1834.svg new file mode 100644 index 0000000..b4a695a --- /dev/null +++ b/resources/imageFiles/emojis/1834.svg @@ -0,0 +1,29 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1835.svg b/resources/imageFiles/emojis/1835.svg new file mode 100644 index 0000000..24316fd --- /dev/null +++ b/resources/imageFiles/emojis/1835.svg @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1836.svg b/resources/imageFiles/emojis/1836.svg new file mode 100644 index 0000000..a7d3dfe --- /dev/null +++ b/resources/imageFiles/emojis/1836.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1837.svg b/resources/imageFiles/emojis/1837.svg new file mode 100644 index 0000000..2392f23 --- /dev/null +++ b/resources/imageFiles/emojis/1837.svg @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1838.svg b/resources/imageFiles/emojis/1838.svg new file mode 100644 index 0000000..8c1ac1c --- /dev/null +++ b/resources/imageFiles/emojis/1838.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1839.svg b/resources/imageFiles/emojis/1839.svg new file mode 100644 index 0000000..e7d92dc --- /dev/null +++ b/resources/imageFiles/emojis/1839.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1840.svg b/resources/imageFiles/emojis/1840.svg new file mode 100644 index 0000000..2b49b8e --- /dev/null +++ b/resources/imageFiles/emojis/1840.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1841.svg b/resources/imageFiles/emojis/1841.svg new file mode 100644 index 0000000..84afc93 --- /dev/null +++ b/resources/imageFiles/emojis/1841.svg @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1842.svg b/resources/imageFiles/emojis/1842.svg new file mode 100644 index 0000000..8aac7bb --- /dev/null +++ b/resources/imageFiles/emojis/1842.svg @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1843.svg b/resources/imageFiles/emojis/1843.svg new file mode 100644 index 0000000..3e4d1ae --- /dev/null +++ b/resources/imageFiles/emojis/1843.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1844.svg b/resources/imageFiles/emojis/1844.svg new file mode 100644 index 0000000..4f4748b --- /dev/null +++ b/resources/imageFiles/emojis/1844.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1845.svg b/resources/imageFiles/emojis/1845.svg new file mode 100644 index 0000000..f6c2467 --- /dev/null +++ b/resources/imageFiles/emojis/1845.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1846.svg b/resources/imageFiles/emojis/1846.svg new file mode 100644 index 0000000..19ef141 --- /dev/null +++ b/resources/imageFiles/emojis/1846.svg @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1847.svg b/resources/imageFiles/emojis/1847.svg new file mode 100644 index 0000000..6892f43 --- /dev/null +++ b/resources/imageFiles/emojis/1847.svg @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1848.svg b/resources/imageFiles/emojis/1848.svg new file mode 100644 index 0000000..5c86210 --- /dev/null +++ b/resources/imageFiles/emojis/1848.svg @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1849.svg b/resources/imageFiles/emojis/1849.svg new file mode 100644 index 0000000..2044a21 --- /dev/null +++ b/resources/imageFiles/emojis/1849.svg @@ -0,0 +1,33 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1850.svg b/resources/imageFiles/emojis/1850.svg new file mode 100644 index 0000000..e910270 --- /dev/null +++ b/resources/imageFiles/emojis/1850.svg @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1851.svg b/resources/imageFiles/emojis/1851.svg new file mode 100644 index 0000000..36ba74f --- /dev/null +++ b/resources/imageFiles/emojis/1851.svg @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1852.svg b/resources/imageFiles/emojis/1852.svg new file mode 100644 index 0000000..d489d3b --- /dev/null +++ b/resources/imageFiles/emojis/1852.svg @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1853.svg b/resources/imageFiles/emojis/1853.svg new file mode 100644 index 0000000..bc6d473 --- /dev/null +++ b/resources/imageFiles/emojis/1853.svg @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1854.svg b/resources/imageFiles/emojis/1854.svg new file mode 100644 index 0000000..6338a9d --- /dev/null +++ b/resources/imageFiles/emojis/1854.svg @@ -0,0 +1,42 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1855.svg b/resources/imageFiles/emojis/1855.svg new file mode 100644 index 0000000..0389d27 --- /dev/null +++ b/resources/imageFiles/emojis/1855.svg @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1856.svg b/resources/imageFiles/emojis/1856.svg new file mode 100644 index 0000000..d103b03 --- /dev/null +++ b/resources/imageFiles/emojis/1856.svg @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1857.svg b/resources/imageFiles/emojis/1857.svg new file mode 100644 index 0000000..68cb8c4 --- /dev/null +++ b/resources/imageFiles/emojis/1857.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1858.svg b/resources/imageFiles/emojis/1858.svg new file mode 100644 index 0000000..cfb6c61 --- /dev/null +++ b/resources/imageFiles/emojis/1858.svg @@ -0,0 +1,45 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1859.svg b/resources/imageFiles/emojis/1859.svg new file mode 100644 index 0000000..188491b --- /dev/null +++ b/resources/imageFiles/emojis/1859.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1860.svg b/resources/imageFiles/emojis/1860.svg new file mode 100644 index 0000000..a011657 --- /dev/null +++ b/resources/imageFiles/emojis/1860.svg @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1861.svg b/resources/imageFiles/emojis/1861.svg new file mode 100644 index 0000000..843e951 --- /dev/null +++ b/resources/imageFiles/emojis/1861.svg @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1862.svg b/resources/imageFiles/emojis/1862.svg new file mode 100644 index 0000000..8e6586d --- /dev/null +++ b/resources/imageFiles/emojis/1862.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1863.svg b/resources/imageFiles/emojis/1863.svg new file mode 100644 index 0000000..2860f67 --- /dev/null +++ b/resources/imageFiles/emojis/1863.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/resources/imageFiles/emojis/1864.svg b/resources/imageFiles/emojis/1864.svg new file mode 100644 index 0000000..0512dc2 --- /dev/null +++ b/resources/imageFiles/emojis/1864.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1865.svg b/resources/imageFiles/emojis/1865.svg new file mode 100644 index 0000000..1f94b35 --- /dev/null +++ b/resources/imageFiles/emojis/1865.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1866.svg b/resources/imageFiles/emojis/1866.svg new file mode 100644 index 0000000..2b5c42e --- /dev/null +++ b/resources/imageFiles/emojis/1866.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1867.svg b/resources/imageFiles/emojis/1867.svg new file mode 100644 index 0000000..ac8a097 --- /dev/null +++ b/resources/imageFiles/emojis/1867.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1868.svg b/resources/imageFiles/emojis/1868.svg new file mode 100644 index 0000000..284c7d8 --- /dev/null +++ b/resources/imageFiles/emojis/1868.svg @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1869.svg b/resources/imageFiles/emojis/1869.svg new file mode 100644 index 0000000..2429bd8 --- /dev/null +++ b/resources/imageFiles/emojis/1869.svg @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1870.svg b/resources/imageFiles/emojis/1870.svg new file mode 100644 index 0000000..c4fa19a --- /dev/null +++ b/resources/imageFiles/emojis/1870.svg @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1871.svg b/resources/imageFiles/emojis/1871.svg new file mode 100644 index 0000000..e81221f --- /dev/null +++ b/resources/imageFiles/emojis/1871.svg @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1872.svg b/resources/imageFiles/emojis/1872.svg new file mode 100644 index 0000000..9269a38 --- /dev/null +++ b/resources/imageFiles/emojis/1872.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1873.svg b/resources/imageFiles/emojis/1873.svg new file mode 100644 index 0000000..840c1a3 --- /dev/null +++ b/resources/imageFiles/emojis/1873.svg @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1874.svg b/resources/imageFiles/emojis/1874.svg new file mode 100644 index 0000000..87cbff5 --- /dev/null +++ b/resources/imageFiles/emojis/1874.svg @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1875.svg b/resources/imageFiles/emojis/1875.svg new file mode 100644 index 0000000..84414ed --- /dev/null +++ b/resources/imageFiles/emojis/1875.svg @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1876.svg b/resources/imageFiles/emojis/1876.svg new file mode 100644 index 0000000..900db45 --- /dev/null +++ b/resources/imageFiles/emojis/1876.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1877.svg b/resources/imageFiles/emojis/1877.svg new file mode 100644 index 0000000..1ea4536 --- /dev/null +++ b/resources/imageFiles/emojis/1877.svg @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1878.svg b/resources/imageFiles/emojis/1878.svg new file mode 100644 index 0000000..ca246e9 --- /dev/null +++ b/resources/imageFiles/emojis/1878.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1879.svg b/resources/imageFiles/emojis/1879.svg new file mode 100644 index 0000000..caf94df --- /dev/null +++ b/resources/imageFiles/emojis/1879.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1880.svg b/resources/imageFiles/emojis/1880.svg new file mode 100644 index 0000000..95eb7cc --- /dev/null +++ b/resources/imageFiles/emojis/1880.svg @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1881.svg b/resources/imageFiles/emojis/1881.svg new file mode 100644 index 0000000..f2d2ec7 --- /dev/null +++ b/resources/imageFiles/emojis/1881.svg @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1882.svg b/resources/imageFiles/emojis/1882.svg new file mode 100644 index 0000000..a4e00af --- /dev/null +++ b/resources/imageFiles/emojis/1882.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1883.svg b/resources/imageFiles/emojis/1883.svg new file mode 100644 index 0000000..734415e --- /dev/null +++ b/resources/imageFiles/emojis/1883.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1884.svg b/resources/imageFiles/emojis/1884.svg new file mode 100644 index 0000000..3cc6981 --- /dev/null +++ b/resources/imageFiles/emojis/1884.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1885.svg b/resources/imageFiles/emojis/1885.svg new file mode 100644 index 0000000..1dc7c48 --- /dev/null +++ b/resources/imageFiles/emojis/1885.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1886.svg b/resources/imageFiles/emojis/1886.svg new file mode 100644 index 0000000..c836c8c --- /dev/null +++ b/resources/imageFiles/emojis/1886.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1887.svg b/resources/imageFiles/emojis/1887.svg new file mode 100644 index 0000000..681c640 --- /dev/null +++ b/resources/imageFiles/emojis/1887.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1888.svg b/resources/imageFiles/emojis/1888.svg new file mode 100644 index 0000000..0d2942c --- /dev/null +++ b/resources/imageFiles/emojis/1888.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1889.svg b/resources/imageFiles/emojis/1889.svg new file mode 100644 index 0000000..6010038 --- /dev/null +++ b/resources/imageFiles/emojis/1889.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1890.svg b/resources/imageFiles/emojis/1890.svg new file mode 100644 index 0000000..2de74a3 --- /dev/null +++ b/resources/imageFiles/emojis/1890.svg @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1891.svg b/resources/imageFiles/emojis/1891.svg new file mode 100644 index 0000000..6d39d4a --- /dev/null +++ b/resources/imageFiles/emojis/1891.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1892.svg b/resources/imageFiles/emojis/1892.svg new file mode 100644 index 0000000..797ff96 --- /dev/null +++ b/resources/imageFiles/emojis/1892.svg @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1893.svg b/resources/imageFiles/emojis/1893.svg new file mode 100644 index 0000000..0bae722 --- /dev/null +++ b/resources/imageFiles/emojis/1893.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1894.svg b/resources/imageFiles/emojis/1894.svg new file mode 100644 index 0000000..ee42306 --- /dev/null +++ b/resources/imageFiles/emojis/1894.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1895.svg b/resources/imageFiles/emojis/1895.svg new file mode 100644 index 0000000..28566bd --- /dev/null +++ b/resources/imageFiles/emojis/1895.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1896.svg b/resources/imageFiles/emojis/1896.svg new file mode 100644 index 0000000..26e648d --- /dev/null +++ b/resources/imageFiles/emojis/1896.svg @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1897.svg b/resources/imageFiles/emojis/1897.svg new file mode 100644 index 0000000..ebb43fb --- /dev/null +++ b/resources/imageFiles/emojis/1897.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1898.svg b/resources/imageFiles/emojis/1898.svg new file mode 100644 index 0000000..dc938b5 --- /dev/null +++ b/resources/imageFiles/emojis/1898.svg @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1899.svg b/resources/imageFiles/emojis/1899.svg new file mode 100644 index 0000000..b3a9767 --- /dev/null +++ b/resources/imageFiles/emojis/1899.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1900.svg b/resources/imageFiles/emojis/1900.svg new file mode 100644 index 0000000..67414bb --- /dev/null +++ b/resources/imageFiles/emojis/1900.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1901.svg b/resources/imageFiles/emojis/1901.svg new file mode 100644 index 0000000..675abbd --- /dev/null +++ b/resources/imageFiles/emojis/1901.svg @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1902.svg b/resources/imageFiles/emojis/1902.svg new file mode 100644 index 0000000..b7f0661 --- /dev/null +++ b/resources/imageFiles/emojis/1902.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1903.svg b/resources/imageFiles/emojis/1903.svg new file mode 100644 index 0000000..b3e736f --- /dev/null +++ b/resources/imageFiles/emojis/1903.svg @@ -0,0 +1,29 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1904.svg b/resources/imageFiles/emojis/1904.svg new file mode 100644 index 0000000..bd11f78 --- /dev/null +++ b/resources/imageFiles/emojis/1904.svg @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1905.svg b/resources/imageFiles/emojis/1905.svg new file mode 100644 index 0000000..4c34d82 --- /dev/null +++ b/resources/imageFiles/emojis/1905.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1906.svg b/resources/imageFiles/emojis/1906.svg new file mode 100644 index 0000000..885b39d --- /dev/null +++ b/resources/imageFiles/emojis/1906.svg @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1907.svg b/resources/imageFiles/emojis/1907.svg new file mode 100644 index 0000000..581f3a9 --- /dev/null +++ b/resources/imageFiles/emojis/1907.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1908.svg b/resources/imageFiles/emojis/1908.svg new file mode 100644 index 0000000..1e6798c --- /dev/null +++ b/resources/imageFiles/emojis/1908.svg @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1909.svg b/resources/imageFiles/emojis/1909.svg new file mode 100644 index 0000000..060c850 --- /dev/null +++ b/resources/imageFiles/emojis/1909.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1910.svg b/resources/imageFiles/emojis/1910.svg new file mode 100644 index 0000000..5025c00 --- /dev/null +++ b/resources/imageFiles/emojis/1910.svg @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1911.svg b/resources/imageFiles/emojis/1911.svg new file mode 100644 index 0000000..2598e2b --- /dev/null +++ b/resources/imageFiles/emojis/1911.svg @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1912.svg b/resources/imageFiles/emojis/1912.svg new file mode 100644 index 0000000..be12387 --- /dev/null +++ b/resources/imageFiles/emojis/1912.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1913.svg b/resources/imageFiles/emojis/1913.svg new file mode 100644 index 0000000..ea60178 --- /dev/null +++ b/resources/imageFiles/emojis/1913.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/resources/imageFiles/emojis/1914.svg b/resources/imageFiles/emojis/1914.svg new file mode 100644 index 0000000..23a9cb2 --- /dev/null +++ b/resources/imageFiles/emojis/1914.svg @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1915.svg b/resources/imageFiles/emojis/1915.svg new file mode 100644 index 0000000..6aafa34 --- /dev/null +++ b/resources/imageFiles/emojis/1915.svg @@ -0,0 +1,34 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1916.svg b/resources/imageFiles/emojis/1916.svg new file mode 100644 index 0000000..4fb72f7 --- /dev/null +++ b/resources/imageFiles/emojis/1916.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1917.svg b/resources/imageFiles/emojis/1917.svg new file mode 100644 index 0000000..aaa5484 --- /dev/null +++ b/resources/imageFiles/emojis/1917.svg @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1918.svg b/resources/imageFiles/emojis/1918.svg new file mode 100644 index 0000000..5443b7e --- /dev/null +++ b/resources/imageFiles/emojis/1918.svg @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1919.svg b/resources/imageFiles/emojis/1919.svg new file mode 100644 index 0000000..a94e404 --- /dev/null +++ b/resources/imageFiles/emojis/1919.svg @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1920.svg b/resources/imageFiles/emojis/1920.svg new file mode 100644 index 0000000..d7e74e6 --- /dev/null +++ b/resources/imageFiles/emojis/1920.svg @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1921.svg b/resources/imageFiles/emojis/1921.svg new file mode 100644 index 0000000..81867d1 --- /dev/null +++ b/resources/imageFiles/emojis/1921.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1922.svg b/resources/imageFiles/emojis/1922.svg new file mode 100644 index 0000000..c67cd63 --- /dev/null +++ b/resources/imageFiles/emojis/1922.svg @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1923.svg b/resources/imageFiles/emojis/1923.svg new file mode 100644 index 0000000..79a2202 --- /dev/null +++ b/resources/imageFiles/emojis/1923.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1924.svg b/resources/imageFiles/emojis/1924.svg new file mode 100644 index 0000000..51f3b3a --- /dev/null +++ b/resources/imageFiles/emojis/1924.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1925.svg b/resources/imageFiles/emojis/1925.svg new file mode 100644 index 0000000..1b490e2 --- /dev/null +++ b/resources/imageFiles/emojis/1925.svg @@ -0,0 +1,32 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1926.svg b/resources/imageFiles/emojis/1926.svg new file mode 100644 index 0000000..11770d8 --- /dev/null +++ b/resources/imageFiles/emojis/1926.svg @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1927.svg b/resources/imageFiles/emojis/1927.svg new file mode 100644 index 0000000..3bddbde --- /dev/null +++ b/resources/imageFiles/emojis/1927.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1928.svg b/resources/imageFiles/emojis/1928.svg new file mode 100644 index 0000000..a1d4e4b --- /dev/null +++ b/resources/imageFiles/emojis/1928.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1929.svg b/resources/imageFiles/emojis/1929.svg new file mode 100644 index 0000000..188a4c8 --- /dev/null +++ b/resources/imageFiles/emojis/1929.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1930.svg b/resources/imageFiles/emojis/1930.svg new file mode 100644 index 0000000..4936077 --- /dev/null +++ b/resources/imageFiles/emojis/1930.svg @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1931.svg b/resources/imageFiles/emojis/1931.svg new file mode 100644 index 0000000..abd0100 --- /dev/null +++ b/resources/imageFiles/emojis/1931.svg @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1932.svg b/resources/imageFiles/emojis/1932.svg new file mode 100644 index 0000000..0633242 --- /dev/null +++ b/resources/imageFiles/emojis/1932.svg @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1933.svg b/resources/imageFiles/emojis/1933.svg new file mode 100644 index 0000000..2f2b459 --- /dev/null +++ b/resources/imageFiles/emojis/1933.svg @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1934.svg b/resources/imageFiles/emojis/1934.svg new file mode 100644 index 0000000..53b611d --- /dev/null +++ b/resources/imageFiles/emojis/1934.svg @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1935.svg b/resources/imageFiles/emojis/1935.svg new file mode 100644 index 0000000..ba1f8e4 --- /dev/null +++ b/resources/imageFiles/emojis/1935.svg @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1936.svg b/resources/imageFiles/emojis/1936.svg new file mode 100644 index 0000000..aade568 --- /dev/null +++ b/resources/imageFiles/emojis/1936.svg @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1937.svg b/resources/imageFiles/emojis/1937.svg new file mode 100644 index 0000000..7e7abd8 --- /dev/null +++ b/resources/imageFiles/emojis/1937.svg @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1938.svg b/resources/imageFiles/emojis/1938.svg new file mode 100644 index 0000000..8e8fdea --- /dev/null +++ b/resources/imageFiles/emojis/1938.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1939.svg b/resources/imageFiles/emojis/1939.svg new file mode 100644 index 0000000..56e1699 --- /dev/null +++ b/resources/imageFiles/emojis/1939.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1940.svg b/resources/imageFiles/emojis/1940.svg new file mode 100644 index 0000000..a018dda --- /dev/null +++ b/resources/imageFiles/emojis/1940.svg @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1941.svg b/resources/imageFiles/emojis/1941.svg new file mode 100644 index 0000000..1c48a58 --- /dev/null +++ b/resources/imageFiles/emojis/1941.svg @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1942.svg b/resources/imageFiles/emojis/1942.svg new file mode 100644 index 0000000..310fe46 --- /dev/null +++ b/resources/imageFiles/emojis/1942.svg @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1943.svg b/resources/imageFiles/emojis/1943.svg new file mode 100644 index 0000000..0f18b56 --- /dev/null +++ b/resources/imageFiles/emojis/1943.svg @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1944.svg b/resources/imageFiles/emojis/1944.svg new file mode 100644 index 0000000..eeac8da --- /dev/null +++ b/resources/imageFiles/emojis/1944.svg @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1945.svg b/resources/imageFiles/emojis/1945.svg new file mode 100644 index 0000000..4146c72 --- /dev/null +++ b/resources/imageFiles/emojis/1945.svg @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1946.svg b/resources/imageFiles/emojis/1946.svg new file mode 100644 index 0000000..9e4b857 --- /dev/null +++ b/resources/imageFiles/emojis/1946.svg @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1947.svg b/resources/imageFiles/emojis/1947.svg new file mode 100644 index 0000000..deb5c17 --- /dev/null +++ b/resources/imageFiles/emojis/1947.svg @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1948.svg b/resources/imageFiles/emojis/1948.svg new file mode 100644 index 0000000..872f58d --- /dev/null +++ b/resources/imageFiles/emojis/1948.svg @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1949.svg b/resources/imageFiles/emojis/1949.svg new file mode 100644 index 0000000..a7d1923 --- /dev/null +++ b/resources/imageFiles/emojis/1949.svg @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1950.svg b/resources/imageFiles/emojis/1950.svg new file mode 100644 index 0000000..dd38701 --- /dev/null +++ b/resources/imageFiles/emojis/1950.svg @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1951.svg b/resources/imageFiles/emojis/1951.svg new file mode 100644 index 0000000..5404a81 --- /dev/null +++ b/resources/imageFiles/emojis/1951.svg @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1952.svg b/resources/imageFiles/emojis/1952.svg new file mode 100644 index 0000000..517213a --- /dev/null +++ b/resources/imageFiles/emojis/1952.svg @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1953.svg b/resources/imageFiles/emojis/1953.svg new file mode 100644 index 0000000..e19d16f --- /dev/null +++ b/resources/imageFiles/emojis/1953.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1954.svg b/resources/imageFiles/emojis/1954.svg new file mode 100644 index 0000000..d3e01d5 --- /dev/null +++ b/resources/imageFiles/emojis/1954.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1955.svg b/resources/imageFiles/emojis/1955.svg new file mode 100644 index 0000000..1921234 --- /dev/null +++ b/resources/imageFiles/emojis/1955.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1956.svg b/resources/imageFiles/emojis/1956.svg new file mode 100644 index 0000000..ab36c7e --- /dev/null +++ b/resources/imageFiles/emojis/1956.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1957.svg b/resources/imageFiles/emojis/1957.svg new file mode 100644 index 0000000..f7305f2 --- /dev/null +++ b/resources/imageFiles/emojis/1957.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1958.svg b/resources/imageFiles/emojis/1958.svg new file mode 100644 index 0000000..0b034ca --- /dev/null +++ b/resources/imageFiles/emojis/1958.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1959.svg b/resources/imageFiles/emojis/1959.svg new file mode 100644 index 0000000..b252796 --- /dev/null +++ b/resources/imageFiles/emojis/1959.svg @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1960.svg b/resources/imageFiles/emojis/1960.svg new file mode 100644 index 0000000..1d14216 --- /dev/null +++ b/resources/imageFiles/emojis/1960.svg @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1961.svg b/resources/imageFiles/emojis/1961.svg new file mode 100644 index 0000000..ff3275f --- /dev/null +++ b/resources/imageFiles/emojis/1961.svg @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1962.svg b/resources/imageFiles/emojis/1962.svg new file mode 100644 index 0000000..a8c375f --- /dev/null +++ b/resources/imageFiles/emojis/1962.svg @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1963.svg b/resources/imageFiles/emojis/1963.svg new file mode 100644 index 0000000..c9bfd9e --- /dev/null +++ b/resources/imageFiles/emojis/1963.svg @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1964.svg b/resources/imageFiles/emojis/1964.svg new file mode 100644 index 0000000..5e75628 --- /dev/null +++ b/resources/imageFiles/emojis/1964.svg @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1965.svg b/resources/imageFiles/emojis/1965.svg new file mode 100644 index 0000000..b0f4276 --- /dev/null +++ b/resources/imageFiles/emojis/1965.svg @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1966.svg b/resources/imageFiles/emojis/1966.svg new file mode 100644 index 0000000..fa2fbfa --- /dev/null +++ b/resources/imageFiles/emojis/1966.svg @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1967.svg b/resources/imageFiles/emojis/1967.svg new file mode 100644 index 0000000..1ec7243 --- /dev/null +++ b/resources/imageFiles/emojis/1967.svg @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1968.svg b/resources/imageFiles/emojis/1968.svg new file mode 100644 index 0000000..e0a25c4 --- /dev/null +++ b/resources/imageFiles/emojis/1968.svg @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1969.svg b/resources/imageFiles/emojis/1969.svg new file mode 100644 index 0000000..d600679 --- /dev/null +++ b/resources/imageFiles/emojis/1969.svg @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1970.svg b/resources/imageFiles/emojis/1970.svg new file mode 100644 index 0000000..6b799de --- /dev/null +++ b/resources/imageFiles/emojis/1970.svg @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1971.svg b/resources/imageFiles/emojis/1971.svg new file mode 100644 index 0000000..a6f582d --- /dev/null +++ b/resources/imageFiles/emojis/1971.svg @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1972.svg b/resources/imageFiles/emojis/1972.svg new file mode 100644 index 0000000..548f5e0 --- /dev/null +++ b/resources/imageFiles/emojis/1972.svg @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1973.svg b/resources/imageFiles/emojis/1973.svg new file mode 100644 index 0000000..5f882ec --- /dev/null +++ b/resources/imageFiles/emojis/1973.svg @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1974.svg b/resources/imageFiles/emojis/1974.svg new file mode 100644 index 0000000..b0aa8dc --- /dev/null +++ b/resources/imageFiles/emojis/1974.svg @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1975.svg b/resources/imageFiles/emojis/1975.svg new file mode 100644 index 0000000..d93caaa --- /dev/null +++ b/resources/imageFiles/emojis/1975.svg @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1976.svg b/resources/imageFiles/emojis/1976.svg new file mode 100644 index 0000000..6ff1354 --- /dev/null +++ b/resources/imageFiles/emojis/1976.svg @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1977.svg b/resources/imageFiles/emojis/1977.svg new file mode 100644 index 0000000..82eae62 --- /dev/null +++ b/resources/imageFiles/emojis/1977.svg @@ -0,0 +1,42 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1978.svg b/resources/imageFiles/emojis/1978.svg new file mode 100644 index 0000000..bdc332d --- /dev/null +++ b/resources/imageFiles/emojis/1978.svg @@ -0,0 +1,42 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1979.svg b/resources/imageFiles/emojis/1979.svg new file mode 100644 index 0000000..e9923bf --- /dev/null +++ b/resources/imageFiles/emojis/1979.svg @@ -0,0 +1,42 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1980.svg b/resources/imageFiles/emojis/1980.svg new file mode 100644 index 0000000..4613c00 --- /dev/null +++ b/resources/imageFiles/emojis/1980.svg @@ -0,0 +1,42 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1981.svg b/resources/imageFiles/emojis/1981.svg new file mode 100644 index 0000000..4184f1c --- /dev/null +++ b/resources/imageFiles/emojis/1981.svg @@ -0,0 +1,42 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1982.svg b/resources/imageFiles/emojis/1982.svg new file mode 100644 index 0000000..b2e42a6 --- /dev/null +++ b/resources/imageFiles/emojis/1982.svg @@ -0,0 +1,42 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1983.svg b/resources/imageFiles/emojis/1983.svg new file mode 100644 index 0000000..c6e7072 --- /dev/null +++ b/resources/imageFiles/emojis/1983.svg @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1984.svg b/resources/imageFiles/emojis/1984.svg new file mode 100644 index 0000000..6db8610 --- /dev/null +++ b/resources/imageFiles/emojis/1984.svg @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1985.svg b/resources/imageFiles/emojis/1985.svg new file mode 100644 index 0000000..1419bc7 --- /dev/null +++ b/resources/imageFiles/emojis/1985.svg @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1986.svg b/resources/imageFiles/emojis/1986.svg new file mode 100644 index 0000000..70f57d2 --- /dev/null +++ b/resources/imageFiles/emojis/1986.svg @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1987.svg b/resources/imageFiles/emojis/1987.svg new file mode 100644 index 0000000..8fe0d7a --- /dev/null +++ b/resources/imageFiles/emojis/1987.svg @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1988.svg b/resources/imageFiles/emojis/1988.svg new file mode 100644 index 0000000..9a6db36 --- /dev/null +++ b/resources/imageFiles/emojis/1988.svg @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1989.svg b/resources/imageFiles/emojis/1989.svg new file mode 100644 index 0000000..fbf992c --- /dev/null +++ b/resources/imageFiles/emojis/1989.svg @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1990.svg b/resources/imageFiles/emojis/1990.svg new file mode 100644 index 0000000..5ba2d6c --- /dev/null +++ b/resources/imageFiles/emojis/1990.svg @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1991.svg b/resources/imageFiles/emojis/1991.svg new file mode 100644 index 0000000..2a42fb8 --- /dev/null +++ b/resources/imageFiles/emojis/1991.svg @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1992.svg b/resources/imageFiles/emojis/1992.svg new file mode 100644 index 0000000..1d3d3f0 --- /dev/null +++ b/resources/imageFiles/emojis/1992.svg @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1993.svg b/resources/imageFiles/emojis/1993.svg new file mode 100644 index 0000000..36d7899 --- /dev/null +++ b/resources/imageFiles/emojis/1993.svg @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1994.svg b/resources/imageFiles/emojis/1994.svg new file mode 100644 index 0000000..956dec9 --- /dev/null +++ b/resources/imageFiles/emojis/1994.svg @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1995.svg b/resources/imageFiles/emojis/1995.svg new file mode 100644 index 0000000..03426de --- /dev/null +++ b/resources/imageFiles/emojis/1995.svg @@ -0,0 +1,51 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1996.svg b/resources/imageFiles/emojis/1996.svg new file mode 100644 index 0000000..cfed889 --- /dev/null +++ b/resources/imageFiles/emojis/1996.svg @@ -0,0 +1,51 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1997.svg b/resources/imageFiles/emojis/1997.svg new file mode 100644 index 0000000..bab4151 --- /dev/null +++ b/resources/imageFiles/emojis/1997.svg @@ -0,0 +1,51 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1998.svg b/resources/imageFiles/emojis/1998.svg new file mode 100644 index 0000000..a018d32 --- /dev/null +++ b/resources/imageFiles/emojis/1998.svg @@ -0,0 +1,51 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/1999.svg b/resources/imageFiles/emojis/1999.svg new file mode 100644 index 0000000..550b779 --- /dev/null +++ b/resources/imageFiles/emojis/1999.svg @@ -0,0 +1,51 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2000.svg b/resources/imageFiles/emojis/2000.svg new file mode 100644 index 0000000..37c1050 --- /dev/null +++ b/resources/imageFiles/emojis/2000.svg @@ -0,0 +1,51 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2001.svg b/resources/imageFiles/emojis/2001.svg new file mode 100644 index 0000000..2a0147d --- /dev/null +++ b/resources/imageFiles/emojis/2001.svg @@ -0,0 +1,38 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2002.svg b/resources/imageFiles/emojis/2002.svg new file mode 100644 index 0000000..3e3f48d --- /dev/null +++ b/resources/imageFiles/emojis/2002.svg @@ -0,0 +1,38 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2003.svg b/resources/imageFiles/emojis/2003.svg new file mode 100644 index 0000000..14b6d87 --- /dev/null +++ b/resources/imageFiles/emojis/2003.svg @@ -0,0 +1,38 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2004.svg b/resources/imageFiles/emojis/2004.svg new file mode 100644 index 0000000..61649ac --- /dev/null +++ b/resources/imageFiles/emojis/2004.svg @@ -0,0 +1,38 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2005.svg b/resources/imageFiles/emojis/2005.svg new file mode 100644 index 0000000..1f13840 --- /dev/null +++ b/resources/imageFiles/emojis/2005.svg @@ -0,0 +1,38 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2006.svg b/resources/imageFiles/emojis/2006.svg new file mode 100644 index 0000000..64f26ac --- /dev/null +++ b/resources/imageFiles/emojis/2006.svg @@ -0,0 +1,38 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2007.svg b/resources/imageFiles/emojis/2007.svg new file mode 100644 index 0000000..1e550bd --- /dev/null +++ b/resources/imageFiles/emojis/2007.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2008.svg b/resources/imageFiles/emojis/2008.svg new file mode 100644 index 0000000..2544143 --- /dev/null +++ b/resources/imageFiles/emojis/2008.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2009.svg b/resources/imageFiles/emojis/2009.svg new file mode 100644 index 0000000..2724d3f --- /dev/null +++ b/resources/imageFiles/emojis/2009.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2010.svg b/resources/imageFiles/emojis/2010.svg new file mode 100644 index 0000000..b0bbe53 --- /dev/null +++ b/resources/imageFiles/emojis/2010.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2011.svg b/resources/imageFiles/emojis/2011.svg new file mode 100644 index 0000000..0c17d73 --- /dev/null +++ b/resources/imageFiles/emojis/2011.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2012.svg b/resources/imageFiles/emojis/2012.svg new file mode 100644 index 0000000..f24199a --- /dev/null +++ b/resources/imageFiles/emojis/2012.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2013.svg b/resources/imageFiles/emojis/2013.svg new file mode 100644 index 0000000..1372d39 --- /dev/null +++ b/resources/imageFiles/emojis/2013.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2014.svg b/resources/imageFiles/emojis/2014.svg new file mode 100644 index 0000000..c6923b8 --- /dev/null +++ b/resources/imageFiles/emojis/2014.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2015.svg b/resources/imageFiles/emojis/2015.svg new file mode 100644 index 0000000..69fa8f5 --- /dev/null +++ b/resources/imageFiles/emojis/2015.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2016.svg b/resources/imageFiles/emojis/2016.svg new file mode 100644 index 0000000..b75fcfb --- /dev/null +++ b/resources/imageFiles/emojis/2016.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2017.svg b/resources/imageFiles/emojis/2017.svg new file mode 100644 index 0000000..38c8739 --- /dev/null +++ b/resources/imageFiles/emojis/2017.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2018.svg b/resources/imageFiles/emojis/2018.svg new file mode 100644 index 0000000..803788c --- /dev/null +++ b/resources/imageFiles/emojis/2018.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2019.svg b/resources/imageFiles/emojis/2019.svg new file mode 100644 index 0000000..b97ada3 --- /dev/null +++ b/resources/imageFiles/emojis/2019.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2020.svg b/resources/imageFiles/emojis/2020.svg new file mode 100644 index 0000000..5ace893 --- /dev/null +++ b/resources/imageFiles/emojis/2020.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2021.svg b/resources/imageFiles/emojis/2021.svg new file mode 100644 index 0000000..c121b7b --- /dev/null +++ b/resources/imageFiles/emojis/2021.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2022.svg b/resources/imageFiles/emojis/2022.svg new file mode 100644 index 0000000..98bb00a --- /dev/null +++ b/resources/imageFiles/emojis/2022.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2023.svg b/resources/imageFiles/emojis/2023.svg new file mode 100644 index 0000000..b7e958c --- /dev/null +++ b/resources/imageFiles/emojis/2023.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2024.svg b/resources/imageFiles/emojis/2024.svg new file mode 100644 index 0000000..7c8bb6f --- /dev/null +++ b/resources/imageFiles/emojis/2024.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2025.svg b/resources/imageFiles/emojis/2025.svg new file mode 100644 index 0000000..78c6f02 --- /dev/null +++ b/resources/imageFiles/emojis/2025.svg @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2026.svg b/resources/imageFiles/emojis/2026.svg new file mode 100644 index 0000000..36f19fe --- /dev/null +++ b/resources/imageFiles/emojis/2026.svg @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2027.svg b/resources/imageFiles/emojis/2027.svg new file mode 100644 index 0000000..e54d876 --- /dev/null +++ b/resources/imageFiles/emojis/2027.svg @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2028.svg b/resources/imageFiles/emojis/2028.svg new file mode 100644 index 0000000..118268b --- /dev/null +++ b/resources/imageFiles/emojis/2028.svg @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2029.svg b/resources/imageFiles/emojis/2029.svg new file mode 100644 index 0000000..2ff6e5a --- /dev/null +++ b/resources/imageFiles/emojis/2029.svg @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2030.svg b/resources/imageFiles/emojis/2030.svg new file mode 100644 index 0000000..b2845e0 --- /dev/null +++ b/resources/imageFiles/emojis/2030.svg @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2031.svg b/resources/imageFiles/emojis/2031.svg new file mode 100644 index 0000000..5e1aa77 --- /dev/null +++ b/resources/imageFiles/emojis/2031.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2032.svg b/resources/imageFiles/emojis/2032.svg new file mode 100644 index 0000000..45dafae --- /dev/null +++ b/resources/imageFiles/emojis/2032.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2033.svg b/resources/imageFiles/emojis/2033.svg new file mode 100644 index 0000000..07a471e --- /dev/null +++ b/resources/imageFiles/emojis/2033.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2034.svg b/resources/imageFiles/emojis/2034.svg new file mode 100644 index 0000000..641852c --- /dev/null +++ b/resources/imageFiles/emojis/2034.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2035.svg b/resources/imageFiles/emojis/2035.svg new file mode 100644 index 0000000..8152aa6 --- /dev/null +++ b/resources/imageFiles/emojis/2035.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2036.svg b/resources/imageFiles/emojis/2036.svg new file mode 100644 index 0000000..cd63005 --- /dev/null +++ b/resources/imageFiles/emojis/2036.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2037.svg b/resources/imageFiles/emojis/2037.svg new file mode 100644 index 0000000..b76bf9d --- /dev/null +++ b/resources/imageFiles/emojis/2037.svg @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2038.svg b/resources/imageFiles/emojis/2038.svg new file mode 100644 index 0000000..8160c38 --- /dev/null +++ b/resources/imageFiles/emojis/2038.svg @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2039.svg b/resources/imageFiles/emojis/2039.svg new file mode 100644 index 0000000..6b74361 --- /dev/null +++ b/resources/imageFiles/emojis/2039.svg @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2040.svg b/resources/imageFiles/emojis/2040.svg new file mode 100644 index 0000000..cbde7f1 --- /dev/null +++ b/resources/imageFiles/emojis/2040.svg @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2041.svg b/resources/imageFiles/emojis/2041.svg new file mode 100644 index 0000000..349aaab --- /dev/null +++ b/resources/imageFiles/emojis/2041.svg @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2042.svg b/resources/imageFiles/emojis/2042.svg new file mode 100644 index 0000000..825e34c --- /dev/null +++ b/resources/imageFiles/emojis/2042.svg @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2043.svg b/resources/imageFiles/emojis/2043.svg new file mode 100644 index 0000000..4b81048 --- /dev/null +++ b/resources/imageFiles/emojis/2043.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2044.svg b/resources/imageFiles/emojis/2044.svg new file mode 100644 index 0000000..6369652 --- /dev/null +++ b/resources/imageFiles/emojis/2044.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2045.svg b/resources/imageFiles/emojis/2045.svg new file mode 100644 index 0000000..05cf4f9 --- /dev/null +++ b/resources/imageFiles/emojis/2045.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2046.svg b/resources/imageFiles/emojis/2046.svg new file mode 100644 index 0000000..17ebbe3 --- /dev/null +++ b/resources/imageFiles/emojis/2046.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2047.svg b/resources/imageFiles/emojis/2047.svg new file mode 100644 index 0000000..944bfa7 --- /dev/null +++ b/resources/imageFiles/emojis/2047.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2048.svg b/resources/imageFiles/emojis/2048.svg new file mode 100644 index 0000000..b9f3e66 --- /dev/null +++ b/resources/imageFiles/emojis/2048.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2049.svg b/resources/imageFiles/emojis/2049.svg new file mode 100644 index 0000000..02b4252 --- /dev/null +++ b/resources/imageFiles/emojis/2049.svg @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2050.svg b/resources/imageFiles/emojis/2050.svg new file mode 100644 index 0000000..22780be --- /dev/null +++ b/resources/imageFiles/emojis/2050.svg @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2051.svg b/resources/imageFiles/emojis/2051.svg new file mode 100644 index 0000000..a1e0055 --- /dev/null +++ b/resources/imageFiles/emojis/2051.svg @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2052.svg b/resources/imageFiles/emojis/2052.svg new file mode 100644 index 0000000..46128a2 --- /dev/null +++ b/resources/imageFiles/emojis/2052.svg @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2053.svg b/resources/imageFiles/emojis/2053.svg new file mode 100644 index 0000000..1b444fe --- /dev/null +++ b/resources/imageFiles/emojis/2053.svg @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2054.svg b/resources/imageFiles/emojis/2054.svg new file mode 100644 index 0000000..01b8da8 --- /dev/null +++ b/resources/imageFiles/emojis/2054.svg @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2055.svg b/resources/imageFiles/emojis/2055.svg new file mode 100644 index 0000000..5eda5f6 --- /dev/null +++ b/resources/imageFiles/emojis/2055.svg @@ -0,0 +1,33 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2056.svg b/resources/imageFiles/emojis/2056.svg new file mode 100644 index 0000000..61bec13 --- /dev/null +++ b/resources/imageFiles/emojis/2056.svg @@ -0,0 +1,33 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2057.svg b/resources/imageFiles/emojis/2057.svg new file mode 100644 index 0000000..961d22d --- /dev/null +++ b/resources/imageFiles/emojis/2057.svg @@ -0,0 +1,33 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2058.svg b/resources/imageFiles/emojis/2058.svg new file mode 100644 index 0000000..da79d94 --- /dev/null +++ b/resources/imageFiles/emojis/2058.svg @@ -0,0 +1,33 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2059.svg b/resources/imageFiles/emojis/2059.svg new file mode 100644 index 0000000..2dff2b0 --- /dev/null +++ b/resources/imageFiles/emojis/2059.svg @@ -0,0 +1,33 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2060.svg b/resources/imageFiles/emojis/2060.svg new file mode 100644 index 0000000..81f29e6 --- /dev/null +++ b/resources/imageFiles/emojis/2060.svg @@ -0,0 +1,33 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2061.svg b/resources/imageFiles/emojis/2061.svg new file mode 100644 index 0000000..5b702cb --- /dev/null +++ b/resources/imageFiles/emojis/2061.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2062.svg b/resources/imageFiles/emojis/2062.svg new file mode 100644 index 0000000..42e371b --- /dev/null +++ b/resources/imageFiles/emojis/2062.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2063.svg b/resources/imageFiles/emojis/2063.svg new file mode 100644 index 0000000..7f8649e --- /dev/null +++ b/resources/imageFiles/emojis/2063.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2064.svg b/resources/imageFiles/emojis/2064.svg new file mode 100644 index 0000000..d668a8b --- /dev/null +++ b/resources/imageFiles/emojis/2064.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2065.svg b/resources/imageFiles/emojis/2065.svg new file mode 100644 index 0000000..719c1cc --- /dev/null +++ b/resources/imageFiles/emojis/2065.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2066.svg b/resources/imageFiles/emojis/2066.svg new file mode 100644 index 0000000..6df7061 --- /dev/null +++ b/resources/imageFiles/emojis/2066.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2067.svg b/resources/imageFiles/emojis/2067.svg new file mode 100644 index 0000000..c823711 --- /dev/null +++ b/resources/imageFiles/emojis/2067.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/resources/imageFiles/emojis/2068.svg b/resources/imageFiles/emojis/2068.svg new file mode 100644 index 0000000..a9715ae --- /dev/null +++ b/resources/imageFiles/emojis/2068.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/resources/imageFiles/emojis/2069.svg b/resources/imageFiles/emojis/2069.svg new file mode 100644 index 0000000..5d50bcf --- /dev/null +++ b/resources/imageFiles/emojis/2069.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/resources/imageFiles/emojis/2070.svg b/resources/imageFiles/emojis/2070.svg new file mode 100644 index 0000000..e851ab4 --- /dev/null +++ b/resources/imageFiles/emojis/2070.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/resources/imageFiles/emojis/2071.svg b/resources/imageFiles/emojis/2071.svg new file mode 100644 index 0000000..a3474ca --- /dev/null +++ b/resources/imageFiles/emojis/2071.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/resources/imageFiles/emojis/2072.svg b/resources/imageFiles/emojis/2072.svg new file mode 100644 index 0000000..9ca5622 --- /dev/null +++ b/resources/imageFiles/emojis/2072.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/resources/imageFiles/emojis/2073.svg b/resources/imageFiles/emojis/2073.svg new file mode 100644 index 0000000..419bbda --- /dev/null +++ b/resources/imageFiles/emojis/2073.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/resources/imageFiles/emojis/2074.svg b/resources/imageFiles/emojis/2074.svg new file mode 100644 index 0000000..156532d --- /dev/null +++ b/resources/imageFiles/emojis/2074.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/resources/imageFiles/emojis/2075.svg b/resources/imageFiles/emojis/2075.svg new file mode 100644 index 0000000..43a6e74 --- /dev/null +++ b/resources/imageFiles/emojis/2075.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/resources/imageFiles/emojis/2076.svg b/resources/imageFiles/emojis/2076.svg new file mode 100644 index 0000000..7e35cc8 --- /dev/null +++ b/resources/imageFiles/emojis/2076.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/resources/imageFiles/emojis/2077.svg b/resources/imageFiles/emojis/2077.svg new file mode 100644 index 0000000..3ce0842 --- /dev/null +++ b/resources/imageFiles/emojis/2077.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/resources/imageFiles/emojis/2078.svg b/resources/imageFiles/emojis/2078.svg new file mode 100644 index 0000000..40a5afd --- /dev/null +++ b/resources/imageFiles/emojis/2078.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/resources/imageFiles/emojis/2079.svg b/resources/imageFiles/emojis/2079.svg new file mode 100644 index 0000000..87bf1bd --- /dev/null +++ b/resources/imageFiles/emojis/2079.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2080.svg b/resources/imageFiles/emojis/2080.svg new file mode 100644 index 0000000..93e9b83 --- /dev/null +++ b/resources/imageFiles/emojis/2080.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2081.svg b/resources/imageFiles/emojis/2081.svg new file mode 100644 index 0000000..93e7c64 --- /dev/null +++ b/resources/imageFiles/emojis/2081.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2082.svg b/resources/imageFiles/emojis/2082.svg new file mode 100644 index 0000000..3622bbe --- /dev/null +++ b/resources/imageFiles/emojis/2082.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2083.svg b/resources/imageFiles/emojis/2083.svg new file mode 100644 index 0000000..8bf07c4 --- /dev/null +++ b/resources/imageFiles/emojis/2083.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2084.svg b/resources/imageFiles/emojis/2084.svg new file mode 100644 index 0000000..06bedc9 --- /dev/null +++ b/resources/imageFiles/emojis/2084.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2085.svg b/resources/imageFiles/emojis/2085.svg new file mode 100644 index 0000000..022df92 --- /dev/null +++ b/resources/imageFiles/emojis/2085.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2086.svg b/resources/imageFiles/emojis/2086.svg new file mode 100644 index 0000000..79d0b22 --- /dev/null +++ b/resources/imageFiles/emojis/2086.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2087.svg b/resources/imageFiles/emojis/2087.svg new file mode 100644 index 0000000..d289fdc --- /dev/null +++ b/resources/imageFiles/emojis/2087.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2088.svg b/resources/imageFiles/emojis/2088.svg new file mode 100644 index 0000000..c150786 --- /dev/null +++ b/resources/imageFiles/emojis/2088.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2089.svg b/resources/imageFiles/emojis/2089.svg new file mode 100644 index 0000000..bb5daec --- /dev/null +++ b/resources/imageFiles/emojis/2089.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2090.svg b/resources/imageFiles/emojis/2090.svg new file mode 100644 index 0000000..885ed0c --- /dev/null +++ b/resources/imageFiles/emojis/2090.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2091.svg b/resources/imageFiles/emojis/2091.svg new file mode 100644 index 0000000..e94852e --- /dev/null +++ b/resources/imageFiles/emojis/2091.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2092.svg b/resources/imageFiles/emojis/2092.svg new file mode 100644 index 0000000..a1209e4 --- /dev/null +++ b/resources/imageFiles/emojis/2092.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2093.svg b/resources/imageFiles/emojis/2093.svg new file mode 100644 index 0000000..fdb3795 --- /dev/null +++ b/resources/imageFiles/emojis/2093.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2094.svg b/resources/imageFiles/emojis/2094.svg new file mode 100644 index 0000000..0e77ee9 --- /dev/null +++ b/resources/imageFiles/emojis/2094.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2095.svg b/resources/imageFiles/emojis/2095.svg new file mode 100644 index 0000000..0dafb42 --- /dev/null +++ b/resources/imageFiles/emojis/2095.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2096.svg b/resources/imageFiles/emojis/2096.svg new file mode 100644 index 0000000..a304d86 --- /dev/null +++ b/resources/imageFiles/emojis/2096.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2097.svg b/resources/imageFiles/emojis/2097.svg new file mode 100644 index 0000000..59df681 --- /dev/null +++ b/resources/imageFiles/emojis/2097.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2098.svg b/resources/imageFiles/emojis/2098.svg new file mode 100644 index 0000000..f61dd57 --- /dev/null +++ b/resources/imageFiles/emojis/2098.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2099.svg b/resources/imageFiles/emojis/2099.svg new file mode 100644 index 0000000..17b2bb5 --- /dev/null +++ b/resources/imageFiles/emojis/2099.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2100.svg b/resources/imageFiles/emojis/2100.svg new file mode 100644 index 0000000..38fd869 --- /dev/null +++ b/resources/imageFiles/emojis/2100.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2101.svg b/resources/imageFiles/emojis/2101.svg new file mode 100644 index 0000000..4b72fb5 --- /dev/null +++ b/resources/imageFiles/emojis/2101.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2102.svg b/resources/imageFiles/emojis/2102.svg new file mode 100644 index 0000000..806383d --- /dev/null +++ b/resources/imageFiles/emojis/2102.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2103.svg b/resources/imageFiles/emojis/2103.svg new file mode 100644 index 0000000..0dbdf15 --- /dev/null +++ b/resources/imageFiles/emojis/2103.svg @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2104.svg b/resources/imageFiles/emojis/2104.svg new file mode 100644 index 0000000..9686a2c --- /dev/null +++ b/resources/imageFiles/emojis/2104.svg @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2105.svg b/resources/imageFiles/emojis/2105.svg new file mode 100644 index 0000000..e9fc1a7 --- /dev/null +++ b/resources/imageFiles/emojis/2105.svg @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2106.svg b/resources/imageFiles/emojis/2106.svg new file mode 100644 index 0000000..1612fab --- /dev/null +++ b/resources/imageFiles/emojis/2106.svg @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2107.svg b/resources/imageFiles/emojis/2107.svg new file mode 100644 index 0000000..f069bcc --- /dev/null +++ b/resources/imageFiles/emojis/2107.svg @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2108.svg b/resources/imageFiles/emojis/2108.svg new file mode 100644 index 0000000..124ad20 --- /dev/null +++ b/resources/imageFiles/emojis/2108.svg @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2109.svg b/resources/imageFiles/emojis/2109.svg new file mode 100644 index 0000000..2841da0 --- /dev/null +++ b/resources/imageFiles/emojis/2109.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2110.svg b/resources/imageFiles/emojis/2110.svg new file mode 100644 index 0000000..e166164 --- /dev/null +++ b/resources/imageFiles/emojis/2110.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2111.svg b/resources/imageFiles/emojis/2111.svg new file mode 100644 index 0000000..7091489 --- /dev/null +++ b/resources/imageFiles/emojis/2111.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2112.svg b/resources/imageFiles/emojis/2112.svg new file mode 100644 index 0000000..c1c6a3e --- /dev/null +++ b/resources/imageFiles/emojis/2112.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2113.svg b/resources/imageFiles/emojis/2113.svg new file mode 100644 index 0000000..2986d9d --- /dev/null +++ b/resources/imageFiles/emojis/2113.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2114.svg b/resources/imageFiles/emojis/2114.svg new file mode 100644 index 0000000..b8d3047 --- /dev/null +++ b/resources/imageFiles/emojis/2114.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2115.svg b/resources/imageFiles/emojis/2115.svg new file mode 100644 index 0000000..ed2697a --- /dev/null +++ b/resources/imageFiles/emojis/2115.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2116.svg b/resources/imageFiles/emojis/2116.svg new file mode 100644 index 0000000..05b6959 --- /dev/null +++ b/resources/imageFiles/emojis/2116.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2117.svg b/resources/imageFiles/emojis/2117.svg new file mode 100644 index 0000000..337877c --- /dev/null +++ b/resources/imageFiles/emojis/2117.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2118.svg b/resources/imageFiles/emojis/2118.svg new file mode 100644 index 0000000..1c913bd --- /dev/null +++ b/resources/imageFiles/emojis/2118.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2119.svg b/resources/imageFiles/emojis/2119.svg new file mode 100644 index 0000000..08a2550 --- /dev/null +++ b/resources/imageFiles/emojis/2119.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2120.svg b/resources/imageFiles/emojis/2120.svg new file mode 100644 index 0000000..4827325 --- /dev/null +++ b/resources/imageFiles/emojis/2120.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2121.svg b/resources/imageFiles/emojis/2121.svg new file mode 100644 index 0000000..73a49fe --- /dev/null +++ b/resources/imageFiles/emojis/2121.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2122.svg b/resources/imageFiles/emojis/2122.svg new file mode 100644 index 0000000..6ef63a6 --- /dev/null +++ b/resources/imageFiles/emojis/2122.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2123.svg b/resources/imageFiles/emojis/2123.svg new file mode 100644 index 0000000..db87c74 --- /dev/null +++ b/resources/imageFiles/emojis/2123.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2124.svg b/resources/imageFiles/emojis/2124.svg new file mode 100644 index 0000000..f4eea8f --- /dev/null +++ b/resources/imageFiles/emojis/2124.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2125.svg b/resources/imageFiles/emojis/2125.svg new file mode 100644 index 0000000..7c5e05e --- /dev/null +++ b/resources/imageFiles/emojis/2125.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2126.svg b/resources/imageFiles/emojis/2126.svg new file mode 100644 index 0000000..411d880 --- /dev/null +++ b/resources/imageFiles/emojis/2126.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2127.svg b/resources/imageFiles/emojis/2127.svg new file mode 100644 index 0000000..ab981a4 --- /dev/null +++ b/resources/imageFiles/emojis/2127.svg @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2128.svg b/resources/imageFiles/emojis/2128.svg new file mode 100644 index 0000000..8af78e3 --- /dev/null +++ b/resources/imageFiles/emojis/2128.svg @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2129.svg b/resources/imageFiles/emojis/2129.svg new file mode 100644 index 0000000..508c5cb --- /dev/null +++ b/resources/imageFiles/emojis/2129.svg @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2130.svg b/resources/imageFiles/emojis/2130.svg new file mode 100644 index 0000000..059dd02 --- /dev/null +++ b/resources/imageFiles/emojis/2130.svg @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2131.svg b/resources/imageFiles/emojis/2131.svg new file mode 100644 index 0000000..7c4c4af --- /dev/null +++ b/resources/imageFiles/emojis/2131.svg @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2132.svg b/resources/imageFiles/emojis/2132.svg new file mode 100644 index 0000000..c27b71b --- /dev/null +++ b/resources/imageFiles/emojis/2132.svg @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2133.svg b/resources/imageFiles/emojis/2133.svg new file mode 100644 index 0000000..3a825a5 --- /dev/null +++ b/resources/imageFiles/emojis/2133.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2134.svg b/resources/imageFiles/emojis/2134.svg new file mode 100644 index 0000000..5f75d4d --- /dev/null +++ b/resources/imageFiles/emojis/2134.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2135.svg b/resources/imageFiles/emojis/2135.svg new file mode 100644 index 0000000..a16454f --- /dev/null +++ b/resources/imageFiles/emojis/2135.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2136.svg b/resources/imageFiles/emojis/2136.svg new file mode 100644 index 0000000..c0aad55 --- /dev/null +++ b/resources/imageFiles/emojis/2136.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2137.svg b/resources/imageFiles/emojis/2137.svg new file mode 100644 index 0000000..4818d86 --- /dev/null +++ b/resources/imageFiles/emojis/2137.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2138.svg b/resources/imageFiles/emojis/2138.svg new file mode 100644 index 0000000..0ffdf40 --- /dev/null +++ b/resources/imageFiles/emojis/2138.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2139.svg b/resources/imageFiles/emojis/2139.svg new file mode 100644 index 0000000..ce14261 --- /dev/null +++ b/resources/imageFiles/emojis/2139.svg @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2140.svg b/resources/imageFiles/emojis/2140.svg new file mode 100644 index 0000000..3360e62 --- /dev/null +++ b/resources/imageFiles/emojis/2140.svg @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2141.svg b/resources/imageFiles/emojis/2141.svg new file mode 100644 index 0000000..51d82db --- /dev/null +++ b/resources/imageFiles/emojis/2141.svg @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2142.svg b/resources/imageFiles/emojis/2142.svg new file mode 100644 index 0000000..c7fb7cc --- /dev/null +++ b/resources/imageFiles/emojis/2142.svg @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2143.svg b/resources/imageFiles/emojis/2143.svg new file mode 100644 index 0000000..f8b3f63 --- /dev/null +++ b/resources/imageFiles/emojis/2143.svg @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2144.svg b/resources/imageFiles/emojis/2144.svg new file mode 100644 index 0000000..bfdc2ce --- /dev/null +++ b/resources/imageFiles/emojis/2144.svg @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2145.svg b/resources/imageFiles/emojis/2145.svg new file mode 100644 index 0000000..6df3179 --- /dev/null +++ b/resources/imageFiles/emojis/2145.svg @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2146.svg b/resources/imageFiles/emojis/2146.svg new file mode 100644 index 0000000..1a5e2d8 --- /dev/null +++ b/resources/imageFiles/emojis/2146.svg @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2147.svg b/resources/imageFiles/emojis/2147.svg new file mode 100644 index 0000000..81bd531 --- /dev/null +++ b/resources/imageFiles/emojis/2147.svg @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2148.svg b/resources/imageFiles/emojis/2148.svg new file mode 100644 index 0000000..f05e0c2 --- /dev/null +++ b/resources/imageFiles/emojis/2148.svg @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2149.svg b/resources/imageFiles/emojis/2149.svg new file mode 100644 index 0000000..876f44c --- /dev/null +++ b/resources/imageFiles/emojis/2149.svg @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2150.svg b/resources/imageFiles/emojis/2150.svg new file mode 100644 index 0000000..f4cb3da --- /dev/null +++ b/resources/imageFiles/emojis/2150.svg @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2151.svg b/resources/imageFiles/emojis/2151.svg new file mode 100644 index 0000000..a5eeb1e --- /dev/null +++ b/resources/imageFiles/emojis/2151.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2152.svg b/resources/imageFiles/emojis/2152.svg new file mode 100644 index 0000000..8e91a0d --- /dev/null +++ b/resources/imageFiles/emojis/2152.svg @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2153.svg b/resources/imageFiles/emojis/2153.svg new file mode 100644 index 0000000..39f35bc --- /dev/null +++ b/resources/imageFiles/emojis/2153.svg @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2154.svg b/resources/imageFiles/emojis/2154.svg new file mode 100644 index 0000000..a2e83f8 --- /dev/null +++ b/resources/imageFiles/emojis/2154.svg @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2155.svg b/resources/imageFiles/emojis/2155.svg new file mode 100644 index 0000000..4458301 --- /dev/null +++ b/resources/imageFiles/emojis/2155.svg @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2156.svg b/resources/imageFiles/emojis/2156.svg new file mode 100644 index 0000000..bc552fa --- /dev/null +++ b/resources/imageFiles/emojis/2156.svg @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2157.svg b/resources/imageFiles/emojis/2157.svg new file mode 100644 index 0000000..05429bf --- /dev/null +++ b/resources/imageFiles/emojis/2157.svg @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2158.svg b/resources/imageFiles/emojis/2158.svg new file mode 100644 index 0000000..7b0f054 --- /dev/null +++ b/resources/imageFiles/emojis/2158.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2159.svg b/resources/imageFiles/emojis/2159.svg new file mode 100644 index 0000000..655907d --- /dev/null +++ b/resources/imageFiles/emojis/2159.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2160.svg b/resources/imageFiles/emojis/2160.svg new file mode 100644 index 0000000..750ba20 --- /dev/null +++ b/resources/imageFiles/emojis/2160.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2161.svg b/resources/imageFiles/emojis/2161.svg new file mode 100644 index 0000000..739ab67 --- /dev/null +++ b/resources/imageFiles/emojis/2161.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2162.svg b/resources/imageFiles/emojis/2162.svg new file mode 100644 index 0000000..c3d2db5 --- /dev/null +++ b/resources/imageFiles/emojis/2162.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2163.svg b/resources/imageFiles/emojis/2163.svg new file mode 100644 index 0000000..657bd84 --- /dev/null +++ b/resources/imageFiles/emojis/2163.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2164.svg b/resources/imageFiles/emojis/2164.svg new file mode 100644 index 0000000..71f637a --- /dev/null +++ b/resources/imageFiles/emojis/2164.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2165.svg b/resources/imageFiles/emojis/2165.svg new file mode 100644 index 0000000..92f2358 --- /dev/null +++ b/resources/imageFiles/emojis/2165.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2166.svg b/resources/imageFiles/emojis/2166.svg new file mode 100644 index 0000000..550bc7d --- /dev/null +++ b/resources/imageFiles/emojis/2166.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2167.svg b/resources/imageFiles/emojis/2167.svg new file mode 100644 index 0000000..c0e4cdb --- /dev/null +++ b/resources/imageFiles/emojis/2167.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2168.svg b/resources/imageFiles/emojis/2168.svg new file mode 100644 index 0000000..ae65f51 --- /dev/null +++ b/resources/imageFiles/emojis/2168.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2169.svg b/resources/imageFiles/emojis/2169.svg new file mode 100644 index 0000000..d398c8b --- /dev/null +++ b/resources/imageFiles/emojis/2169.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2170.svg b/resources/imageFiles/emojis/2170.svg new file mode 100644 index 0000000..d5a8160 --- /dev/null +++ b/resources/imageFiles/emojis/2170.svg @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2171.svg b/resources/imageFiles/emojis/2171.svg new file mode 100644 index 0000000..b386ecf --- /dev/null +++ b/resources/imageFiles/emojis/2171.svg @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2172.svg b/resources/imageFiles/emojis/2172.svg new file mode 100644 index 0000000..973c761 --- /dev/null +++ b/resources/imageFiles/emojis/2172.svg @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2173.svg b/resources/imageFiles/emojis/2173.svg new file mode 100644 index 0000000..dd536da --- /dev/null +++ b/resources/imageFiles/emojis/2173.svg @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2174.svg b/resources/imageFiles/emojis/2174.svg new file mode 100644 index 0000000..6e9166d --- /dev/null +++ b/resources/imageFiles/emojis/2174.svg @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2175.svg b/resources/imageFiles/emojis/2175.svg new file mode 100644 index 0000000..3c5d77e --- /dev/null +++ b/resources/imageFiles/emojis/2175.svg @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2176.svg b/resources/imageFiles/emojis/2176.svg new file mode 100644 index 0000000..df42718 --- /dev/null +++ b/resources/imageFiles/emojis/2176.svg @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2177.svg b/resources/imageFiles/emojis/2177.svg new file mode 100644 index 0000000..030b5ed --- /dev/null +++ b/resources/imageFiles/emojis/2177.svg @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2178.svg b/resources/imageFiles/emojis/2178.svg new file mode 100644 index 0000000..2780f6c --- /dev/null +++ b/resources/imageFiles/emojis/2178.svg @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2179.svg b/resources/imageFiles/emojis/2179.svg new file mode 100644 index 0000000..8207c21 --- /dev/null +++ b/resources/imageFiles/emojis/2179.svg @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2180.svg b/resources/imageFiles/emojis/2180.svg new file mode 100644 index 0000000..f79f553 --- /dev/null +++ b/resources/imageFiles/emojis/2180.svg @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2181.svg b/resources/imageFiles/emojis/2181.svg new file mode 100644 index 0000000..63c22e7 --- /dev/null +++ b/resources/imageFiles/emojis/2181.svg @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2182.svg b/resources/imageFiles/emojis/2182.svg new file mode 100644 index 0000000..876bc36 --- /dev/null +++ b/resources/imageFiles/emojis/2182.svg @@ -0,0 +1,35 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2183.svg b/resources/imageFiles/emojis/2183.svg new file mode 100644 index 0000000..e5d2db1 --- /dev/null +++ b/resources/imageFiles/emojis/2183.svg @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2184.svg b/resources/imageFiles/emojis/2184.svg new file mode 100644 index 0000000..02a6daa --- /dev/null +++ b/resources/imageFiles/emojis/2184.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2185.svg b/resources/imageFiles/emojis/2185.svg new file mode 100644 index 0000000..7985895 --- /dev/null +++ b/resources/imageFiles/emojis/2185.svg @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2186.svg b/resources/imageFiles/emojis/2186.svg new file mode 100644 index 0000000..53a0d14 --- /dev/null +++ b/resources/imageFiles/emojis/2186.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2187.svg b/resources/imageFiles/emojis/2187.svg new file mode 100644 index 0000000..301ec65 --- /dev/null +++ b/resources/imageFiles/emojis/2187.svg @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2188.svg b/resources/imageFiles/emojis/2188.svg new file mode 100644 index 0000000..e132b9c --- /dev/null +++ b/resources/imageFiles/emojis/2188.svg @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2189.svg b/resources/imageFiles/emojis/2189.svg new file mode 100644 index 0000000..fbc642c --- /dev/null +++ b/resources/imageFiles/emojis/2189.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2190.svg b/resources/imageFiles/emojis/2190.svg new file mode 100644 index 0000000..8b3358a --- /dev/null +++ b/resources/imageFiles/emojis/2190.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2191.svg b/resources/imageFiles/emojis/2191.svg new file mode 100644 index 0000000..46ef387 --- /dev/null +++ b/resources/imageFiles/emojis/2191.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/resources/imageFiles/emojis/2192.svg b/resources/imageFiles/emojis/2192.svg new file mode 100644 index 0000000..1bafa44 --- /dev/null +++ b/resources/imageFiles/emojis/2192.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2193.svg b/resources/imageFiles/emojis/2193.svg new file mode 100644 index 0000000..dfd723a --- /dev/null +++ b/resources/imageFiles/emojis/2193.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2194.svg b/resources/imageFiles/emojis/2194.svg new file mode 100644 index 0000000..84327f0 --- /dev/null +++ b/resources/imageFiles/emojis/2194.svg @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2195.svg b/resources/imageFiles/emojis/2195.svg new file mode 100644 index 0000000..5c38e63 --- /dev/null +++ b/resources/imageFiles/emojis/2195.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2196.svg b/resources/imageFiles/emojis/2196.svg new file mode 100644 index 0000000..14c99ae --- /dev/null +++ b/resources/imageFiles/emojis/2196.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2197.svg b/resources/imageFiles/emojis/2197.svg new file mode 100644 index 0000000..f079d9b --- /dev/null +++ b/resources/imageFiles/emojis/2197.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/resources/imageFiles/emojis/2198.svg b/resources/imageFiles/emojis/2198.svg new file mode 100644 index 0000000..b977813 --- /dev/null +++ b/resources/imageFiles/emojis/2198.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2199.svg b/resources/imageFiles/emojis/2199.svg new file mode 100644 index 0000000..5e05712 --- /dev/null +++ b/resources/imageFiles/emojis/2199.svg @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2200.svg b/resources/imageFiles/emojis/2200.svg new file mode 100644 index 0000000..666acbf --- /dev/null +++ b/resources/imageFiles/emojis/2200.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2201.svg b/resources/imageFiles/emojis/2201.svg new file mode 100644 index 0000000..611f87c --- /dev/null +++ b/resources/imageFiles/emojis/2201.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/resources/imageFiles/emojis/2202.svg b/resources/imageFiles/emojis/2202.svg new file mode 100644 index 0000000..bb093af --- /dev/null +++ b/resources/imageFiles/emojis/2202.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2203.svg b/resources/imageFiles/emojis/2203.svg new file mode 100644 index 0000000..e6451b0 --- /dev/null +++ b/resources/imageFiles/emojis/2203.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2204.svg b/resources/imageFiles/emojis/2204.svg new file mode 100644 index 0000000..74c7006 --- /dev/null +++ b/resources/imageFiles/emojis/2204.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2205.svg b/resources/imageFiles/emojis/2205.svg new file mode 100644 index 0000000..86625d2 --- /dev/null +++ b/resources/imageFiles/emojis/2205.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/resources/imageFiles/emojis/2206.svg b/resources/imageFiles/emojis/2206.svg new file mode 100644 index 0000000..64b8853 --- /dev/null +++ b/resources/imageFiles/emojis/2206.svg @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2207.svg b/resources/imageFiles/emojis/2207.svg new file mode 100644 index 0000000..9643930 --- /dev/null +++ b/resources/imageFiles/emojis/2207.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2208.svg b/resources/imageFiles/emojis/2208.svg new file mode 100644 index 0000000..3b5b5c9 --- /dev/null +++ b/resources/imageFiles/emojis/2208.svg @@ -0,0 +1,36 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2209.svg b/resources/imageFiles/emojis/2209.svg new file mode 100644 index 0000000..a6f8b66 --- /dev/null +++ b/resources/imageFiles/emojis/2209.svg @@ -0,0 +1,32 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2210.svg b/resources/imageFiles/emojis/2210.svg new file mode 100644 index 0000000..511b840 --- /dev/null +++ b/resources/imageFiles/emojis/2210.svg @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2211.svg b/resources/imageFiles/emojis/2211.svg new file mode 100644 index 0000000..8624f7c --- /dev/null +++ b/resources/imageFiles/emojis/2211.svg @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2212.svg b/resources/imageFiles/emojis/2212.svg new file mode 100644 index 0000000..ede8c6f --- /dev/null +++ b/resources/imageFiles/emojis/2212.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2213.svg b/resources/imageFiles/emojis/2213.svg new file mode 100644 index 0000000..1471c7a --- /dev/null +++ b/resources/imageFiles/emojis/2213.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2214.svg b/resources/imageFiles/emojis/2214.svg new file mode 100644 index 0000000..72d09ed --- /dev/null +++ b/resources/imageFiles/emojis/2214.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2215.svg b/resources/imageFiles/emojis/2215.svg new file mode 100644 index 0000000..d0f0a88 --- /dev/null +++ b/resources/imageFiles/emojis/2215.svg @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2216.svg b/resources/imageFiles/emojis/2216.svg new file mode 100644 index 0000000..917a8a5 --- /dev/null +++ b/resources/imageFiles/emojis/2216.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/resources/imageFiles/emojis/2217.svg b/resources/imageFiles/emojis/2217.svg new file mode 100644 index 0000000..e0b11bb --- /dev/null +++ b/resources/imageFiles/emojis/2217.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/resources/imageFiles/emojis/2218.svg b/resources/imageFiles/emojis/2218.svg new file mode 100644 index 0000000..8b43ae0 --- /dev/null +++ b/resources/imageFiles/emojis/2218.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2219.svg b/resources/imageFiles/emojis/2219.svg new file mode 100644 index 0000000..bf042b6 --- /dev/null +++ b/resources/imageFiles/emojis/2219.svg @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2220.svg b/resources/imageFiles/emojis/2220.svg new file mode 100644 index 0000000..ad091cf --- /dev/null +++ b/resources/imageFiles/emojis/2220.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2221.svg b/resources/imageFiles/emojis/2221.svg new file mode 100644 index 0000000..31cc2f8 --- /dev/null +++ b/resources/imageFiles/emojis/2221.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2222.svg b/resources/imageFiles/emojis/2222.svg new file mode 100644 index 0000000..0d8f517 --- /dev/null +++ b/resources/imageFiles/emojis/2222.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2223.svg b/resources/imageFiles/emojis/2223.svg new file mode 100644 index 0000000..7313b4d --- /dev/null +++ b/resources/imageFiles/emojis/2223.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2224.svg b/resources/imageFiles/emojis/2224.svg new file mode 100644 index 0000000..b6ccf4c --- /dev/null +++ b/resources/imageFiles/emojis/2224.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2225.svg b/resources/imageFiles/emojis/2225.svg new file mode 100644 index 0000000..3908c96 --- /dev/null +++ b/resources/imageFiles/emojis/2225.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2226.svg b/resources/imageFiles/emojis/2226.svg new file mode 100644 index 0000000..953c92f --- /dev/null +++ b/resources/imageFiles/emojis/2226.svg @@ -0,0 +1,42 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2227.svg b/resources/imageFiles/emojis/2227.svg new file mode 100644 index 0000000..24013b9 --- /dev/null +++ b/resources/imageFiles/emojis/2227.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2228.svg b/resources/imageFiles/emojis/2228.svg new file mode 100644 index 0000000..fd78340 --- /dev/null +++ b/resources/imageFiles/emojis/2228.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2229.svg b/resources/imageFiles/emojis/2229.svg new file mode 100644 index 0000000..372b56b --- /dev/null +++ b/resources/imageFiles/emojis/2229.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2230.svg b/resources/imageFiles/emojis/2230.svg new file mode 100644 index 0000000..5528b94 --- /dev/null +++ b/resources/imageFiles/emojis/2230.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2231.svg b/resources/imageFiles/emojis/2231.svg new file mode 100644 index 0000000..d3df24c --- /dev/null +++ b/resources/imageFiles/emojis/2231.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2232.svg b/resources/imageFiles/emojis/2232.svg new file mode 100644 index 0000000..08fabdf --- /dev/null +++ b/resources/imageFiles/emojis/2232.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2233.svg b/resources/imageFiles/emojis/2233.svg new file mode 100644 index 0000000..5e21396 --- /dev/null +++ b/resources/imageFiles/emojis/2233.svg @@ -0,0 +1,31 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2234.svg b/resources/imageFiles/emojis/2234.svg new file mode 100644 index 0000000..d6511c3 --- /dev/null +++ b/resources/imageFiles/emojis/2234.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2235.svg b/resources/imageFiles/emojis/2235.svg new file mode 100644 index 0000000..aacdb27 --- /dev/null +++ b/resources/imageFiles/emojis/2235.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2236.svg b/resources/imageFiles/emojis/2236.svg new file mode 100644 index 0000000..c746104 --- /dev/null +++ b/resources/imageFiles/emojis/2236.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2237.svg b/resources/imageFiles/emojis/2237.svg new file mode 100644 index 0000000..2855a51 --- /dev/null +++ b/resources/imageFiles/emojis/2237.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2238.svg b/resources/imageFiles/emojis/2238.svg new file mode 100644 index 0000000..4c75505 --- /dev/null +++ b/resources/imageFiles/emojis/2238.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2239.svg b/resources/imageFiles/emojis/2239.svg new file mode 100644 index 0000000..52f2267 --- /dev/null +++ b/resources/imageFiles/emojis/2239.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2240.svg b/resources/imageFiles/emojis/2240.svg new file mode 100644 index 0000000..19e24b5 --- /dev/null +++ b/resources/imageFiles/emojis/2240.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/resources/imageFiles/emojis/2241.svg b/resources/imageFiles/emojis/2241.svg new file mode 100644 index 0000000..c2d5d9b --- /dev/null +++ b/resources/imageFiles/emojis/2241.svg @@ -0,0 +1,34 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2242.svg b/resources/imageFiles/emojis/2242.svg new file mode 100644 index 0000000..d1627da --- /dev/null +++ b/resources/imageFiles/emojis/2242.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2243.svg b/resources/imageFiles/emojis/2243.svg new file mode 100644 index 0000000..4c6acbc --- /dev/null +++ b/resources/imageFiles/emojis/2243.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2244.svg b/resources/imageFiles/emojis/2244.svg new file mode 100644 index 0000000..d2b27b5 --- /dev/null +++ b/resources/imageFiles/emojis/2244.svg @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2245.svg b/resources/imageFiles/emojis/2245.svg new file mode 100644 index 0000000..1e4549a --- /dev/null +++ b/resources/imageFiles/emojis/2245.svg @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2246.svg b/resources/imageFiles/emojis/2246.svg new file mode 100644 index 0000000..349b9a8 --- /dev/null +++ b/resources/imageFiles/emojis/2246.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2247.svg b/resources/imageFiles/emojis/2247.svg new file mode 100644 index 0000000..38a5c07 --- /dev/null +++ b/resources/imageFiles/emojis/2247.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2248.svg b/resources/imageFiles/emojis/2248.svg new file mode 100644 index 0000000..7f31df2 --- /dev/null +++ b/resources/imageFiles/emojis/2248.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2249.svg b/resources/imageFiles/emojis/2249.svg new file mode 100644 index 0000000..1313046 --- /dev/null +++ b/resources/imageFiles/emojis/2249.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2250.svg b/resources/imageFiles/emojis/2250.svg new file mode 100644 index 0000000..fdccb19 --- /dev/null +++ b/resources/imageFiles/emojis/2250.svg @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2251.svg b/resources/imageFiles/emojis/2251.svg new file mode 100644 index 0000000..53017dc --- /dev/null +++ b/resources/imageFiles/emojis/2251.svg @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2252.svg b/resources/imageFiles/emojis/2252.svg new file mode 100644 index 0000000..bb4e0bf --- /dev/null +++ b/resources/imageFiles/emojis/2252.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/resources/imageFiles/emojis/2253.svg b/resources/imageFiles/emojis/2253.svg new file mode 100644 index 0000000..bf1aec2 --- /dev/null +++ b/resources/imageFiles/emojis/2253.svg @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2254.svg b/resources/imageFiles/emojis/2254.svg new file mode 100644 index 0000000..2da932d --- /dev/null +++ b/resources/imageFiles/emojis/2254.svg @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2255.svg b/resources/imageFiles/emojis/2255.svg new file mode 100644 index 0000000..353db5c --- /dev/null +++ b/resources/imageFiles/emojis/2255.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2256.svg b/resources/imageFiles/emojis/2256.svg new file mode 100644 index 0000000..43a69ea --- /dev/null +++ b/resources/imageFiles/emojis/2256.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/resources/imageFiles/emojis/2257.svg b/resources/imageFiles/emojis/2257.svg new file mode 100644 index 0000000..6dbfd02 --- /dev/null +++ b/resources/imageFiles/emojis/2257.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2258.svg b/resources/imageFiles/emojis/2258.svg new file mode 100644 index 0000000..3f650dd --- /dev/null +++ b/resources/imageFiles/emojis/2258.svg @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2259.svg b/resources/imageFiles/emojis/2259.svg new file mode 100644 index 0000000..a906ae6 --- /dev/null +++ b/resources/imageFiles/emojis/2259.svg @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2260.svg b/resources/imageFiles/emojis/2260.svg new file mode 100644 index 0000000..69d3b62 --- /dev/null +++ b/resources/imageFiles/emojis/2260.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2261.svg b/resources/imageFiles/emojis/2261.svg new file mode 100644 index 0000000..9fea5bd --- /dev/null +++ b/resources/imageFiles/emojis/2261.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2262.svg b/resources/imageFiles/emojis/2262.svg new file mode 100644 index 0000000..1ad93e7 --- /dev/null +++ b/resources/imageFiles/emojis/2262.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2263.svg b/resources/imageFiles/emojis/2263.svg new file mode 100644 index 0000000..3a3406e --- /dev/null +++ b/resources/imageFiles/emojis/2263.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2264.svg b/resources/imageFiles/emojis/2264.svg new file mode 100644 index 0000000..8753d43 --- /dev/null +++ b/resources/imageFiles/emojis/2264.svg @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2265.svg b/resources/imageFiles/emojis/2265.svg new file mode 100644 index 0000000..406be12 --- /dev/null +++ b/resources/imageFiles/emojis/2265.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2266.svg b/resources/imageFiles/emojis/2266.svg new file mode 100644 index 0000000..ab0e858 --- /dev/null +++ b/resources/imageFiles/emojis/2266.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2267.svg b/resources/imageFiles/emojis/2267.svg new file mode 100644 index 0000000..175bbf6 --- /dev/null +++ b/resources/imageFiles/emojis/2267.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2268.svg b/resources/imageFiles/emojis/2268.svg new file mode 100644 index 0000000..5b28895 --- /dev/null +++ b/resources/imageFiles/emojis/2268.svg @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2269.svg b/resources/imageFiles/emojis/2269.svg new file mode 100644 index 0000000..ec2fb83 --- /dev/null +++ b/resources/imageFiles/emojis/2269.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2270.svg b/resources/imageFiles/emojis/2270.svg new file mode 100644 index 0000000..61e82f2 --- /dev/null +++ b/resources/imageFiles/emojis/2270.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2271.svg b/resources/imageFiles/emojis/2271.svg new file mode 100644 index 0000000..2a1263c --- /dev/null +++ b/resources/imageFiles/emojis/2271.svg @@ -0,0 +1,36 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2272.svg b/resources/imageFiles/emojis/2272.svg new file mode 100644 index 0000000..f31ecb3 --- /dev/null +++ b/resources/imageFiles/emojis/2272.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2273.svg b/resources/imageFiles/emojis/2273.svg new file mode 100644 index 0000000..0084513 --- /dev/null +++ b/resources/imageFiles/emojis/2273.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2274.svg b/resources/imageFiles/emojis/2274.svg new file mode 100644 index 0000000..14e6258 --- /dev/null +++ b/resources/imageFiles/emojis/2274.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2275.svg b/resources/imageFiles/emojis/2275.svg new file mode 100644 index 0000000..b2dc67d --- /dev/null +++ b/resources/imageFiles/emojis/2275.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2276.svg b/resources/imageFiles/emojis/2276.svg new file mode 100644 index 0000000..16a4697 --- /dev/null +++ b/resources/imageFiles/emojis/2276.svg @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2277.svg b/resources/imageFiles/emojis/2277.svg new file mode 100644 index 0000000..6e3a1b4 --- /dev/null +++ b/resources/imageFiles/emojis/2277.svg @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2278.svg b/resources/imageFiles/emojis/2278.svg new file mode 100644 index 0000000..cb80d95 --- /dev/null +++ b/resources/imageFiles/emojis/2278.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2279.svg b/resources/imageFiles/emojis/2279.svg new file mode 100644 index 0000000..cd22b29 --- /dev/null +++ b/resources/imageFiles/emojis/2279.svg @@ -0,0 +1,46 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2280.svg b/resources/imageFiles/emojis/2280.svg new file mode 100644 index 0000000..a7cde13 --- /dev/null +++ b/resources/imageFiles/emojis/2280.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2281.svg b/resources/imageFiles/emojis/2281.svg new file mode 100644 index 0000000..13889f0 --- /dev/null +++ b/resources/imageFiles/emojis/2281.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2282.svg b/resources/imageFiles/emojis/2282.svg new file mode 100644 index 0000000..e92fbff --- /dev/null +++ b/resources/imageFiles/emojis/2282.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2283.svg b/resources/imageFiles/emojis/2283.svg new file mode 100644 index 0000000..fe320c8 --- /dev/null +++ b/resources/imageFiles/emojis/2283.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/resources/imageFiles/emojis/2284.svg b/resources/imageFiles/emojis/2284.svg new file mode 100644 index 0000000..1adefd7 --- /dev/null +++ b/resources/imageFiles/emojis/2284.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2285.svg b/resources/imageFiles/emojis/2285.svg new file mode 100644 index 0000000..7fbf47f --- /dev/null +++ b/resources/imageFiles/emojis/2285.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2286.svg b/resources/imageFiles/emojis/2286.svg new file mode 100644 index 0000000..26fb397 --- /dev/null +++ b/resources/imageFiles/emojis/2286.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2287.svg b/resources/imageFiles/emojis/2287.svg new file mode 100644 index 0000000..2ae1bf7 --- /dev/null +++ b/resources/imageFiles/emojis/2287.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2288.svg b/resources/imageFiles/emojis/2288.svg new file mode 100644 index 0000000..2a24faa --- /dev/null +++ b/resources/imageFiles/emojis/2288.svg @@ -0,0 +1,41 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2289.svg b/resources/imageFiles/emojis/2289.svg new file mode 100644 index 0000000..92a1448 --- /dev/null +++ b/resources/imageFiles/emojis/2289.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2290.svg b/resources/imageFiles/emojis/2290.svg new file mode 100644 index 0000000..0aec517 --- /dev/null +++ b/resources/imageFiles/emojis/2290.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2291.svg b/resources/imageFiles/emojis/2291.svg new file mode 100644 index 0000000..7e72b57 --- /dev/null +++ b/resources/imageFiles/emojis/2291.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/resources/imageFiles/emojis/2292.svg b/resources/imageFiles/emojis/2292.svg new file mode 100644 index 0000000..6940c36 --- /dev/null +++ b/resources/imageFiles/emojis/2292.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2293.svg b/resources/imageFiles/emojis/2293.svg new file mode 100644 index 0000000..87aa6e5 --- /dev/null +++ b/resources/imageFiles/emojis/2293.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2294.svg b/resources/imageFiles/emojis/2294.svg new file mode 100644 index 0000000..eff8e9b --- /dev/null +++ b/resources/imageFiles/emojis/2294.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2295.svg b/resources/imageFiles/emojis/2295.svg new file mode 100644 index 0000000..f7444f6 --- /dev/null +++ b/resources/imageFiles/emojis/2295.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2296.svg b/resources/imageFiles/emojis/2296.svg new file mode 100644 index 0000000..e351578 --- /dev/null +++ b/resources/imageFiles/emojis/2296.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/resources/imageFiles/emojis/2297.svg b/resources/imageFiles/emojis/2297.svg new file mode 100644 index 0000000..a2de368 --- /dev/null +++ b/resources/imageFiles/emojis/2297.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2298.svg b/resources/imageFiles/emojis/2298.svg new file mode 100644 index 0000000..b0feb86 --- /dev/null +++ b/resources/imageFiles/emojis/2298.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2299.svg b/resources/imageFiles/emojis/2299.svg new file mode 100644 index 0000000..07ff4aa --- /dev/null +++ b/resources/imageFiles/emojis/2299.svg @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2300.svg b/resources/imageFiles/emojis/2300.svg new file mode 100644 index 0000000..32648e3 --- /dev/null +++ b/resources/imageFiles/emojis/2300.svg @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2301.svg b/resources/imageFiles/emojis/2301.svg new file mode 100644 index 0000000..3c61ec7 --- /dev/null +++ b/resources/imageFiles/emojis/2301.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2302.svg b/resources/imageFiles/emojis/2302.svg new file mode 100644 index 0000000..be6f9e5 --- /dev/null +++ b/resources/imageFiles/emojis/2302.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2303.svg b/resources/imageFiles/emojis/2303.svg new file mode 100644 index 0000000..e0dc8a7 --- /dev/null +++ b/resources/imageFiles/emojis/2303.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2304.svg b/resources/imageFiles/emojis/2304.svg new file mode 100644 index 0000000..037e03e --- /dev/null +++ b/resources/imageFiles/emojis/2304.svg @@ -0,0 +1,43 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2305.svg b/resources/imageFiles/emojis/2305.svg new file mode 100644 index 0000000..eed7aed --- /dev/null +++ b/resources/imageFiles/emojis/2305.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2306.svg b/resources/imageFiles/emojis/2306.svg new file mode 100644 index 0000000..36bf5be --- /dev/null +++ b/resources/imageFiles/emojis/2306.svg @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2307.svg b/resources/imageFiles/emojis/2307.svg new file mode 100644 index 0000000..387df16 --- /dev/null +++ b/resources/imageFiles/emojis/2307.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2308.svg b/resources/imageFiles/emojis/2308.svg new file mode 100644 index 0000000..29467a9 --- /dev/null +++ b/resources/imageFiles/emojis/2308.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2309.svg b/resources/imageFiles/emojis/2309.svg new file mode 100644 index 0000000..6fc8784 --- /dev/null +++ b/resources/imageFiles/emojis/2309.svg @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2310.svg b/resources/imageFiles/emojis/2310.svg new file mode 100644 index 0000000..9bcef44 --- /dev/null +++ b/resources/imageFiles/emojis/2310.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2311.svg b/resources/imageFiles/emojis/2311.svg new file mode 100644 index 0000000..98e7894 --- /dev/null +++ b/resources/imageFiles/emojis/2311.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2312.svg b/resources/imageFiles/emojis/2312.svg new file mode 100644 index 0000000..f5d7d8d --- /dev/null +++ b/resources/imageFiles/emojis/2312.svg @@ -0,0 +1,33 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2313.svg b/resources/imageFiles/emojis/2313.svg new file mode 100644 index 0000000..dd6085c --- /dev/null +++ b/resources/imageFiles/emojis/2313.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2314.svg b/resources/imageFiles/emojis/2314.svg new file mode 100644 index 0000000..8896210 --- /dev/null +++ b/resources/imageFiles/emojis/2314.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2315.svg b/resources/imageFiles/emojis/2315.svg new file mode 100644 index 0000000..8e2eeee --- /dev/null +++ b/resources/imageFiles/emojis/2315.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2316.svg b/resources/imageFiles/emojis/2316.svg new file mode 100644 index 0000000..cbe7a9a --- /dev/null +++ b/resources/imageFiles/emojis/2316.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2317.svg b/resources/imageFiles/emojis/2317.svg new file mode 100644 index 0000000..0452856 --- /dev/null +++ b/resources/imageFiles/emojis/2317.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/resources/imageFiles/emojis/2318.svg b/resources/imageFiles/emojis/2318.svg new file mode 100644 index 0000000..c4d59e3 --- /dev/null +++ b/resources/imageFiles/emojis/2318.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2319.svg b/resources/imageFiles/emojis/2319.svg new file mode 100644 index 0000000..f24acbc --- /dev/null +++ b/resources/imageFiles/emojis/2319.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/resources/imageFiles/emojis/2320.svg b/resources/imageFiles/emojis/2320.svg new file mode 100644 index 0000000..d6f6d0d --- /dev/null +++ b/resources/imageFiles/emojis/2320.svg @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2321.svg b/resources/imageFiles/emojis/2321.svg new file mode 100644 index 0000000..b65bfed --- /dev/null +++ b/resources/imageFiles/emojis/2321.svg @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2322.svg b/resources/imageFiles/emojis/2322.svg new file mode 100644 index 0000000..13f31d8 --- /dev/null +++ b/resources/imageFiles/emojis/2322.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2323.svg b/resources/imageFiles/emojis/2323.svg new file mode 100644 index 0000000..560cbe7 --- /dev/null +++ b/resources/imageFiles/emojis/2323.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2324.svg b/resources/imageFiles/emojis/2324.svg new file mode 100644 index 0000000..dfa356b --- /dev/null +++ b/resources/imageFiles/emojis/2324.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2325.svg b/resources/imageFiles/emojis/2325.svg new file mode 100644 index 0000000..502d9e5 --- /dev/null +++ b/resources/imageFiles/emojis/2325.svg @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2326.svg b/resources/imageFiles/emojis/2326.svg new file mode 100644 index 0000000..03259dc --- /dev/null +++ b/resources/imageFiles/emojis/2326.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2327.svg b/resources/imageFiles/emojis/2327.svg new file mode 100644 index 0000000..dac483e --- /dev/null +++ b/resources/imageFiles/emojis/2327.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2328.svg b/resources/imageFiles/emojis/2328.svg new file mode 100644 index 0000000..312ae71 --- /dev/null +++ b/resources/imageFiles/emojis/2328.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2329.svg b/resources/imageFiles/emojis/2329.svg new file mode 100644 index 0000000..e283a3e --- /dev/null +++ b/resources/imageFiles/emojis/2329.svg @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2330.svg b/resources/imageFiles/emojis/2330.svg new file mode 100644 index 0000000..955054f --- /dev/null +++ b/resources/imageFiles/emojis/2330.svg @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2331.svg b/resources/imageFiles/emojis/2331.svg new file mode 100644 index 0000000..015fbb8 --- /dev/null +++ b/resources/imageFiles/emojis/2331.svg @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2332.svg b/resources/imageFiles/emojis/2332.svg new file mode 100644 index 0000000..2a338a3 --- /dev/null +++ b/resources/imageFiles/emojis/2332.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2333.svg b/resources/imageFiles/emojis/2333.svg new file mode 100644 index 0000000..a8cd7d3 --- /dev/null +++ b/resources/imageFiles/emojis/2333.svg @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2334.svg b/resources/imageFiles/emojis/2334.svg new file mode 100644 index 0000000..b55425b --- /dev/null +++ b/resources/imageFiles/emojis/2334.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2335.svg b/resources/imageFiles/emojis/2335.svg new file mode 100644 index 0000000..5b0f7ce --- /dev/null +++ b/resources/imageFiles/emojis/2335.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2336.svg b/resources/imageFiles/emojis/2336.svg new file mode 100644 index 0000000..819f4e7 --- /dev/null +++ b/resources/imageFiles/emojis/2336.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2337.svg b/resources/imageFiles/emojis/2337.svg new file mode 100644 index 0000000..a042b32 --- /dev/null +++ b/resources/imageFiles/emojis/2337.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2338.svg b/resources/imageFiles/emojis/2338.svg new file mode 100644 index 0000000..15bf308 --- /dev/null +++ b/resources/imageFiles/emojis/2338.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2339.svg b/resources/imageFiles/emojis/2339.svg new file mode 100644 index 0000000..7b70711 --- /dev/null +++ b/resources/imageFiles/emojis/2339.svg @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2340.svg b/resources/imageFiles/emojis/2340.svg new file mode 100644 index 0000000..b97fb6e --- /dev/null +++ b/resources/imageFiles/emojis/2340.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2341.svg b/resources/imageFiles/emojis/2341.svg new file mode 100644 index 0000000..b0da578 --- /dev/null +++ b/resources/imageFiles/emojis/2341.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2342.svg b/resources/imageFiles/emojis/2342.svg new file mode 100644 index 0000000..90a0c8e --- /dev/null +++ b/resources/imageFiles/emojis/2342.svg @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2343.svg b/resources/imageFiles/emojis/2343.svg new file mode 100644 index 0000000..1993ff4 --- /dev/null +++ b/resources/imageFiles/emojis/2343.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2344.svg b/resources/imageFiles/emojis/2344.svg new file mode 100644 index 0000000..5009760 --- /dev/null +++ b/resources/imageFiles/emojis/2344.svg @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2345.svg b/resources/imageFiles/emojis/2345.svg new file mode 100644 index 0000000..1059963 --- /dev/null +++ b/resources/imageFiles/emojis/2345.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2346.svg b/resources/imageFiles/emojis/2346.svg new file mode 100644 index 0000000..383ad1b --- /dev/null +++ b/resources/imageFiles/emojis/2346.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2347.svg b/resources/imageFiles/emojis/2347.svg new file mode 100644 index 0000000..6d764fc --- /dev/null +++ b/resources/imageFiles/emojis/2347.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2348.svg b/resources/imageFiles/emojis/2348.svg new file mode 100644 index 0000000..b362fff --- /dev/null +++ b/resources/imageFiles/emojis/2348.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/resources/imageFiles/emojis/2349.svg b/resources/imageFiles/emojis/2349.svg new file mode 100644 index 0000000..9bb6f36 --- /dev/null +++ b/resources/imageFiles/emojis/2349.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/resources/imageFiles/emojis/2350.svg b/resources/imageFiles/emojis/2350.svg new file mode 100644 index 0000000..09793f5 --- /dev/null +++ b/resources/imageFiles/emojis/2350.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2351.svg b/resources/imageFiles/emojis/2351.svg new file mode 100644 index 0000000..772f203 --- /dev/null +++ b/resources/imageFiles/emojis/2351.svg @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2352.svg b/resources/imageFiles/emojis/2352.svg new file mode 100644 index 0000000..8c397d1 --- /dev/null +++ b/resources/imageFiles/emojis/2352.svg @@ -0,0 +1,32 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2353.svg b/resources/imageFiles/emojis/2353.svg new file mode 100644 index 0000000..53107f1 --- /dev/null +++ b/resources/imageFiles/emojis/2353.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2354.svg b/resources/imageFiles/emojis/2354.svg new file mode 100644 index 0000000..8abbbce --- /dev/null +++ b/resources/imageFiles/emojis/2354.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2355.svg b/resources/imageFiles/emojis/2355.svg new file mode 100644 index 0000000..72216c8 --- /dev/null +++ b/resources/imageFiles/emojis/2355.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2356.svg b/resources/imageFiles/emojis/2356.svg new file mode 100644 index 0000000..5086493 --- /dev/null +++ b/resources/imageFiles/emojis/2356.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2357.svg b/resources/imageFiles/emojis/2357.svg new file mode 100644 index 0000000..af14883 --- /dev/null +++ b/resources/imageFiles/emojis/2357.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2358.svg b/resources/imageFiles/emojis/2358.svg new file mode 100644 index 0000000..cb33531 --- /dev/null +++ b/resources/imageFiles/emojis/2358.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2359.svg b/resources/imageFiles/emojis/2359.svg new file mode 100644 index 0000000..125cc72 --- /dev/null +++ b/resources/imageFiles/emojis/2359.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2360.svg b/resources/imageFiles/emojis/2360.svg new file mode 100644 index 0000000..b661d9c --- /dev/null +++ b/resources/imageFiles/emojis/2360.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/resources/imageFiles/emojis/2361.svg b/resources/imageFiles/emojis/2361.svg new file mode 100644 index 0000000..1b0a880 --- /dev/null +++ b/resources/imageFiles/emojis/2361.svg @@ -0,0 +1,29 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2362.svg b/resources/imageFiles/emojis/2362.svg new file mode 100644 index 0000000..9c367b3 --- /dev/null +++ b/resources/imageFiles/emojis/2362.svg @@ -0,0 +1,29 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2363.svg b/resources/imageFiles/emojis/2363.svg new file mode 100644 index 0000000..b12955e --- /dev/null +++ b/resources/imageFiles/emojis/2363.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2364.svg b/resources/imageFiles/emojis/2364.svg new file mode 100644 index 0000000..c7cd4a2 --- /dev/null +++ b/resources/imageFiles/emojis/2364.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2365.svg b/resources/imageFiles/emojis/2365.svg new file mode 100644 index 0000000..a50fbb3 --- /dev/null +++ b/resources/imageFiles/emojis/2365.svg @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2366.svg b/resources/imageFiles/emojis/2366.svg new file mode 100644 index 0000000..d184fd6 --- /dev/null +++ b/resources/imageFiles/emojis/2366.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/resources/imageFiles/emojis/2367.svg b/resources/imageFiles/emojis/2367.svg new file mode 100644 index 0000000..0b4e339 --- /dev/null +++ b/resources/imageFiles/emojis/2367.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2368.svg b/resources/imageFiles/emojis/2368.svg new file mode 100644 index 0000000..ffc6297 --- /dev/null +++ b/resources/imageFiles/emojis/2368.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/resources/imageFiles/emojis/2369.svg b/resources/imageFiles/emojis/2369.svg new file mode 100644 index 0000000..8db2381 --- /dev/null +++ b/resources/imageFiles/emojis/2369.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2370.svg b/resources/imageFiles/emojis/2370.svg new file mode 100644 index 0000000..02b6f09 --- /dev/null +++ b/resources/imageFiles/emojis/2370.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2371.svg b/resources/imageFiles/emojis/2371.svg new file mode 100644 index 0000000..86827e2 --- /dev/null +++ b/resources/imageFiles/emojis/2371.svg @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2372.svg b/resources/imageFiles/emojis/2372.svg new file mode 100644 index 0000000..7878ce2 --- /dev/null +++ b/resources/imageFiles/emojis/2372.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2373.svg b/resources/imageFiles/emojis/2373.svg new file mode 100644 index 0000000..305fd9b --- /dev/null +++ b/resources/imageFiles/emojis/2373.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2374.svg b/resources/imageFiles/emojis/2374.svg new file mode 100644 index 0000000..7c68a58 --- /dev/null +++ b/resources/imageFiles/emojis/2374.svg @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2375.svg b/resources/imageFiles/emojis/2375.svg new file mode 100644 index 0000000..118ceec --- /dev/null +++ b/resources/imageFiles/emojis/2375.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2376.svg b/resources/imageFiles/emojis/2376.svg new file mode 100644 index 0000000..310612a --- /dev/null +++ b/resources/imageFiles/emojis/2376.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2377.svg b/resources/imageFiles/emojis/2377.svg new file mode 100644 index 0000000..b2fe7ef --- /dev/null +++ b/resources/imageFiles/emojis/2377.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2378.svg b/resources/imageFiles/emojis/2378.svg new file mode 100644 index 0000000..4b12f31 --- /dev/null +++ b/resources/imageFiles/emojis/2378.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/resources/imageFiles/emojis/2379.svg b/resources/imageFiles/emojis/2379.svg new file mode 100644 index 0000000..8cfb870 --- /dev/null +++ b/resources/imageFiles/emojis/2379.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2380.svg b/resources/imageFiles/emojis/2380.svg new file mode 100644 index 0000000..49f1e16 --- /dev/null +++ b/resources/imageFiles/emojis/2380.svg @@ -0,0 +1,35 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2381.svg b/resources/imageFiles/emojis/2381.svg new file mode 100644 index 0000000..d8c8ba9 --- /dev/null +++ b/resources/imageFiles/emojis/2381.svg @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2382.svg b/resources/imageFiles/emojis/2382.svg new file mode 100644 index 0000000..898c0e7 --- /dev/null +++ b/resources/imageFiles/emojis/2382.svg @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2383.svg b/resources/imageFiles/emojis/2383.svg new file mode 100644 index 0000000..7974803 --- /dev/null +++ b/resources/imageFiles/emojis/2383.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2384.svg b/resources/imageFiles/emojis/2384.svg new file mode 100644 index 0000000..f66c549 --- /dev/null +++ b/resources/imageFiles/emojis/2384.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2385.svg b/resources/imageFiles/emojis/2385.svg new file mode 100644 index 0000000..f8c2787 --- /dev/null +++ b/resources/imageFiles/emojis/2385.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2386.svg b/resources/imageFiles/emojis/2386.svg new file mode 100644 index 0000000..11b6a3d --- /dev/null +++ b/resources/imageFiles/emojis/2386.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/resources/imageFiles/emojis/2387.svg b/resources/imageFiles/emojis/2387.svg new file mode 100644 index 0000000..ff40d5a --- /dev/null +++ b/resources/imageFiles/emojis/2387.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2388.svg b/resources/imageFiles/emojis/2388.svg new file mode 100644 index 0000000..46e9527 --- /dev/null +++ b/resources/imageFiles/emojis/2388.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2389.svg b/resources/imageFiles/emojis/2389.svg new file mode 100644 index 0000000..2919e30 --- /dev/null +++ b/resources/imageFiles/emojis/2389.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2390.svg b/resources/imageFiles/emojis/2390.svg new file mode 100644 index 0000000..98649f8 --- /dev/null +++ b/resources/imageFiles/emojis/2390.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2391.svg b/resources/imageFiles/emojis/2391.svg new file mode 100644 index 0000000..e0d38c8 --- /dev/null +++ b/resources/imageFiles/emojis/2391.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2392.svg b/resources/imageFiles/emojis/2392.svg new file mode 100644 index 0000000..2347768 --- /dev/null +++ b/resources/imageFiles/emojis/2392.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2393.svg b/resources/imageFiles/emojis/2393.svg new file mode 100644 index 0000000..2ac8823 --- /dev/null +++ b/resources/imageFiles/emojis/2393.svg @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2394.svg b/resources/imageFiles/emojis/2394.svg new file mode 100644 index 0000000..fa73992 --- /dev/null +++ b/resources/imageFiles/emojis/2394.svg @@ -0,0 +1,35 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2395.svg b/resources/imageFiles/emojis/2395.svg new file mode 100644 index 0000000..0152e8a --- /dev/null +++ b/resources/imageFiles/emojis/2395.svg @@ -0,0 +1,32 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2396.svg b/resources/imageFiles/emojis/2396.svg new file mode 100644 index 0000000..c99c042 --- /dev/null +++ b/resources/imageFiles/emojis/2396.svg @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2397.svg b/resources/imageFiles/emojis/2397.svg new file mode 100644 index 0000000..61fb67d --- /dev/null +++ b/resources/imageFiles/emojis/2397.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2398.svg b/resources/imageFiles/emojis/2398.svg new file mode 100644 index 0000000..348bd92 --- /dev/null +++ b/resources/imageFiles/emojis/2398.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2399.svg b/resources/imageFiles/emojis/2399.svg new file mode 100644 index 0000000..ecaf264 --- /dev/null +++ b/resources/imageFiles/emojis/2399.svg @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2400.svg b/resources/imageFiles/emojis/2400.svg new file mode 100644 index 0000000..07d0c62 --- /dev/null +++ b/resources/imageFiles/emojis/2400.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2401.svg b/resources/imageFiles/emojis/2401.svg new file mode 100644 index 0000000..8b9ceb2 --- /dev/null +++ b/resources/imageFiles/emojis/2401.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2402.svg b/resources/imageFiles/emojis/2402.svg new file mode 100644 index 0000000..10c82ab --- /dev/null +++ b/resources/imageFiles/emojis/2402.svg @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2403.svg b/resources/imageFiles/emojis/2403.svg new file mode 100644 index 0000000..7d6574c --- /dev/null +++ b/resources/imageFiles/emojis/2403.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2404.svg b/resources/imageFiles/emojis/2404.svg new file mode 100644 index 0000000..be3358c --- /dev/null +++ b/resources/imageFiles/emojis/2404.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2405.svg b/resources/imageFiles/emojis/2405.svg new file mode 100644 index 0000000..2557443 --- /dev/null +++ b/resources/imageFiles/emojis/2405.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2406.svg b/resources/imageFiles/emojis/2406.svg new file mode 100644 index 0000000..88a2f2a --- /dev/null +++ b/resources/imageFiles/emojis/2406.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/resources/imageFiles/emojis/2407.svg b/resources/imageFiles/emojis/2407.svg new file mode 100644 index 0000000..a3168d0 --- /dev/null +++ b/resources/imageFiles/emojis/2407.svg @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2408.svg b/resources/imageFiles/emojis/2408.svg new file mode 100644 index 0000000..3801186 --- /dev/null +++ b/resources/imageFiles/emojis/2408.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2409.svg b/resources/imageFiles/emojis/2409.svg new file mode 100644 index 0000000..f1be150 --- /dev/null +++ b/resources/imageFiles/emojis/2409.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2410.svg b/resources/imageFiles/emojis/2410.svg new file mode 100644 index 0000000..253d1bd --- /dev/null +++ b/resources/imageFiles/emojis/2410.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/resources/imageFiles/emojis/2411.svg b/resources/imageFiles/emojis/2411.svg new file mode 100644 index 0000000..d2165f4 --- /dev/null +++ b/resources/imageFiles/emojis/2411.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2412.svg b/resources/imageFiles/emojis/2412.svg new file mode 100644 index 0000000..6b62e41 --- /dev/null +++ b/resources/imageFiles/emojis/2412.svg @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2413.svg b/resources/imageFiles/emojis/2413.svg new file mode 100644 index 0000000..421283d --- /dev/null +++ b/resources/imageFiles/emojis/2413.svg @@ -0,0 +1,34 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2414.svg b/resources/imageFiles/emojis/2414.svg new file mode 100644 index 0000000..356ecb1 --- /dev/null +++ b/resources/imageFiles/emojis/2414.svg @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2415.svg b/resources/imageFiles/emojis/2415.svg new file mode 100644 index 0000000..04083d7 --- /dev/null +++ b/resources/imageFiles/emojis/2415.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2416.svg b/resources/imageFiles/emojis/2416.svg new file mode 100644 index 0000000..02cd813 --- /dev/null +++ b/resources/imageFiles/emojis/2416.svg @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2417.svg b/resources/imageFiles/emojis/2417.svg new file mode 100644 index 0000000..a55da45 --- /dev/null +++ b/resources/imageFiles/emojis/2417.svg @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2418.svg b/resources/imageFiles/emojis/2418.svg new file mode 100644 index 0000000..9810e1b --- /dev/null +++ b/resources/imageFiles/emojis/2418.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2419.svg b/resources/imageFiles/emojis/2419.svg new file mode 100644 index 0000000..b181100 --- /dev/null +++ b/resources/imageFiles/emojis/2419.svg @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2420.svg b/resources/imageFiles/emojis/2420.svg new file mode 100644 index 0000000..1be8529 --- /dev/null +++ b/resources/imageFiles/emojis/2420.svg @@ -0,0 +1,43 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2421.svg b/resources/imageFiles/emojis/2421.svg new file mode 100644 index 0000000..cc29989 --- /dev/null +++ b/resources/imageFiles/emojis/2421.svg @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2422.svg b/resources/imageFiles/emojis/2422.svg new file mode 100644 index 0000000..eb0bd9b --- /dev/null +++ b/resources/imageFiles/emojis/2422.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/resources/imageFiles/emojis/2423.svg b/resources/imageFiles/emojis/2423.svg new file mode 100644 index 0000000..5fa06a5 --- /dev/null +++ b/resources/imageFiles/emojis/2423.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2424.svg b/resources/imageFiles/emojis/2424.svg new file mode 100644 index 0000000..ac85f5d --- /dev/null +++ b/resources/imageFiles/emojis/2424.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2425.svg b/resources/imageFiles/emojis/2425.svg new file mode 100644 index 0000000..9ccaf2a --- /dev/null +++ b/resources/imageFiles/emojis/2425.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2426.svg b/resources/imageFiles/emojis/2426.svg new file mode 100644 index 0000000..6b75000 --- /dev/null +++ b/resources/imageFiles/emojis/2426.svg @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2427.svg b/resources/imageFiles/emojis/2427.svg new file mode 100644 index 0000000..717ccc1 --- /dev/null +++ b/resources/imageFiles/emojis/2427.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2428.svg b/resources/imageFiles/emojis/2428.svg new file mode 100644 index 0000000..2d1ce06 --- /dev/null +++ b/resources/imageFiles/emojis/2428.svg @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2429.svg b/resources/imageFiles/emojis/2429.svg new file mode 100644 index 0000000..d35bf01 --- /dev/null +++ b/resources/imageFiles/emojis/2429.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2430.svg b/resources/imageFiles/emojis/2430.svg new file mode 100644 index 0000000..6272cc8 --- /dev/null +++ b/resources/imageFiles/emojis/2430.svg @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2431.svg b/resources/imageFiles/emojis/2431.svg new file mode 100644 index 0000000..540cd5e --- /dev/null +++ b/resources/imageFiles/emojis/2431.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2432.svg b/resources/imageFiles/emojis/2432.svg new file mode 100644 index 0000000..3d892a1 --- /dev/null +++ b/resources/imageFiles/emojis/2432.svg @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2433.svg b/resources/imageFiles/emojis/2433.svg new file mode 100644 index 0000000..5140f73 --- /dev/null +++ b/resources/imageFiles/emojis/2433.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/resources/imageFiles/emojis/2434.svg b/resources/imageFiles/emojis/2434.svg new file mode 100644 index 0000000..933da28 --- /dev/null +++ b/resources/imageFiles/emojis/2434.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2435.svg b/resources/imageFiles/emojis/2435.svg new file mode 100644 index 0000000..25b7235 --- /dev/null +++ b/resources/imageFiles/emojis/2435.svg @@ -0,0 +1,47 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2436.svg b/resources/imageFiles/emojis/2436.svg new file mode 100644 index 0000000..7b1f878 --- /dev/null +++ b/resources/imageFiles/emojis/2436.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2437.svg b/resources/imageFiles/emojis/2437.svg new file mode 100644 index 0000000..6104139 --- /dev/null +++ b/resources/imageFiles/emojis/2437.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2438.svg b/resources/imageFiles/emojis/2438.svg new file mode 100644 index 0000000..78b8331 --- /dev/null +++ b/resources/imageFiles/emojis/2438.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2439.svg b/resources/imageFiles/emojis/2439.svg new file mode 100644 index 0000000..ea34931 --- /dev/null +++ b/resources/imageFiles/emojis/2439.svg @@ -0,0 +1,29 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2440.svg b/resources/imageFiles/emojis/2440.svg new file mode 100644 index 0000000..5b53dfe --- /dev/null +++ b/resources/imageFiles/emojis/2440.svg @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2441.svg b/resources/imageFiles/emojis/2441.svg new file mode 100644 index 0000000..c166f6b --- /dev/null +++ b/resources/imageFiles/emojis/2441.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2442.svg b/resources/imageFiles/emojis/2442.svg new file mode 100644 index 0000000..0104fd3 --- /dev/null +++ b/resources/imageFiles/emojis/2442.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2443.svg b/resources/imageFiles/emojis/2443.svg new file mode 100644 index 0000000..36cf1ac --- /dev/null +++ b/resources/imageFiles/emojis/2443.svg @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2444.svg b/resources/imageFiles/emojis/2444.svg new file mode 100644 index 0000000..fd278e7 --- /dev/null +++ b/resources/imageFiles/emojis/2444.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2445.svg b/resources/imageFiles/emojis/2445.svg new file mode 100644 index 0000000..51bbd4d --- /dev/null +++ b/resources/imageFiles/emojis/2445.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2446.svg b/resources/imageFiles/emojis/2446.svg new file mode 100644 index 0000000..28dfdda --- /dev/null +++ b/resources/imageFiles/emojis/2446.svg @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2447.svg b/resources/imageFiles/emojis/2447.svg new file mode 100644 index 0000000..d23aaed --- /dev/null +++ b/resources/imageFiles/emojis/2447.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/resources/imageFiles/emojis/2448.svg b/resources/imageFiles/emojis/2448.svg new file mode 100644 index 0000000..d0827eb --- /dev/null +++ b/resources/imageFiles/emojis/2448.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2449.svg b/resources/imageFiles/emojis/2449.svg new file mode 100644 index 0000000..71d95e9 --- /dev/null +++ b/resources/imageFiles/emojis/2449.svg @@ -0,0 +1,34 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2450.svg b/resources/imageFiles/emojis/2450.svg new file mode 100644 index 0000000..f8e743d --- /dev/null +++ b/resources/imageFiles/emojis/2450.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2451.svg b/resources/imageFiles/emojis/2451.svg new file mode 100644 index 0000000..c114264 --- /dev/null +++ b/resources/imageFiles/emojis/2451.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2452.svg b/resources/imageFiles/emojis/2452.svg new file mode 100644 index 0000000..0b8f1be --- /dev/null +++ b/resources/imageFiles/emojis/2452.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/resources/imageFiles/emojis/2453.svg b/resources/imageFiles/emojis/2453.svg new file mode 100644 index 0000000..af049b6 --- /dev/null +++ b/resources/imageFiles/emojis/2453.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2454.svg b/resources/imageFiles/emojis/2454.svg new file mode 100644 index 0000000..a9139bf --- /dev/null +++ b/resources/imageFiles/emojis/2454.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/resources/imageFiles/emojis/2455.svg b/resources/imageFiles/emojis/2455.svg new file mode 100644 index 0000000..4b39d7d --- /dev/null +++ b/resources/imageFiles/emojis/2455.svg @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2456.svg b/resources/imageFiles/emojis/2456.svg new file mode 100644 index 0000000..aff9cdd --- /dev/null +++ b/resources/imageFiles/emojis/2456.svg @@ -0,0 +1,31 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2457.svg b/resources/imageFiles/emojis/2457.svg new file mode 100644 index 0000000..73c4044 --- /dev/null +++ b/resources/imageFiles/emojis/2457.svg @@ -0,0 +1,47 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2458.svg b/resources/imageFiles/emojis/2458.svg new file mode 100644 index 0000000..fce527c --- /dev/null +++ b/resources/imageFiles/emojis/2458.svg @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2459.svg b/resources/imageFiles/emojis/2459.svg new file mode 100644 index 0000000..0b7727d --- /dev/null +++ b/resources/imageFiles/emojis/2459.svg @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2460.svg b/resources/imageFiles/emojis/2460.svg new file mode 100644 index 0000000..dd9266c --- /dev/null +++ b/resources/imageFiles/emojis/2460.svg @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2461.svg b/resources/imageFiles/emojis/2461.svg new file mode 100644 index 0000000..8c89390 --- /dev/null +++ b/resources/imageFiles/emojis/2461.svg @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2462.svg b/resources/imageFiles/emojis/2462.svg new file mode 100644 index 0000000..bcfa01e --- /dev/null +++ b/resources/imageFiles/emojis/2462.svg @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2463.svg b/resources/imageFiles/emojis/2463.svg new file mode 100644 index 0000000..bd5da7c --- /dev/null +++ b/resources/imageFiles/emojis/2463.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2464.svg b/resources/imageFiles/emojis/2464.svg new file mode 100644 index 0000000..3dae6ee --- /dev/null +++ b/resources/imageFiles/emojis/2464.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2465.svg b/resources/imageFiles/emojis/2465.svg new file mode 100644 index 0000000..f5e7bf1 --- /dev/null +++ b/resources/imageFiles/emojis/2465.svg @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2466.svg b/resources/imageFiles/emojis/2466.svg new file mode 100644 index 0000000..08ef0e9 --- /dev/null +++ b/resources/imageFiles/emojis/2466.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2467.svg b/resources/imageFiles/emojis/2467.svg new file mode 100644 index 0000000..609a124 --- /dev/null +++ b/resources/imageFiles/emojis/2467.svg @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2468.svg b/resources/imageFiles/emojis/2468.svg new file mode 100644 index 0000000..38c27c6 --- /dev/null +++ b/resources/imageFiles/emojis/2468.svg @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2469.svg b/resources/imageFiles/emojis/2469.svg new file mode 100644 index 0000000..fd975c7 --- /dev/null +++ b/resources/imageFiles/emojis/2469.svg @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2470.svg b/resources/imageFiles/emojis/2470.svg new file mode 100644 index 0000000..7adb076 --- /dev/null +++ b/resources/imageFiles/emojis/2470.svg @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2471.svg b/resources/imageFiles/emojis/2471.svg new file mode 100644 index 0000000..99ac71d --- /dev/null +++ b/resources/imageFiles/emojis/2471.svg @@ -0,0 +1,29 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2472.svg b/resources/imageFiles/emojis/2472.svg new file mode 100644 index 0000000..3729718 --- /dev/null +++ b/resources/imageFiles/emojis/2472.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2473.svg b/resources/imageFiles/emojis/2473.svg new file mode 100644 index 0000000..60b2207 --- /dev/null +++ b/resources/imageFiles/emojis/2473.svg @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2474.svg b/resources/imageFiles/emojis/2474.svg new file mode 100644 index 0000000..1ad859b --- /dev/null +++ b/resources/imageFiles/emojis/2474.svg @@ -0,0 +1,49 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2475.svg b/resources/imageFiles/emojis/2475.svg new file mode 100644 index 0000000..35d60eb --- /dev/null +++ b/resources/imageFiles/emojis/2475.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2476.svg b/resources/imageFiles/emojis/2476.svg new file mode 100644 index 0000000..313cee0 --- /dev/null +++ b/resources/imageFiles/emojis/2476.svg @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2477.svg b/resources/imageFiles/emojis/2477.svg new file mode 100644 index 0000000..6970a65 --- /dev/null +++ b/resources/imageFiles/emojis/2477.svg @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2478.svg b/resources/imageFiles/emojis/2478.svg new file mode 100644 index 0000000..9d63cce --- /dev/null +++ b/resources/imageFiles/emojis/2478.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2479.svg b/resources/imageFiles/emojis/2479.svg new file mode 100644 index 0000000..27b248a --- /dev/null +++ b/resources/imageFiles/emojis/2479.svg @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2480.svg b/resources/imageFiles/emojis/2480.svg new file mode 100644 index 0000000..5eefb68 --- /dev/null +++ b/resources/imageFiles/emojis/2480.svg @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2481.svg b/resources/imageFiles/emojis/2481.svg new file mode 100644 index 0000000..1109c82 --- /dev/null +++ b/resources/imageFiles/emojis/2481.svg @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2482.svg b/resources/imageFiles/emojis/2482.svg new file mode 100644 index 0000000..5081fa9 --- /dev/null +++ b/resources/imageFiles/emojis/2482.svg @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2483.svg b/resources/imageFiles/emojis/2483.svg new file mode 100644 index 0000000..ca174f6 --- /dev/null +++ b/resources/imageFiles/emojis/2483.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2484.svg b/resources/imageFiles/emojis/2484.svg new file mode 100644 index 0000000..aa7a8ed --- /dev/null +++ b/resources/imageFiles/emojis/2484.svg @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2485.svg b/resources/imageFiles/emojis/2485.svg new file mode 100644 index 0000000..b2063a0 --- /dev/null +++ b/resources/imageFiles/emojis/2485.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2486.svg b/resources/imageFiles/emojis/2486.svg new file mode 100644 index 0000000..e7c55bd --- /dev/null +++ b/resources/imageFiles/emojis/2486.svg @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2487.svg b/resources/imageFiles/emojis/2487.svg new file mode 100644 index 0000000..629c2bf --- /dev/null +++ b/resources/imageFiles/emojis/2487.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2488.svg b/resources/imageFiles/emojis/2488.svg new file mode 100644 index 0000000..137cb27 --- /dev/null +++ b/resources/imageFiles/emojis/2488.svg @@ -0,0 +1,35 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2489.svg b/resources/imageFiles/emojis/2489.svg new file mode 100644 index 0000000..aef06fc --- /dev/null +++ b/resources/imageFiles/emojis/2489.svg @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2490.svg b/resources/imageFiles/emojis/2490.svg new file mode 100644 index 0000000..dcabb51 --- /dev/null +++ b/resources/imageFiles/emojis/2490.svg @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2491.svg b/resources/imageFiles/emojis/2491.svg new file mode 100644 index 0000000..68b103c --- /dev/null +++ b/resources/imageFiles/emojis/2491.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2492.svg b/resources/imageFiles/emojis/2492.svg new file mode 100644 index 0000000..a1e6fa6 --- /dev/null +++ b/resources/imageFiles/emojis/2492.svg @@ -0,0 +1,51 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2493.svg b/resources/imageFiles/emojis/2493.svg new file mode 100644 index 0000000..f3c95fe --- /dev/null +++ b/resources/imageFiles/emojis/2493.svg @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2494.svg b/resources/imageFiles/emojis/2494.svg new file mode 100644 index 0000000..14affbf --- /dev/null +++ b/resources/imageFiles/emojis/2494.svg @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2495.svg b/resources/imageFiles/emojis/2495.svg new file mode 100644 index 0000000..38ee64f --- /dev/null +++ b/resources/imageFiles/emojis/2495.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2496.svg b/resources/imageFiles/emojis/2496.svg new file mode 100644 index 0000000..9dd2f05 --- /dev/null +++ b/resources/imageFiles/emojis/2496.svg @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2497.svg b/resources/imageFiles/emojis/2497.svg new file mode 100644 index 0000000..4989199 --- /dev/null +++ b/resources/imageFiles/emojis/2497.svg @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2498.svg b/resources/imageFiles/emojis/2498.svg new file mode 100644 index 0000000..f4ba1be --- /dev/null +++ b/resources/imageFiles/emojis/2498.svg @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2499.svg b/resources/imageFiles/emojis/2499.svg new file mode 100644 index 0000000..af7914a --- /dev/null +++ b/resources/imageFiles/emojis/2499.svg @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2500.svg b/resources/imageFiles/emojis/2500.svg new file mode 100644 index 0000000..a5bb637 --- /dev/null +++ b/resources/imageFiles/emojis/2500.svg @@ -0,0 +1,60 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2501.svg b/resources/imageFiles/emojis/2501.svg new file mode 100644 index 0000000..3140c5b --- /dev/null +++ b/resources/imageFiles/emojis/2501.svg @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2502.svg b/resources/imageFiles/emojis/2502.svg new file mode 100644 index 0000000..d6c9c05 --- /dev/null +++ b/resources/imageFiles/emojis/2502.svg @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2503.svg b/resources/imageFiles/emojis/2503.svg new file mode 100644 index 0000000..8d04d7e --- /dev/null +++ b/resources/imageFiles/emojis/2503.svg @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2504.svg b/resources/imageFiles/emojis/2504.svg new file mode 100644 index 0000000..8bb542f --- /dev/null +++ b/resources/imageFiles/emojis/2504.svg @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2505.svg b/resources/imageFiles/emojis/2505.svg new file mode 100644 index 0000000..c895264 --- /dev/null +++ b/resources/imageFiles/emojis/2505.svg @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2506.svg b/resources/imageFiles/emojis/2506.svg new file mode 100644 index 0000000..b8cea9c --- /dev/null +++ b/resources/imageFiles/emojis/2506.svg @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2507.svg b/resources/imageFiles/emojis/2507.svg new file mode 100644 index 0000000..98abb78 --- /dev/null +++ b/resources/imageFiles/emojis/2507.svg @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2508.svg b/resources/imageFiles/emojis/2508.svg new file mode 100644 index 0000000..d694d08 --- /dev/null +++ b/resources/imageFiles/emojis/2508.svg @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2509.svg b/resources/imageFiles/emojis/2509.svg new file mode 100644 index 0000000..0bec7ab --- /dev/null +++ b/resources/imageFiles/emojis/2509.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2510.svg b/resources/imageFiles/emojis/2510.svg new file mode 100644 index 0000000..4c1741c --- /dev/null +++ b/resources/imageFiles/emojis/2510.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2511.svg b/resources/imageFiles/emojis/2511.svg new file mode 100644 index 0000000..8efef17 --- /dev/null +++ b/resources/imageFiles/emojis/2511.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2512.svg b/resources/imageFiles/emojis/2512.svg new file mode 100644 index 0000000..7ff4539 --- /dev/null +++ b/resources/imageFiles/emojis/2512.svg @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2513.svg b/resources/imageFiles/emojis/2513.svg new file mode 100644 index 0000000..9d5e365 --- /dev/null +++ b/resources/imageFiles/emojis/2513.svg @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2514.svg b/resources/imageFiles/emojis/2514.svg new file mode 100644 index 0000000..d86b8b0 --- /dev/null +++ b/resources/imageFiles/emojis/2514.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2515.svg b/resources/imageFiles/emojis/2515.svg new file mode 100644 index 0000000..b317b28 --- /dev/null +++ b/resources/imageFiles/emojis/2515.svg @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2516.svg b/resources/imageFiles/emojis/2516.svg new file mode 100644 index 0000000..aad5263 --- /dev/null +++ b/resources/imageFiles/emojis/2516.svg @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2517.svg b/resources/imageFiles/emojis/2517.svg new file mode 100644 index 0000000..1745f04 --- /dev/null +++ b/resources/imageFiles/emojis/2517.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2518.svg b/resources/imageFiles/emojis/2518.svg new file mode 100644 index 0000000..de861d2 --- /dev/null +++ b/resources/imageFiles/emojis/2518.svg @@ -0,0 +1,43 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2519.svg b/resources/imageFiles/emojis/2519.svg new file mode 100644 index 0000000..c969d4e --- /dev/null +++ b/resources/imageFiles/emojis/2519.svg @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2520.svg b/resources/imageFiles/emojis/2520.svg new file mode 100644 index 0000000..a501a25 --- /dev/null +++ b/resources/imageFiles/emojis/2520.svg @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2521.svg b/resources/imageFiles/emojis/2521.svg new file mode 100644 index 0000000..ea664be --- /dev/null +++ b/resources/imageFiles/emojis/2521.svg @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2522.svg b/resources/imageFiles/emojis/2522.svg new file mode 100644 index 0000000..df29870 --- /dev/null +++ b/resources/imageFiles/emojis/2522.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2523.svg b/resources/imageFiles/emojis/2523.svg new file mode 100644 index 0000000..50e5efd --- /dev/null +++ b/resources/imageFiles/emojis/2523.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2524.svg b/resources/imageFiles/emojis/2524.svg new file mode 100644 index 0000000..c8840c1 --- /dev/null +++ b/resources/imageFiles/emojis/2524.svg @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2525.svg b/resources/imageFiles/emojis/2525.svg new file mode 100644 index 0000000..07bbec3 --- /dev/null +++ b/resources/imageFiles/emojis/2525.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2526.svg b/resources/imageFiles/emojis/2526.svg new file mode 100644 index 0000000..239a38b --- /dev/null +++ b/resources/imageFiles/emojis/2526.svg @@ -0,0 +1,38 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2527.svg b/resources/imageFiles/emojis/2527.svg new file mode 100644 index 0000000..16beac1 --- /dev/null +++ b/resources/imageFiles/emojis/2527.svg @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2528.svg b/resources/imageFiles/emojis/2528.svg new file mode 100644 index 0000000..c833ba3 --- /dev/null +++ b/resources/imageFiles/emojis/2528.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2529.svg b/resources/imageFiles/emojis/2529.svg new file mode 100644 index 0000000..2c18b6f --- /dev/null +++ b/resources/imageFiles/emojis/2529.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2530.svg b/resources/imageFiles/emojis/2530.svg new file mode 100644 index 0000000..c1f7872 --- /dev/null +++ b/resources/imageFiles/emojis/2530.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2531.svg b/resources/imageFiles/emojis/2531.svg new file mode 100644 index 0000000..08db95f --- /dev/null +++ b/resources/imageFiles/emojis/2531.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2532.svg b/resources/imageFiles/emojis/2532.svg new file mode 100644 index 0000000..daf4163 --- /dev/null +++ b/resources/imageFiles/emojis/2532.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2533.svg b/resources/imageFiles/emojis/2533.svg new file mode 100644 index 0000000..80e482e --- /dev/null +++ b/resources/imageFiles/emojis/2533.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2534.svg b/resources/imageFiles/emojis/2534.svg new file mode 100644 index 0000000..a7fd878 --- /dev/null +++ b/resources/imageFiles/emojis/2534.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2535.svg b/resources/imageFiles/emojis/2535.svg new file mode 100644 index 0000000..aa290b8 --- /dev/null +++ b/resources/imageFiles/emojis/2535.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2536.svg b/resources/imageFiles/emojis/2536.svg new file mode 100644 index 0000000..ed320ae --- /dev/null +++ b/resources/imageFiles/emojis/2536.svg @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2537.svg b/resources/imageFiles/emojis/2537.svg new file mode 100644 index 0000000..5a70df5 --- /dev/null +++ b/resources/imageFiles/emojis/2537.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2538.svg b/resources/imageFiles/emojis/2538.svg new file mode 100644 index 0000000..85413b8 --- /dev/null +++ b/resources/imageFiles/emojis/2538.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2539.svg b/resources/imageFiles/emojis/2539.svg new file mode 100644 index 0000000..ef9d22e --- /dev/null +++ b/resources/imageFiles/emojis/2539.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2540.svg b/resources/imageFiles/emojis/2540.svg new file mode 100644 index 0000000..1756d0d --- /dev/null +++ b/resources/imageFiles/emojis/2540.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2541.svg b/resources/imageFiles/emojis/2541.svg new file mode 100644 index 0000000..9fb0f21 --- /dev/null +++ b/resources/imageFiles/emojis/2541.svg @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2542.svg b/resources/imageFiles/emojis/2542.svg new file mode 100644 index 0000000..a4cd675 --- /dev/null +++ b/resources/imageFiles/emojis/2542.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2543.svg b/resources/imageFiles/emojis/2543.svg new file mode 100644 index 0000000..8be68e4 --- /dev/null +++ b/resources/imageFiles/emojis/2543.svg @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2544.svg b/resources/imageFiles/emojis/2544.svg new file mode 100644 index 0000000..09033fb --- /dev/null +++ b/resources/imageFiles/emojis/2544.svg @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2545.svg b/resources/imageFiles/emojis/2545.svg new file mode 100644 index 0000000..1483e06 --- /dev/null +++ b/resources/imageFiles/emojis/2545.svg @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2546.svg b/resources/imageFiles/emojis/2546.svg new file mode 100644 index 0000000..8dc9db5 --- /dev/null +++ b/resources/imageFiles/emojis/2546.svg @@ -0,0 +1,35 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2547.svg b/resources/imageFiles/emojis/2547.svg new file mode 100644 index 0000000..77a9631 --- /dev/null +++ b/resources/imageFiles/emojis/2547.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/resources/imageFiles/emojis/2548.svg b/resources/imageFiles/emojis/2548.svg new file mode 100644 index 0000000..e155680 --- /dev/null +++ b/resources/imageFiles/emojis/2548.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2549.svg b/resources/imageFiles/emojis/2549.svg new file mode 100644 index 0000000..f57f034 --- /dev/null +++ b/resources/imageFiles/emojis/2549.svg @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2550.svg b/resources/imageFiles/emojis/2550.svg new file mode 100644 index 0000000..e8d801f --- /dev/null +++ b/resources/imageFiles/emojis/2550.svg @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2551.svg b/resources/imageFiles/emojis/2551.svg new file mode 100644 index 0000000..1be5ff4 --- /dev/null +++ b/resources/imageFiles/emojis/2551.svg @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2552.svg b/resources/imageFiles/emojis/2552.svg new file mode 100644 index 0000000..ca47191 --- /dev/null +++ b/resources/imageFiles/emojis/2552.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2553.svg b/resources/imageFiles/emojis/2553.svg new file mode 100644 index 0000000..3212d6f --- /dev/null +++ b/resources/imageFiles/emojis/2553.svg @@ -0,0 +1,36 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2554.svg b/resources/imageFiles/emojis/2554.svg new file mode 100644 index 0000000..923bef7 --- /dev/null +++ b/resources/imageFiles/emojis/2554.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2555.svg b/resources/imageFiles/emojis/2555.svg new file mode 100644 index 0000000..8109f68 --- /dev/null +++ b/resources/imageFiles/emojis/2555.svg @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2556.svg b/resources/imageFiles/emojis/2556.svg new file mode 100644 index 0000000..33eed4d --- /dev/null +++ b/resources/imageFiles/emojis/2556.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2557.svg b/resources/imageFiles/emojis/2557.svg new file mode 100644 index 0000000..d25f797 --- /dev/null +++ b/resources/imageFiles/emojis/2557.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2558.svg b/resources/imageFiles/emojis/2558.svg new file mode 100644 index 0000000..fa4168f --- /dev/null +++ b/resources/imageFiles/emojis/2558.svg @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2559.svg b/resources/imageFiles/emojis/2559.svg new file mode 100644 index 0000000..3927f47 --- /dev/null +++ b/resources/imageFiles/emojis/2559.svg @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2560.svg b/resources/imageFiles/emojis/2560.svg new file mode 100644 index 0000000..9a51798 --- /dev/null +++ b/resources/imageFiles/emojis/2560.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2561.svg b/resources/imageFiles/emojis/2561.svg new file mode 100644 index 0000000..33a9355 --- /dev/null +++ b/resources/imageFiles/emojis/2561.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2562.svg b/resources/imageFiles/emojis/2562.svg new file mode 100644 index 0000000..dcb92ff --- /dev/null +++ b/resources/imageFiles/emojis/2562.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2563.svg b/resources/imageFiles/emojis/2563.svg new file mode 100644 index 0000000..7a1188c --- /dev/null +++ b/resources/imageFiles/emojis/2563.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2564.svg b/resources/imageFiles/emojis/2564.svg new file mode 100644 index 0000000..fe13cd4 --- /dev/null +++ b/resources/imageFiles/emojis/2564.svg @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2565.svg b/resources/imageFiles/emojis/2565.svg new file mode 100644 index 0000000..2f6f427 --- /dev/null +++ b/resources/imageFiles/emojis/2565.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2566.svg b/resources/imageFiles/emojis/2566.svg new file mode 100644 index 0000000..b437b69 --- /dev/null +++ b/resources/imageFiles/emojis/2566.svg @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2567.svg b/resources/imageFiles/emojis/2567.svg new file mode 100644 index 0000000..e0d53ea --- /dev/null +++ b/resources/imageFiles/emojis/2567.svg @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2568.svg b/resources/imageFiles/emojis/2568.svg new file mode 100644 index 0000000..8d95236 --- /dev/null +++ b/resources/imageFiles/emojis/2568.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2569.svg b/resources/imageFiles/emojis/2569.svg new file mode 100644 index 0000000..540a22f --- /dev/null +++ b/resources/imageFiles/emojis/2569.svg @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2570.svg b/resources/imageFiles/emojis/2570.svg new file mode 100644 index 0000000..d835fb1 --- /dev/null +++ b/resources/imageFiles/emojis/2570.svg @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2571.svg b/resources/imageFiles/emojis/2571.svg new file mode 100644 index 0000000..545a362 --- /dev/null +++ b/resources/imageFiles/emojis/2571.svg @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2572.svg b/resources/imageFiles/emojis/2572.svg new file mode 100644 index 0000000..7c5edae --- /dev/null +++ b/resources/imageFiles/emojis/2572.svg @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2573.svg b/resources/imageFiles/emojis/2573.svg new file mode 100644 index 0000000..8032e5d --- /dev/null +++ b/resources/imageFiles/emojis/2573.svg @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2574.svg b/resources/imageFiles/emojis/2574.svg new file mode 100644 index 0000000..3ed6cf4 --- /dev/null +++ b/resources/imageFiles/emojis/2574.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2575.svg b/resources/imageFiles/emojis/2575.svg new file mode 100644 index 0000000..6618547 --- /dev/null +++ b/resources/imageFiles/emojis/2575.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/resources/imageFiles/emojis/2576.svg b/resources/imageFiles/emojis/2576.svg new file mode 100644 index 0000000..4cf8c19 --- /dev/null +++ b/resources/imageFiles/emojis/2576.svg @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2577.svg b/resources/imageFiles/emojis/2577.svg new file mode 100644 index 0000000..85369b5 --- /dev/null +++ b/resources/imageFiles/emojis/2577.svg @@ -0,0 +1,29 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2578.svg b/resources/imageFiles/emojis/2578.svg new file mode 100644 index 0000000..5e7cea6 --- /dev/null +++ b/resources/imageFiles/emojis/2578.svg @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2579.svg b/resources/imageFiles/emojis/2579.svg new file mode 100644 index 0000000..dea3dc8 --- /dev/null +++ b/resources/imageFiles/emojis/2579.svg @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2580.svg b/resources/imageFiles/emojis/2580.svg new file mode 100644 index 0000000..36c70e4 --- /dev/null +++ b/resources/imageFiles/emojis/2580.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2581.svg b/resources/imageFiles/emojis/2581.svg new file mode 100644 index 0000000..cfd6bb6 --- /dev/null +++ b/resources/imageFiles/emojis/2581.svg @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2582.svg b/resources/imageFiles/emojis/2582.svg new file mode 100644 index 0000000..991cd66 --- /dev/null +++ b/resources/imageFiles/emojis/2582.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2583.svg b/resources/imageFiles/emojis/2583.svg new file mode 100644 index 0000000..c9ae8df --- /dev/null +++ b/resources/imageFiles/emojis/2583.svg @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2584.svg b/resources/imageFiles/emojis/2584.svg new file mode 100644 index 0000000..04e7e9a --- /dev/null +++ b/resources/imageFiles/emojis/2584.svg @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2585.svg b/resources/imageFiles/emojis/2585.svg new file mode 100644 index 0000000..ef8e2a9 --- /dev/null +++ b/resources/imageFiles/emojis/2585.svg @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2586.svg b/resources/imageFiles/emojis/2586.svg new file mode 100644 index 0000000..7b29174 --- /dev/null +++ b/resources/imageFiles/emojis/2586.svg @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2587.svg b/resources/imageFiles/emojis/2587.svg new file mode 100644 index 0000000..ba393a5 --- /dev/null +++ b/resources/imageFiles/emojis/2587.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2588.svg b/resources/imageFiles/emojis/2588.svg new file mode 100644 index 0000000..9f08a07 --- /dev/null +++ b/resources/imageFiles/emojis/2588.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2589.svg b/resources/imageFiles/emojis/2589.svg new file mode 100644 index 0000000..20e986e --- /dev/null +++ b/resources/imageFiles/emojis/2589.svg @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2590.svg b/resources/imageFiles/emojis/2590.svg new file mode 100644 index 0000000..64637c3 --- /dev/null +++ b/resources/imageFiles/emojis/2590.svg @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2591.svg b/resources/imageFiles/emojis/2591.svg new file mode 100644 index 0000000..668912f --- /dev/null +++ b/resources/imageFiles/emojis/2591.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2592.svg b/resources/imageFiles/emojis/2592.svg new file mode 100644 index 0000000..fdca7b6 --- /dev/null +++ b/resources/imageFiles/emojis/2592.svg @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2593.svg b/resources/imageFiles/emojis/2593.svg new file mode 100644 index 0000000..79938fb --- /dev/null +++ b/resources/imageFiles/emojis/2593.svg @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2594.svg b/resources/imageFiles/emojis/2594.svg new file mode 100644 index 0000000..f47cae7 --- /dev/null +++ b/resources/imageFiles/emojis/2594.svg @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2595.svg b/resources/imageFiles/emojis/2595.svg new file mode 100644 index 0000000..9a209af --- /dev/null +++ b/resources/imageFiles/emojis/2595.svg @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2596.svg b/resources/imageFiles/emojis/2596.svg new file mode 100644 index 0000000..fc3b285 --- /dev/null +++ b/resources/imageFiles/emojis/2596.svg @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2597.svg b/resources/imageFiles/emojis/2597.svg new file mode 100644 index 0000000..c7bc94f --- /dev/null +++ b/resources/imageFiles/emojis/2597.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2598.svg b/resources/imageFiles/emojis/2598.svg new file mode 100644 index 0000000..d9cad72 --- /dev/null +++ b/resources/imageFiles/emojis/2598.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2599.svg b/resources/imageFiles/emojis/2599.svg new file mode 100644 index 0000000..c4fdab7 --- /dev/null +++ b/resources/imageFiles/emojis/2599.svg @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2600.svg b/resources/imageFiles/emojis/2600.svg new file mode 100644 index 0000000..b51d602 --- /dev/null +++ b/resources/imageFiles/emojis/2600.svg @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2601.svg b/resources/imageFiles/emojis/2601.svg new file mode 100644 index 0000000..1bba4f8 --- /dev/null +++ b/resources/imageFiles/emojis/2601.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2602.svg b/resources/imageFiles/emojis/2602.svg new file mode 100644 index 0000000..4b684d4 --- /dev/null +++ b/resources/imageFiles/emojis/2602.svg @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2603.svg b/resources/imageFiles/emojis/2603.svg new file mode 100644 index 0000000..01b1660 --- /dev/null +++ b/resources/imageFiles/emojis/2603.svg @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2604.svg b/resources/imageFiles/emojis/2604.svg new file mode 100644 index 0000000..965771d --- /dev/null +++ b/resources/imageFiles/emojis/2604.svg @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2605.svg b/resources/imageFiles/emojis/2605.svg new file mode 100644 index 0000000..1681d0a --- /dev/null +++ b/resources/imageFiles/emojis/2605.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2606.svg b/resources/imageFiles/emojis/2606.svg new file mode 100644 index 0000000..fb55be4 --- /dev/null +++ b/resources/imageFiles/emojis/2606.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2607.svg b/resources/imageFiles/emojis/2607.svg new file mode 100644 index 0000000..3cb27b0 --- /dev/null +++ b/resources/imageFiles/emojis/2607.svg @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2608.svg b/resources/imageFiles/emojis/2608.svg new file mode 100644 index 0000000..d4a16bd --- /dev/null +++ b/resources/imageFiles/emojis/2608.svg @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2609.svg b/resources/imageFiles/emojis/2609.svg new file mode 100644 index 0000000..b86e3de --- /dev/null +++ b/resources/imageFiles/emojis/2609.svg @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2610.svg b/resources/imageFiles/emojis/2610.svg new file mode 100644 index 0000000..56e5319 --- /dev/null +++ b/resources/imageFiles/emojis/2610.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2611.svg b/resources/imageFiles/emojis/2611.svg new file mode 100644 index 0000000..2ad36a0 --- /dev/null +++ b/resources/imageFiles/emojis/2611.svg @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2612.svg b/resources/imageFiles/emojis/2612.svg new file mode 100644 index 0000000..324278a --- /dev/null +++ b/resources/imageFiles/emojis/2612.svg @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2613.svg b/resources/imageFiles/emojis/2613.svg new file mode 100644 index 0000000..7909df5 --- /dev/null +++ b/resources/imageFiles/emojis/2613.svg @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2614.svg b/resources/imageFiles/emojis/2614.svg new file mode 100644 index 0000000..2386a91 --- /dev/null +++ b/resources/imageFiles/emojis/2614.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2615.svg b/resources/imageFiles/emojis/2615.svg new file mode 100644 index 0000000..7bd1f38 --- /dev/null +++ b/resources/imageFiles/emojis/2615.svg @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2616.svg b/resources/imageFiles/emojis/2616.svg new file mode 100644 index 0000000..8a3be11 --- /dev/null +++ b/resources/imageFiles/emojis/2616.svg @@ -0,0 +1,32 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2617.svg b/resources/imageFiles/emojis/2617.svg new file mode 100644 index 0000000..12437df --- /dev/null +++ b/resources/imageFiles/emojis/2617.svg @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2618.svg b/resources/imageFiles/emojis/2618.svg new file mode 100644 index 0000000..a08d443 --- /dev/null +++ b/resources/imageFiles/emojis/2618.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2619.svg b/resources/imageFiles/emojis/2619.svg new file mode 100644 index 0000000..7388dec --- /dev/null +++ b/resources/imageFiles/emojis/2619.svg @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2620.svg b/resources/imageFiles/emojis/2620.svg new file mode 100644 index 0000000..7276c26 --- /dev/null +++ b/resources/imageFiles/emojis/2620.svg @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2621.svg b/resources/imageFiles/emojis/2621.svg new file mode 100644 index 0000000..424225a --- /dev/null +++ b/resources/imageFiles/emojis/2621.svg @@ -0,0 +1,52 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2622.svg b/resources/imageFiles/emojis/2622.svg new file mode 100644 index 0000000..5453612 --- /dev/null +++ b/resources/imageFiles/emojis/2622.svg @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2623.svg b/resources/imageFiles/emojis/2623.svg new file mode 100644 index 0000000..ec341c0 --- /dev/null +++ b/resources/imageFiles/emojis/2623.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2624.svg b/resources/imageFiles/emojis/2624.svg new file mode 100644 index 0000000..73c39e0 --- /dev/null +++ b/resources/imageFiles/emojis/2624.svg @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2625.svg b/resources/imageFiles/emojis/2625.svg new file mode 100644 index 0000000..59b14f2 --- /dev/null +++ b/resources/imageFiles/emojis/2625.svg @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2626.svg b/resources/imageFiles/emojis/2626.svg new file mode 100644 index 0000000..e1a07ae --- /dev/null +++ b/resources/imageFiles/emojis/2626.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2627.svg b/resources/imageFiles/emojis/2627.svg new file mode 100644 index 0000000..33e21d0 --- /dev/null +++ b/resources/imageFiles/emojis/2627.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2628.svg b/resources/imageFiles/emojis/2628.svg new file mode 100644 index 0000000..270ac8d --- /dev/null +++ b/resources/imageFiles/emojis/2628.svg @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2629.svg b/resources/imageFiles/emojis/2629.svg new file mode 100644 index 0000000..43baa11 --- /dev/null +++ b/resources/imageFiles/emojis/2629.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2630.svg b/resources/imageFiles/emojis/2630.svg new file mode 100644 index 0000000..d3497ef --- /dev/null +++ b/resources/imageFiles/emojis/2630.svg @@ -0,0 +1,33 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2631.svg b/resources/imageFiles/emojis/2631.svg new file mode 100644 index 0000000..65d3d88 --- /dev/null +++ b/resources/imageFiles/emojis/2631.svg @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2632.svg b/resources/imageFiles/emojis/2632.svg new file mode 100644 index 0000000..028e136 --- /dev/null +++ b/resources/imageFiles/emojis/2632.svg @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2633.svg b/resources/imageFiles/emojis/2633.svg new file mode 100644 index 0000000..c03080e --- /dev/null +++ b/resources/imageFiles/emojis/2633.svg @@ -0,0 +1,31 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2634.svg b/resources/imageFiles/emojis/2634.svg new file mode 100644 index 0000000..a1f9c0f --- /dev/null +++ b/resources/imageFiles/emojis/2634.svg @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2635.svg b/resources/imageFiles/emojis/2635.svg new file mode 100644 index 0000000..6d496c3 --- /dev/null +++ b/resources/imageFiles/emojis/2635.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2636.svg b/resources/imageFiles/emojis/2636.svg new file mode 100644 index 0000000..072c3b3 --- /dev/null +++ b/resources/imageFiles/emojis/2636.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2637.svg b/resources/imageFiles/emojis/2637.svg new file mode 100644 index 0000000..a2daf51 --- /dev/null +++ b/resources/imageFiles/emojis/2637.svg @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2638.svg b/resources/imageFiles/emojis/2638.svg new file mode 100644 index 0000000..1963116 --- /dev/null +++ b/resources/imageFiles/emojis/2638.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2639.svg b/resources/imageFiles/emojis/2639.svg new file mode 100644 index 0000000..2d283a7 --- /dev/null +++ b/resources/imageFiles/emojis/2639.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2640.svg b/resources/imageFiles/emojis/2640.svg new file mode 100644 index 0000000..ba03742 --- /dev/null +++ b/resources/imageFiles/emojis/2640.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2641.svg b/resources/imageFiles/emojis/2641.svg new file mode 100644 index 0000000..2ec2dd8 --- /dev/null +++ b/resources/imageFiles/emojis/2641.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2642.svg b/resources/imageFiles/emojis/2642.svg new file mode 100644 index 0000000..45e1232 --- /dev/null +++ b/resources/imageFiles/emojis/2642.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2643.svg b/resources/imageFiles/emojis/2643.svg new file mode 100644 index 0000000..80ea902 --- /dev/null +++ b/resources/imageFiles/emojis/2643.svg @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2644.svg b/resources/imageFiles/emojis/2644.svg new file mode 100644 index 0000000..e60c82e --- /dev/null +++ b/resources/imageFiles/emojis/2644.svg @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2645.svg b/resources/imageFiles/emojis/2645.svg new file mode 100644 index 0000000..20633d7 --- /dev/null +++ b/resources/imageFiles/emojis/2645.svg @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2646.svg b/resources/imageFiles/emojis/2646.svg new file mode 100644 index 0000000..4349231 --- /dev/null +++ b/resources/imageFiles/emojis/2646.svg @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2647.svg b/resources/imageFiles/emojis/2647.svg new file mode 100644 index 0000000..d5ba644 --- /dev/null +++ b/resources/imageFiles/emojis/2647.svg @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2648.svg b/resources/imageFiles/emojis/2648.svg new file mode 100644 index 0000000..fe3557b --- /dev/null +++ b/resources/imageFiles/emojis/2648.svg @@ -0,0 +1,29 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2649.svg b/resources/imageFiles/emojis/2649.svg new file mode 100644 index 0000000..5adfe53 --- /dev/null +++ b/resources/imageFiles/emojis/2649.svg @@ -0,0 +1,46 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2650.svg b/resources/imageFiles/emojis/2650.svg new file mode 100644 index 0000000..72bae58 --- /dev/null +++ b/resources/imageFiles/emojis/2650.svg @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2651.svg b/resources/imageFiles/emojis/2651.svg new file mode 100644 index 0000000..0925bc5 --- /dev/null +++ b/resources/imageFiles/emojis/2651.svg @@ -0,0 +1,32 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2652.svg b/resources/imageFiles/emojis/2652.svg new file mode 100644 index 0000000..b3b78e7 --- /dev/null +++ b/resources/imageFiles/emojis/2652.svg @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2653.svg b/resources/imageFiles/emojis/2653.svg new file mode 100644 index 0000000..2423853 --- /dev/null +++ b/resources/imageFiles/emojis/2653.svg @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2654.svg b/resources/imageFiles/emojis/2654.svg new file mode 100644 index 0000000..06ec5ad --- /dev/null +++ b/resources/imageFiles/emojis/2654.svg @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2655.svg b/resources/imageFiles/emojis/2655.svg new file mode 100644 index 0000000..9f3f9b8 --- /dev/null +++ b/resources/imageFiles/emojis/2655.svg @@ -0,0 +1,36 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2656.svg b/resources/imageFiles/emojis/2656.svg new file mode 100644 index 0000000..ed32123 --- /dev/null +++ b/resources/imageFiles/emojis/2656.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2657.svg b/resources/imageFiles/emojis/2657.svg new file mode 100644 index 0000000..9584092 --- /dev/null +++ b/resources/imageFiles/emojis/2657.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2658.svg b/resources/imageFiles/emojis/2658.svg new file mode 100644 index 0000000..204e166 --- /dev/null +++ b/resources/imageFiles/emojis/2658.svg @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2659.svg b/resources/imageFiles/emojis/2659.svg new file mode 100644 index 0000000..d6127e3 --- /dev/null +++ b/resources/imageFiles/emojis/2659.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2660.svg b/resources/imageFiles/emojis/2660.svg new file mode 100644 index 0000000..d625efd --- /dev/null +++ b/resources/imageFiles/emojis/2660.svg @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2661.svg b/resources/imageFiles/emojis/2661.svg new file mode 100644 index 0000000..86d1689 --- /dev/null +++ b/resources/imageFiles/emojis/2661.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2662.svg b/resources/imageFiles/emojis/2662.svg new file mode 100644 index 0000000..fd2cb84 --- /dev/null +++ b/resources/imageFiles/emojis/2662.svg @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2663.svg b/resources/imageFiles/emojis/2663.svg new file mode 100644 index 0000000..ffac2c3 --- /dev/null +++ b/resources/imageFiles/emojis/2663.svg @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2664.svg b/resources/imageFiles/emojis/2664.svg new file mode 100644 index 0000000..f6d246a --- /dev/null +++ b/resources/imageFiles/emojis/2664.svg @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2665.svg b/resources/imageFiles/emojis/2665.svg new file mode 100644 index 0000000..7604deb --- /dev/null +++ b/resources/imageFiles/emojis/2665.svg @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2666.svg b/resources/imageFiles/emojis/2666.svg new file mode 100644 index 0000000..a63398b --- /dev/null +++ b/resources/imageFiles/emojis/2666.svg @@ -0,0 +1,29 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2667.svg b/resources/imageFiles/emojis/2667.svg new file mode 100644 index 0000000..b09f863 --- /dev/null +++ b/resources/imageFiles/emojis/2667.svg @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2668.svg b/resources/imageFiles/emojis/2668.svg new file mode 100644 index 0000000..74efad7 --- /dev/null +++ b/resources/imageFiles/emojis/2668.svg @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2669.svg b/resources/imageFiles/emojis/2669.svg new file mode 100644 index 0000000..f9d53f8 --- /dev/null +++ b/resources/imageFiles/emojis/2669.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2670.svg b/resources/imageFiles/emojis/2670.svg new file mode 100644 index 0000000..25eeae5 --- /dev/null +++ b/resources/imageFiles/emojis/2670.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2671.svg b/resources/imageFiles/emojis/2671.svg new file mode 100644 index 0000000..2bbbb7b --- /dev/null +++ b/resources/imageFiles/emojis/2671.svg @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2672.svg b/resources/imageFiles/emojis/2672.svg new file mode 100644 index 0000000..def0871 --- /dev/null +++ b/resources/imageFiles/emojis/2672.svg @@ -0,0 +1,54 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2673.svg b/resources/imageFiles/emojis/2673.svg new file mode 100644 index 0000000..3862e81 --- /dev/null +++ b/resources/imageFiles/emojis/2673.svg @@ -0,0 +1,44 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2674.svg b/resources/imageFiles/emojis/2674.svg new file mode 100644 index 0000000..9ac3f64 --- /dev/null +++ b/resources/imageFiles/emojis/2674.svg @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2675.svg b/resources/imageFiles/emojis/2675.svg new file mode 100644 index 0000000..9bb7bbd --- /dev/null +++ b/resources/imageFiles/emojis/2675.svg @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2676.svg b/resources/imageFiles/emojis/2676.svg new file mode 100644 index 0000000..1dd180a --- /dev/null +++ b/resources/imageFiles/emojis/2676.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2677.svg b/resources/imageFiles/emojis/2677.svg new file mode 100644 index 0000000..b6aa287 --- /dev/null +++ b/resources/imageFiles/emojis/2677.svg @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2678.svg b/resources/imageFiles/emojis/2678.svg new file mode 100644 index 0000000..a11167f --- /dev/null +++ b/resources/imageFiles/emojis/2678.svg @@ -0,0 +1,33 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2679.svg b/resources/imageFiles/emojis/2679.svg new file mode 100644 index 0000000..219b6d2 --- /dev/null +++ b/resources/imageFiles/emojis/2679.svg @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2680.svg b/resources/imageFiles/emojis/2680.svg new file mode 100644 index 0000000..7c31b3d --- /dev/null +++ b/resources/imageFiles/emojis/2680.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2681.svg b/resources/imageFiles/emojis/2681.svg new file mode 100644 index 0000000..9e6e833 --- /dev/null +++ b/resources/imageFiles/emojis/2681.svg @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2682.svg b/resources/imageFiles/emojis/2682.svg new file mode 100644 index 0000000..e12f67d --- /dev/null +++ b/resources/imageFiles/emojis/2682.svg @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2683.svg b/resources/imageFiles/emojis/2683.svg new file mode 100644 index 0000000..e09b7c2 --- /dev/null +++ b/resources/imageFiles/emojis/2683.svg @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2684.svg b/resources/imageFiles/emojis/2684.svg new file mode 100644 index 0000000..5d7171f --- /dev/null +++ b/resources/imageFiles/emojis/2684.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2685.svg b/resources/imageFiles/emojis/2685.svg new file mode 100644 index 0000000..ed7a434 --- /dev/null +++ b/resources/imageFiles/emojis/2685.svg @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2686.svg b/resources/imageFiles/emojis/2686.svg new file mode 100644 index 0000000..d5f29d1 --- /dev/null +++ b/resources/imageFiles/emojis/2686.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2687.svg b/resources/imageFiles/emojis/2687.svg new file mode 100644 index 0000000..5945c38 --- /dev/null +++ b/resources/imageFiles/emojis/2687.svg @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2688.svg b/resources/imageFiles/emojis/2688.svg new file mode 100644 index 0000000..7e733ec --- /dev/null +++ b/resources/imageFiles/emojis/2688.svg @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2689.svg b/resources/imageFiles/emojis/2689.svg new file mode 100644 index 0000000..b03a954 --- /dev/null +++ b/resources/imageFiles/emojis/2689.svg @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2690.svg b/resources/imageFiles/emojis/2690.svg new file mode 100644 index 0000000..b8138f0 --- /dev/null +++ b/resources/imageFiles/emojis/2690.svg @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2691.svg b/resources/imageFiles/emojis/2691.svg new file mode 100644 index 0000000..bfdc8e4 --- /dev/null +++ b/resources/imageFiles/emojis/2691.svg @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2692.svg b/resources/imageFiles/emojis/2692.svg new file mode 100644 index 0000000..0cadd98 --- /dev/null +++ b/resources/imageFiles/emojis/2692.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2693.svg b/resources/imageFiles/emojis/2693.svg new file mode 100644 index 0000000..91f348a --- /dev/null +++ b/resources/imageFiles/emojis/2693.svg @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2694.svg b/resources/imageFiles/emojis/2694.svg new file mode 100644 index 0000000..34b1c46 --- /dev/null +++ b/resources/imageFiles/emojis/2694.svg @@ -0,0 +1,57 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2695.svg b/resources/imageFiles/emojis/2695.svg new file mode 100644 index 0000000..8909cee --- /dev/null +++ b/resources/imageFiles/emojis/2695.svg @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2696.svg b/resources/imageFiles/emojis/2696.svg new file mode 100644 index 0000000..d354019 --- /dev/null +++ b/resources/imageFiles/emojis/2696.svg @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2697.svg b/resources/imageFiles/emojis/2697.svg new file mode 100644 index 0000000..232a9d9 --- /dev/null +++ b/resources/imageFiles/emojis/2697.svg @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2698.svg b/resources/imageFiles/emojis/2698.svg new file mode 100644 index 0000000..7053338 --- /dev/null +++ b/resources/imageFiles/emojis/2698.svg @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2699.svg b/resources/imageFiles/emojis/2699.svg new file mode 100644 index 0000000..e9f7a7d --- /dev/null +++ b/resources/imageFiles/emojis/2699.svg @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2700.svg b/resources/imageFiles/emojis/2700.svg new file mode 100644 index 0000000..e0b1aa2 --- /dev/null +++ b/resources/imageFiles/emojis/2700.svg @@ -0,0 +1,66 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2701.svg b/resources/imageFiles/emojis/2701.svg new file mode 100644 index 0000000..580f4ca --- /dev/null +++ b/resources/imageFiles/emojis/2701.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2702.svg b/resources/imageFiles/emojis/2702.svg new file mode 100644 index 0000000..0fd8678 --- /dev/null +++ b/resources/imageFiles/emojis/2702.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2703.svg b/resources/imageFiles/emojis/2703.svg new file mode 100644 index 0000000..e6ef46c --- /dev/null +++ b/resources/imageFiles/emojis/2703.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2704.svg b/resources/imageFiles/emojis/2704.svg new file mode 100644 index 0000000..dbbcef5 --- /dev/null +++ b/resources/imageFiles/emojis/2704.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2705.svg b/resources/imageFiles/emojis/2705.svg new file mode 100644 index 0000000..dbed8bf --- /dev/null +++ b/resources/imageFiles/emojis/2705.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2706.svg b/resources/imageFiles/emojis/2706.svg new file mode 100644 index 0000000..ebd8d63 --- /dev/null +++ b/resources/imageFiles/emojis/2706.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2707.svg b/resources/imageFiles/emojis/2707.svg new file mode 100644 index 0000000..7a6e9a8 --- /dev/null +++ b/resources/imageFiles/emojis/2707.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2708.svg b/resources/imageFiles/emojis/2708.svg new file mode 100644 index 0000000..db647ee --- /dev/null +++ b/resources/imageFiles/emojis/2708.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2709.svg b/resources/imageFiles/emojis/2709.svg new file mode 100644 index 0000000..78896fd --- /dev/null +++ b/resources/imageFiles/emojis/2709.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2710.svg b/resources/imageFiles/emojis/2710.svg new file mode 100644 index 0000000..b12f1a8 --- /dev/null +++ b/resources/imageFiles/emojis/2710.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2711.svg b/resources/imageFiles/emojis/2711.svg new file mode 100644 index 0000000..775ef05 --- /dev/null +++ b/resources/imageFiles/emojis/2711.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2712.svg b/resources/imageFiles/emojis/2712.svg new file mode 100644 index 0000000..6522b11 --- /dev/null +++ b/resources/imageFiles/emojis/2712.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2713.svg b/resources/imageFiles/emojis/2713.svg new file mode 100644 index 0000000..417f0a4 --- /dev/null +++ b/resources/imageFiles/emojis/2713.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2714.svg b/resources/imageFiles/emojis/2714.svg new file mode 100644 index 0000000..7682b97 --- /dev/null +++ b/resources/imageFiles/emojis/2714.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2715.svg b/resources/imageFiles/emojis/2715.svg new file mode 100644 index 0000000..d2900d8 --- /dev/null +++ b/resources/imageFiles/emojis/2715.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/resources/imageFiles/emojis/2716.svg b/resources/imageFiles/emojis/2716.svg new file mode 100644 index 0000000..bfa5399 --- /dev/null +++ b/resources/imageFiles/emojis/2716.svg @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2717.svg b/resources/imageFiles/emojis/2717.svg new file mode 100644 index 0000000..0d038ed --- /dev/null +++ b/resources/imageFiles/emojis/2717.svg @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2718.svg b/resources/imageFiles/emojis/2718.svg new file mode 100644 index 0000000..676a9a1 --- /dev/null +++ b/resources/imageFiles/emojis/2718.svg @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2719.svg b/resources/imageFiles/emojis/2719.svg new file mode 100644 index 0000000..fce1280 --- /dev/null +++ b/resources/imageFiles/emojis/2719.svg @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2720.svg b/resources/imageFiles/emojis/2720.svg new file mode 100644 index 0000000..21988bc --- /dev/null +++ b/resources/imageFiles/emojis/2720.svg @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2721.svg b/resources/imageFiles/emojis/2721.svg new file mode 100644 index 0000000..06eaf8e --- /dev/null +++ b/resources/imageFiles/emojis/2721.svg @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2722.svg b/resources/imageFiles/emojis/2722.svg new file mode 100644 index 0000000..0d651a0 --- /dev/null +++ b/resources/imageFiles/emojis/2722.svg @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2723.svg b/resources/imageFiles/emojis/2723.svg new file mode 100644 index 0000000..66b68be --- /dev/null +++ b/resources/imageFiles/emojis/2723.svg @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2724.svg b/resources/imageFiles/emojis/2724.svg new file mode 100644 index 0000000..3c01516 --- /dev/null +++ b/resources/imageFiles/emojis/2724.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2725.svg b/resources/imageFiles/emojis/2725.svg new file mode 100644 index 0000000..afe7187 --- /dev/null +++ b/resources/imageFiles/emojis/2725.svg @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2726.svg b/resources/imageFiles/emojis/2726.svg new file mode 100644 index 0000000..f0447b1 --- /dev/null +++ b/resources/imageFiles/emojis/2726.svg @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2727.svg b/resources/imageFiles/emojis/2727.svg new file mode 100644 index 0000000..08d7b14 --- /dev/null +++ b/resources/imageFiles/emojis/2727.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2728.svg b/resources/imageFiles/emojis/2728.svg new file mode 100644 index 0000000..096c1ff --- /dev/null +++ b/resources/imageFiles/emojis/2728.svg @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2729.svg b/resources/imageFiles/emojis/2729.svg new file mode 100644 index 0000000..0e13ec3 --- /dev/null +++ b/resources/imageFiles/emojis/2729.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2730.svg b/resources/imageFiles/emojis/2730.svg new file mode 100644 index 0000000..48706c3 --- /dev/null +++ b/resources/imageFiles/emojis/2730.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2731.svg b/resources/imageFiles/emojis/2731.svg new file mode 100644 index 0000000..a1e32c6 --- /dev/null +++ b/resources/imageFiles/emojis/2731.svg @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2732.svg b/resources/imageFiles/emojis/2732.svg new file mode 100644 index 0000000..d637c69 --- /dev/null +++ b/resources/imageFiles/emojis/2732.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/resources/imageFiles/emojis/2733.svg b/resources/imageFiles/emojis/2733.svg new file mode 100644 index 0000000..8cd41c7 --- /dev/null +++ b/resources/imageFiles/emojis/2733.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2734.svg b/resources/imageFiles/emojis/2734.svg new file mode 100644 index 0000000..a91845f --- /dev/null +++ b/resources/imageFiles/emojis/2734.svg @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2735.svg b/resources/imageFiles/emojis/2735.svg new file mode 100644 index 0000000..373bdc5 --- /dev/null +++ b/resources/imageFiles/emojis/2735.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2736.svg b/resources/imageFiles/emojis/2736.svg new file mode 100644 index 0000000..c4658d0 --- /dev/null +++ b/resources/imageFiles/emojis/2736.svg @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2737.svg b/resources/imageFiles/emojis/2737.svg new file mode 100644 index 0000000..37cdddd --- /dev/null +++ b/resources/imageFiles/emojis/2737.svg @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2738.svg b/resources/imageFiles/emojis/2738.svg new file mode 100644 index 0000000..437b40c --- /dev/null +++ b/resources/imageFiles/emojis/2738.svg @@ -0,0 +1,31 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2739.svg b/resources/imageFiles/emojis/2739.svg new file mode 100644 index 0000000..d2ae57f --- /dev/null +++ b/resources/imageFiles/emojis/2739.svg @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2740.svg b/resources/imageFiles/emojis/2740.svg new file mode 100644 index 0000000..28bab85 --- /dev/null +++ b/resources/imageFiles/emojis/2740.svg @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2741.svg b/resources/imageFiles/emojis/2741.svg new file mode 100644 index 0000000..2eec755 --- /dev/null +++ b/resources/imageFiles/emojis/2741.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2742.svg b/resources/imageFiles/emojis/2742.svg new file mode 100644 index 0000000..c08d124 --- /dev/null +++ b/resources/imageFiles/emojis/2742.svg @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2743.svg b/resources/imageFiles/emojis/2743.svg new file mode 100644 index 0000000..a7210dc --- /dev/null +++ b/resources/imageFiles/emojis/2743.svg @@ -0,0 +1,39 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2744.svg b/resources/imageFiles/emojis/2744.svg new file mode 100644 index 0000000..aa881ff --- /dev/null +++ b/resources/imageFiles/emojis/2744.svg @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2745.svg b/resources/imageFiles/emojis/2745.svg new file mode 100644 index 0000000..277c3d5 --- /dev/null +++ b/resources/imageFiles/emojis/2745.svg @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2746.svg b/resources/imageFiles/emojis/2746.svg new file mode 100644 index 0000000..7a622b1 --- /dev/null +++ b/resources/imageFiles/emojis/2746.svg @@ -0,0 +1,59 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2747.svg b/resources/imageFiles/emojis/2747.svg new file mode 100644 index 0000000..b2c14d3 --- /dev/null +++ b/resources/imageFiles/emojis/2747.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2748.svg b/resources/imageFiles/emojis/2748.svg new file mode 100644 index 0000000..8dad086 --- /dev/null +++ b/resources/imageFiles/emojis/2748.svg @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2749.svg b/resources/imageFiles/emojis/2749.svg new file mode 100644 index 0000000..5f63229 --- /dev/null +++ b/resources/imageFiles/emojis/2749.svg @@ -0,0 +1,32 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2750.svg b/resources/imageFiles/emojis/2750.svg new file mode 100644 index 0000000..a3c6565 --- /dev/null +++ b/resources/imageFiles/emojis/2750.svg @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2751.svg b/resources/imageFiles/emojis/2751.svg new file mode 100644 index 0000000..7b88f7f --- /dev/null +++ b/resources/imageFiles/emojis/2751.svg @@ -0,0 +1,40 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2752.svg b/resources/imageFiles/emojis/2752.svg new file mode 100644 index 0000000..ed8103e --- /dev/null +++ b/resources/imageFiles/emojis/2752.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2753.svg b/resources/imageFiles/emojis/2753.svg new file mode 100644 index 0000000..f27cf8f --- /dev/null +++ b/resources/imageFiles/emojis/2753.svg @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2754.svg b/resources/imageFiles/emojis/2754.svg new file mode 100644 index 0000000..6373bea --- /dev/null +++ b/resources/imageFiles/emojis/2754.svg @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2755.svg b/resources/imageFiles/emojis/2755.svg new file mode 100644 index 0000000..950c8de --- /dev/null +++ b/resources/imageFiles/emojis/2755.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2756.svg b/resources/imageFiles/emojis/2756.svg new file mode 100644 index 0000000..97ed776 --- /dev/null +++ b/resources/imageFiles/emojis/2756.svg @@ -0,0 +1,31 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2757.svg b/resources/imageFiles/emojis/2757.svg new file mode 100644 index 0000000..7b4ff1c --- /dev/null +++ b/resources/imageFiles/emojis/2757.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2758.svg b/resources/imageFiles/emojis/2758.svg new file mode 100644 index 0000000..482f211 --- /dev/null +++ b/resources/imageFiles/emojis/2758.svg @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2759.svg b/resources/imageFiles/emojis/2759.svg new file mode 100644 index 0000000..3ab7785 --- /dev/null +++ b/resources/imageFiles/emojis/2759.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2760.svg b/resources/imageFiles/emojis/2760.svg new file mode 100644 index 0000000..86fd522 --- /dev/null +++ b/resources/imageFiles/emojis/2760.svg @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2761.svg b/resources/imageFiles/emojis/2761.svg new file mode 100644 index 0000000..031c66d --- /dev/null +++ b/resources/imageFiles/emojis/2761.svg @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2762.svg b/resources/imageFiles/emojis/2762.svg new file mode 100644 index 0000000..0a5cc9d --- /dev/null +++ b/resources/imageFiles/emojis/2762.svg @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2763.svg b/resources/imageFiles/emojis/2763.svg new file mode 100644 index 0000000..b11f2fb --- /dev/null +++ b/resources/imageFiles/emojis/2763.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2764.svg b/resources/imageFiles/emojis/2764.svg new file mode 100644 index 0000000..b91c030 --- /dev/null +++ b/resources/imageFiles/emojis/2764.svg @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2765.svg b/resources/imageFiles/emojis/2765.svg new file mode 100644 index 0000000..9220a3a --- /dev/null +++ b/resources/imageFiles/emojis/2765.svg @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2766.svg b/resources/imageFiles/emojis/2766.svg new file mode 100644 index 0000000..0425d11 --- /dev/null +++ b/resources/imageFiles/emojis/2766.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2767.svg b/resources/imageFiles/emojis/2767.svg new file mode 100644 index 0000000..05008e9 --- /dev/null +++ b/resources/imageFiles/emojis/2767.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2768.svg b/resources/imageFiles/emojis/2768.svg new file mode 100644 index 0000000..8cfa8fd --- /dev/null +++ b/resources/imageFiles/emojis/2768.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2769.svg b/resources/imageFiles/emojis/2769.svg new file mode 100644 index 0000000..0a7ea42 --- /dev/null +++ b/resources/imageFiles/emojis/2769.svg @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2770.svg b/resources/imageFiles/emojis/2770.svg new file mode 100644 index 0000000..68914ee --- /dev/null +++ b/resources/imageFiles/emojis/2770.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2771.svg b/resources/imageFiles/emojis/2771.svg new file mode 100644 index 0000000..ccbf896 --- /dev/null +++ b/resources/imageFiles/emojis/2771.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2772.svg b/resources/imageFiles/emojis/2772.svg new file mode 100644 index 0000000..2a23424 --- /dev/null +++ b/resources/imageFiles/emojis/2772.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2773.svg b/resources/imageFiles/emojis/2773.svg new file mode 100644 index 0000000..c77078d --- /dev/null +++ b/resources/imageFiles/emojis/2773.svg @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2774.svg b/resources/imageFiles/emojis/2774.svg new file mode 100644 index 0000000..3124f4d --- /dev/null +++ b/resources/imageFiles/emojis/2774.svg @@ -0,0 +1,33 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2775.svg b/resources/imageFiles/emojis/2775.svg new file mode 100644 index 0000000..6c6a831 --- /dev/null +++ b/resources/imageFiles/emojis/2775.svg @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2776.svg b/resources/imageFiles/emojis/2776.svg new file mode 100644 index 0000000..bebcbf2 --- /dev/null +++ b/resources/imageFiles/emojis/2776.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2777.svg b/resources/imageFiles/emojis/2777.svg new file mode 100644 index 0000000..f1646d2 --- /dev/null +++ b/resources/imageFiles/emojis/2777.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2778.svg b/resources/imageFiles/emojis/2778.svg new file mode 100644 index 0000000..97dbe17 --- /dev/null +++ b/resources/imageFiles/emojis/2778.svg @@ -0,0 +1,38 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2779.svg b/resources/imageFiles/emojis/2779.svg new file mode 100644 index 0000000..40a38d5 --- /dev/null +++ b/resources/imageFiles/emojis/2779.svg @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2780.svg b/resources/imageFiles/emojis/2780.svg new file mode 100644 index 0000000..76e8aca --- /dev/null +++ b/resources/imageFiles/emojis/2780.svg @@ -0,0 +1,29 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2781.svg b/resources/imageFiles/emojis/2781.svg new file mode 100644 index 0000000..f73c7ec --- /dev/null +++ b/resources/imageFiles/emojis/2781.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2782.svg b/resources/imageFiles/emojis/2782.svg new file mode 100644 index 0000000..39c80da --- /dev/null +++ b/resources/imageFiles/emojis/2782.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2783.svg b/resources/imageFiles/emojis/2783.svg new file mode 100644 index 0000000..465b076 --- /dev/null +++ b/resources/imageFiles/emojis/2783.svg @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2784.svg b/resources/imageFiles/emojis/2784.svg new file mode 100644 index 0000000..d594b63 --- /dev/null +++ b/resources/imageFiles/emojis/2784.svg @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2785.svg b/resources/imageFiles/emojis/2785.svg new file mode 100644 index 0000000..e3868e5 --- /dev/null +++ b/resources/imageFiles/emojis/2785.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2786.svg b/resources/imageFiles/emojis/2786.svg new file mode 100644 index 0000000..d4354d8 --- /dev/null +++ b/resources/imageFiles/emojis/2786.svg @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2787.svg b/resources/imageFiles/emojis/2787.svg new file mode 100644 index 0000000..fa45c6d --- /dev/null +++ b/resources/imageFiles/emojis/2787.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2788.svg b/resources/imageFiles/emojis/2788.svg new file mode 100644 index 0000000..f8dd90f --- /dev/null +++ b/resources/imageFiles/emojis/2788.svg @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2789.svg b/resources/imageFiles/emojis/2789.svg new file mode 100644 index 0000000..e496589 --- /dev/null +++ b/resources/imageFiles/emojis/2789.svg @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2790.svg b/resources/imageFiles/emojis/2790.svg new file mode 100644 index 0000000..6026bfc --- /dev/null +++ b/resources/imageFiles/emojis/2790.svg @@ -0,0 +1,33 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2791.svg b/resources/imageFiles/emojis/2791.svg new file mode 100644 index 0000000..c13d849 --- /dev/null +++ b/resources/imageFiles/emojis/2791.svg @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2792.svg b/resources/imageFiles/emojis/2792.svg new file mode 100644 index 0000000..828302f --- /dev/null +++ b/resources/imageFiles/emojis/2792.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2793.svg b/resources/imageFiles/emojis/2793.svg new file mode 100644 index 0000000..682467f --- /dev/null +++ b/resources/imageFiles/emojis/2793.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2794.svg b/resources/imageFiles/emojis/2794.svg new file mode 100644 index 0000000..540179e --- /dev/null +++ b/resources/imageFiles/emojis/2794.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2795.svg b/resources/imageFiles/emojis/2795.svg new file mode 100644 index 0000000..085020b --- /dev/null +++ b/resources/imageFiles/emojis/2795.svg @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2796.svg b/resources/imageFiles/emojis/2796.svg new file mode 100644 index 0000000..d924eaa --- /dev/null +++ b/resources/imageFiles/emojis/2796.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2797.svg b/resources/imageFiles/emojis/2797.svg new file mode 100644 index 0000000..11518ce --- /dev/null +++ b/resources/imageFiles/emojis/2797.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2798.svg b/resources/imageFiles/emojis/2798.svg new file mode 100644 index 0000000..6b61b6a --- /dev/null +++ b/resources/imageFiles/emojis/2798.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2799.svg b/resources/imageFiles/emojis/2799.svg new file mode 100644 index 0000000..eff9005 --- /dev/null +++ b/resources/imageFiles/emojis/2799.svg @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2800.svg b/resources/imageFiles/emojis/2800.svg new file mode 100644 index 0000000..1b1b004 --- /dev/null +++ b/resources/imageFiles/emojis/2800.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2801.svg b/resources/imageFiles/emojis/2801.svg new file mode 100644 index 0000000..f9644db --- /dev/null +++ b/resources/imageFiles/emojis/2801.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2802.svg b/resources/imageFiles/emojis/2802.svg new file mode 100644 index 0000000..0545c03 --- /dev/null +++ b/resources/imageFiles/emojis/2802.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2803.svg b/resources/imageFiles/emojis/2803.svg new file mode 100644 index 0000000..8813c16 --- /dev/null +++ b/resources/imageFiles/emojis/2803.svg @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2804.svg b/resources/imageFiles/emojis/2804.svg new file mode 100644 index 0000000..2974225 --- /dev/null +++ b/resources/imageFiles/emojis/2804.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2805.svg b/resources/imageFiles/emojis/2805.svg new file mode 100644 index 0000000..fecc84d --- /dev/null +++ b/resources/imageFiles/emojis/2805.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2806.svg b/resources/imageFiles/emojis/2806.svg new file mode 100644 index 0000000..1773fd1 --- /dev/null +++ b/resources/imageFiles/emojis/2806.svg @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2807.svg b/resources/imageFiles/emojis/2807.svg new file mode 100644 index 0000000..f867601 --- /dev/null +++ b/resources/imageFiles/emojis/2807.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2808.svg b/resources/imageFiles/emojis/2808.svg new file mode 100644 index 0000000..8ff1c53 --- /dev/null +++ b/resources/imageFiles/emojis/2808.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2809.svg b/resources/imageFiles/emojis/2809.svg new file mode 100644 index 0000000..002f315 --- /dev/null +++ b/resources/imageFiles/emojis/2809.svg @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2810.svg b/resources/imageFiles/emojis/2810.svg new file mode 100644 index 0000000..73ae7c6 --- /dev/null +++ b/resources/imageFiles/emojis/2810.svg @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2811.svg b/resources/imageFiles/emojis/2811.svg new file mode 100644 index 0000000..f3e39f3 --- /dev/null +++ b/resources/imageFiles/emojis/2811.svg @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2812.svg b/resources/imageFiles/emojis/2812.svg new file mode 100644 index 0000000..0c1c357 --- /dev/null +++ b/resources/imageFiles/emojis/2812.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2813.svg b/resources/imageFiles/emojis/2813.svg new file mode 100644 index 0000000..16eada2 --- /dev/null +++ b/resources/imageFiles/emojis/2813.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2814.svg b/resources/imageFiles/emojis/2814.svg new file mode 100644 index 0000000..8935216 --- /dev/null +++ b/resources/imageFiles/emojis/2814.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/resources/imageFiles/emojis/2815.svg b/resources/imageFiles/emojis/2815.svg new file mode 100644 index 0000000..8050776 --- /dev/null +++ b/resources/imageFiles/emojis/2815.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/resources/imageFiles/emojis/2816.svg b/resources/imageFiles/emojis/2816.svg new file mode 100644 index 0000000..77c0456 --- /dev/null +++ b/resources/imageFiles/emojis/2816.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2817.svg b/resources/imageFiles/emojis/2817.svg new file mode 100644 index 0000000..7a3640c --- /dev/null +++ b/resources/imageFiles/emojis/2817.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2818.svg b/resources/imageFiles/emojis/2818.svg new file mode 100644 index 0000000..4285772 --- /dev/null +++ b/resources/imageFiles/emojis/2818.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2819.svg b/resources/imageFiles/emojis/2819.svg new file mode 100644 index 0000000..bcb976e --- /dev/null +++ b/resources/imageFiles/emojis/2819.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2820.svg b/resources/imageFiles/emojis/2820.svg new file mode 100644 index 0000000..7626cce --- /dev/null +++ b/resources/imageFiles/emojis/2820.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2821.svg b/resources/imageFiles/emojis/2821.svg new file mode 100644 index 0000000..c5b1808 --- /dev/null +++ b/resources/imageFiles/emojis/2821.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2822.svg b/resources/imageFiles/emojis/2822.svg new file mode 100644 index 0000000..79ba4cf --- /dev/null +++ b/resources/imageFiles/emojis/2822.svg @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2823.svg b/resources/imageFiles/emojis/2823.svg new file mode 100644 index 0000000..f4847d9 --- /dev/null +++ b/resources/imageFiles/emojis/2823.svg @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2824.svg b/resources/imageFiles/emojis/2824.svg new file mode 100644 index 0000000..ae21c1c --- /dev/null +++ b/resources/imageFiles/emojis/2824.svg @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2825.svg b/resources/imageFiles/emojis/2825.svg new file mode 100644 index 0000000..366ae51 --- /dev/null +++ b/resources/imageFiles/emojis/2825.svg @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2826.svg b/resources/imageFiles/emojis/2826.svg new file mode 100644 index 0000000..403f682 --- /dev/null +++ b/resources/imageFiles/emojis/2826.svg @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2827.svg b/resources/imageFiles/emojis/2827.svg new file mode 100644 index 0000000..95d8d5c --- /dev/null +++ b/resources/imageFiles/emojis/2827.svg @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2828.svg b/resources/imageFiles/emojis/2828.svg new file mode 100644 index 0000000..79279fd --- /dev/null +++ b/resources/imageFiles/emojis/2828.svg @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2829.svg b/resources/imageFiles/emojis/2829.svg new file mode 100644 index 0000000..e6553e1 --- /dev/null +++ b/resources/imageFiles/emojis/2829.svg @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2830.svg b/resources/imageFiles/emojis/2830.svg new file mode 100644 index 0000000..a9856c1 --- /dev/null +++ b/resources/imageFiles/emojis/2830.svg @@ -0,0 +1,38 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2831.svg b/resources/imageFiles/emojis/2831.svg new file mode 100644 index 0000000..14af414 --- /dev/null +++ b/resources/imageFiles/emojis/2831.svg @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2832.svg b/resources/imageFiles/emojis/2832.svg new file mode 100644 index 0000000..a618d1c --- /dev/null +++ b/resources/imageFiles/emojis/2832.svg @@ -0,0 +1,35 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2833.svg b/resources/imageFiles/emojis/2833.svg new file mode 100644 index 0000000..d865eaf --- /dev/null +++ b/resources/imageFiles/emojis/2833.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2834.svg b/resources/imageFiles/emojis/2834.svg new file mode 100644 index 0000000..2fef1ec --- /dev/null +++ b/resources/imageFiles/emojis/2834.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/resources/imageFiles/emojis/2835.svg b/resources/imageFiles/emojis/2835.svg new file mode 100644 index 0000000..b01ef6e --- /dev/null +++ b/resources/imageFiles/emojis/2835.svg @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2836.svg b/resources/imageFiles/emojis/2836.svg new file mode 100644 index 0000000..7455e14 --- /dev/null +++ b/resources/imageFiles/emojis/2836.svg @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2837.svg b/resources/imageFiles/emojis/2837.svg new file mode 100644 index 0000000..1b4e541 --- /dev/null +++ b/resources/imageFiles/emojis/2837.svg @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2838.svg b/resources/imageFiles/emojis/2838.svg new file mode 100644 index 0000000..d87b2c3 --- /dev/null +++ b/resources/imageFiles/emojis/2838.svg @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2839.svg b/resources/imageFiles/emojis/2839.svg new file mode 100644 index 0000000..0332103 --- /dev/null +++ b/resources/imageFiles/emojis/2839.svg @@ -0,0 +1,54 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2840.svg b/resources/imageFiles/emojis/2840.svg new file mode 100644 index 0000000..fa651fe --- /dev/null +++ b/resources/imageFiles/emojis/2840.svg @@ -0,0 +1,55 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2841.svg b/resources/imageFiles/emojis/2841.svg new file mode 100644 index 0000000..063aa7a --- /dev/null +++ b/resources/imageFiles/emojis/2841.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2842.svg b/resources/imageFiles/emojis/2842.svg new file mode 100644 index 0000000..9407099 --- /dev/null +++ b/resources/imageFiles/emojis/2842.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2843.svg b/resources/imageFiles/emojis/2843.svg new file mode 100644 index 0000000..0e14fdb --- /dev/null +++ b/resources/imageFiles/emojis/2843.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2844.svg b/resources/imageFiles/emojis/2844.svg new file mode 100644 index 0000000..2807605 --- /dev/null +++ b/resources/imageFiles/emojis/2844.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2845.svg b/resources/imageFiles/emojis/2845.svg new file mode 100644 index 0000000..69885b1 --- /dev/null +++ b/resources/imageFiles/emojis/2845.svg @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2846.svg b/resources/imageFiles/emojis/2846.svg new file mode 100644 index 0000000..6324c78 --- /dev/null +++ b/resources/imageFiles/emojis/2846.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2847.svg b/resources/imageFiles/emojis/2847.svg new file mode 100644 index 0000000..894155c --- /dev/null +++ b/resources/imageFiles/emojis/2847.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2848.svg b/resources/imageFiles/emojis/2848.svg new file mode 100644 index 0000000..db9a72b --- /dev/null +++ b/resources/imageFiles/emojis/2848.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2849.svg b/resources/imageFiles/emojis/2849.svg new file mode 100644 index 0000000..60c4424 --- /dev/null +++ b/resources/imageFiles/emojis/2849.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/resources/imageFiles/emojis/2850.svg b/resources/imageFiles/emojis/2850.svg new file mode 100644 index 0000000..ae09d67 --- /dev/null +++ b/resources/imageFiles/emojis/2850.svg @@ -0,0 +1,39 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2851.svg b/resources/imageFiles/emojis/2851.svg new file mode 100644 index 0000000..25c3c20 --- /dev/null +++ b/resources/imageFiles/emojis/2851.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2852.svg b/resources/imageFiles/emojis/2852.svg new file mode 100644 index 0000000..efcec7f --- /dev/null +++ b/resources/imageFiles/emojis/2852.svg @@ -0,0 +1,50 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2853.svg b/resources/imageFiles/emojis/2853.svg new file mode 100644 index 0000000..5e47277 --- /dev/null +++ b/resources/imageFiles/emojis/2853.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2854.svg b/resources/imageFiles/emojis/2854.svg new file mode 100644 index 0000000..8b1b00c --- /dev/null +++ b/resources/imageFiles/emojis/2854.svg @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2855.svg b/resources/imageFiles/emojis/2855.svg new file mode 100644 index 0000000..335a101 --- /dev/null +++ b/resources/imageFiles/emojis/2855.svg @@ -0,0 +1,40 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2856.svg b/resources/imageFiles/emojis/2856.svg new file mode 100644 index 0000000..1c1d85e --- /dev/null +++ b/resources/imageFiles/emojis/2856.svg @@ -0,0 +1,47 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2857.svg b/resources/imageFiles/emojis/2857.svg new file mode 100644 index 0000000..5bb09ee --- /dev/null +++ b/resources/imageFiles/emojis/2857.svg @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2858.svg b/resources/imageFiles/emojis/2858.svg new file mode 100644 index 0000000..ea0913a --- /dev/null +++ b/resources/imageFiles/emojis/2858.svg @@ -0,0 +1,53 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2859.svg b/resources/imageFiles/emojis/2859.svg new file mode 100644 index 0000000..51596eb --- /dev/null +++ b/resources/imageFiles/emojis/2859.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2860.svg b/resources/imageFiles/emojis/2860.svg new file mode 100644 index 0000000..ca18b9f --- /dev/null +++ b/resources/imageFiles/emojis/2860.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2861.svg b/resources/imageFiles/emojis/2861.svg new file mode 100644 index 0000000..b37851d --- /dev/null +++ b/resources/imageFiles/emojis/2861.svg @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2862.svg b/resources/imageFiles/emojis/2862.svg new file mode 100644 index 0000000..a7641d3 --- /dev/null +++ b/resources/imageFiles/emojis/2862.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2863.svg b/resources/imageFiles/emojis/2863.svg new file mode 100644 index 0000000..1d66260 --- /dev/null +++ b/resources/imageFiles/emojis/2863.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2864.svg b/resources/imageFiles/emojis/2864.svg new file mode 100644 index 0000000..7a237dc --- /dev/null +++ b/resources/imageFiles/emojis/2864.svg @@ -0,0 +1,29 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2865.svg b/resources/imageFiles/emojis/2865.svg new file mode 100644 index 0000000..cde9b3f --- /dev/null +++ b/resources/imageFiles/emojis/2865.svg @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2866.svg b/resources/imageFiles/emojis/2866.svg new file mode 100644 index 0000000..4ec7faa --- /dev/null +++ b/resources/imageFiles/emojis/2866.svg @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2867.svg b/resources/imageFiles/emojis/2867.svg new file mode 100644 index 0000000..6117d5c --- /dev/null +++ b/resources/imageFiles/emojis/2867.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2868.svg b/resources/imageFiles/emojis/2868.svg new file mode 100644 index 0000000..d1bc4f7 --- /dev/null +++ b/resources/imageFiles/emojis/2868.svg @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2869.svg b/resources/imageFiles/emojis/2869.svg new file mode 100644 index 0000000..e98d441 --- /dev/null +++ b/resources/imageFiles/emojis/2869.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2870.svg b/resources/imageFiles/emojis/2870.svg new file mode 100644 index 0000000..ec4ef8f --- /dev/null +++ b/resources/imageFiles/emojis/2870.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2871.svg b/resources/imageFiles/emojis/2871.svg new file mode 100644 index 0000000..14dc29c --- /dev/null +++ b/resources/imageFiles/emojis/2871.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2872.svg b/resources/imageFiles/emojis/2872.svg new file mode 100644 index 0000000..8cea183 --- /dev/null +++ b/resources/imageFiles/emojis/2872.svg @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2873.svg b/resources/imageFiles/emojis/2873.svg new file mode 100644 index 0000000..89a34c2 --- /dev/null +++ b/resources/imageFiles/emojis/2873.svg @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2874.svg b/resources/imageFiles/emojis/2874.svg new file mode 100644 index 0000000..c796f8a --- /dev/null +++ b/resources/imageFiles/emojis/2874.svg @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2875.svg b/resources/imageFiles/emojis/2875.svg new file mode 100644 index 0000000..3ef7bd4 --- /dev/null +++ b/resources/imageFiles/emojis/2875.svg @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2876.svg b/resources/imageFiles/emojis/2876.svg new file mode 100644 index 0000000..e9d2d8b --- /dev/null +++ b/resources/imageFiles/emojis/2876.svg @@ -0,0 +1,29 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2877.svg b/resources/imageFiles/emojis/2877.svg new file mode 100644 index 0000000..cffeb3a --- /dev/null +++ b/resources/imageFiles/emojis/2877.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2878.svg b/resources/imageFiles/emojis/2878.svg new file mode 100644 index 0000000..f0ce230 --- /dev/null +++ b/resources/imageFiles/emojis/2878.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2879.svg b/resources/imageFiles/emojis/2879.svg new file mode 100644 index 0000000..2992515 --- /dev/null +++ b/resources/imageFiles/emojis/2879.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2880.svg b/resources/imageFiles/emojis/2880.svg new file mode 100644 index 0000000..be4f878 --- /dev/null +++ b/resources/imageFiles/emojis/2880.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2881.svg b/resources/imageFiles/emojis/2881.svg new file mode 100644 index 0000000..cca50a2 --- /dev/null +++ b/resources/imageFiles/emojis/2881.svg @@ -0,0 +1,39 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2882.svg b/resources/imageFiles/emojis/2882.svg new file mode 100644 index 0000000..cec0d21 --- /dev/null +++ b/resources/imageFiles/emojis/2882.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2883.svg b/resources/imageFiles/emojis/2883.svg new file mode 100644 index 0000000..aac7e35 --- /dev/null +++ b/resources/imageFiles/emojis/2883.svg @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2884.svg b/resources/imageFiles/emojis/2884.svg new file mode 100644 index 0000000..b05bf31 --- /dev/null +++ b/resources/imageFiles/emojis/2884.svg @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2885.svg b/resources/imageFiles/emojis/2885.svg new file mode 100644 index 0000000..f2cecbe --- /dev/null +++ b/resources/imageFiles/emojis/2885.svg @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2886.svg b/resources/imageFiles/emojis/2886.svg new file mode 100644 index 0000000..3a1ce20 --- /dev/null +++ b/resources/imageFiles/emojis/2886.svg @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2887.svg b/resources/imageFiles/emojis/2887.svg new file mode 100644 index 0000000..0d72bc3 --- /dev/null +++ b/resources/imageFiles/emojis/2887.svg @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2888.svg b/resources/imageFiles/emojis/2888.svg new file mode 100644 index 0000000..13f8e07 --- /dev/null +++ b/resources/imageFiles/emojis/2888.svg @@ -0,0 +1,53 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2889.svg b/resources/imageFiles/emojis/2889.svg new file mode 100644 index 0000000..47b7a16 --- /dev/null +++ b/resources/imageFiles/emojis/2889.svg @@ -0,0 +1,57 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2890.svg b/resources/imageFiles/emojis/2890.svg new file mode 100644 index 0000000..2029e18 --- /dev/null +++ b/resources/imageFiles/emojis/2890.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2891.svg b/resources/imageFiles/emojis/2891.svg new file mode 100644 index 0000000..797151d --- /dev/null +++ b/resources/imageFiles/emojis/2891.svg @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2892.svg b/resources/imageFiles/emojis/2892.svg new file mode 100644 index 0000000..863393d --- /dev/null +++ b/resources/imageFiles/emojis/2892.svg @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2893.svg b/resources/imageFiles/emojis/2893.svg new file mode 100644 index 0000000..e0055db --- /dev/null +++ b/resources/imageFiles/emojis/2893.svg @@ -0,0 +1,89 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2894.svg b/resources/imageFiles/emojis/2894.svg new file mode 100644 index 0000000..2c8f397 --- /dev/null +++ b/resources/imageFiles/emojis/2894.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2895.svg b/resources/imageFiles/emojis/2895.svg new file mode 100644 index 0000000..288b7b5 --- /dev/null +++ b/resources/imageFiles/emojis/2895.svg @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2896.svg b/resources/imageFiles/emojis/2896.svg new file mode 100644 index 0000000..846bb0e --- /dev/null +++ b/resources/imageFiles/emojis/2896.svg @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2897.svg b/resources/imageFiles/emojis/2897.svg new file mode 100644 index 0000000..d1a4e95 --- /dev/null +++ b/resources/imageFiles/emojis/2897.svg @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2898.svg b/resources/imageFiles/emojis/2898.svg new file mode 100644 index 0000000..f4b04de --- /dev/null +++ b/resources/imageFiles/emojis/2898.svg @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2899.svg b/resources/imageFiles/emojis/2899.svg new file mode 100644 index 0000000..0f64e19 --- /dev/null +++ b/resources/imageFiles/emojis/2899.svg @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2900.svg b/resources/imageFiles/emojis/2900.svg new file mode 100644 index 0000000..5ba7686 --- /dev/null +++ b/resources/imageFiles/emojis/2900.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2901.svg b/resources/imageFiles/emojis/2901.svg new file mode 100644 index 0000000..c8a434a --- /dev/null +++ b/resources/imageFiles/emojis/2901.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2902.svg b/resources/imageFiles/emojis/2902.svg new file mode 100644 index 0000000..4c4e4ca --- /dev/null +++ b/resources/imageFiles/emojis/2902.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2903.svg b/resources/imageFiles/emojis/2903.svg new file mode 100644 index 0000000..2ea14ca --- /dev/null +++ b/resources/imageFiles/emojis/2903.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2904.svg b/resources/imageFiles/emojis/2904.svg new file mode 100644 index 0000000..1163d33 --- /dev/null +++ b/resources/imageFiles/emojis/2904.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2905.svg b/resources/imageFiles/emojis/2905.svg new file mode 100644 index 0000000..3db7152 --- /dev/null +++ b/resources/imageFiles/emojis/2905.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2906.svg b/resources/imageFiles/emojis/2906.svg new file mode 100644 index 0000000..56eed43 --- /dev/null +++ b/resources/imageFiles/emojis/2906.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2907.svg b/resources/imageFiles/emojis/2907.svg new file mode 100644 index 0000000..417b741 --- /dev/null +++ b/resources/imageFiles/emojis/2907.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2908.svg b/resources/imageFiles/emojis/2908.svg new file mode 100644 index 0000000..32d0255 --- /dev/null +++ b/resources/imageFiles/emojis/2908.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2909.svg b/resources/imageFiles/emojis/2909.svg new file mode 100644 index 0000000..20e5558 --- /dev/null +++ b/resources/imageFiles/emojis/2909.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2910.svg b/resources/imageFiles/emojis/2910.svg new file mode 100644 index 0000000..e0d1311 --- /dev/null +++ b/resources/imageFiles/emojis/2910.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2911.svg b/resources/imageFiles/emojis/2911.svg new file mode 100644 index 0000000..54089d3 --- /dev/null +++ b/resources/imageFiles/emojis/2911.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2912.svg b/resources/imageFiles/emojis/2912.svg new file mode 100644 index 0000000..60a67ee --- /dev/null +++ b/resources/imageFiles/emojis/2912.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2913.svg b/resources/imageFiles/emojis/2913.svg new file mode 100644 index 0000000..22f27bf --- /dev/null +++ b/resources/imageFiles/emojis/2913.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2914.svg b/resources/imageFiles/emojis/2914.svg new file mode 100644 index 0000000..f7bfd7e --- /dev/null +++ b/resources/imageFiles/emojis/2914.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2915.svg b/resources/imageFiles/emojis/2915.svg new file mode 100644 index 0000000..5ea862f --- /dev/null +++ b/resources/imageFiles/emojis/2915.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2916.svg b/resources/imageFiles/emojis/2916.svg new file mode 100644 index 0000000..8a39eb3 --- /dev/null +++ b/resources/imageFiles/emojis/2916.svg @@ -0,0 +1,29 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2917.svg b/resources/imageFiles/emojis/2917.svg new file mode 100644 index 0000000..433225b --- /dev/null +++ b/resources/imageFiles/emojis/2917.svg @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2918.svg b/resources/imageFiles/emojis/2918.svg new file mode 100644 index 0000000..c776baa --- /dev/null +++ b/resources/imageFiles/emojis/2918.svg @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2919.svg b/resources/imageFiles/emojis/2919.svg new file mode 100644 index 0000000..54f8c62 --- /dev/null +++ b/resources/imageFiles/emojis/2919.svg @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2920.svg b/resources/imageFiles/emojis/2920.svg new file mode 100644 index 0000000..a0e21b8 --- /dev/null +++ b/resources/imageFiles/emojis/2920.svg @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2921.svg b/resources/imageFiles/emojis/2921.svg new file mode 100644 index 0000000..c912ccf --- /dev/null +++ b/resources/imageFiles/emojis/2921.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2922.svg b/resources/imageFiles/emojis/2922.svg new file mode 100644 index 0000000..89fe6b9 --- /dev/null +++ b/resources/imageFiles/emojis/2922.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2923.svg b/resources/imageFiles/emojis/2923.svg new file mode 100644 index 0000000..b65e5b7 --- /dev/null +++ b/resources/imageFiles/emojis/2923.svg @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2924.svg b/resources/imageFiles/emojis/2924.svg new file mode 100644 index 0000000..8d12a57 --- /dev/null +++ b/resources/imageFiles/emojis/2924.svg @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2925.svg b/resources/imageFiles/emojis/2925.svg new file mode 100644 index 0000000..6fbc2fd --- /dev/null +++ b/resources/imageFiles/emojis/2925.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2926.svg b/resources/imageFiles/emojis/2926.svg new file mode 100644 index 0000000..c0a2389 --- /dev/null +++ b/resources/imageFiles/emojis/2926.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2927.svg b/resources/imageFiles/emojis/2927.svg new file mode 100644 index 0000000..0135eb9 --- /dev/null +++ b/resources/imageFiles/emojis/2927.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2928.svg b/resources/imageFiles/emojis/2928.svg new file mode 100644 index 0000000..b09a29c --- /dev/null +++ b/resources/imageFiles/emojis/2928.svg @@ -0,0 +1,33 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2929.svg b/resources/imageFiles/emojis/2929.svg new file mode 100644 index 0000000..c4f43a9 --- /dev/null +++ b/resources/imageFiles/emojis/2929.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2930.svg b/resources/imageFiles/emojis/2930.svg new file mode 100644 index 0000000..b478a10 --- /dev/null +++ b/resources/imageFiles/emojis/2930.svg @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2931.svg b/resources/imageFiles/emojis/2931.svg new file mode 100644 index 0000000..df6ee4b --- /dev/null +++ b/resources/imageFiles/emojis/2931.svg @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2932.svg b/resources/imageFiles/emojis/2932.svg new file mode 100644 index 0000000..2589e7e --- /dev/null +++ b/resources/imageFiles/emojis/2932.svg @@ -0,0 +1,35 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2933.svg b/resources/imageFiles/emojis/2933.svg new file mode 100644 index 0000000..7612426 --- /dev/null +++ b/resources/imageFiles/emojis/2933.svg @@ -0,0 +1,35 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2934.svg b/resources/imageFiles/emojis/2934.svg new file mode 100644 index 0000000..3703345 --- /dev/null +++ b/resources/imageFiles/emojis/2934.svg @@ -0,0 +1,35 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2935.svg b/resources/imageFiles/emojis/2935.svg new file mode 100644 index 0000000..f55d4b3 --- /dev/null +++ b/resources/imageFiles/emojis/2935.svg @@ -0,0 +1,35 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2936.svg b/resources/imageFiles/emojis/2936.svg new file mode 100644 index 0000000..5b9624f --- /dev/null +++ b/resources/imageFiles/emojis/2936.svg @@ -0,0 +1,35 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2937.svg b/resources/imageFiles/emojis/2937.svg new file mode 100644 index 0000000..58a33b7 --- /dev/null +++ b/resources/imageFiles/emojis/2937.svg @@ -0,0 +1,35 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2938.svg b/resources/imageFiles/emojis/2938.svg new file mode 100644 index 0000000..4c2d15f --- /dev/null +++ b/resources/imageFiles/emojis/2938.svg @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2939.svg b/resources/imageFiles/emojis/2939.svg new file mode 100644 index 0000000..51c2232 --- /dev/null +++ b/resources/imageFiles/emojis/2939.svg @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2940.svg b/resources/imageFiles/emojis/2940.svg new file mode 100644 index 0000000..3dc266a --- /dev/null +++ b/resources/imageFiles/emojis/2940.svg @@ -0,0 +1,44 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2941.svg b/resources/imageFiles/emojis/2941.svg new file mode 100644 index 0000000..574f939 --- /dev/null +++ b/resources/imageFiles/emojis/2941.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2942.svg b/resources/imageFiles/emojis/2942.svg new file mode 100644 index 0000000..7e4d5a8 --- /dev/null +++ b/resources/imageFiles/emojis/2942.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2943.svg b/resources/imageFiles/emojis/2943.svg new file mode 100644 index 0000000..8dd3806 --- /dev/null +++ b/resources/imageFiles/emojis/2943.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2944.svg b/resources/imageFiles/emojis/2944.svg new file mode 100644 index 0000000..51eed21 --- /dev/null +++ b/resources/imageFiles/emojis/2944.svg @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2945.svg b/resources/imageFiles/emojis/2945.svg new file mode 100644 index 0000000..839e712 --- /dev/null +++ b/resources/imageFiles/emojis/2945.svg @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2946.svg b/resources/imageFiles/emojis/2946.svg new file mode 100644 index 0000000..da4be8f --- /dev/null +++ b/resources/imageFiles/emojis/2946.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2947.svg b/resources/imageFiles/emojis/2947.svg new file mode 100644 index 0000000..29e8246 --- /dev/null +++ b/resources/imageFiles/emojis/2947.svg @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2948.svg b/resources/imageFiles/emojis/2948.svg new file mode 100644 index 0000000..830a6d0 --- /dev/null +++ b/resources/imageFiles/emojis/2948.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2949.svg b/resources/imageFiles/emojis/2949.svg new file mode 100644 index 0000000..d9d135d --- /dev/null +++ b/resources/imageFiles/emojis/2949.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2950.svg b/resources/imageFiles/emojis/2950.svg new file mode 100644 index 0000000..69b1c33 --- /dev/null +++ b/resources/imageFiles/emojis/2950.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2951.svg b/resources/imageFiles/emojis/2951.svg new file mode 100644 index 0000000..e5be331 --- /dev/null +++ b/resources/imageFiles/emojis/2951.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2952.svg b/resources/imageFiles/emojis/2952.svg new file mode 100644 index 0000000..ad48057 --- /dev/null +++ b/resources/imageFiles/emojis/2952.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2953.svg b/resources/imageFiles/emojis/2953.svg new file mode 100644 index 0000000..08fef35 --- /dev/null +++ b/resources/imageFiles/emojis/2953.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2954.svg b/resources/imageFiles/emojis/2954.svg new file mode 100644 index 0000000..4efc9cd --- /dev/null +++ b/resources/imageFiles/emojis/2954.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2955.svg b/resources/imageFiles/emojis/2955.svg new file mode 100644 index 0000000..2f71c92 --- /dev/null +++ b/resources/imageFiles/emojis/2955.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2956.svg b/resources/imageFiles/emojis/2956.svg new file mode 100644 index 0000000..eec87c0 --- /dev/null +++ b/resources/imageFiles/emojis/2956.svg @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2957.svg b/resources/imageFiles/emojis/2957.svg new file mode 100644 index 0000000..5bc2c25 --- /dev/null +++ b/resources/imageFiles/emojis/2957.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2958.svg b/resources/imageFiles/emojis/2958.svg new file mode 100644 index 0000000..0f1d0ac --- /dev/null +++ b/resources/imageFiles/emojis/2958.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2959.svg b/resources/imageFiles/emojis/2959.svg new file mode 100644 index 0000000..839dc30 --- /dev/null +++ b/resources/imageFiles/emojis/2959.svg @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2960.svg b/resources/imageFiles/emojis/2960.svg new file mode 100644 index 0000000..5275bb8 --- /dev/null +++ b/resources/imageFiles/emojis/2960.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2961.svg b/resources/imageFiles/emojis/2961.svg new file mode 100644 index 0000000..63bd518 --- /dev/null +++ b/resources/imageFiles/emojis/2961.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2962.svg b/resources/imageFiles/emojis/2962.svg new file mode 100644 index 0000000..1634bd9 --- /dev/null +++ b/resources/imageFiles/emojis/2962.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2963.svg b/resources/imageFiles/emojis/2963.svg new file mode 100644 index 0000000..4fef353 --- /dev/null +++ b/resources/imageFiles/emojis/2963.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2964.svg b/resources/imageFiles/emojis/2964.svg new file mode 100644 index 0000000..a89893e --- /dev/null +++ b/resources/imageFiles/emojis/2964.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2965.svg b/resources/imageFiles/emojis/2965.svg new file mode 100644 index 0000000..7a54c1c --- /dev/null +++ b/resources/imageFiles/emojis/2965.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2966.svg b/resources/imageFiles/emojis/2966.svg new file mode 100644 index 0000000..901c010 --- /dev/null +++ b/resources/imageFiles/emojis/2966.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2967.svg b/resources/imageFiles/emojis/2967.svg new file mode 100644 index 0000000..7d87dd9 --- /dev/null +++ b/resources/imageFiles/emojis/2967.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2968.svg b/resources/imageFiles/emojis/2968.svg new file mode 100644 index 0000000..e9124c3 --- /dev/null +++ b/resources/imageFiles/emojis/2968.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2969.svg b/resources/imageFiles/emojis/2969.svg new file mode 100644 index 0000000..5a264ef --- /dev/null +++ b/resources/imageFiles/emojis/2969.svg @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2970.svg b/resources/imageFiles/emojis/2970.svg new file mode 100644 index 0000000..c0c08e6 --- /dev/null +++ b/resources/imageFiles/emojis/2970.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2971.svg b/resources/imageFiles/emojis/2971.svg new file mode 100644 index 0000000..21f0d3a --- /dev/null +++ b/resources/imageFiles/emojis/2971.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2972.svg b/resources/imageFiles/emojis/2972.svg new file mode 100644 index 0000000..9b9007b --- /dev/null +++ b/resources/imageFiles/emojis/2972.svg @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2973.svg b/resources/imageFiles/emojis/2973.svg new file mode 100644 index 0000000..41faedd --- /dev/null +++ b/resources/imageFiles/emojis/2973.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2974.svg b/resources/imageFiles/emojis/2974.svg new file mode 100644 index 0000000..d26021c --- /dev/null +++ b/resources/imageFiles/emojis/2974.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2975.svg b/resources/imageFiles/emojis/2975.svg new file mode 100644 index 0000000..3b980d2 --- /dev/null +++ b/resources/imageFiles/emojis/2975.svg @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2976.svg b/resources/imageFiles/emojis/2976.svg new file mode 100644 index 0000000..2d53842 --- /dev/null +++ b/resources/imageFiles/emojis/2976.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2977.svg b/resources/imageFiles/emojis/2977.svg new file mode 100644 index 0000000..02d472b --- /dev/null +++ b/resources/imageFiles/emojis/2977.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2978.svg b/resources/imageFiles/emojis/2978.svg new file mode 100644 index 0000000..d2fddcf --- /dev/null +++ b/resources/imageFiles/emojis/2978.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2979.svg b/resources/imageFiles/emojis/2979.svg new file mode 100644 index 0000000..081c83d --- /dev/null +++ b/resources/imageFiles/emojis/2979.svg @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2980.svg b/resources/imageFiles/emojis/2980.svg new file mode 100644 index 0000000..6dc5eb6 --- /dev/null +++ b/resources/imageFiles/emojis/2980.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2981.svg b/resources/imageFiles/emojis/2981.svg new file mode 100644 index 0000000..43e0713 --- /dev/null +++ b/resources/imageFiles/emojis/2981.svg @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2982.svg b/resources/imageFiles/emojis/2982.svg new file mode 100644 index 0000000..7cd5972 --- /dev/null +++ b/resources/imageFiles/emojis/2982.svg @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2983.svg b/resources/imageFiles/emojis/2983.svg new file mode 100644 index 0000000..d49bf0c --- /dev/null +++ b/resources/imageFiles/emojis/2983.svg @@ -0,0 +1,38 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2984.svg b/resources/imageFiles/emojis/2984.svg new file mode 100644 index 0000000..a670ccc --- /dev/null +++ b/resources/imageFiles/emojis/2984.svg @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2985.svg b/resources/imageFiles/emojis/2985.svg new file mode 100644 index 0000000..4889381 --- /dev/null +++ b/resources/imageFiles/emojis/2985.svg @@ -0,0 +1,17 @@ + + + d + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2986.svg b/resources/imageFiles/emojis/2986.svg new file mode 100644 index 0000000..bc36c34 --- /dev/null +++ b/resources/imageFiles/emojis/2986.svg @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2987.svg b/resources/imageFiles/emojis/2987.svg new file mode 100644 index 0000000..c99e78d --- /dev/null +++ b/resources/imageFiles/emojis/2987.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2988.svg b/resources/imageFiles/emojis/2988.svg new file mode 100644 index 0000000..d1e8780 --- /dev/null +++ b/resources/imageFiles/emojis/2988.svg @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2989.svg b/resources/imageFiles/emojis/2989.svg new file mode 100644 index 0000000..35d5e3c --- /dev/null +++ b/resources/imageFiles/emojis/2989.svg @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2990.svg b/resources/imageFiles/emojis/2990.svg new file mode 100644 index 0000000..08035eb --- /dev/null +++ b/resources/imageFiles/emojis/2990.svg @@ -0,0 +1,29 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2991.svg b/resources/imageFiles/emojis/2991.svg new file mode 100644 index 0000000..9b64548 --- /dev/null +++ b/resources/imageFiles/emojis/2991.svg @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2992.svg b/resources/imageFiles/emojis/2992.svg new file mode 100644 index 0000000..82315d4 --- /dev/null +++ b/resources/imageFiles/emojis/2992.svg @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2993.svg b/resources/imageFiles/emojis/2993.svg new file mode 100644 index 0000000..e35cb7c --- /dev/null +++ b/resources/imageFiles/emojis/2993.svg @@ -0,0 +1,35 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2994.svg b/resources/imageFiles/emojis/2994.svg new file mode 100644 index 0000000..7b49de0 --- /dev/null +++ b/resources/imageFiles/emojis/2994.svg @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2995.svg b/resources/imageFiles/emojis/2995.svg new file mode 100644 index 0000000..029988f --- /dev/null +++ b/resources/imageFiles/emojis/2995.svg @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2996.svg b/resources/imageFiles/emojis/2996.svg new file mode 100644 index 0000000..841ba9a --- /dev/null +++ b/resources/imageFiles/emojis/2996.svg @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2997.svg b/resources/imageFiles/emojis/2997.svg new file mode 100644 index 0000000..c730a20 --- /dev/null +++ b/resources/imageFiles/emojis/2997.svg @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2998.svg b/resources/imageFiles/emojis/2998.svg new file mode 100644 index 0000000..7778044 --- /dev/null +++ b/resources/imageFiles/emojis/2998.svg @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/2999.svg b/resources/imageFiles/emojis/2999.svg new file mode 100644 index 0000000..d678916 --- /dev/null +++ b/resources/imageFiles/emojis/2999.svg @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3000.svg b/resources/imageFiles/emojis/3000.svg new file mode 100644 index 0000000..6400679 --- /dev/null +++ b/resources/imageFiles/emojis/3000.svg @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3001.svg b/resources/imageFiles/emojis/3001.svg new file mode 100644 index 0000000..7add1a0 --- /dev/null +++ b/resources/imageFiles/emojis/3001.svg @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3002.svg b/resources/imageFiles/emojis/3002.svg new file mode 100644 index 0000000..733fdf3 --- /dev/null +++ b/resources/imageFiles/emojis/3002.svg @@ -0,0 +1,29 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3003.svg b/resources/imageFiles/emojis/3003.svg new file mode 100644 index 0000000..8d8c7a8 --- /dev/null +++ b/resources/imageFiles/emojis/3003.svg @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3004.svg b/resources/imageFiles/emojis/3004.svg new file mode 100644 index 0000000..f8ef6c2 --- /dev/null +++ b/resources/imageFiles/emojis/3004.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3005.svg b/resources/imageFiles/emojis/3005.svg new file mode 100644 index 0000000..09fcfa3 --- /dev/null +++ b/resources/imageFiles/emojis/3005.svg @@ -0,0 +1,64 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3006.svg b/resources/imageFiles/emojis/3006.svg new file mode 100644 index 0000000..e04788a --- /dev/null +++ b/resources/imageFiles/emojis/3006.svg @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3007.svg b/resources/imageFiles/emojis/3007.svg new file mode 100644 index 0000000..8b9b548 --- /dev/null +++ b/resources/imageFiles/emojis/3007.svg @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3008.svg b/resources/imageFiles/emojis/3008.svg new file mode 100644 index 0000000..b0042ee --- /dev/null +++ b/resources/imageFiles/emojis/3008.svg @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3009.svg b/resources/imageFiles/emojis/3009.svg new file mode 100644 index 0000000..6b4174d --- /dev/null +++ b/resources/imageFiles/emojis/3009.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3010.svg b/resources/imageFiles/emojis/3010.svg new file mode 100644 index 0000000..bfc9fba --- /dev/null +++ b/resources/imageFiles/emojis/3010.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3011.svg b/resources/imageFiles/emojis/3011.svg new file mode 100644 index 0000000..4822131 --- /dev/null +++ b/resources/imageFiles/emojis/3011.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3012.svg b/resources/imageFiles/emojis/3012.svg new file mode 100644 index 0000000..b2da36d --- /dev/null +++ b/resources/imageFiles/emojis/3012.svg @@ -0,0 +1,50 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3013.svg b/resources/imageFiles/emojis/3013.svg new file mode 100644 index 0000000..9660f24 --- /dev/null +++ b/resources/imageFiles/emojis/3013.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3014.svg b/resources/imageFiles/emojis/3014.svg new file mode 100644 index 0000000..b25a5b5 --- /dev/null +++ b/resources/imageFiles/emojis/3014.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3015.svg b/resources/imageFiles/emojis/3015.svg new file mode 100644 index 0000000..54eb8ec --- /dev/null +++ b/resources/imageFiles/emojis/3015.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3016.svg b/resources/imageFiles/emojis/3016.svg new file mode 100644 index 0000000..03451e0 --- /dev/null +++ b/resources/imageFiles/emojis/3016.svg @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3017.svg b/resources/imageFiles/emojis/3017.svg new file mode 100644 index 0000000..32547e2 --- /dev/null +++ b/resources/imageFiles/emojis/3017.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/resources/imageFiles/emojis/3018.svg b/resources/imageFiles/emojis/3018.svg new file mode 100644 index 0000000..f363a56 --- /dev/null +++ b/resources/imageFiles/emojis/3018.svg @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3019.svg b/resources/imageFiles/emojis/3019.svg new file mode 100644 index 0000000..2d42464 --- /dev/null +++ b/resources/imageFiles/emojis/3019.svg @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3020.svg b/resources/imageFiles/emojis/3020.svg new file mode 100644 index 0000000..54d70d1 --- /dev/null +++ b/resources/imageFiles/emojis/3020.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3021.svg b/resources/imageFiles/emojis/3021.svg new file mode 100644 index 0000000..e56908b --- /dev/null +++ b/resources/imageFiles/emojis/3021.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3022.svg b/resources/imageFiles/emojis/3022.svg new file mode 100644 index 0000000..0d03696 --- /dev/null +++ b/resources/imageFiles/emojis/3022.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3023.svg b/resources/imageFiles/emojis/3023.svg new file mode 100644 index 0000000..3413061 --- /dev/null +++ b/resources/imageFiles/emojis/3023.svg @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3024.svg b/resources/imageFiles/emojis/3024.svg new file mode 100644 index 0000000..457c67a --- /dev/null +++ b/resources/imageFiles/emojis/3024.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3025.svg b/resources/imageFiles/emojis/3025.svg new file mode 100644 index 0000000..7ffe6c6 --- /dev/null +++ b/resources/imageFiles/emojis/3025.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/resources/imageFiles/emojis/3026.svg b/resources/imageFiles/emojis/3026.svg new file mode 100644 index 0000000..5554a51 --- /dev/null +++ b/resources/imageFiles/emojis/3026.svg @@ -0,0 +1,49 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3027.svg b/resources/imageFiles/emojis/3027.svg new file mode 100644 index 0000000..5c619bc --- /dev/null +++ b/resources/imageFiles/emojis/3027.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3028.svg b/resources/imageFiles/emojis/3028.svg new file mode 100644 index 0000000..58614c6 --- /dev/null +++ b/resources/imageFiles/emojis/3028.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3029.svg b/resources/imageFiles/emojis/3029.svg new file mode 100644 index 0000000..c6fbade --- /dev/null +++ b/resources/imageFiles/emojis/3029.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3030.svg b/resources/imageFiles/emojis/3030.svg new file mode 100644 index 0000000..524b637 --- /dev/null +++ b/resources/imageFiles/emojis/3030.svg @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3031.svg b/resources/imageFiles/emojis/3031.svg new file mode 100644 index 0000000..c450f4b --- /dev/null +++ b/resources/imageFiles/emojis/3031.svg @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3032.svg b/resources/imageFiles/emojis/3032.svg new file mode 100644 index 0000000..5177e6c --- /dev/null +++ b/resources/imageFiles/emojis/3032.svg @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3033.svg b/resources/imageFiles/emojis/3033.svg new file mode 100644 index 0000000..41eb102 --- /dev/null +++ b/resources/imageFiles/emojis/3033.svg @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3034.svg b/resources/imageFiles/emojis/3034.svg new file mode 100644 index 0000000..f0702ba --- /dev/null +++ b/resources/imageFiles/emojis/3034.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3035.svg b/resources/imageFiles/emojis/3035.svg new file mode 100644 index 0000000..ee03ed0 --- /dev/null +++ b/resources/imageFiles/emojis/3035.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3036.svg b/resources/imageFiles/emojis/3036.svg new file mode 100644 index 0000000..cfd9149 --- /dev/null +++ b/resources/imageFiles/emojis/3036.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3037.svg b/resources/imageFiles/emojis/3037.svg new file mode 100644 index 0000000..324a88e --- /dev/null +++ b/resources/imageFiles/emojis/3037.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3038.svg b/resources/imageFiles/emojis/3038.svg new file mode 100644 index 0000000..3163238 --- /dev/null +++ b/resources/imageFiles/emojis/3038.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3039.svg b/resources/imageFiles/emojis/3039.svg new file mode 100644 index 0000000..d68c7f9 --- /dev/null +++ b/resources/imageFiles/emojis/3039.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3040.svg b/resources/imageFiles/emojis/3040.svg new file mode 100644 index 0000000..1f6da4d --- /dev/null +++ b/resources/imageFiles/emojis/3040.svg @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3041.svg b/resources/imageFiles/emojis/3041.svg new file mode 100644 index 0000000..54eb229 --- /dev/null +++ b/resources/imageFiles/emojis/3041.svg @@ -0,0 +1,50 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3042.svg b/resources/imageFiles/emojis/3042.svg new file mode 100644 index 0000000..4776576 --- /dev/null +++ b/resources/imageFiles/emojis/3042.svg @@ -0,0 +1,38 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3043.svg b/resources/imageFiles/emojis/3043.svg new file mode 100644 index 0000000..9e08fbf --- /dev/null +++ b/resources/imageFiles/emojis/3043.svg @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3044.svg b/resources/imageFiles/emojis/3044.svg new file mode 100644 index 0000000..2422e2c --- /dev/null +++ b/resources/imageFiles/emojis/3044.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3045.svg b/resources/imageFiles/emojis/3045.svg new file mode 100644 index 0000000..e919a27 --- /dev/null +++ b/resources/imageFiles/emojis/3045.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3046.svg b/resources/imageFiles/emojis/3046.svg new file mode 100644 index 0000000..3207b63 --- /dev/null +++ b/resources/imageFiles/emojis/3046.svg @@ -0,0 +1,42 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3047.svg b/resources/imageFiles/emojis/3047.svg new file mode 100644 index 0000000..3950d18 --- /dev/null +++ b/resources/imageFiles/emojis/3047.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3048.svg b/resources/imageFiles/emojis/3048.svg new file mode 100644 index 0000000..f7a7b30 --- /dev/null +++ b/resources/imageFiles/emojis/3048.svg @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3049.svg b/resources/imageFiles/emojis/3049.svg new file mode 100644 index 0000000..19bcadf --- /dev/null +++ b/resources/imageFiles/emojis/3049.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3050.svg b/resources/imageFiles/emojis/3050.svg new file mode 100644 index 0000000..22b4705 --- /dev/null +++ b/resources/imageFiles/emojis/3050.svg @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3051.svg b/resources/imageFiles/emojis/3051.svg new file mode 100644 index 0000000..e2dc9d1 --- /dev/null +++ b/resources/imageFiles/emojis/3051.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3052.svg b/resources/imageFiles/emojis/3052.svg new file mode 100644 index 0000000..0fde8f0 --- /dev/null +++ b/resources/imageFiles/emojis/3052.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3053.svg b/resources/imageFiles/emojis/3053.svg new file mode 100644 index 0000000..745268c --- /dev/null +++ b/resources/imageFiles/emojis/3053.svg @@ -0,0 +1,36 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3054.svg b/resources/imageFiles/emojis/3054.svg new file mode 100644 index 0000000..7c2d5be --- /dev/null +++ b/resources/imageFiles/emojis/3054.svg @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3055.svg b/resources/imageFiles/emojis/3055.svg new file mode 100644 index 0000000..5bfb75b --- /dev/null +++ b/resources/imageFiles/emojis/3055.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3056.svg b/resources/imageFiles/emojis/3056.svg new file mode 100644 index 0000000..3cd8afb --- /dev/null +++ b/resources/imageFiles/emojis/3056.svg @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3057.svg b/resources/imageFiles/emojis/3057.svg new file mode 100644 index 0000000..571bcb8 --- /dev/null +++ b/resources/imageFiles/emojis/3057.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3058.svg b/resources/imageFiles/emojis/3058.svg new file mode 100644 index 0000000..542bd2d --- /dev/null +++ b/resources/imageFiles/emojis/3058.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3059.svg b/resources/imageFiles/emojis/3059.svg new file mode 100644 index 0000000..95c7714 --- /dev/null +++ b/resources/imageFiles/emojis/3059.svg @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3060.svg b/resources/imageFiles/emojis/3060.svg new file mode 100644 index 0000000..e5d5b32 --- /dev/null +++ b/resources/imageFiles/emojis/3060.svg @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3061.svg b/resources/imageFiles/emojis/3061.svg new file mode 100644 index 0000000..6114b8f --- /dev/null +++ b/resources/imageFiles/emojis/3061.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3062.svg b/resources/imageFiles/emojis/3062.svg new file mode 100644 index 0000000..d9e9941 --- /dev/null +++ b/resources/imageFiles/emojis/3062.svg @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3063.svg b/resources/imageFiles/emojis/3063.svg new file mode 100644 index 0000000..3d21e63 --- /dev/null +++ b/resources/imageFiles/emojis/3063.svg @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3064.svg b/resources/imageFiles/emojis/3064.svg new file mode 100644 index 0000000..6e6d01f --- /dev/null +++ b/resources/imageFiles/emojis/3064.svg @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3065.svg b/resources/imageFiles/emojis/3065.svg new file mode 100644 index 0000000..5ae203e --- /dev/null +++ b/resources/imageFiles/emojis/3065.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3066.svg b/resources/imageFiles/emojis/3066.svg new file mode 100644 index 0000000..552bb3b --- /dev/null +++ b/resources/imageFiles/emojis/3066.svg @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3067.svg b/resources/imageFiles/emojis/3067.svg new file mode 100644 index 0000000..9e2eee9 --- /dev/null +++ b/resources/imageFiles/emojis/3067.svg @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3068.svg b/resources/imageFiles/emojis/3068.svg new file mode 100644 index 0000000..311adf2 --- /dev/null +++ b/resources/imageFiles/emojis/3068.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3069.svg b/resources/imageFiles/emojis/3069.svg new file mode 100644 index 0000000..2cdc062 --- /dev/null +++ b/resources/imageFiles/emojis/3069.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3070.svg b/resources/imageFiles/emojis/3070.svg new file mode 100644 index 0000000..14fbdeb --- /dev/null +++ b/resources/imageFiles/emojis/3070.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3071.svg b/resources/imageFiles/emojis/3071.svg new file mode 100644 index 0000000..4abb0b3 --- /dev/null +++ b/resources/imageFiles/emojis/3071.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3072.svg b/resources/imageFiles/emojis/3072.svg new file mode 100644 index 0000000..3688850 --- /dev/null +++ b/resources/imageFiles/emojis/3072.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3073.svg b/resources/imageFiles/emojis/3073.svg new file mode 100644 index 0000000..897d48e --- /dev/null +++ b/resources/imageFiles/emojis/3073.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3074.svg b/resources/imageFiles/emojis/3074.svg new file mode 100644 index 0000000..b0fa7d5 --- /dev/null +++ b/resources/imageFiles/emojis/3074.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3075.svg b/resources/imageFiles/emojis/3075.svg new file mode 100644 index 0000000..38b4f3a --- /dev/null +++ b/resources/imageFiles/emojis/3075.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3076.svg b/resources/imageFiles/emojis/3076.svg new file mode 100644 index 0000000..adb3b72 --- /dev/null +++ b/resources/imageFiles/emojis/3076.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3077.svg b/resources/imageFiles/emojis/3077.svg new file mode 100644 index 0000000..7d5a7d9 --- /dev/null +++ b/resources/imageFiles/emojis/3077.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3078.svg b/resources/imageFiles/emojis/3078.svg new file mode 100644 index 0000000..a22a6d0 --- /dev/null +++ b/resources/imageFiles/emojis/3078.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3079.svg b/resources/imageFiles/emojis/3079.svg new file mode 100644 index 0000000..ac648cb --- /dev/null +++ b/resources/imageFiles/emojis/3079.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3080.svg b/resources/imageFiles/emojis/3080.svg new file mode 100644 index 0000000..40e0933 --- /dev/null +++ b/resources/imageFiles/emojis/3080.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3081.svg b/resources/imageFiles/emojis/3081.svg new file mode 100644 index 0000000..62ef433 --- /dev/null +++ b/resources/imageFiles/emojis/3081.svg @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3082.svg b/resources/imageFiles/emojis/3082.svg new file mode 100644 index 0000000..ea322d4 --- /dev/null +++ b/resources/imageFiles/emojis/3082.svg @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3083.svg b/resources/imageFiles/emojis/3083.svg new file mode 100644 index 0000000..e9ad217 --- /dev/null +++ b/resources/imageFiles/emojis/3083.svg @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3084.svg b/resources/imageFiles/emojis/3084.svg new file mode 100644 index 0000000..4416618 --- /dev/null +++ b/resources/imageFiles/emojis/3084.svg @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3085.svg b/resources/imageFiles/emojis/3085.svg new file mode 100644 index 0000000..ffe38ec --- /dev/null +++ b/resources/imageFiles/emojis/3085.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3086.svg b/resources/imageFiles/emojis/3086.svg new file mode 100644 index 0000000..39f3926 --- /dev/null +++ b/resources/imageFiles/emojis/3086.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3087.svg b/resources/imageFiles/emojis/3087.svg new file mode 100644 index 0000000..a8c7ef6 --- /dev/null +++ b/resources/imageFiles/emojis/3087.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/resources/imageFiles/emojis/3088.svg b/resources/imageFiles/emojis/3088.svg new file mode 100644 index 0000000..e85e2b5 --- /dev/null +++ b/resources/imageFiles/emojis/3088.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/resources/imageFiles/emojis/3089.svg b/resources/imageFiles/emojis/3089.svg new file mode 100644 index 0000000..4b12d56 --- /dev/null +++ b/resources/imageFiles/emojis/3089.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/resources/imageFiles/emojis/3090.svg b/resources/imageFiles/emojis/3090.svg new file mode 100644 index 0000000..ecea040 --- /dev/null +++ b/resources/imageFiles/emojis/3090.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3091.svg b/resources/imageFiles/emojis/3091.svg new file mode 100644 index 0000000..f133bd2 --- /dev/null +++ b/resources/imageFiles/emojis/3091.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3092.svg b/resources/imageFiles/emojis/3092.svg new file mode 100644 index 0000000..a6bf650 --- /dev/null +++ b/resources/imageFiles/emojis/3092.svg @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3093.svg b/resources/imageFiles/emojis/3093.svg new file mode 100644 index 0000000..ae314e9 --- /dev/null +++ b/resources/imageFiles/emojis/3093.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3094.svg b/resources/imageFiles/emojis/3094.svg new file mode 100644 index 0000000..9bf7624 --- /dev/null +++ b/resources/imageFiles/emojis/3094.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3095.svg b/resources/imageFiles/emojis/3095.svg new file mode 100644 index 0000000..9f76a33 --- /dev/null +++ b/resources/imageFiles/emojis/3095.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3096.svg b/resources/imageFiles/emojis/3096.svg new file mode 100644 index 0000000..9f9e193 --- /dev/null +++ b/resources/imageFiles/emojis/3096.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3097.svg b/resources/imageFiles/emojis/3097.svg new file mode 100644 index 0000000..8f946fa --- /dev/null +++ b/resources/imageFiles/emojis/3097.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3098.svg b/resources/imageFiles/emojis/3098.svg new file mode 100644 index 0000000..7a55b8a --- /dev/null +++ b/resources/imageFiles/emojis/3098.svg @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3099.svg b/resources/imageFiles/emojis/3099.svg new file mode 100644 index 0000000..d8b228f --- /dev/null +++ b/resources/imageFiles/emojis/3099.svg @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3100.svg b/resources/imageFiles/emojis/3100.svg new file mode 100644 index 0000000..9a375b9 --- /dev/null +++ b/resources/imageFiles/emojis/3100.svg @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3101.svg b/resources/imageFiles/emojis/3101.svg new file mode 100644 index 0000000..5274e66 --- /dev/null +++ b/resources/imageFiles/emojis/3101.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3102.svg b/resources/imageFiles/emojis/3102.svg new file mode 100644 index 0000000..7fe13cd --- /dev/null +++ b/resources/imageFiles/emojis/3102.svg @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3103.svg b/resources/imageFiles/emojis/3103.svg new file mode 100644 index 0000000..20d45c3 --- /dev/null +++ b/resources/imageFiles/emojis/3103.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3104.svg b/resources/imageFiles/emojis/3104.svg new file mode 100644 index 0000000..77127fa --- /dev/null +++ b/resources/imageFiles/emojis/3104.svg @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3105.svg b/resources/imageFiles/emojis/3105.svg new file mode 100644 index 0000000..4884c47 --- /dev/null +++ b/resources/imageFiles/emojis/3105.svg @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3106.svg b/resources/imageFiles/emojis/3106.svg new file mode 100644 index 0000000..4dea8af --- /dev/null +++ b/resources/imageFiles/emojis/3106.svg @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3107.svg b/resources/imageFiles/emojis/3107.svg new file mode 100644 index 0000000..e364d10 --- /dev/null +++ b/resources/imageFiles/emojis/3107.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3108.svg b/resources/imageFiles/emojis/3108.svg new file mode 100644 index 0000000..c4f8e5e --- /dev/null +++ b/resources/imageFiles/emojis/3108.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3109.svg b/resources/imageFiles/emojis/3109.svg new file mode 100644 index 0000000..91af10c --- /dev/null +++ b/resources/imageFiles/emojis/3109.svg @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3110.svg b/resources/imageFiles/emojis/3110.svg new file mode 100644 index 0000000..0cc0d85 --- /dev/null +++ b/resources/imageFiles/emojis/3110.svg @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3111.svg b/resources/imageFiles/emojis/3111.svg new file mode 100644 index 0000000..34ec1b0 --- /dev/null +++ b/resources/imageFiles/emojis/3111.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3112.svg b/resources/imageFiles/emojis/3112.svg new file mode 100644 index 0000000..fa7ae71 --- /dev/null +++ b/resources/imageFiles/emojis/3112.svg @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3113.svg b/resources/imageFiles/emojis/3113.svg new file mode 100644 index 0000000..e949f0f --- /dev/null +++ b/resources/imageFiles/emojis/3113.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3114.svg b/resources/imageFiles/emojis/3114.svg new file mode 100644 index 0000000..fc98324 --- /dev/null +++ b/resources/imageFiles/emojis/3114.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3115.svg b/resources/imageFiles/emojis/3115.svg new file mode 100644 index 0000000..3ab077f --- /dev/null +++ b/resources/imageFiles/emojis/3115.svg @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3116.svg b/resources/imageFiles/emojis/3116.svg new file mode 100644 index 0000000..1dbb549 --- /dev/null +++ b/resources/imageFiles/emojis/3116.svg @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3117.svg b/resources/imageFiles/emojis/3117.svg new file mode 100644 index 0000000..acde5f3 --- /dev/null +++ b/resources/imageFiles/emojis/3117.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3118.svg b/resources/imageFiles/emojis/3118.svg new file mode 100644 index 0000000..a8bdbea --- /dev/null +++ b/resources/imageFiles/emojis/3118.svg @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3119.svg b/resources/imageFiles/emojis/3119.svg new file mode 100644 index 0000000..2d19ce3 --- /dev/null +++ b/resources/imageFiles/emojis/3119.svg @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3120.svg b/resources/imageFiles/emojis/3120.svg new file mode 100644 index 0000000..e38504e --- /dev/null +++ b/resources/imageFiles/emojis/3120.svg @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3121.svg b/resources/imageFiles/emojis/3121.svg new file mode 100644 index 0000000..a7c5761 --- /dev/null +++ b/resources/imageFiles/emojis/3121.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3122.svg b/resources/imageFiles/emojis/3122.svg new file mode 100644 index 0000000..5b938ef --- /dev/null +++ b/resources/imageFiles/emojis/3122.svg @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3123.svg b/resources/imageFiles/emojis/3123.svg new file mode 100644 index 0000000..70429c7 --- /dev/null +++ b/resources/imageFiles/emojis/3123.svg @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3124.svg b/resources/imageFiles/emojis/3124.svg new file mode 100644 index 0000000..55852ff --- /dev/null +++ b/resources/imageFiles/emojis/3124.svg @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3125.svg b/resources/imageFiles/emojis/3125.svg new file mode 100644 index 0000000..302d7b4 --- /dev/null +++ b/resources/imageFiles/emojis/3125.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3126.svg b/resources/imageFiles/emojis/3126.svg new file mode 100644 index 0000000..0ebefa2 --- /dev/null +++ b/resources/imageFiles/emojis/3126.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3127.svg b/resources/imageFiles/emojis/3127.svg new file mode 100644 index 0000000..660cbfb --- /dev/null +++ b/resources/imageFiles/emojis/3127.svg @@ -0,0 +1,46 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3128.svg b/resources/imageFiles/emojis/3128.svg new file mode 100644 index 0000000..a408a7f --- /dev/null +++ b/resources/imageFiles/emojis/3128.svg @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3129.svg b/resources/imageFiles/emojis/3129.svg new file mode 100644 index 0000000..d8822cc --- /dev/null +++ b/resources/imageFiles/emojis/3129.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3130.svg b/resources/imageFiles/emojis/3130.svg new file mode 100644 index 0000000..9efb503 --- /dev/null +++ b/resources/imageFiles/emojis/3130.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3131.svg b/resources/imageFiles/emojis/3131.svg new file mode 100644 index 0000000..3f85bd5 --- /dev/null +++ b/resources/imageFiles/emojis/3131.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3132.svg b/resources/imageFiles/emojis/3132.svg new file mode 100644 index 0000000..a39d5de --- /dev/null +++ b/resources/imageFiles/emojis/3132.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/resources/imageFiles/emojis/3133.svg b/resources/imageFiles/emojis/3133.svg new file mode 100644 index 0000000..ad03cb1 --- /dev/null +++ b/resources/imageFiles/emojis/3133.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3134.svg b/resources/imageFiles/emojis/3134.svg new file mode 100644 index 0000000..6e00dd5 --- /dev/null +++ b/resources/imageFiles/emojis/3134.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3135.svg b/resources/imageFiles/emojis/3135.svg new file mode 100644 index 0000000..4b60f90 --- /dev/null +++ b/resources/imageFiles/emojis/3135.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3136.svg b/resources/imageFiles/emojis/3136.svg new file mode 100644 index 0000000..ed55a7a --- /dev/null +++ b/resources/imageFiles/emojis/3136.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3137.svg b/resources/imageFiles/emojis/3137.svg new file mode 100644 index 0000000..e1c3ca8 --- /dev/null +++ b/resources/imageFiles/emojis/3137.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3138.svg b/resources/imageFiles/emojis/3138.svg new file mode 100644 index 0000000..aac0adf --- /dev/null +++ b/resources/imageFiles/emojis/3138.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3139.svg b/resources/imageFiles/emojis/3139.svg new file mode 100644 index 0000000..d418854 --- /dev/null +++ b/resources/imageFiles/emojis/3139.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3140.svg b/resources/imageFiles/emojis/3140.svg new file mode 100644 index 0000000..4ec691e --- /dev/null +++ b/resources/imageFiles/emojis/3140.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/resources/imageFiles/emojis/3141.svg b/resources/imageFiles/emojis/3141.svg new file mode 100644 index 0000000..fe1e254 --- /dev/null +++ b/resources/imageFiles/emojis/3141.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/resources/imageFiles/emojis/3142.svg b/resources/imageFiles/emojis/3142.svg new file mode 100644 index 0000000..6a9e954 --- /dev/null +++ b/resources/imageFiles/emojis/3142.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3143.svg b/resources/imageFiles/emojis/3143.svg new file mode 100644 index 0000000..b1b3676 --- /dev/null +++ b/resources/imageFiles/emojis/3143.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3144.svg b/resources/imageFiles/emojis/3144.svg new file mode 100644 index 0000000..582219c --- /dev/null +++ b/resources/imageFiles/emojis/3144.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3145.svg b/resources/imageFiles/emojis/3145.svg new file mode 100644 index 0000000..2410202 --- /dev/null +++ b/resources/imageFiles/emojis/3145.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/resources/imageFiles/emojis/3146.svg b/resources/imageFiles/emojis/3146.svg new file mode 100644 index 0000000..3257a26 --- /dev/null +++ b/resources/imageFiles/emojis/3146.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3147.svg b/resources/imageFiles/emojis/3147.svg new file mode 100644 index 0000000..cbe82df --- /dev/null +++ b/resources/imageFiles/emojis/3147.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3148.svg b/resources/imageFiles/emojis/3148.svg new file mode 100644 index 0000000..34131fd --- /dev/null +++ b/resources/imageFiles/emojis/3148.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3149.svg b/resources/imageFiles/emojis/3149.svg new file mode 100644 index 0000000..4075956 --- /dev/null +++ b/resources/imageFiles/emojis/3149.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/resources/imageFiles/emojis/3150.svg b/resources/imageFiles/emojis/3150.svg new file mode 100644 index 0000000..05be06a --- /dev/null +++ b/resources/imageFiles/emojis/3150.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/resources/imageFiles/emojis/3151.svg b/resources/imageFiles/emojis/3151.svg new file mode 100644 index 0000000..70fc29a --- /dev/null +++ b/resources/imageFiles/emojis/3151.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3152.svg b/resources/imageFiles/emojis/3152.svg new file mode 100644 index 0000000..56a5e3d --- /dev/null +++ b/resources/imageFiles/emojis/3152.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3153.svg b/resources/imageFiles/emojis/3153.svg new file mode 100644 index 0000000..979ee8e --- /dev/null +++ b/resources/imageFiles/emojis/3153.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3154.svg b/resources/imageFiles/emojis/3154.svg new file mode 100644 index 0000000..30bc220 --- /dev/null +++ b/resources/imageFiles/emojis/3154.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3155.svg b/resources/imageFiles/emojis/3155.svg new file mode 100644 index 0000000..8413021 --- /dev/null +++ b/resources/imageFiles/emojis/3155.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3156.svg b/resources/imageFiles/emojis/3156.svg new file mode 100644 index 0000000..5385bb3 --- /dev/null +++ b/resources/imageFiles/emojis/3156.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3157.svg b/resources/imageFiles/emojis/3157.svg new file mode 100644 index 0000000..dd26ebd --- /dev/null +++ b/resources/imageFiles/emojis/3157.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/resources/imageFiles/emojis/3158.svg b/resources/imageFiles/emojis/3158.svg new file mode 100644 index 0000000..5e00eba --- /dev/null +++ b/resources/imageFiles/emojis/3158.svg @@ -0,0 +1,51 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3159.svg b/resources/imageFiles/emojis/3159.svg new file mode 100644 index 0000000..d875b61 --- /dev/null +++ b/resources/imageFiles/emojis/3159.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3160.svg b/resources/imageFiles/emojis/3160.svg new file mode 100644 index 0000000..a9ca8a0 --- /dev/null +++ b/resources/imageFiles/emojis/3160.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3161.svg b/resources/imageFiles/emojis/3161.svg new file mode 100644 index 0000000..a68914e --- /dev/null +++ b/resources/imageFiles/emojis/3161.svg @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3162.svg b/resources/imageFiles/emojis/3162.svg new file mode 100644 index 0000000..6102f10 --- /dev/null +++ b/resources/imageFiles/emojis/3162.svg @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3163.svg b/resources/imageFiles/emojis/3163.svg new file mode 100644 index 0000000..c542760 --- /dev/null +++ b/resources/imageFiles/emojis/3163.svg @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3164.svg b/resources/imageFiles/emojis/3164.svg new file mode 100644 index 0000000..a10713b --- /dev/null +++ b/resources/imageFiles/emojis/3164.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3165.svg b/resources/imageFiles/emojis/3165.svg new file mode 100644 index 0000000..4bfe431 --- /dev/null +++ b/resources/imageFiles/emojis/3165.svg @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3166.svg b/resources/imageFiles/emojis/3166.svg new file mode 100644 index 0000000..bdf2d2e --- /dev/null +++ b/resources/imageFiles/emojis/3166.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3167.svg b/resources/imageFiles/emojis/3167.svg new file mode 100644 index 0000000..2c5910a --- /dev/null +++ b/resources/imageFiles/emojis/3167.svg @@ -0,0 +1,29 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3168.svg b/resources/imageFiles/emojis/3168.svg new file mode 100644 index 0000000..618f095 --- /dev/null +++ b/resources/imageFiles/emojis/3168.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3169.svg b/resources/imageFiles/emojis/3169.svg new file mode 100644 index 0000000..27e750e --- /dev/null +++ b/resources/imageFiles/emojis/3169.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3170.svg b/resources/imageFiles/emojis/3170.svg new file mode 100644 index 0000000..4cffe08 --- /dev/null +++ b/resources/imageFiles/emojis/3170.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3171.svg b/resources/imageFiles/emojis/3171.svg new file mode 100644 index 0000000..cac4e05 --- /dev/null +++ b/resources/imageFiles/emojis/3171.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3172.svg b/resources/imageFiles/emojis/3172.svg new file mode 100644 index 0000000..22d84c8 --- /dev/null +++ b/resources/imageFiles/emojis/3172.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/resources/imageFiles/emojis/3173.svg b/resources/imageFiles/emojis/3173.svg new file mode 100644 index 0000000..c63410a --- /dev/null +++ b/resources/imageFiles/emojis/3173.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3174.svg b/resources/imageFiles/emojis/3174.svg new file mode 100644 index 0000000..e2b8e1d --- /dev/null +++ b/resources/imageFiles/emojis/3174.svg @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3175.svg b/resources/imageFiles/emojis/3175.svg new file mode 100644 index 0000000..4c1b219 --- /dev/null +++ b/resources/imageFiles/emojis/3175.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3176.svg b/resources/imageFiles/emojis/3176.svg new file mode 100644 index 0000000..0b86fb4 --- /dev/null +++ b/resources/imageFiles/emojis/3176.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3177.svg b/resources/imageFiles/emojis/3177.svg new file mode 100644 index 0000000..aeb89f2 --- /dev/null +++ b/resources/imageFiles/emojis/3177.svg @@ -0,0 +1,36 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3178.svg b/resources/imageFiles/emojis/3178.svg new file mode 100644 index 0000000..6d06265 --- /dev/null +++ b/resources/imageFiles/emojis/3178.svg @@ -0,0 +1,32 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3179.svg b/resources/imageFiles/emojis/3179.svg new file mode 100644 index 0000000..8ffe8c3 --- /dev/null +++ b/resources/imageFiles/emojis/3179.svg @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3180.svg b/resources/imageFiles/emojis/3180.svg new file mode 100644 index 0000000..8c3c4b9 --- /dev/null +++ b/resources/imageFiles/emojis/3180.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3181.svg b/resources/imageFiles/emojis/3181.svg new file mode 100644 index 0000000..294caa8 --- /dev/null +++ b/resources/imageFiles/emojis/3181.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3182.svg b/resources/imageFiles/emojis/3182.svg new file mode 100644 index 0000000..c67015d --- /dev/null +++ b/resources/imageFiles/emojis/3182.svg @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3183.svg b/resources/imageFiles/emojis/3183.svg new file mode 100644 index 0000000..8da210f --- /dev/null +++ b/resources/imageFiles/emojis/3183.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3184.svg b/resources/imageFiles/emojis/3184.svg new file mode 100644 index 0000000..f51dcee --- /dev/null +++ b/resources/imageFiles/emojis/3184.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3185.svg b/resources/imageFiles/emojis/3185.svg new file mode 100644 index 0000000..16edc63 --- /dev/null +++ b/resources/imageFiles/emojis/3185.svg @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3186.svg b/resources/imageFiles/emojis/3186.svg new file mode 100644 index 0000000..ee6238a --- /dev/null +++ b/resources/imageFiles/emojis/3186.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3187.svg b/resources/imageFiles/emojis/3187.svg new file mode 100644 index 0000000..631b6ab --- /dev/null +++ b/resources/imageFiles/emojis/3187.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/resources/imageFiles/emojis/3188.svg b/resources/imageFiles/emojis/3188.svg new file mode 100644 index 0000000..a29bc8e --- /dev/null +++ b/resources/imageFiles/emojis/3188.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3189.svg b/resources/imageFiles/emojis/3189.svg new file mode 100644 index 0000000..92ec997 --- /dev/null +++ b/resources/imageFiles/emojis/3189.svg @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3190.svg b/resources/imageFiles/emojis/3190.svg new file mode 100644 index 0000000..f7af862 --- /dev/null +++ b/resources/imageFiles/emojis/3190.svg @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3191.svg b/resources/imageFiles/emojis/3191.svg new file mode 100644 index 0000000..3a575a0 --- /dev/null +++ b/resources/imageFiles/emojis/3191.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3192.svg b/resources/imageFiles/emojis/3192.svg new file mode 100644 index 0000000..9ee75dd --- /dev/null +++ b/resources/imageFiles/emojis/3192.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3193.svg b/resources/imageFiles/emojis/3193.svg new file mode 100644 index 0000000..f719fac --- /dev/null +++ b/resources/imageFiles/emojis/3193.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3194.svg b/resources/imageFiles/emojis/3194.svg new file mode 100644 index 0000000..e4edc2c --- /dev/null +++ b/resources/imageFiles/emojis/3194.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3195.svg b/resources/imageFiles/emojis/3195.svg new file mode 100644 index 0000000..9585b05 --- /dev/null +++ b/resources/imageFiles/emojis/3195.svg @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3196.svg b/resources/imageFiles/emojis/3196.svg new file mode 100644 index 0000000..cc5d442 --- /dev/null +++ b/resources/imageFiles/emojis/3196.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3197.svg b/resources/imageFiles/emojis/3197.svg new file mode 100644 index 0000000..3e9de29 --- /dev/null +++ b/resources/imageFiles/emojis/3197.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3198.svg b/resources/imageFiles/emojis/3198.svg new file mode 100644 index 0000000..233d432 --- /dev/null +++ b/resources/imageFiles/emojis/3198.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3199.svg b/resources/imageFiles/emojis/3199.svg new file mode 100644 index 0000000..0d93791 --- /dev/null +++ b/resources/imageFiles/emojis/3199.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3200.svg b/resources/imageFiles/emojis/3200.svg new file mode 100644 index 0000000..1627a1f --- /dev/null +++ b/resources/imageFiles/emojis/3200.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3201.svg b/resources/imageFiles/emojis/3201.svg new file mode 100644 index 0000000..c531b20 --- /dev/null +++ b/resources/imageFiles/emojis/3201.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3202.svg b/resources/imageFiles/emojis/3202.svg new file mode 100644 index 0000000..65ed712 --- /dev/null +++ b/resources/imageFiles/emojis/3202.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3203.svg b/resources/imageFiles/emojis/3203.svg new file mode 100644 index 0000000..595b60a --- /dev/null +++ b/resources/imageFiles/emojis/3203.svg @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3204.svg b/resources/imageFiles/emojis/3204.svg new file mode 100644 index 0000000..9b1fa3a --- /dev/null +++ b/resources/imageFiles/emojis/3204.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3205.svg b/resources/imageFiles/emojis/3205.svg new file mode 100644 index 0000000..5a24477 --- /dev/null +++ b/resources/imageFiles/emojis/3205.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3206.svg b/resources/imageFiles/emojis/3206.svg new file mode 100644 index 0000000..6411e3d --- /dev/null +++ b/resources/imageFiles/emojis/3206.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3207.svg b/resources/imageFiles/emojis/3207.svg new file mode 100644 index 0000000..b09afed --- /dev/null +++ b/resources/imageFiles/emojis/3207.svg @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3208.svg b/resources/imageFiles/emojis/3208.svg new file mode 100644 index 0000000..d12478f --- /dev/null +++ b/resources/imageFiles/emojis/3208.svg @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3209.svg b/resources/imageFiles/emojis/3209.svg new file mode 100644 index 0000000..a74e176 --- /dev/null +++ b/resources/imageFiles/emojis/3209.svg @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3210.svg b/resources/imageFiles/emojis/3210.svg new file mode 100644 index 0000000..ef968f1 --- /dev/null +++ b/resources/imageFiles/emojis/3210.svg @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3211.svg b/resources/imageFiles/emojis/3211.svg new file mode 100644 index 0000000..7092278 --- /dev/null +++ b/resources/imageFiles/emojis/3211.svg @@ -0,0 +1,35 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3212.svg b/resources/imageFiles/emojis/3212.svg new file mode 100644 index 0000000..01ac198 --- /dev/null +++ b/resources/imageFiles/emojis/3212.svg @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3213.svg b/resources/imageFiles/emojis/3213.svg new file mode 100644 index 0000000..b996119 --- /dev/null +++ b/resources/imageFiles/emojis/3213.svg @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3214.svg b/resources/imageFiles/emojis/3214.svg new file mode 100644 index 0000000..f2c5bf3 --- /dev/null +++ b/resources/imageFiles/emojis/3214.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3215.svg b/resources/imageFiles/emojis/3215.svg new file mode 100644 index 0000000..47bb507 --- /dev/null +++ b/resources/imageFiles/emojis/3215.svg @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3216.svg b/resources/imageFiles/emojis/3216.svg new file mode 100644 index 0000000..f92f207 --- /dev/null +++ b/resources/imageFiles/emojis/3216.svg @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3217.svg b/resources/imageFiles/emojis/3217.svg new file mode 100644 index 0000000..c27aa85 --- /dev/null +++ b/resources/imageFiles/emojis/3217.svg @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3218.svg b/resources/imageFiles/emojis/3218.svg new file mode 100644 index 0000000..dfce798 --- /dev/null +++ b/resources/imageFiles/emojis/3218.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3219.svg b/resources/imageFiles/emojis/3219.svg new file mode 100644 index 0000000..f1209bf --- /dev/null +++ b/resources/imageFiles/emojis/3219.svg @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3220.svg b/resources/imageFiles/emojis/3220.svg new file mode 100644 index 0000000..e5f2d43 --- /dev/null +++ b/resources/imageFiles/emojis/3220.svg @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3221.svg b/resources/imageFiles/emojis/3221.svg new file mode 100644 index 0000000..8c39b57 --- /dev/null +++ b/resources/imageFiles/emojis/3221.svg @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3222.svg b/resources/imageFiles/emojis/3222.svg new file mode 100644 index 0000000..6cfcf5d --- /dev/null +++ b/resources/imageFiles/emojis/3222.svg @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3223.svg b/resources/imageFiles/emojis/3223.svg new file mode 100644 index 0000000..a542b11 --- /dev/null +++ b/resources/imageFiles/emojis/3223.svg @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3224.svg b/resources/imageFiles/emojis/3224.svg new file mode 100644 index 0000000..17aa308 --- /dev/null +++ b/resources/imageFiles/emojis/3224.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3225.svg b/resources/imageFiles/emojis/3225.svg new file mode 100644 index 0000000..c62b375 --- /dev/null +++ b/resources/imageFiles/emojis/3225.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3226.svg b/resources/imageFiles/emojis/3226.svg new file mode 100644 index 0000000..847bc28 --- /dev/null +++ b/resources/imageFiles/emojis/3226.svg @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3227.svg b/resources/imageFiles/emojis/3227.svg new file mode 100644 index 0000000..bd9a1ad --- /dev/null +++ b/resources/imageFiles/emojis/3227.svg @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3228.svg b/resources/imageFiles/emojis/3228.svg new file mode 100644 index 0000000..714b358 --- /dev/null +++ b/resources/imageFiles/emojis/3228.svg @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3229.svg b/resources/imageFiles/emojis/3229.svg new file mode 100644 index 0000000..d6a7b7a --- /dev/null +++ b/resources/imageFiles/emojis/3229.svg @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3230.svg b/resources/imageFiles/emojis/3230.svg new file mode 100644 index 0000000..eb64fc3 --- /dev/null +++ b/resources/imageFiles/emojis/3230.svg @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3231.svg b/resources/imageFiles/emojis/3231.svg new file mode 100644 index 0000000..1501c0a --- /dev/null +++ b/resources/imageFiles/emojis/3231.svg @@ -0,0 +1,34 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3232.svg b/resources/imageFiles/emojis/3232.svg new file mode 100644 index 0000000..c1b7f90 --- /dev/null +++ b/resources/imageFiles/emojis/3232.svg @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3233.svg b/resources/imageFiles/emojis/3233.svg new file mode 100644 index 0000000..ba85bea --- /dev/null +++ b/resources/imageFiles/emojis/3233.svg @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3234.svg b/resources/imageFiles/emojis/3234.svg new file mode 100644 index 0000000..1a2e093 --- /dev/null +++ b/resources/imageFiles/emojis/3234.svg @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3235.svg b/resources/imageFiles/emojis/3235.svg new file mode 100644 index 0000000..97fda76 --- /dev/null +++ b/resources/imageFiles/emojis/3235.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3236.svg b/resources/imageFiles/emojis/3236.svg new file mode 100644 index 0000000..9cdb4ef --- /dev/null +++ b/resources/imageFiles/emojis/3236.svg @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3237.svg b/resources/imageFiles/emojis/3237.svg new file mode 100644 index 0000000..708895f --- /dev/null +++ b/resources/imageFiles/emojis/3237.svg @@ -0,0 +1,67 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3238.svg b/resources/imageFiles/emojis/3238.svg new file mode 100644 index 0000000..5bddcd5 --- /dev/null +++ b/resources/imageFiles/emojis/3238.svg @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3239.svg b/resources/imageFiles/emojis/3239.svg new file mode 100644 index 0000000..96f060e --- /dev/null +++ b/resources/imageFiles/emojis/3239.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3240.svg b/resources/imageFiles/emojis/3240.svg new file mode 100644 index 0000000..4d8f7a5 --- /dev/null +++ b/resources/imageFiles/emojis/3240.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3241.svg b/resources/imageFiles/emojis/3241.svg new file mode 100644 index 0000000..56cb375 --- /dev/null +++ b/resources/imageFiles/emojis/3241.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3242.svg b/resources/imageFiles/emojis/3242.svg new file mode 100644 index 0000000..77ef18e --- /dev/null +++ b/resources/imageFiles/emojis/3242.svg @@ -0,0 +1,45 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3243.svg b/resources/imageFiles/emojis/3243.svg new file mode 100644 index 0000000..38b5d20 --- /dev/null +++ b/resources/imageFiles/emojis/3243.svg @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3244.svg b/resources/imageFiles/emojis/3244.svg new file mode 100644 index 0000000..e5085ee --- /dev/null +++ b/resources/imageFiles/emojis/3244.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3245.svg b/resources/imageFiles/emojis/3245.svg new file mode 100644 index 0000000..56d298c --- /dev/null +++ b/resources/imageFiles/emojis/3245.svg @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3246.svg b/resources/imageFiles/emojis/3246.svg new file mode 100644 index 0000000..5deabf8 --- /dev/null +++ b/resources/imageFiles/emojis/3246.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3247.svg b/resources/imageFiles/emojis/3247.svg new file mode 100644 index 0000000..398a0e0 --- /dev/null +++ b/resources/imageFiles/emojis/3247.svg @@ -0,0 +1,33 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3248.svg b/resources/imageFiles/emojis/3248.svg new file mode 100644 index 0000000..e14bb9e --- /dev/null +++ b/resources/imageFiles/emojis/3248.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3249.svg b/resources/imageFiles/emojis/3249.svg new file mode 100644 index 0000000..2df8975 --- /dev/null +++ b/resources/imageFiles/emojis/3249.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3250.svg b/resources/imageFiles/emojis/3250.svg new file mode 100644 index 0000000..b9201bd --- /dev/null +++ b/resources/imageFiles/emojis/3250.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3251.svg b/resources/imageFiles/emojis/3251.svg new file mode 100644 index 0000000..6642cd2 --- /dev/null +++ b/resources/imageFiles/emojis/3251.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3252.svg b/resources/imageFiles/emojis/3252.svg new file mode 100644 index 0000000..7773d75 --- /dev/null +++ b/resources/imageFiles/emojis/3252.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3253.svg b/resources/imageFiles/emojis/3253.svg new file mode 100644 index 0000000..849826b --- /dev/null +++ b/resources/imageFiles/emojis/3253.svg @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3254.svg b/resources/imageFiles/emojis/3254.svg new file mode 100644 index 0000000..a4ccb93 --- /dev/null +++ b/resources/imageFiles/emojis/3254.svg @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3255.svg b/resources/imageFiles/emojis/3255.svg new file mode 100644 index 0000000..7cfb080 --- /dev/null +++ b/resources/imageFiles/emojis/3255.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3256.svg b/resources/imageFiles/emojis/3256.svg new file mode 100644 index 0000000..eb0366f --- /dev/null +++ b/resources/imageFiles/emojis/3256.svg @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3257.svg b/resources/imageFiles/emojis/3257.svg new file mode 100644 index 0000000..ae6a53e --- /dev/null +++ b/resources/imageFiles/emojis/3257.svg @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3258.svg b/resources/imageFiles/emojis/3258.svg new file mode 100644 index 0000000..cda87e0 --- /dev/null +++ b/resources/imageFiles/emojis/3258.svg @@ -0,0 +1,31 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3259.svg b/resources/imageFiles/emojis/3259.svg new file mode 100644 index 0000000..23ba6e6 --- /dev/null +++ b/resources/imageFiles/emojis/3259.svg @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3260.svg b/resources/imageFiles/emojis/3260.svg new file mode 100644 index 0000000..5c0dc8b --- /dev/null +++ b/resources/imageFiles/emojis/3260.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3261.svg b/resources/imageFiles/emojis/3261.svg new file mode 100644 index 0000000..1a1caff --- /dev/null +++ b/resources/imageFiles/emojis/3261.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3262.svg b/resources/imageFiles/emojis/3262.svg new file mode 100644 index 0000000..045293b --- /dev/null +++ b/resources/imageFiles/emojis/3262.svg @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3263.svg b/resources/imageFiles/emojis/3263.svg new file mode 100644 index 0000000..56ad5ac --- /dev/null +++ b/resources/imageFiles/emojis/3263.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3264.svg b/resources/imageFiles/emojis/3264.svg new file mode 100644 index 0000000..d066742 --- /dev/null +++ b/resources/imageFiles/emojis/3264.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3265.svg b/resources/imageFiles/emojis/3265.svg new file mode 100644 index 0000000..cf22e9d --- /dev/null +++ b/resources/imageFiles/emojis/3265.svg @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3266.svg b/resources/imageFiles/emojis/3266.svg new file mode 100644 index 0000000..1a479b5 --- /dev/null +++ b/resources/imageFiles/emojis/3266.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3267.svg b/resources/imageFiles/emojis/3267.svg new file mode 100644 index 0000000..f353463 --- /dev/null +++ b/resources/imageFiles/emojis/3267.svg @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3268.svg b/resources/imageFiles/emojis/3268.svg new file mode 100644 index 0000000..f239069 --- /dev/null +++ b/resources/imageFiles/emojis/3268.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3269.svg b/resources/imageFiles/emojis/3269.svg new file mode 100644 index 0000000..e9ccc2d --- /dev/null +++ b/resources/imageFiles/emojis/3269.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/resources/imageFiles/emojis/3270.svg b/resources/imageFiles/emojis/3270.svg new file mode 100644 index 0000000..66a1ea2 --- /dev/null +++ b/resources/imageFiles/emojis/3270.svg @@ -0,0 +1,38 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3271.svg b/resources/imageFiles/emojis/3271.svg new file mode 100644 index 0000000..c6a782f --- /dev/null +++ b/resources/imageFiles/emojis/3271.svg @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3272.svg b/resources/imageFiles/emojis/3272.svg new file mode 100644 index 0000000..1ad9401 --- /dev/null +++ b/resources/imageFiles/emojis/3272.svg @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3273.svg b/resources/imageFiles/emojis/3273.svg new file mode 100644 index 0000000..9ecfba3 --- /dev/null +++ b/resources/imageFiles/emojis/3273.svg @@ -0,0 +1,36 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3274.svg b/resources/imageFiles/emojis/3274.svg new file mode 100644 index 0000000..82a2852 --- /dev/null +++ b/resources/imageFiles/emojis/3274.svg @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3275.svg b/resources/imageFiles/emojis/3275.svg new file mode 100644 index 0000000..f7ccb11 --- /dev/null +++ b/resources/imageFiles/emojis/3275.svg @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3276.svg b/resources/imageFiles/emojis/3276.svg new file mode 100644 index 0000000..6eabf5b --- /dev/null +++ b/resources/imageFiles/emojis/3276.svg @@ -0,0 +1,29 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3277.svg b/resources/imageFiles/emojis/3277.svg new file mode 100644 index 0000000..47bcbd6 --- /dev/null +++ b/resources/imageFiles/emojis/3277.svg @@ -0,0 +1,29 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3278.svg b/resources/imageFiles/emojis/3278.svg new file mode 100644 index 0000000..6ede869 --- /dev/null +++ b/resources/imageFiles/emojis/3278.svg @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3279.svg b/resources/imageFiles/emojis/3279.svg new file mode 100644 index 0000000..cb2cd3d --- /dev/null +++ b/resources/imageFiles/emojis/3279.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3280.svg b/resources/imageFiles/emojis/3280.svg new file mode 100644 index 0000000..85f0983 --- /dev/null +++ b/resources/imageFiles/emojis/3280.svg @@ -0,0 +1,39 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3281.svg b/resources/imageFiles/emojis/3281.svg new file mode 100644 index 0000000..687653e --- /dev/null +++ b/resources/imageFiles/emojis/3281.svg @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3282.svg b/resources/imageFiles/emojis/3282.svg new file mode 100644 index 0000000..1146e10 --- /dev/null +++ b/resources/imageFiles/emojis/3282.svg @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3283.svg b/resources/imageFiles/emojis/3283.svg new file mode 100644 index 0000000..20035ad --- /dev/null +++ b/resources/imageFiles/emojis/3283.svg @@ -0,0 +1,81 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3284.svg b/resources/imageFiles/emojis/3284.svg new file mode 100644 index 0000000..614b96f --- /dev/null +++ b/resources/imageFiles/emojis/3284.svg @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3285.svg b/resources/imageFiles/emojis/3285.svg new file mode 100644 index 0000000..39ef329 --- /dev/null +++ b/resources/imageFiles/emojis/3285.svg @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3286.svg b/resources/imageFiles/emojis/3286.svg new file mode 100644 index 0000000..6d9b5c7 --- /dev/null +++ b/resources/imageFiles/emojis/3286.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3287.svg b/resources/imageFiles/emojis/3287.svg new file mode 100644 index 0000000..6e448d6 --- /dev/null +++ b/resources/imageFiles/emojis/3287.svg @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3288.svg b/resources/imageFiles/emojis/3288.svg new file mode 100644 index 0000000..43513df --- /dev/null +++ b/resources/imageFiles/emojis/3288.svg @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3289.svg b/resources/imageFiles/emojis/3289.svg new file mode 100644 index 0000000..55e32d7 --- /dev/null +++ b/resources/imageFiles/emojis/3289.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/resources/imageFiles/emojis/3290.svg b/resources/imageFiles/emojis/3290.svg new file mode 100644 index 0000000..f06aaae --- /dev/null +++ b/resources/imageFiles/emojis/3290.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3291.svg b/resources/imageFiles/emojis/3291.svg new file mode 100644 index 0000000..b243b62 --- /dev/null +++ b/resources/imageFiles/emojis/3291.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3292.svg b/resources/imageFiles/emojis/3292.svg new file mode 100644 index 0000000..6b2917a --- /dev/null +++ b/resources/imageFiles/emojis/3292.svg @@ -0,0 +1,39 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3293.svg b/resources/imageFiles/emojis/3293.svg new file mode 100644 index 0000000..d4a408f --- /dev/null +++ b/resources/imageFiles/emojis/3293.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3294.svg b/resources/imageFiles/emojis/3294.svg new file mode 100644 index 0000000..1975272 --- /dev/null +++ b/resources/imageFiles/emojis/3294.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3295.svg b/resources/imageFiles/emojis/3295.svg new file mode 100644 index 0000000..1f42967 --- /dev/null +++ b/resources/imageFiles/emojis/3295.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3296.svg b/resources/imageFiles/emojis/3296.svg new file mode 100644 index 0000000..5f4f09e --- /dev/null +++ b/resources/imageFiles/emojis/3296.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3297.svg b/resources/imageFiles/emojis/3297.svg new file mode 100644 index 0000000..ce33e47 --- /dev/null +++ b/resources/imageFiles/emojis/3297.svg @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3298.svg b/resources/imageFiles/emojis/3298.svg new file mode 100644 index 0000000..b4a2d8c --- /dev/null +++ b/resources/imageFiles/emojis/3298.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3299.svg b/resources/imageFiles/emojis/3299.svg new file mode 100644 index 0000000..5ed7b1b --- /dev/null +++ b/resources/imageFiles/emojis/3299.svg @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3300.svg b/resources/imageFiles/emojis/3300.svg new file mode 100644 index 0000000..f5ad49c --- /dev/null +++ b/resources/imageFiles/emojis/3300.svg @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3301.svg b/resources/imageFiles/emojis/3301.svg new file mode 100644 index 0000000..21170aa --- /dev/null +++ b/resources/imageFiles/emojis/3301.svg @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3302.svg b/resources/imageFiles/emojis/3302.svg new file mode 100644 index 0000000..631e5a9 --- /dev/null +++ b/resources/imageFiles/emojis/3302.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3303.svg b/resources/imageFiles/emojis/3303.svg new file mode 100644 index 0000000..4fc4e92 --- /dev/null +++ b/resources/imageFiles/emojis/3303.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3304.svg b/resources/imageFiles/emojis/3304.svg new file mode 100644 index 0000000..35bc0a2 --- /dev/null +++ b/resources/imageFiles/emojis/3304.svg @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3305.svg b/resources/imageFiles/emojis/3305.svg new file mode 100644 index 0000000..8056696 --- /dev/null +++ b/resources/imageFiles/emojis/3305.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3306.svg b/resources/imageFiles/emojis/3306.svg new file mode 100644 index 0000000..f892bfe --- /dev/null +++ b/resources/imageFiles/emojis/3306.svg @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3307.svg b/resources/imageFiles/emojis/3307.svg new file mode 100644 index 0000000..386e1f1 --- /dev/null +++ b/resources/imageFiles/emojis/3307.svg @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3308.svg b/resources/imageFiles/emojis/3308.svg new file mode 100644 index 0000000..b5385f3 --- /dev/null +++ b/resources/imageFiles/emojis/3308.svg @@ -0,0 +1,35 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3309.svg b/resources/imageFiles/emojis/3309.svg new file mode 100644 index 0000000..bdbc0c9 --- /dev/null +++ b/resources/imageFiles/emojis/3309.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3310.svg b/resources/imageFiles/emojis/3310.svg new file mode 100644 index 0000000..2b65ee5 --- /dev/null +++ b/resources/imageFiles/emojis/3310.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3311.svg b/resources/imageFiles/emojis/3311.svg new file mode 100644 index 0000000..e450f7e --- /dev/null +++ b/resources/imageFiles/emojis/3311.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3312.svg b/resources/imageFiles/emojis/3312.svg new file mode 100644 index 0000000..63a892e --- /dev/null +++ b/resources/imageFiles/emojis/3312.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3313.svg b/resources/imageFiles/emojis/3313.svg new file mode 100644 index 0000000..bf4a020 --- /dev/null +++ b/resources/imageFiles/emojis/3313.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3314.svg b/resources/imageFiles/emojis/3314.svg new file mode 100644 index 0000000..0269598 --- /dev/null +++ b/resources/imageFiles/emojis/3314.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3315.svg b/resources/imageFiles/emojis/3315.svg new file mode 100644 index 0000000..2011bf4 --- /dev/null +++ b/resources/imageFiles/emojis/3315.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3316.svg b/resources/imageFiles/emojis/3316.svg new file mode 100644 index 0000000..1718d9f --- /dev/null +++ b/resources/imageFiles/emojis/3316.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3317.svg b/resources/imageFiles/emojis/3317.svg new file mode 100644 index 0000000..001065f --- /dev/null +++ b/resources/imageFiles/emojis/3317.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3318.svg b/resources/imageFiles/emojis/3318.svg new file mode 100644 index 0000000..edbdea4 --- /dev/null +++ b/resources/imageFiles/emojis/3318.svg @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3319.svg b/resources/imageFiles/emojis/3319.svg new file mode 100644 index 0000000..07794a4 --- /dev/null +++ b/resources/imageFiles/emojis/3319.svg @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3320.svg b/resources/imageFiles/emojis/3320.svg new file mode 100644 index 0000000..99f1298 --- /dev/null +++ b/resources/imageFiles/emojis/3320.svg @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3321.svg b/resources/imageFiles/emojis/3321.svg new file mode 100644 index 0000000..75f1fff --- /dev/null +++ b/resources/imageFiles/emojis/3321.svg @@ -0,0 +1,39 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3322.svg b/resources/imageFiles/emojis/3322.svg new file mode 100644 index 0000000..2d85fdd --- /dev/null +++ b/resources/imageFiles/emojis/3322.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3323.svg b/resources/imageFiles/emojis/3323.svg new file mode 100644 index 0000000..ceeb915 --- /dev/null +++ b/resources/imageFiles/emojis/3323.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3324.svg b/resources/imageFiles/emojis/3324.svg new file mode 100644 index 0000000..bdf674f --- /dev/null +++ b/resources/imageFiles/emojis/3324.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3325.svg b/resources/imageFiles/emojis/3325.svg new file mode 100644 index 0000000..8d260ea --- /dev/null +++ b/resources/imageFiles/emojis/3325.svg @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3326.svg b/resources/imageFiles/emojis/3326.svg new file mode 100644 index 0000000..aa3776f --- /dev/null +++ b/resources/imageFiles/emojis/3326.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3327.svg b/resources/imageFiles/emojis/3327.svg new file mode 100644 index 0000000..488b14a --- /dev/null +++ b/resources/imageFiles/emojis/3327.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3328.svg b/resources/imageFiles/emojis/3328.svg new file mode 100644 index 0000000..a3c2715 --- /dev/null +++ b/resources/imageFiles/emojis/3328.svg @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3329.svg b/resources/imageFiles/emojis/3329.svg new file mode 100644 index 0000000..56b9940 --- /dev/null +++ b/resources/imageFiles/emojis/3329.svg @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3330.svg b/resources/imageFiles/emojis/3330.svg new file mode 100644 index 0000000..883700a --- /dev/null +++ b/resources/imageFiles/emojis/3330.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/resources/imageFiles/emojis/3331.svg b/resources/imageFiles/emojis/3331.svg new file mode 100644 index 0000000..3626352 --- /dev/null +++ b/resources/imageFiles/emojis/3331.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3332.svg b/resources/imageFiles/emojis/3332.svg new file mode 100644 index 0000000..1df00e3 --- /dev/null +++ b/resources/imageFiles/emojis/3332.svg @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3333.svg b/resources/imageFiles/emojis/3333.svg new file mode 100644 index 0000000..cea1632 --- /dev/null +++ b/resources/imageFiles/emojis/3333.svg @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3334.svg b/resources/imageFiles/emojis/3334.svg new file mode 100644 index 0000000..cd5b869 --- /dev/null +++ b/resources/imageFiles/emojis/3334.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3335.svg b/resources/imageFiles/emojis/3335.svg new file mode 100644 index 0000000..0b06051 --- /dev/null +++ b/resources/imageFiles/emojis/3335.svg @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3336.svg b/resources/imageFiles/emojis/3336.svg new file mode 100644 index 0000000..13ae957 --- /dev/null +++ b/resources/imageFiles/emojis/3336.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3337.svg b/resources/imageFiles/emojis/3337.svg new file mode 100644 index 0000000..f30ca8d --- /dev/null +++ b/resources/imageFiles/emojis/3337.svg @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3338.svg b/resources/imageFiles/emojis/3338.svg new file mode 100644 index 0000000..701ed1c --- /dev/null +++ b/resources/imageFiles/emojis/3338.svg @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3339.svg b/resources/imageFiles/emojis/3339.svg new file mode 100644 index 0000000..8ca25b6 --- /dev/null +++ b/resources/imageFiles/emojis/3339.svg @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3340.svg b/resources/imageFiles/emojis/3340.svg new file mode 100644 index 0000000..258c70c --- /dev/null +++ b/resources/imageFiles/emojis/3340.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3341.svg b/resources/imageFiles/emojis/3341.svg new file mode 100644 index 0000000..13db1b8 --- /dev/null +++ b/resources/imageFiles/emojis/3341.svg @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3342.svg b/resources/imageFiles/emojis/3342.svg new file mode 100644 index 0000000..d1752c9 --- /dev/null +++ b/resources/imageFiles/emojis/3342.svg @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3343.svg b/resources/imageFiles/emojis/3343.svg new file mode 100644 index 0000000..3a690d7 --- /dev/null +++ b/resources/imageFiles/emojis/3343.svg @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3344.svg b/resources/imageFiles/emojis/3344.svg new file mode 100644 index 0000000..cab2218 --- /dev/null +++ b/resources/imageFiles/emojis/3344.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3345.svg b/resources/imageFiles/emojis/3345.svg new file mode 100644 index 0000000..61a322f --- /dev/null +++ b/resources/imageFiles/emojis/3345.svg @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3346.svg b/resources/imageFiles/emojis/3346.svg new file mode 100644 index 0000000..3d1d55a --- /dev/null +++ b/resources/imageFiles/emojis/3346.svg @@ -0,0 +1,29 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3347.svg b/resources/imageFiles/emojis/3347.svg new file mode 100644 index 0000000..619baaa --- /dev/null +++ b/resources/imageFiles/emojis/3347.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3348.svg b/resources/imageFiles/emojis/3348.svg new file mode 100644 index 0000000..591b54e --- /dev/null +++ b/resources/imageFiles/emojis/3348.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3349.svg b/resources/imageFiles/emojis/3349.svg new file mode 100644 index 0000000..91a4748 --- /dev/null +++ b/resources/imageFiles/emojis/3349.svg @@ -0,0 +1,29 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3350.svg b/resources/imageFiles/emojis/3350.svg new file mode 100644 index 0000000..f28cc13 --- /dev/null +++ b/resources/imageFiles/emojis/3350.svg @@ -0,0 +1,40 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3351.svg b/resources/imageFiles/emojis/3351.svg new file mode 100644 index 0000000..88e0480 --- /dev/null +++ b/resources/imageFiles/emojis/3351.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3352.svg b/resources/imageFiles/emojis/3352.svg new file mode 100644 index 0000000..227a33c --- /dev/null +++ b/resources/imageFiles/emojis/3352.svg @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3353.svg b/resources/imageFiles/emojis/3353.svg new file mode 100644 index 0000000..2b6eb12 --- /dev/null +++ b/resources/imageFiles/emojis/3353.svg @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3354.svg b/resources/imageFiles/emojis/3354.svg new file mode 100644 index 0000000..2481af0 --- /dev/null +++ b/resources/imageFiles/emojis/3354.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3355.svg b/resources/imageFiles/emojis/3355.svg new file mode 100644 index 0000000..3f5a6f1 --- /dev/null +++ b/resources/imageFiles/emojis/3355.svg @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3356.svg b/resources/imageFiles/emojis/3356.svg new file mode 100644 index 0000000..7186035 --- /dev/null +++ b/resources/imageFiles/emojis/3356.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3357.svg b/resources/imageFiles/emojis/3357.svg new file mode 100644 index 0000000..ad3a57b --- /dev/null +++ b/resources/imageFiles/emojis/3357.svg @@ -0,0 +1,29 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3358.svg b/resources/imageFiles/emojis/3358.svg new file mode 100644 index 0000000..ffd19eb --- /dev/null +++ b/resources/imageFiles/emojis/3358.svg @@ -0,0 +1,41 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3359.svg b/resources/imageFiles/emojis/3359.svg new file mode 100644 index 0000000..5f29140 --- /dev/null +++ b/resources/imageFiles/emojis/3359.svg @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3360.svg b/resources/imageFiles/emojis/3360.svg new file mode 100644 index 0000000..17cd4cd --- /dev/null +++ b/resources/imageFiles/emojis/3360.svg @@ -0,0 +1,32 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3361.svg b/resources/imageFiles/emojis/3361.svg new file mode 100644 index 0000000..a1caf2d --- /dev/null +++ b/resources/imageFiles/emojis/3361.svg @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3362.svg b/resources/imageFiles/emojis/3362.svg new file mode 100644 index 0000000..671fd3e --- /dev/null +++ b/resources/imageFiles/emojis/3362.svg @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3363.svg b/resources/imageFiles/emojis/3363.svg new file mode 100644 index 0000000..7a310e2 --- /dev/null +++ b/resources/imageFiles/emojis/3363.svg @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3364.svg b/resources/imageFiles/emojis/3364.svg new file mode 100644 index 0000000..5680178 --- /dev/null +++ b/resources/imageFiles/emojis/3364.svg @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3365.svg b/resources/imageFiles/emojis/3365.svg new file mode 100644 index 0000000..e75305f --- /dev/null +++ b/resources/imageFiles/emojis/3365.svg @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3366.svg b/resources/imageFiles/emojis/3366.svg new file mode 100644 index 0000000..f21c2b5 --- /dev/null +++ b/resources/imageFiles/emojis/3366.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3367.svg b/resources/imageFiles/emojis/3367.svg new file mode 100644 index 0000000..fd952b3 --- /dev/null +++ b/resources/imageFiles/emojis/3367.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3368.svg b/resources/imageFiles/emojis/3368.svg new file mode 100644 index 0000000..163c3f1 --- /dev/null +++ b/resources/imageFiles/emojis/3368.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3369.svg b/resources/imageFiles/emojis/3369.svg new file mode 100644 index 0000000..f12101a --- /dev/null +++ b/resources/imageFiles/emojis/3369.svg @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3370.svg b/resources/imageFiles/emojis/3370.svg new file mode 100644 index 0000000..ad7c533 --- /dev/null +++ b/resources/imageFiles/emojis/3370.svg @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3371.svg b/resources/imageFiles/emojis/3371.svg new file mode 100644 index 0000000..f034327 --- /dev/null +++ b/resources/imageFiles/emojis/3371.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3372.svg b/resources/imageFiles/emojis/3372.svg new file mode 100644 index 0000000..533a21a --- /dev/null +++ b/resources/imageFiles/emojis/3372.svg @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3373.svg b/resources/imageFiles/emojis/3373.svg new file mode 100644 index 0000000..c8bcb7d --- /dev/null +++ b/resources/imageFiles/emojis/3373.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3374.svg b/resources/imageFiles/emojis/3374.svg new file mode 100644 index 0000000..e36c2f6 --- /dev/null +++ b/resources/imageFiles/emojis/3374.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3375.svg b/resources/imageFiles/emojis/3375.svg new file mode 100644 index 0000000..c4f6fac --- /dev/null +++ b/resources/imageFiles/emojis/3375.svg @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3376.svg b/resources/imageFiles/emojis/3376.svg new file mode 100644 index 0000000..e38dc5c --- /dev/null +++ b/resources/imageFiles/emojis/3376.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3377.svg b/resources/imageFiles/emojis/3377.svg new file mode 100644 index 0000000..7738824 --- /dev/null +++ b/resources/imageFiles/emojis/3377.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3378.svg b/resources/imageFiles/emojis/3378.svg new file mode 100644 index 0000000..290f7df --- /dev/null +++ b/resources/imageFiles/emojis/3378.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3379.svg b/resources/imageFiles/emojis/3379.svg new file mode 100644 index 0000000..40f1b2d --- /dev/null +++ b/resources/imageFiles/emojis/3379.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3380.svg b/resources/imageFiles/emojis/3380.svg new file mode 100644 index 0000000..2625324 --- /dev/null +++ b/resources/imageFiles/emojis/3380.svg @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3381.svg b/resources/imageFiles/emojis/3381.svg new file mode 100644 index 0000000..a773e83 --- /dev/null +++ b/resources/imageFiles/emojis/3381.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3382.svg b/resources/imageFiles/emojis/3382.svg new file mode 100644 index 0000000..0918c3d --- /dev/null +++ b/resources/imageFiles/emojis/3382.svg @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3383.svg b/resources/imageFiles/emojis/3383.svg new file mode 100644 index 0000000..bae4e5e --- /dev/null +++ b/resources/imageFiles/emojis/3383.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3384.svg b/resources/imageFiles/emojis/3384.svg new file mode 100644 index 0000000..f7725cd --- /dev/null +++ b/resources/imageFiles/emojis/3384.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/resources/imageFiles/emojis/3385.svg b/resources/imageFiles/emojis/3385.svg new file mode 100644 index 0000000..18c9454 --- /dev/null +++ b/resources/imageFiles/emojis/3385.svg @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3386.svg b/resources/imageFiles/emojis/3386.svg new file mode 100644 index 0000000..216d62a --- /dev/null +++ b/resources/imageFiles/emojis/3386.svg @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3387.svg b/resources/imageFiles/emojis/3387.svg new file mode 100644 index 0000000..11d46fe --- /dev/null +++ b/resources/imageFiles/emojis/3387.svg @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3388.svg b/resources/imageFiles/emojis/3388.svg new file mode 100644 index 0000000..c88564c --- /dev/null +++ b/resources/imageFiles/emojis/3388.svg @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3389.svg b/resources/imageFiles/emojis/3389.svg new file mode 100644 index 0000000..2d2db16 --- /dev/null +++ b/resources/imageFiles/emojis/3389.svg @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3390.svg b/resources/imageFiles/emojis/3390.svg new file mode 100644 index 0000000..d311f76 --- /dev/null +++ b/resources/imageFiles/emojis/3390.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3391.svg b/resources/imageFiles/emojis/3391.svg new file mode 100644 index 0000000..16ed34b --- /dev/null +++ b/resources/imageFiles/emojis/3391.svg @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3392.svg b/resources/imageFiles/emojis/3392.svg new file mode 100644 index 0000000..c386158 --- /dev/null +++ b/resources/imageFiles/emojis/3392.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3393.svg b/resources/imageFiles/emojis/3393.svg new file mode 100644 index 0000000..2a1969c --- /dev/null +++ b/resources/imageFiles/emojis/3393.svg @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3394.svg b/resources/imageFiles/emojis/3394.svg new file mode 100644 index 0000000..5d40efd --- /dev/null +++ b/resources/imageFiles/emojis/3394.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3395.svg b/resources/imageFiles/emojis/3395.svg new file mode 100644 index 0000000..e93ba43 --- /dev/null +++ b/resources/imageFiles/emojis/3395.svg @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3396.svg b/resources/imageFiles/emojis/3396.svg new file mode 100644 index 0000000..88523c4 --- /dev/null +++ b/resources/imageFiles/emojis/3396.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3397.svg b/resources/imageFiles/emojis/3397.svg new file mode 100644 index 0000000..e5f55fe --- /dev/null +++ b/resources/imageFiles/emojis/3397.svg @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3398.svg b/resources/imageFiles/emojis/3398.svg new file mode 100644 index 0000000..25357c6 --- /dev/null +++ b/resources/imageFiles/emojis/3398.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/resources/imageFiles/emojis/3399.svg b/resources/imageFiles/emojis/3399.svg new file mode 100644 index 0000000..b84b834 --- /dev/null +++ b/resources/imageFiles/emojis/3399.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3400.svg b/resources/imageFiles/emojis/3400.svg new file mode 100644 index 0000000..3ae9d4a --- /dev/null +++ b/resources/imageFiles/emojis/3400.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3401.svg b/resources/imageFiles/emojis/3401.svg new file mode 100644 index 0000000..02843b3 --- /dev/null +++ b/resources/imageFiles/emojis/3401.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3402.svg b/resources/imageFiles/emojis/3402.svg new file mode 100644 index 0000000..2b7273a --- /dev/null +++ b/resources/imageFiles/emojis/3402.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/resources/imageFiles/emojis/3403.svg b/resources/imageFiles/emojis/3403.svg new file mode 100644 index 0000000..83cb0b5 --- /dev/null +++ b/resources/imageFiles/emojis/3403.svg @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3404.svg b/resources/imageFiles/emojis/3404.svg new file mode 100644 index 0000000..0f68ca3 --- /dev/null +++ b/resources/imageFiles/emojis/3404.svg @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3405.svg b/resources/imageFiles/emojis/3405.svg new file mode 100644 index 0000000..09ef195 --- /dev/null +++ b/resources/imageFiles/emojis/3405.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/resources/imageFiles/emojis/3406.svg b/resources/imageFiles/emojis/3406.svg new file mode 100644 index 0000000..58ddd3f --- /dev/null +++ b/resources/imageFiles/emojis/3406.svg @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3407.svg b/resources/imageFiles/emojis/3407.svg new file mode 100644 index 0000000..ef0c0d4 --- /dev/null +++ b/resources/imageFiles/emojis/3407.svg @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3408.svg b/resources/imageFiles/emojis/3408.svg new file mode 100644 index 0000000..eba7166 --- /dev/null +++ b/resources/imageFiles/emojis/3408.svg @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3409.svg b/resources/imageFiles/emojis/3409.svg new file mode 100644 index 0000000..b973ee8 --- /dev/null +++ b/resources/imageFiles/emojis/3409.svg @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3410.svg b/resources/imageFiles/emojis/3410.svg new file mode 100644 index 0000000..16c6126 --- /dev/null +++ b/resources/imageFiles/emojis/3410.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3411.svg b/resources/imageFiles/emojis/3411.svg new file mode 100644 index 0000000..68fdebe --- /dev/null +++ b/resources/imageFiles/emojis/3411.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3412.svg b/resources/imageFiles/emojis/3412.svg new file mode 100644 index 0000000..41bd7ff --- /dev/null +++ b/resources/imageFiles/emojis/3412.svg @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3413.svg b/resources/imageFiles/emojis/3413.svg new file mode 100644 index 0000000..456a543 --- /dev/null +++ b/resources/imageFiles/emojis/3413.svg @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3414.svg b/resources/imageFiles/emojis/3414.svg new file mode 100644 index 0000000..dd83f96 --- /dev/null +++ b/resources/imageFiles/emojis/3414.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3415.svg b/resources/imageFiles/emojis/3415.svg new file mode 100644 index 0000000..6bdccb4 --- /dev/null +++ b/resources/imageFiles/emojis/3415.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3416.svg b/resources/imageFiles/emojis/3416.svg new file mode 100644 index 0000000..5553e28 --- /dev/null +++ b/resources/imageFiles/emojis/3416.svg @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3417.svg b/resources/imageFiles/emojis/3417.svg new file mode 100644 index 0000000..141e7ed --- /dev/null +++ b/resources/imageFiles/emojis/3417.svg @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3418.svg b/resources/imageFiles/emojis/3418.svg new file mode 100644 index 0000000..075d65b --- /dev/null +++ b/resources/imageFiles/emojis/3418.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3419.svg b/resources/imageFiles/emojis/3419.svg new file mode 100644 index 0000000..fb94651 --- /dev/null +++ b/resources/imageFiles/emojis/3419.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3420.svg b/resources/imageFiles/emojis/3420.svg new file mode 100644 index 0000000..0778173 --- /dev/null +++ b/resources/imageFiles/emojis/3420.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3421.svg b/resources/imageFiles/emojis/3421.svg new file mode 100644 index 0000000..639a6dc --- /dev/null +++ b/resources/imageFiles/emojis/3421.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3422.svg b/resources/imageFiles/emojis/3422.svg new file mode 100644 index 0000000..f922af7 --- /dev/null +++ b/resources/imageFiles/emojis/3422.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3423.svg b/resources/imageFiles/emojis/3423.svg new file mode 100644 index 0000000..cac313f --- /dev/null +++ b/resources/imageFiles/emojis/3423.svg @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3424.svg b/resources/imageFiles/emojis/3424.svg new file mode 100644 index 0000000..7a1c56f --- /dev/null +++ b/resources/imageFiles/emojis/3424.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/resources/imageFiles/emojis/3425.svg b/resources/imageFiles/emojis/3425.svg new file mode 100644 index 0000000..0e04b17 --- /dev/null +++ b/resources/imageFiles/emojis/3425.svg @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3426.svg b/resources/imageFiles/emojis/3426.svg new file mode 100644 index 0000000..ea34fe1 --- /dev/null +++ b/resources/imageFiles/emojis/3426.svg @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3427.svg b/resources/imageFiles/emojis/3427.svg new file mode 100644 index 0000000..b5cae10 --- /dev/null +++ b/resources/imageFiles/emojis/3427.svg @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3428.svg b/resources/imageFiles/emojis/3428.svg new file mode 100644 index 0000000..254f234 --- /dev/null +++ b/resources/imageFiles/emojis/3428.svg @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3429.svg b/resources/imageFiles/emojis/3429.svg new file mode 100644 index 0000000..c4b7cce --- /dev/null +++ b/resources/imageFiles/emojis/3429.svg @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3430.svg b/resources/imageFiles/emojis/3430.svg new file mode 100644 index 0000000..046b134 --- /dev/null +++ b/resources/imageFiles/emojis/3430.svg @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3431.svg b/resources/imageFiles/emojis/3431.svg new file mode 100644 index 0000000..2d72c6a --- /dev/null +++ b/resources/imageFiles/emojis/3431.svg @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3432.svg b/resources/imageFiles/emojis/3432.svg new file mode 100644 index 0000000..84e269c --- /dev/null +++ b/resources/imageFiles/emojis/3432.svg @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3433.svg b/resources/imageFiles/emojis/3433.svg new file mode 100644 index 0000000..2850e35 --- /dev/null +++ b/resources/imageFiles/emojis/3433.svg @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3434.svg b/resources/imageFiles/emojis/3434.svg new file mode 100644 index 0000000..4783076 --- /dev/null +++ b/resources/imageFiles/emojis/3434.svg @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3435.svg b/resources/imageFiles/emojis/3435.svg new file mode 100644 index 0000000..4d2699a --- /dev/null +++ b/resources/imageFiles/emojis/3435.svg @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3436.svg b/resources/imageFiles/emojis/3436.svg new file mode 100644 index 0000000..29674bd --- /dev/null +++ b/resources/imageFiles/emojis/3436.svg @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3437.svg b/resources/imageFiles/emojis/3437.svg new file mode 100644 index 0000000..f34f051 --- /dev/null +++ b/resources/imageFiles/emojis/3437.svg @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3438.svg b/resources/imageFiles/emojis/3438.svg new file mode 100644 index 0000000..f861aad --- /dev/null +++ b/resources/imageFiles/emojis/3438.svg @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3439.svg b/resources/imageFiles/emojis/3439.svg new file mode 100644 index 0000000..bc229eb --- /dev/null +++ b/resources/imageFiles/emojis/3439.svg @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3440.svg b/resources/imageFiles/emojis/3440.svg new file mode 100644 index 0000000..f4a8e13 --- /dev/null +++ b/resources/imageFiles/emojis/3440.svg @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3441.svg b/resources/imageFiles/emojis/3441.svg new file mode 100644 index 0000000..317c19f --- /dev/null +++ b/resources/imageFiles/emojis/3441.svg @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3442.svg b/resources/imageFiles/emojis/3442.svg new file mode 100644 index 0000000..368aaf9 --- /dev/null +++ b/resources/imageFiles/emojis/3442.svg @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3443.svg b/resources/imageFiles/emojis/3443.svg new file mode 100644 index 0000000..5bdc375 --- /dev/null +++ b/resources/imageFiles/emojis/3443.svg @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3444.svg b/resources/imageFiles/emojis/3444.svg new file mode 100644 index 0000000..2837e50 --- /dev/null +++ b/resources/imageFiles/emojis/3444.svg @@ -0,0 +1,29 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3445.svg b/resources/imageFiles/emojis/3445.svg new file mode 100644 index 0000000..b5871a4 --- /dev/null +++ b/resources/imageFiles/emojis/3445.svg @@ -0,0 +1,38 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3446.svg b/resources/imageFiles/emojis/3446.svg new file mode 100644 index 0000000..f57946b --- /dev/null +++ b/resources/imageFiles/emojis/3446.svg @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3447.svg b/resources/imageFiles/emojis/3447.svg new file mode 100644 index 0000000..fa2faf7 --- /dev/null +++ b/resources/imageFiles/emojis/3447.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3448.svg b/resources/imageFiles/emojis/3448.svg new file mode 100644 index 0000000..b608294 --- /dev/null +++ b/resources/imageFiles/emojis/3448.svg @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3449.svg b/resources/imageFiles/emojis/3449.svg new file mode 100644 index 0000000..3fb1542 --- /dev/null +++ b/resources/imageFiles/emojis/3449.svg @@ -0,0 +1,46 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3450.svg b/resources/imageFiles/emojis/3450.svg new file mode 100644 index 0000000..90f46c4 --- /dev/null +++ b/resources/imageFiles/emojis/3450.svg @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3451.svg b/resources/imageFiles/emojis/3451.svg new file mode 100644 index 0000000..8e8c581 --- /dev/null +++ b/resources/imageFiles/emojis/3451.svg @@ -0,0 +1,42 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3452.svg b/resources/imageFiles/emojis/3452.svg new file mode 100644 index 0000000..b43fa7f --- /dev/null +++ b/resources/imageFiles/emojis/3452.svg @@ -0,0 +1,32 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3453.svg b/resources/imageFiles/emojis/3453.svg new file mode 100644 index 0000000..028d7f0 --- /dev/null +++ b/resources/imageFiles/emojis/3453.svg @@ -0,0 +1,34 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3454.svg b/resources/imageFiles/emojis/3454.svg new file mode 100644 index 0000000..f319dfe --- /dev/null +++ b/resources/imageFiles/emojis/3454.svg @@ -0,0 +1,36 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3455.svg b/resources/imageFiles/emojis/3455.svg new file mode 100644 index 0000000..43f1a3e --- /dev/null +++ b/resources/imageFiles/emojis/3455.svg @@ -0,0 +1,53 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3456.svg b/resources/imageFiles/emojis/3456.svg new file mode 100644 index 0000000..266f55f --- /dev/null +++ b/resources/imageFiles/emojis/3456.svg @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3457.svg b/resources/imageFiles/emojis/3457.svg new file mode 100644 index 0000000..8e222e8 --- /dev/null +++ b/resources/imageFiles/emojis/3457.svg @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3458.svg b/resources/imageFiles/emojis/3458.svg new file mode 100644 index 0000000..af9516c --- /dev/null +++ b/resources/imageFiles/emojis/3458.svg @@ -0,0 +1,53 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3459.svg b/resources/imageFiles/emojis/3459.svg new file mode 100644 index 0000000..71baefa --- /dev/null +++ b/resources/imageFiles/emojis/3459.svg @@ -0,0 +1,41 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3460.svg b/resources/imageFiles/emojis/3460.svg new file mode 100644 index 0000000..b700270 --- /dev/null +++ b/resources/imageFiles/emojis/3460.svg @@ -0,0 +1,35 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3461.svg b/resources/imageFiles/emojis/3461.svg new file mode 100644 index 0000000..3359e6b --- /dev/null +++ b/resources/imageFiles/emojis/3461.svg @@ -0,0 +1,39 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3462.svg b/resources/imageFiles/emojis/3462.svg new file mode 100644 index 0000000..5fad793 --- /dev/null +++ b/resources/imageFiles/emojis/3462.svg @@ -0,0 +1,35 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3463.svg b/resources/imageFiles/emojis/3463.svg new file mode 100644 index 0000000..cf41e9c --- /dev/null +++ b/resources/imageFiles/emojis/3463.svg @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3464.svg b/resources/imageFiles/emojis/3464.svg new file mode 100644 index 0000000..e74ca5e --- /dev/null +++ b/resources/imageFiles/emojis/3464.svg @@ -0,0 +1,29 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3465.svg b/resources/imageFiles/emojis/3465.svg new file mode 100644 index 0000000..4a3d92d --- /dev/null +++ b/resources/imageFiles/emojis/3465.svg @@ -0,0 +1,31 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3466.svg b/resources/imageFiles/emojis/3466.svg new file mode 100644 index 0000000..b711f14 --- /dev/null +++ b/resources/imageFiles/emojis/3466.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3467.svg b/resources/imageFiles/emojis/3467.svg new file mode 100644 index 0000000..2440091 --- /dev/null +++ b/resources/imageFiles/emojis/3467.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3468.svg b/resources/imageFiles/emojis/3468.svg new file mode 100644 index 0000000..9b3dc43 --- /dev/null +++ b/resources/imageFiles/emojis/3468.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3469.svg b/resources/imageFiles/emojis/3469.svg new file mode 100644 index 0000000..0c77102 --- /dev/null +++ b/resources/imageFiles/emojis/3469.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3470.svg b/resources/imageFiles/emojis/3470.svg new file mode 100644 index 0000000..a8b26a5 --- /dev/null +++ b/resources/imageFiles/emojis/3470.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3471.svg b/resources/imageFiles/emojis/3471.svg new file mode 100644 index 0000000..770c8f0 --- /dev/null +++ b/resources/imageFiles/emojis/3471.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3472.svg b/resources/imageFiles/emojis/3472.svg new file mode 100644 index 0000000..ced789e --- /dev/null +++ b/resources/imageFiles/emojis/3472.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3473.svg b/resources/imageFiles/emojis/3473.svg new file mode 100644 index 0000000..21e634c --- /dev/null +++ b/resources/imageFiles/emojis/3473.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3474.svg b/resources/imageFiles/emojis/3474.svg new file mode 100644 index 0000000..25b538b --- /dev/null +++ b/resources/imageFiles/emojis/3474.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3475.svg b/resources/imageFiles/emojis/3475.svg new file mode 100644 index 0000000..980196a --- /dev/null +++ b/resources/imageFiles/emojis/3475.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3476.svg b/resources/imageFiles/emojis/3476.svg new file mode 100644 index 0000000..83b9ef1 --- /dev/null +++ b/resources/imageFiles/emojis/3476.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3477.svg b/resources/imageFiles/emojis/3477.svg new file mode 100644 index 0000000..a871ee1 --- /dev/null +++ b/resources/imageFiles/emojis/3477.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3478.svg b/resources/imageFiles/emojis/3478.svg new file mode 100644 index 0000000..caacca8 --- /dev/null +++ b/resources/imageFiles/emojis/3478.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3479.svg b/resources/imageFiles/emojis/3479.svg new file mode 100644 index 0000000..0055beb --- /dev/null +++ b/resources/imageFiles/emojis/3479.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3480.svg b/resources/imageFiles/emojis/3480.svg new file mode 100644 index 0000000..d23909a --- /dev/null +++ b/resources/imageFiles/emojis/3480.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3481.svg b/resources/imageFiles/emojis/3481.svg new file mode 100644 index 0000000..f60c300 --- /dev/null +++ b/resources/imageFiles/emojis/3481.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3482.svg b/resources/imageFiles/emojis/3482.svg new file mode 100644 index 0000000..fcefc30 --- /dev/null +++ b/resources/imageFiles/emojis/3482.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3483.svg b/resources/imageFiles/emojis/3483.svg new file mode 100644 index 0000000..d463b4e --- /dev/null +++ b/resources/imageFiles/emojis/3483.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3484.svg b/resources/imageFiles/emojis/3484.svg new file mode 100644 index 0000000..ede2d88 --- /dev/null +++ b/resources/imageFiles/emojis/3484.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3485.svg b/resources/imageFiles/emojis/3485.svg new file mode 100644 index 0000000..3a59849 --- /dev/null +++ b/resources/imageFiles/emojis/3485.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3486.svg b/resources/imageFiles/emojis/3486.svg new file mode 100644 index 0000000..a156dc4 --- /dev/null +++ b/resources/imageFiles/emojis/3486.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3487.svg b/resources/imageFiles/emojis/3487.svg new file mode 100644 index 0000000..b1f144f --- /dev/null +++ b/resources/imageFiles/emojis/3487.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3488.svg b/resources/imageFiles/emojis/3488.svg new file mode 100644 index 0000000..7782424 --- /dev/null +++ b/resources/imageFiles/emojis/3488.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3489.svg b/resources/imageFiles/emojis/3489.svg new file mode 100644 index 0000000..4766612 --- /dev/null +++ b/resources/imageFiles/emojis/3489.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3490.svg b/resources/imageFiles/emojis/3490.svg new file mode 100644 index 0000000..1cdeef3 --- /dev/null +++ b/resources/imageFiles/emojis/3490.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3491.svg b/resources/imageFiles/emojis/3491.svg new file mode 100644 index 0000000..d86b416 --- /dev/null +++ b/resources/imageFiles/emojis/3491.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3492.svg b/resources/imageFiles/emojis/3492.svg new file mode 100644 index 0000000..a741ff2 --- /dev/null +++ b/resources/imageFiles/emojis/3492.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3493.svg b/resources/imageFiles/emojis/3493.svg new file mode 100644 index 0000000..1df2920 --- /dev/null +++ b/resources/imageFiles/emojis/3493.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3494.svg b/resources/imageFiles/emojis/3494.svg new file mode 100644 index 0000000..e5e58e3 --- /dev/null +++ b/resources/imageFiles/emojis/3494.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3495.svg b/resources/imageFiles/emojis/3495.svg new file mode 100644 index 0000000..07f6a61 --- /dev/null +++ b/resources/imageFiles/emojis/3495.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3496.svg b/resources/imageFiles/emojis/3496.svg new file mode 100644 index 0000000..4ba42db --- /dev/null +++ b/resources/imageFiles/emojis/3496.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3497.svg b/resources/imageFiles/emojis/3497.svg new file mode 100644 index 0000000..28d8500 --- /dev/null +++ b/resources/imageFiles/emojis/3497.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3498.svg b/resources/imageFiles/emojis/3498.svg new file mode 100644 index 0000000..b6b678b --- /dev/null +++ b/resources/imageFiles/emojis/3498.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3499.svg b/resources/imageFiles/emojis/3499.svg new file mode 100644 index 0000000..6a34689 --- /dev/null +++ b/resources/imageFiles/emojis/3499.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3500.svg b/resources/imageFiles/emojis/3500.svg new file mode 100644 index 0000000..568fd70 --- /dev/null +++ b/resources/imageFiles/emojis/3500.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3501.svg b/resources/imageFiles/emojis/3501.svg new file mode 100644 index 0000000..31f8978 --- /dev/null +++ b/resources/imageFiles/emojis/3501.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3502.svg b/resources/imageFiles/emojis/3502.svg new file mode 100644 index 0000000..fde59c4 --- /dev/null +++ b/resources/imageFiles/emojis/3502.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3503.svg b/resources/imageFiles/emojis/3503.svg new file mode 100644 index 0000000..6e22a45 --- /dev/null +++ b/resources/imageFiles/emojis/3503.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3504.svg b/resources/imageFiles/emojis/3504.svg new file mode 100644 index 0000000..dccc99c --- /dev/null +++ b/resources/imageFiles/emojis/3504.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3505.svg b/resources/imageFiles/emojis/3505.svg new file mode 100644 index 0000000..fbd49dd --- /dev/null +++ b/resources/imageFiles/emojis/3505.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3506.svg b/resources/imageFiles/emojis/3506.svg new file mode 100644 index 0000000..33450dc --- /dev/null +++ b/resources/imageFiles/emojis/3506.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3507.svg b/resources/imageFiles/emojis/3507.svg new file mode 100644 index 0000000..a2957dd --- /dev/null +++ b/resources/imageFiles/emojis/3507.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3508.svg b/resources/imageFiles/emojis/3508.svg new file mode 100644 index 0000000..7be33bb --- /dev/null +++ b/resources/imageFiles/emojis/3508.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3509.svg b/resources/imageFiles/emojis/3509.svg new file mode 100644 index 0000000..92657cc --- /dev/null +++ b/resources/imageFiles/emojis/3509.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3510.svg b/resources/imageFiles/emojis/3510.svg new file mode 100644 index 0000000..7b21cf1 --- /dev/null +++ b/resources/imageFiles/emojis/3510.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3511.svg b/resources/imageFiles/emojis/3511.svg new file mode 100644 index 0000000..c2df515 --- /dev/null +++ b/resources/imageFiles/emojis/3511.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3512.svg b/resources/imageFiles/emojis/3512.svg new file mode 100644 index 0000000..1a83eaa --- /dev/null +++ b/resources/imageFiles/emojis/3512.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3513.svg b/resources/imageFiles/emojis/3513.svg new file mode 100644 index 0000000..7939478 --- /dev/null +++ b/resources/imageFiles/emojis/3513.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3514.svg b/resources/imageFiles/emojis/3514.svg new file mode 100644 index 0000000..4f2c8b7 --- /dev/null +++ b/resources/imageFiles/emojis/3514.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3515.svg b/resources/imageFiles/emojis/3515.svg new file mode 100644 index 0000000..5c1fc9d --- /dev/null +++ b/resources/imageFiles/emojis/3515.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3516.svg b/resources/imageFiles/emojis/3516.svg new file mode 100644 index 0000000..12a8271 --- /dev/null +++ b/resources/imageFiles/emojis/3516.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/resources/imageFiles/emojis/3517.svg b/resources/imageFiles/emojis/3517.svg new file mode 100644 index 0000000..b0242bb --- /dev/null +++ b/resources/imageFiles/emojis/3517.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3518.svg b/resources/imageFiles/emojis/3518.svg new file mode 100644 index 0000000..0c7f152 --- /dev/null +++ b/resources/imageFiles/emojis/3518.svg @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3519.svg b/resources/imageFiles/emojis/3519.svg new file mode 100644 index 0000000..0378642 --- /dev/null +++ b/resources/imageFiles/emojis/3519.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3520.svg b/resources/imageFiles/emojis/3520.svg new file mode 100644 index 0000000..e453d61 --- /dev/null +++ b/resources/imageFiles/emojis/3520.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3521.svg b/resources/imageFiles/emojis/3521.svg new file mode 100644 index 0000000..98260a1 --- /dev/null +++ b/resources/imageFiles/emojis/3521.svg @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3522.svg b/resources/imageFiles/emojis/3522.svg new file mode 100644 index 0000000..5775f4c --- /dev/null +++ b/resources/imageFiles/emojis/3522.svg @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3523.svg b/resources/imageFiles/emojis/3523.svg new file mode 100644 index 0000000..eaf1fb6 --- /dev/null +++ b/resources/imageFiles/emojis/3523.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3524.svg b/resources/imageFiles/emojis/3524.svg new file mode 100644 index 0000000..231ca33 --- /dev/null +++ b/resources/imageFiles/emojis/3524.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3525.svg b/resources/imageFiles/emojis/3525.svg new file mode 100644 index 0000000..67b778c --- /dev/null +++ b/resources/imageFiles/emojis/3525.svg @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3526.svg b/resources/imageFiles/emojis/3526.svg new file mode 100644 index 0000000..78f5eaa --- /dev/null +++ b/resources/imageFiles/emojis/3526.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/resources/imageFiles/emojis/3527.svg b/resources/imageFiles/emojis/3527.svg new file mode 100644 index 0000000..94d05ea --- /dev/null +++ b/resources/imageFiles/emojis/3527.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3528.svg b/resources/imageFiles/emojis/3528.svg new file mode 100644 index 0000000..b9b8eb6 --- /dev/null +++ b/resources/imageFiles/emojis/3528.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3529.svg b/resources/imageFiles/emojis/3529.svg new file mode 100644 index 0000000..9e02078 --- /dev/null +++ b/resources/imageFiles/emojis/3529.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3530.svg b/resources/imageFiles/emojis/3530.svg new file mode 100644 index 0000000..1824173 --- /dev/null +++ b/resources/imageFiles/emojis/3530.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3531.svg b/resources/imageFiles/emojis/3531.svg new file mode 100644 index 0000000..33aaf06 --- /dev/null +++ b/resources/imageFiles/emojis/3531.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/resources/imageFiles/emojis/3532.svg b/resources/imageFiles/emojis/3532.svg new file mode 100644 index 0000000..ae6d086 --- /dev/null +++ b/resources/imageFiles/emojis/3532.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3533.svg b/resources/imageFiles/emojis/3533.svg new file mode 100644 index 0000000..e5ebaff --- /dev/null +++ b/resources/imageFiles/emojis/3533.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3534.svg b/resources/imageFiles/emojis/3534.svg new file mode 100644 index 0000000..af313a1 --- /dev/null +++ b/resources/imageFiles/emojis/3534.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3535.svg b/resources/imageFiles/emojis/3535.svg new file mode 100644 index 0000000..c7ea383 --- /dev/null +++ b/resources/imageFiles/emojis/3535.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/resources/imageFiles/emojis/3536.svg b/resources/imageFiles/emojis/3536.svg new file mode 100644 index 0000000..6df2e56 --- /dev/null +++ b/resources/imageFiles/emojis/3536.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3537.svg b/resources/imageFiles/emojis/3537.svg new file mode 100644 index 0000000..ee593a6 --- /dev/null +++ b/resources/imageFiles/emojis/3537.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3538.svg b/resources/imageFiles/emojis/3538.svg new file mode 100644 index 0000000..7b5c7ef --- /dev/null +++ b/resources/imageFiles/emojis/3538.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3539.svg b/resources/imageFiles/emojis/3539.svg new file mode 100644 index 0000000..c3c30ce --- /dev/null +++ b/resources/imageFiles/emojis/3539.svg @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3540.svg b/resources/imageFiles/emojis/3540.svg new file mode 100644 index 0000000..79d98bb --- /dev/null +++ b/resources/imageFiles/emojis/3540.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/resources/imageFiles/emojis/3541.svg b/resources/imageFiles/emojis/3541.svg new file mode 100644 index 0000000..afa8607 --- /dev/null +++ b/resources/imageFiles/emojis/3541.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3542.svg b/resources/imageFiles/emojis/3542.svg new file mode 100644 index 0000000..5163cec --- /dev/null +++ b/resources/imageFiles/emojis/3542.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3543.svg b/resources/imageFiles/emojis/3543.svg new file mode 100644 index 0000000..fc96e67 --- /dev/null +++ b/resources/imageFiles/emojis/3543.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3544.svg b/resources/imageFiles/emojis/3544.svg new file mode 100644 index 0000000..9eac929 --- /dev/null +++ b/resources/imageFiles/emojis/3544.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/resources/imageFiles/emojis/3545.svg b/resources/imageFiles/emojis/3545.svg new file mode 100644 index 0000000..abbd9c6 --- /dev/null +++ b/resources/imageFiles/emojis/3545.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3546.svg b/resources/imageFiles/emojis/3546.svg new file mode 100644 index 0000000..5d0215f --- /dev/null +++ b/resources/imageFiles/emojis/3546.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3547.svg b/resources/imageFiles/emojis/3547.svg new file mode 100644 index 0000000..ac5d229 --- /dev/null +++ b/resources/imageFiles/emojis/3547.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3548.svg b/resources/imageFiles/emojis/3548.svg new file mode 100644 index 0000000..b1e3ab2 --- /dev/null +++ b/resources/imageFiles/emojis/3548.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3549.svg b/resources/imageFiles/emojis/3549.svg new file mode 100644 index 0000000..343585f --- /dev/null +++ b/resources/imageFiles/emojis/3549.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3550.svg b/resources/imageFiles/emojis/3550.svg new file mode 100644 index 0000000..b06b184 --- /dev/null +++ b/resources/imageFiles/emojis/3550.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3551.svg b/resources/imageFiles/emojis/3551.svg new file mode 100644 index 0000000..9b8b053 --- /dev/null +++ b/resources/imageFiles/emojis/3551.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/resources/imageFiles/emojis/3552.svg b/resources/imageFiles/emojis/3552.svg new file mode 100644 index 0000000..e418c6c --- /dev/null +++ b/resources/imageFiles/emojis/3552.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3553.svg b/resources/imageFiles/emojis/3553.svg new file mode 100644 index 0000000..023badb --- /dev/null +++ b/resources/imageFiles/emojis/3553.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3554.svg b/resources/imageFiles/emojis/3554.svg new file mode 100644 index 0000000..ff2a58b --- /dev/null +++ b/resources/imageFiles/emojis/3554.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3555.svg b/resources/imageFiles/emojis/3555.svg new file mode 100644 index 0000000..0aa758a --- /dev/null +++ b/resources/imageFiles/emojis/3555.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3556.svg b/resources/imageFiles/emojis/3556.svg new file mode 100644 index 0000000..d818352 --- /dev/null +++ b/resources/imageFiles/emojis/3556.svg @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3557.svg b/resources/imageFiles/emojis/3557.svg new file mode 100644 index 0000000..af41ff8 --- /dev/null +++ b/resources/imageFiles/emojis/3557.svg @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3558.svg b/resources/imageFiles/emojis/3558.svg new file mode 100644 index 0000000..0ab4ecf --- /dev/null +++ b/resources/imageFiles/emojis/3558.svg @@ -0,0 +1,42 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3559.svg b/resources/imageFiles/emojis/3559.svg new file mode 100644 index 0000000..b39c5d0 --- /dev/null +++ b/resources/imageFiles/emojis/3559.svg @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3560.svg b/resources/imageFiles/emojis/3560.svg new file mode 100644 index 0000000..904da44 --- /dev/null +++ b/resources/imageFiles/emojis/3560.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3561.svg b/resources/imageFiles/emojis/3561.svg new file mode 100644 index 0000000..2b6cbbc --- /dev/null +++ b/resources/imageFiles/emojis/3561.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3562.svg b/resources/imageFiles/emojis/3562.svg new file mode 100644 index 0000000..df7a801 --- /dev/null +++ b/resources/imageFiles/emojis/3562.svg @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3563.svg b/resources/imageFiles/emojis/3563.svg new file mode 100644 index 0000000..dfb3f53 --- /dev/null +++ b/resources/imageFiles/emojis/3563.svg @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3564.svg b/resources/imageFiles/emojis/3564.svg new file mode 100644 index 0000000..803a1b9 --- /dev/null +++ b/resources/imageFiles/emojis/3564.svg @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3565.svg b/resources/imageFiles/emojis/3565.svg new file mode 100644 index 0000000..6cdc5a4 --- /dev/null +++ b/resources/imageFiles/emojis/3565.svg @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3566.svg b/resources/imageFiles/emojis/3566.svg new file mode 100644 index 0000000..7304152 --- /dev/null +++ b/resources/imageFiles/emojis/3566.svg @@ -0,0 +1,38 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3567.svg b/resources/imageFiles/emojis/3567.svg new file mode 100644 index 0000000..eb88a96 --- /dev/null +++ b/resources/imageFiles/emojis/3567.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3568.svg b/resources/imageFiles/emojis/3568.svg new file mode 100644 index 0000000..f1461cc --- /dev/null +++ b/resources/imageFiles/emojis/3568.svg @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3569.svg b/resources/imageFiles/emojis/3569.svg new file mode 100644 index 0000000..02c5ded --- /dev/null +++ b/resources/imageFiles/emojis/3569.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3570.svg b/resources/imageFiles/emojis/3570.svg new file mode 100644 index 0000000..0e73e68 --- /dev/null +++ b/resources/imageFiles/emojis/3570.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3571.svg b/resources/imageFiles/emojis/3571.svg new file mode 100644 index 0000000..f6672f8 --- /dev/null +++ b/resources/imageFiles/emojis/3571.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3572.svg b/resources/imageFiles/emojis/3572.svg new file mode 100644 index 0000000..905b54d --- /dev/null +++ b/resources/imageFiles/emojis/3572.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3573.svg b/resources/imageFiles/emojis/3573.svg new file mode 100644 index 0000000..c6a24af --- /dev/null +++ b/resources/imageFiles/emojis/3573.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3574.svg b/resources/imageFiles/emojis/3574.svg new file mode 100644 index 0000000..af2f783 --- /dev/null +++ b/resources/imageFiles/emojis/3574.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3575.svg b/resources/imageFiles/emojis/3575.svg new file mode 100644 index 0000000..72b470e --- /dev/null +++ b/resources/imageFiles/emojis/3575.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3576.svg b/resources/imageFiles/emojis/3576.svg new file mode 100644 index 0000000..219b54d --- /dev/null +++ b/resources/imageFiles/emojis/3576.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3577.svg b/resources/imageFiles/emojis/3577.svg new file mode 100644 index 0000000..95754ea --- /dev/null +++ b/resources/imageFiles/emojis/3577.svg @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3578.svg b/resources/imageFiles/emojis/3578.svg new file mode 100644 index 0000000..e6aff7c --- /dev/null +++ b/resources/imageFiles/emojis/3578.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3579.svg b/resources/imageFiles/emojis/3579.svg new file mode 100644 index 0000000..3858fb5 --- /dev/null +++ b/resources/imageFiles/emojis/3579.svg @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3580.svg b/resources/imageFiles/emojis/3580.svg new file mode 100644 index 0000000..3ddfc58 --- /dev/null +++ b/resources/imageFiles/emojis/3580.svg @@ -0,0 +1,32 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3581.svg b/resources/imageFiles/emojis/3581.svg new file mode 100644 index 0000000..1785354 --- /dev/null +++ b/resources/imageFiles/emojis/3581.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3582.svg b/resources/imageFiles/emojis/3582.svg new file mode 100644 index 0000000..017ca9c --- /dev/null +++ b/resources/imageFiles/emojis/3582.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3583.svg b/resources/imageFiles/emojis/3583.svg new file mode 100644 index 0000000..1c09aaf --- /dev/null +++ b/resources/imageFiles/emojis/3583.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3584.svg b/resources/imageFiles/emojis/3584.svg new file mode 100644 index 0000000..f228849 --- /dev/null +++ b/resources/imageFiles/emojis/3584.svg @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3585.svg b/resources/imageFiles/emojis/3585.svg new file mode 100644 index 0000000..614ae64 --- /dev/null +++ b/resources/imageFiles/emojis/3585.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3586.svg b/resources/imageFiles/emojis/3586.svg new file mode 100644 index 0000000..e3242d2 --- /dev/null +++ b/resources/imageFiles/emojis/3586.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3587.svg b/resources/imageFiles/emojis/3587.svg new file mode 100644 index 0000000..dd207de --- /dev/null +++ b/resources/imageFiles/emojis/3587.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3588.svg b/resources/imageFiles/emojis/3588.svg new file mode 100644 index 0000000..b3983ed --- /dev/null +++ b/resources/imageFiles/emojis/3588.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3589.svg b/resources/imageFiles/emojis/3589.svg new file mode 100644 index 0000000..b33ca07 --- /dev/null +++ b/resources/imageFiles/emojis/3589.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3590.svg b/resources/imageFiles/emojis/3590.svg new file mode 100644 index 0000000..9be4d3c --- /dev/null +++ b/resources/imageFiles/emojis/3590.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3591.svg b/resources/imageFiles/emojis/3591.svg new file mode 100644 index 0000000..1a7568f --- /dev/null +++ b/resources/imageFiles/emojis/3591.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3592.svg b/resources/imageFiles/emojis/3592.svg new file mode 100644 index 0000000..4be64be --- /dev/null +++ b/resources/imageFiles/emojis/3592.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/resources/imageFiles/emojis/3593.svg b/resources/imageFiles/emojis/3593.svg new file mode 100644 index 0000000..3ebb298 --- /dev/null +++ b/resources/imageFiles/emojis/3593.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/resources/imageFiles/emojis/3594.svg b/resources/imageFiles/emojis/3594.svg new file mode 100644 index 0000000..d1fa4da --- /dev/null +++ b/resources/imageFiles/emojis/3594.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/resources/imageFiles/emojis/3595.svg b/resources/imageFiles/emojis/3595.svg new file mode 100644 index 0000000..316c28c --- /dev/null +++ b/resources/imageFiles/emojis/3595.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/resources/imageFiles/emojis/3596.svg b/resources/imageFiles/emojis/3596.svg new file mode 100644 index 0000000..63db1cd --- /dev/null +++ b/resources/imageFiles/emojis/3596.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3597.svg b/resources/imageFiles/emojis/3597.svg new file mode 100644 index 0000000..17c9317 --- /dev/null +++ b/resources/imageFiles/emojis/3597.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/resources/imageFiles/emojis/3598.svg b/resources/imageFiles/emojis/3598.svg new file mode 100644 index 0000000..858d0d8 --- /dev/null +++ b/resources/imageFiles/emojis/3598.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3599.svg b/resources/imageFiles/emojis/3599.svg new file mode 100644 index 0000000..0bb696b --- /dev/null +++ b/resources/imageFiles/emojis/3599.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/resources/imageFiles/emojis/3600.svg b/resources/imageFiles/emojis/3600.svg new file mode 100644 index 0000000..f8aef05 --- /dev/null +++ b/resources/imageFiles/emojis/3600.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3601.svg b/resources/imageFiles/emojis/3601.svg new file mode 100644 index 0000000..55ff45c --- /dev/null +++ b/resources/imageFiles/emojis/3601.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3602.svg b/resources/imageFiles/emojis/3602.svg new file mode 100644 index 0000000..405867f --- /dev/null +++ b/resources/imageFiles/emojis/3602.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3603.svg b/resources/imageFiles/emojis/3603.svg new file mode 100644 index 0000000..35bfbb9 --- /dev/null +++ b/resources/imageFiles/emojis/3603.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3604.svg b/resources/imageFiles/emojis/3604.svg new file mode 100644 index 0000000..5c184b0 --- /dev/null +++ b/resources/imageFiles/emojis/3604.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3605.svg b/resources/imageFiles/emojis/3605.svg new file mode 100644 index 0000000..8c509c4 --- /dev/null +++ b/resources/imageFiles/emojis/3605.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3606.svg b/resources/imageFiles/emojis/3606.svg new file mode 100644 index 0000000..d7eb62a --- /dev/null +++ b/resources/imageFiles/emojis/3606.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3607.svg b/resources/imageFiles/emojis/3607.svg new file mode 100644 index 0000000..cc96d87 --- /dev/null +++ b/resources/imageFiles/emojis/3607.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3608.svg b/resources/imageFiles/emojis/3608.svg new file mode 100644 index 0000000..2a2d5fb --- /dev/null +++ b/resources/imageFiles/emojis/3608.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3609.svg b/resources/imageFiles/emojis/3609.svg new file mode 100644 index 0000000..310f71f --- /dev/null +++ b/resources/imageFiles/emojis/3609.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3610.svg b/resources/imageFiles/emojis/3610.svg new file mode 100644 index 0000000..4acbe3f --- /dev/null +++ b/resources/imageFiles/emojis/3610.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3611.svg b/resources/imageFiles/emojis/3611.svg new file mode 100644 index 0000000..c04c15c --- /dev/null +++ b/resources/imageFiles/emojis/3611.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3612.svg b/resources/imageFiles/emojis/3612.svg new file mode 100644 index 0000000..d54d2db --- /dev/null +++ b/resources/imageFiles/emojis/3612.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3613.svg b/resources/imageFiles/emojis/3613.svg new file mode 100644 index 0000000..43fbc5d --- /dev/null +++ b/resources/imageFiles/emojis/3613.svg @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3614.svg b/resources/imageFiles/emojis/3614.svg new file mode 100644 index 0000000..23f387f --- /dev/null +++ b/resources/imageFiles/emojis/3614.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3615.svg b/resources/imageFiles/emojis/3615.svg new file mode 100644 index 0000000..6635b70 --- /dev/null +++ b/resources/imageFiles/emojis/3615.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3616.svg b/resources/imageFiles/emojis/3616.svg new file mode 100644 index 0000000..45a1981 --- /dev/null +++ b/resources/imageFiles/emojis/3616.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3617.svg b/resources/imageFiles/emojis/3617.svg new file mode 100644 index 0000000..a83a977 --- /dev/null +++ b/resources/imageFiles/emojis/3617.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3618.svg b/resources/imageFiles/emojis/3618.svg new file mode 100644 index 0000000..a890dc6 --- /dev/null +++ b/resources/imageFiles/emojis/3618.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3619.svg b/resources/imageFiles/emojis/3619.svg new file mode 100644 index 0000000..dad7931 --- /dev/null +++ b/resources/imageFiles/emojis/3619.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3620.svg b/resources/imageFiles/emojis/3620.svg new file mode 100644 index 0000000..a6b2530 --- /dev/null +++ b/resources/imageFiles/emojis/3620.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3621.svg b/resources/imageFiles/emojis/3621.svg new file mode 100644 index 0000000..e8cb6ef --- /dev/null +++ b/resources/imageFiles/emojis/3621.svg @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3622.svg b/resources/imageFiles/emojis/3622.svg new file mode 100644 index 0000000..713f2c1 --- /dev/null +++ b/resources/imageFiles/emojis/3622.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3623.svg b/resources/imageFiles/emojis/3623.svg new file mode 100644 index 0000000..4a29b6d --- /dev/null +++ b/resources/imageFiles/emojis/3623.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3624.svg b/resources/imageFiles/emojis/3624.svg new file mode 100644 index 0000000..cd4e400 --- /dev/null +++ b/resources/imageFiles/emojis/3624.svg @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3625.svg b/resources/imageFiles/emojis/3625.svg new file mode 100644 index 0000000..7c6919c --- /dev/null +++ b/resources/imageFiles/emojis/3625.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3626.svg b/resources/imageFiles/emojis/3626.svg new file mode 100644 index 0000000..8560afb --- /dev/null +++ b/resources/imageFiles/emojis/3626.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3627.svg b/resources/imageFiles/emojis/3627.svg new file mode 100644 index 0000000..6be3265 --- /dev/null +++ b/resources/imageFiles/emojis/3627.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3628.svg b/resources/imageFiles/emojis/3628.svg new file mode 100644 index 0000000..2fd8aa4 --- /dev/null +++ b/resources/imageFiles/emojis/3628.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3629.svg b/resources/imageFiles/emojis/3629.svg new file mode 100644 index 0000000..baa7b76 --- /dev/null +++ b/resources/imageFiles/emojis/3629.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/resources/imageFiles/emojis/3630.svg b/resources/imageFiles/emojis/3630.svg new file mode 100644 index 0000000..548da12 --- /dev/null +++ b/resources/imageFiles/emojis/3630.svg @@ -0,0 +1,44 @@ + + + diff --git a/resources/imageFiles/emojis/3631.svg b/resources/imageFiles/emojis/3631.svg new file mode 100644 index 0000000..b6d46ee --- /dev/null +++ b/resources/imageFiles/emojis/3631.svg @@ -0,0 +1,41 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/resources/imageFiles/icons.go b/resources/imageFiles/icons.go new file mode 100644 index 0000000..70d82c7 --- /dev/null +++ b/resources/imageFiles/icons.go @@ -0,0 +1,318 @@ +package imageFiles + +//TODO: Add Ignore/Unignore + +import _ "embed" + +import "errors" + +//go:embed icons/broadcast.svg +var Icon_Broadcast []byte + +//go:embed icons/buildQuestionnaire.svg +var Icon_BuildQuestionnaire []byte + +//go:embed icons/cardano.svg +var Icon_Cardano []byte + +//go:embed icons/chat.svg +var Icon_Chat []byte + +//go:embed icons/check.svg +var Icon_Check []byte + +//go:embed icons/choice.svg +var Icon_Choice []byte + +//go:embed icons/clipboard.svg +var Icon_Clipboard []byte + +//go:embed icons/contacts.svg +var Icon_Contacts []byte + +//go:embed icons/controversy.svg +var Icon_Controversy []byte + +//go:embed icons/couple.svg +var Icon_Couple []byte + +//go:embed icons/desires.svg +var Icon_Desires []byte + +//go:embed icons/entry.svg +var Icon_Entry []byte + +//go:embed icons/error.svg +var Icon_Error []byte + +//go:embed icons/ethereum.svg +var Icon_Ethereum []byte + +//go:embed icons/funds.svg +var Icon_Funds []byte + +//go:embed icons/general.svg +var Icon_General []byte + +//go:embed icons/genome.svg +var Icon_Genome []byte + +//go:embed icons/greet.svg +var Icon_Greet []byte + +//go:embed icons/home.svg +var Icon_Home []byte + +//go:embed icons/host.svg +var Icon_Host []byte + +//go:embed icons/info.svg +var Icon_Info []byte + +//go:embed icons/inspectText.svg +var Icon_InspectText []byte + +//go:embed icons/insufficient.svg +var Icon_Insufficient []byte + +//go:embed icons/lifestyle.svg +var Icon_Lifestyle []byte + +//go:embed icons/like.svg +var Icon_Like []byte + +//go:embed icons/local.svg +var Icon_Local []byte + +//go:embed icons/matchScore.svg +var Icon_MatchScore []byte + +//go:embed icons/mate.svg +var Icon_Mate []byte + +//go:embed icons/mental.svg +var Icon_Mental []byte + +//go:embed icons/message.svg +var Icon_Message []byte + +//go:embed icons/moderate.svg +var Icon_Moderate []byte + +//go:embed icons/moon.svg +var Icon_Moon []byte + +//go:embed icons/ocean.svg +var Icon_Ocean []byte + +//go:embed icons/person.svg +var Icon_Person []byte + +//go:embed icons/photo.svg +var Icon_Photo []byte + +//go:embed icons/plus.svg +var Icon_Plus []byte + +//go:embed icons/profile.svg +var Icon_Profile []byte + +//go:embed icons/qr.svg +var Icon_QR []byte + +//go:embed icons/questionnaire.svg +var Icon_Questionnaire []byte + +//go:embed icons/reject.svg +var Icon_Reject []byte + +//go:embed icons/score.svg +var Icon_Score []byte + +//go:embed icons/settings.svg +var Icon_Settings []byte + +//go:embed icons/stats.svg +var Icon_Stats []byte + +//go:embed icons/sufficient.svg +var Icon_Sufficient []byte + +//go:embed icons/sun.svg +var Icon_Sun []byte + +//go:embed icons/sync.svg +var Icon_Sync []byte + +//go:embed icons/toggleOff.svg +var Icon_ToggleOff []byte + +//go:embed icons/toggleOn.svg +var Icon_ToggleOn []byte + +//go:embed icons/unlike.svg +var Icon_Unlike []byte + +//go:embed icons/users.svg +var Icon_Users []byte + + +func GetIconFileBytesFromName(iconName string)([]byte, error){ + + switch iconName{ + + case "Broadcast":{ + return Icon_Broadcast, nil + } + case "BuildQuestionnaire":{ + return Icon_BuildQuestionnaire, nil + } + case "Cardano":{ + return Icon_Cardano, nil + } + case "Chat":{ + return Icon_Chat, nil + } + case "Check":{ + return Icon_Check, nil + } + case "Choice":{ + return Icon_Choice, nil + } + case "Clipboard":{ + return Icon_Clipboard, nil + } + case "Contacts":{ + return Icon_Contacts, nil + } + case "Controversy":{ + return Icon_Controversy, nil + } + case "Couple":{ + return Icon_Couple, nil + } + case "Desires":{ + return Icon_Desires, nil + } + case "Entry":{ + return Icon_Entry, nil + } + case "Error":{ + return Icon_Error, nil + } + case "Ethereum":{ + return Icon_Ethereum, nil + } + case "Funds":{ + return Icon_Funds, nil + } + case "General":{ + return Icon_General, nil + } + case "Genome":{ + return Icon_Genome, nil + } + case "Greet":{ + return Icon_Greet, nil + } + case "Home":{ + return Icon_Home, nil + } + case "Host":{ + return Icon_Host, nil + } + case "Info":{ + return Icon_Info, nil + } + case "InspectText":{ + return Icon_InspectText, nil + } + case "Insufficient":{ + return Icon_Insufficient, nil + } + case "Lifestyle":{ + return Icon_Lifestyle, nil + } + case "Like":{ + return Icon_Like, nil + } + case "Local":{ + return Icon_Local, nil + } + case "MatchScore":{ + return Icon_MatchScore, nil + } + case "Mate":{ + return Icon_Mate, nil + } + case "Mental":{ + return Icon_Mental, nil + } + case "Message":{ + return Icon_Message, nil + } + case "Moderate":{ + return Icon_Moderate, nil + } + case "Moon":{ + return Icon_Moon, nil + } + case "Ocean":{ + return Icon_Ocean, nil + } + case "Person":{ + return Icon_Person, nil + } + case "Photo":{ + return Icon_Photo, nil + } + case "Plus":{ + return Icon_Plus, nil + } + case "Profile":{ + return Icon_Profile, nil + } + case "QR":{ + return Icon_QR, nil + } + case "Questionnaire":{ + return Icon_Questionnaire, nil + } + case "Reject":{ + return Icon_Reject, nil + } + case "Score":{ + return Icon_Score, nil + } + case "Settings":{ + return Icon_Settings, nil + } + case "Stats":{ + return Icon_Stats, nil + } + case "Sufficient":{ + return Icon_Sufficient, nil + } + case "Sun":{ + return Icon_Sun, nil + } + case "Sync":{ + return Icon_Sync, nil + } + case "ToggleOff":{ + return Icon_ToggleOff, nil + } + case "ToggleOn":{ + return Icon_ToggleOn, nil + } + case "Unlike":{ + return Icon_Unlike, nil + } + case "Users":{ + return Icon_Users, nil + } + } + + return nil, errors.New("GetIconFileBytesFromName called with unknown icon: " + iconName) +} + diff --git a/resources/imageFiles/icons/broadcast.svg b/resources/imageFiles/icons/broadcast.svg new file mode 100644 index 0000000..d6a7b7a --- /dev/null +++ b/resources/imageFiles/icons/broadcast.svg @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/icons/buildQuestionnaire.svg b/resources/imageFiles/icons/buildQuestionnaire.svg new file mode 100644 index 0000000..64637c3 --- /dev/null +++ b/resources/imageFiles/icons/buildQuestionnaire.svg @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/icons/cardano.svg b/resources/imageFiles/icons/cardano.svg new file mode 100644 index 0000000..b6d46ee --- /dev/null +++ b/resources/imageFiles/icons/cardano.svg @@ -0,0 +1,41 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/resources/imageFiles/icons/chat.svg b/resources/imageFiles/icons/chat.svg new file mode 100644 index 0000000..e36c2f6 --- /dev/null +++ b/resources/imageFiles/icons/chat.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/icons/check.svg b/resources/imageFiles/icons/check.svg new file mode 100644 index 0000000..8da210f --- /dev/null +++ b/resources/imageFiles/icons/check.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/resources/imageFiles/icons/choice.svg b/resources/imageFiles/icons/choice.svg new file mode 100644 index 0000000..11d46fe --- /dev/null +++ b/resources/imageFiles/icons/choice.svg @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/icons/clipboard.svg b/resources/imageFiles/icons/clipboard.svg new file mode 100644 index 0000000..7c5edae --- /dev/null +++ b/resources/imageFiles/icons/clipboard.svg @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/icons/contacts.svg b/resources/imageFiles/icons/contacts.svg new file mode 100644 index 0000000..0918c3d --- /dev/null +++ b/resources/imageFiles/icons/contacts.svg @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/icons/controversy.svg b/resources/imageFiles/icons/controversy.svg new file mode 100644 index 0000000..e9124c3 --- /dev/null +++ b/resources/imageFiles/icons/controversy.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/resources/imageFiles/icons/couple.svg b/resources/imageFiles/icons/couple.svg new file mode 100644 index 0000000..5b8ad82 --- /dev/null +++ b/resources/imageFiles/icons/couple.svg @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/icons/desires.svg b/resources/imageFiles/icons/desires.svg new file mode 100644 index 0000000..f21c2b5 --- /dev/null +++ b/resources/imageFiles/icons/desires.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/resources/imageFiles/icons/entry.svg b/resources/imageFiles/icons/entry.svg new file mode 100644 index 0000000..1f42967 --- /dev/null +++ b/resources/imageFiles/icons/entry.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/resources/imageFiles/icons/error.svg b/resources/imageFiles/icons/error.svg new file mode 100644 index 0000000..ae314e9 --- /dev/null +++ b/resources/imageFiles/icons/error.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/icons/ethereum.svg b/resources/imageFiles/icons/ethereum.svg new file mode 100644 index 0000000..baa7b76 --- /dev/null +++ b/resources/imageFiles/icons/ethereum.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/resources/imageFiles/icons/funds.svg b/resources/imageFiles/icons/funds.svg new file mode 100644 index 0000000..9e08fbf --- /dev/null +++ b/resources/imageFiles/icons/funds.svg @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/icons/general.svg b/resources/imageFiles/icons/general.svg new file mode 100644 index 0000000..6411e3d --- /dev/null +++ b/resources/imageFiles/icons/general.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/resources/imageFiles/icons/genome.svg b/resources/imageFiles/icons/genome.svg new file mode 100644 index 0000000..289041e --- /dev/null +++ b/resources/imageFiles/icons/genome.svg @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/icons/greet.svg b/resources/imageFiles/icons/greet.svg new file mode 100644 index 0000000..e19d16f --- /dev/null +++ b/resources/imageFiles/icons/greet.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/icons/home.svg b/resources/imageFiles/icons/home.svg new file mode 100644 index 0000000..5f4f09e --- /dev/null +++ b/resources/imageFiles/icons/home.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/resources/imageFiles/icons/host.svg b/resources/imageFiles/icons/host.svg new file mode 100644 index 0000000..40f1b2d --- /dev/null +++ b/resources/imageFiles/icons/host.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/resources/imageFiles/icons/info.svg b/resources/imageFiles/icons/info.svg new file mode 100644 index 0000000..c04c15c --- /dev/null +++ b/resources/imageFiles/icons/info.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/resources/imageFiles/icons/inspectText.svg b/resources/imageFiles/icons/inspectText.svg new file mode 100644 index 0000000..18c9454 --- /dev/null +++ b/resources/imageFiles/icons/inspectText.svg @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/icons/insufficient.svg b/resources/imageFiles/icons/insufficient.svg new file mode 100644 index 0000000..3f85bd5 --- /dev/null +++ b/resources/imageFiles/icons/insufficient.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/resources/imageFiles/icons/lifestyle.svg b/resources/imageFiles/icons/lifestyle.svg new file mode 100644 index 0000000..4f2eede --- /dev/null +++ b/resources/imageFiles/icons/lifestyle.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/resources/imageFiles/icons/like.svg b/resources/imageFiles/icons/like.svg new file mode 100644 index 0000000..b0f4276 --- /dev/null +++ b/resources/imageFiles/icons/like.svg @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/icons/local.svg b/resources/imageFiles/icons/local.svg new file mode 100644 index 0000000..a08d443 --- /dev/null +++ b/resources/imageFiles/icons/local.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/icons/matchScore.svg b/resources/imageFiles/icons/matchScore.svg new file mode 100644 index 0000000..8ffe8c3 --- /dev/null +++ b/resources/imageFiles/icons/matchScore.svg @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/icons/mate.svg b/resources/imageFiles/icons/mate.svg new file mode 100644 index 0000000..cc5d442 --- /dev/null +++ b/resources/imageFiles/icons/mate.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/resources/imageFiles/icons/mental.svg b/resources/imageFiles/icons/mental.svg new file mode 100644 index 0000000..a1e32c6 --- /dev/null +++ b/resources/imageFiles/icons/mental.svg @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/icons/message.svg b/resources/imageFiles/icons/message.svg new file mode 100644 index 0000000..1bba4f8 --- /dev/null +++ b/resources/imageFiles/icons/message.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/resources/imageFiles/icons/moderate.svg b/resources/imageFiles/icons/moderate.svg new file mode 100644 index 0000000..c8bcb7d --- /dev/null +++ b/resources/imageFiles/icons/moderate.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/icons/moon.svg b/resources/imageFiles/icons/moon.svg new file mode 100644 index 0000000..51596eb --- /dev/null +++ b/resources/imageFiles/icons/moon.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/resources/imageFiles/icons/ocean.svg b/resources/imageFiles/icons/ocean.svg new file mode 100644 index 0000000..b91c030 --- /dev/null +++ b/resources/imageFiles/icons/ocean.svg @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/icons/person.svg b/resources/imageFiles/icons/person.svg new file mode 100644 index 0000000..0ae925c --- /dev/null +++ b/resources/imageFiles/icons/person.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/icons/photo.svg b/resources/imageFiles/icons/photo.svg new file mode 100644 index 0000000..270ac8d --- /dev/null +++ b/resources/imageFiles/icons/photo.svg @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/icons/plus.svg b/resources/imageFiles/icons/plus.svg new file mode 100644 index 0000000..0d93791 --- /dev/null +++ b/resources/imageFiles/icons/plus.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/resources/imageFiles/icons/profile.svg b/resources/imageFiles/icons/profile.svg new file mode 100644 index 0000000..c4f43a9 --- /dev/null +++ b/resources/imageFiles/icons/profile.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/resources/imageFiles/icons/qr.svg b/resources/imageFiles/icons/qr.svg new file mode 100644 index 0000000..708895f --- /dev/null +++ b/resources/imageFiles/icons/qr.svg @@ -0,0 +1,67 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/icons/questionnaire.svg b/resources/imageFiles/icons/questionnaire.svg new file mode 100644 index 0000000..20e986e --- /dev/null +++ b/resources/imageFiles/icons/questionnaire.svg @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/icons/reject.svg b/resources/imageFiles/icons/reject.svg new file mode 100644 index 0000000..4b81048 --- /dev/null +++ b/resources/imageFiles/icons/reject.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/resources/imageFiles/icons/score.svg b/resources/imageFiles/icons/score.svg new file mode 100644 index 0000000..d1a4e95 --- /dev/null +++ b/resources/imageFiles/icons/score.svg @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/icons/settings.svg b/resources/imageFiles/icons/settings.svg new file mode 100644 index 0000000..294caa8 --- /dev/null +++ b/resources/imageFiles/icons/settings.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/resources/imageFiles/icons/stats.svg b/resources/imageFiles/icons/stats.svg new file mode 100644 index 0000000..545a362 --- /dev/null +++ b/resources/imageFiles/icons/stats.svg @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/icons/sufficient.svg b/resources/imageFiles/icons/sufficient.svg new file mode 100644 index 0000000..631b6ab --- /dev/null +++ b/resources/imageFiles/icons/sufficient.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/resources/imageFiles/icons/sun.svg b/resources/imageFiles/icons/sun.svg new file mode 100644 index 0000000..2f71c92 --- /dev/null +++ b/resources/imageFiles/icons/sun.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/icons/sync.svg b/resources/imageFiles/icons/sync.svg new file mode 100644 index 0000000..08fef35 --- /dev/null +++ b/resources/imageFiles/icons/sync.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/resources/imageFiles/icons/toggleOff.svg b/resources/imageFiles/icons/toggleOff.svg new file mode 100644 index 0000000..fd952b3 --- /dev/null +++ b/resources/imageFiles/icons/toggleOff.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/icons/toggleOn.svg b/resources/imageFiles/icons/toggleOn.svg new file mode 100644 index 0000000..f034327 --- /dev/null +++ b/resources/imageFiles/icons/toggleOn.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/icons/unlike.svg b/resources/imageFiles/icons/unlike.svg new file mode 100644 index 0000000..a6f582d --- /dev/null +++ b/resources/imageFiles/icons/unlike.svg @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/icons/users.svg b/resources/imageFiles/icons/users.svg new file mode 100644 index 0000000..c4f6fac --- /dev/null +++ b/resources/imageFiles/icons/users.svg @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/imageFiles/imageFiles_test.go b/resources/imageFiles/imageFiles_test.go new file mode 100644 index 0000000..2baa1db --- /dev/null +++ b/resources/imageFiles/imageFiles_test.go @@ -0,0 +1,99 @@ + +// imageFiles provides functions to retrieve image files +// The image files are embedded in the binary + +package imageFiles_test + +import "seekia/resources/imageFiles" + +import "seekia/internal/helpers" +import "seekia/internal/imagery" +import "seekia/internal/cryptography/blake3" + +import "testing" + + +func TestEmojis(t *testing.T){ + + numberOfEmojis := 3631 + + // We make sure no duplicate emoji identifiers exist in the categories + foundIdentifiersMap := make(map[int]struct{}) + + categoryNamesList := imageFiles.GetEmojiCategoriesList() + + for _, categoryName := range categoryNamesList{ + + emojisList, err := imageFiles.GetEmojisInCategoryList(categoryName) + if (err != nil) { + t.Fatalf("Failed to get emojis in category list: " + err.Error()) + } + + for _, emojiIdentifier := range emojisList{ + + if (emojiIdentifier < 1 || emojiIdentifier > numberOfEmojis){ + emojiIdentifierString := helpers.ConvertIntToString(emojiIdentifier) + t.Fatalf("Invalid emoji identifier found in category " + categoryName + ": " + emojiIdentifierString) + } + + _, exists := foundIdentifiersMap[emojiIdentifier] + if (exists == true){ + + emojiIdentifierString := helpers.ConvertIntToString(emojiIdentifier) + t.Fatalf("Duplicate emoji found in category " + categoryName + ": " + emojiIdentifierString) + } + + foundIdentifiersMap[emojiIdentifier] = struct{}{} + } + } + + // We use this map to make sure no file duplicates exist + fileHashesMap := make(map[string]struct{}) + + for i:=1; i <= numberOfEmojis; i++{ + + emojiFileBytes, err := imageFiles.GetEmojiFileBytesFromIdentifier(i) + if (err != nil) { + t.Fatalf("Failed to get emoji from identifier: " + err.Error()) + } + + fileHash, err := blake3.GetBlake3HashAsHexString(16, emojiFileBytes) + if (err != nil){ + t.Fatalf("Failed to hash emoji file bytes: " + err.Error()) + } + + _, exists := fileHashesMap[fileHash] + if (exists == true){ + emojiIdentifierString := helpers.ConvertIntToString(i) + t.Fatal("Duplicate emoji exists: " + emojiIdentifierString) + } + + fileHashesMap[fileHash] = struct{}{} + + // We make sure each emoji exists within a category + _, exists = foundIdentifiersMap[i] + if (exists == false){ + emojiIdentifierString := helpers.ConvertIntToString(i) + t.Fatalf("Emoji is missing from categories list: " + emojiIdentifierString) + } + } + + // Now we make sure svg reader can read all svgs + // This will take a while + + for i:=1; i <= numberOfEmojis; i++{ + + emojiFileBytes, err := imageFiles.GetEmojiFileBytesFromIdentifier(i) + if (err != nil) { + t.Fatalf("Failed to get emoji from identifier: " + err.Error()) + } + + _, err = imagery.ConvertSVGImageFileBytesToGolangImage(emojiFileBytes) + if (err != nil){ + emojiIdentifierString := helpers.ConvertIntToString(i) + t.Fatalf("Failed to convert emoji " + emojiIdentifierString + " to go image: " + err.Error()) + } + } +} + + diff --git a/resources/imageFiles/pngs.go b/resources/imageFiles/pngs.go new file mode 100644 index 0000000..14bf36e --- /dev/null +++ b/resources/imageFiles/pngs.go @@ -0,0 +1,26 @@ +package imageFiles + +import _ "embed" + +//go:embed pngs/seekiaLogo.png +var PNG_SeekiaLogo []byte + +//go:embed pngs/23andMeLogo.png +var PNG_23andMeLogo []byte + +// The rest are used for creating fake profiles + +//go:embed pngs/pumpkin.png +var PNG_Pumpkin []byte + +//go:embed pngs/smileyA.png +var PNG_SmileyA []byte + +//go:embed pngs/smileyB.png +var PNG_SmileyB []byte + +//go:embed pngs/ultrawide.png +var PNG_Ultrawide []byte + +//go:embed pngs/ultrathin.png +var PNG_Ultrathin []byte diff --git a/resources/imageFiles/pngs/23andMeLogo.png b/resources/imageFiles/pngs/23andMeLogo.png new file mode 100644 index 0000000..d7ae215 Binary files /dev/null and b/resources/imageFiles/pngs/23andMeLogo.png differ diff --git a/resources/imageFiles/pngs/pumpkin.png b/resources/imageFiles/pngs/pumpkin.png new file mode 100644 index 0000000..fac74db Binary files /dev/null and b/resources/imageFiles/pngs/pumpkin.png differ diff --git a/resources/imageFiles/pngs/seekiaLogo.png b/resources/imageFiles/pngs/seekiaLogo.png new file mode 100644 index 0000000..a3b886f Binary files /dev/null and b/resources/imageFiles/pngs/seekiaLogo.png differ diff --git a/resources/imageFiles/pngs/smileyA.png b/resources/imageFiles/pngs/smileyA.png new file mode 100644 index 0000000..17c8438 Binary files /dev/null and b/resources/imageFiles/pngs/smileyA.png differ diff --git a/resources/imageFiles/pngs/smileyB.png b/resources/imageFiles/pngs/smileyB.png new file mode 100644 index 0000000..470884d Binary files /dev/null and b/resources/imageFiles/pngs/smileyB.png differ diff --git a/resources/imageFiles/pngs/ultrathin.png b/resources/imageFiles/pngs/ultrathin.png new file mode 100644 index 0000000..7fde992 Binary files /dev/null and b/resources/imageFiles/pngs/ultrathin.png differ diff --git a/resources/imageFiles/pngs/ultrawide.png b/resources/imageFiles/pngs/ultrawide.png new file mode 100644 index 0000000..8bc5019 Binary files /dev/null and b/resources/imageFiles/pngs/ultrawide.png differ diff --git a/resources/markdownImages/readme.md b/resources/markdownImages/readme.md new file mode 100644 index 0000000..94adb13 --- /dev/null +++ b/resources/markdownImages/readme.md @@ -0,0 +1,2 @@ + +These are images that are used within the ReadMe.md and documentation markdown documents. diff --git a/resources/markdownImages/seekiaHomepage.jpg b/resources/markdownImages/seekiaHomepage.jpg new file mode 100644 index 0000000..a4244f3 Binary files /dev/null and b/resources/markdownImages/seekiaHomepage.jpg differ diff --git a/resources/markdownImages/seekiaLogoWithSubtitle.jpg b/resources/markdownImages/seekiaLogoWithSubtitle.jpg new file mode 100644 index 0000000..53b2f33 Binary files /dev/null and b/resources/markdownImages/seekiaLogoWithSubtitle.jpg differ diff --git a/resources/wordLists/english.go b/resources/wordLists/english.go new file mode 100755 index 0000000..695d145 --- /dev/null +++ b/resources/wordLists/english.go @@ -0,0 +1,2054 @@ +package wordLists + +var englishWordList = []string{ + "abandon", + "ability", + "able", + "about", + "above", + "absent", + "absorb", + "abstract", + "absurd", + "abuse", + "access", + "accident", + "account", + "accuse", + "achieve", + "acid", + "acoustic", + "acquire", + "across", + "act", + "action", + "actor", + "actress", + "actual", + "adapt", + "add", + "addict", + "address", + "adjust", + "admit", + "adult", + "advance", + "advice", + "aerobic", + "affair", + "afford", + "afraid", + "again", + "age", + "agent", + "agree", + "ahead", + "aim", + "air", + "airport", + "aisle", + "alarm", + "album", + "alcohol", + "alert", + "alien", + "all", + "alley", + "allow", + "almost", + "alone", + "alpha", + "already", + "also", + "alter", + "always", + "amateur", + "amazing", + "among", + "amount", + "amused", + "analyst", + "anchor", + "ancient", + "anger", + "angle", + "angry", + "animal", + "ankle", + "announce", + "annual", + "another", + "answer", + "antenna", + "antique", + "anxiety", + "any", + "apart", + "apology", + "appear", + "apple", + "approve", + "april", + "arch", + "arctic", + "area", + "arena", + "argue", + "arm", + "armed", + "armor", + "army", + "around", + "arrange", + "arrest", + "arrive", + "arrow", + "art", + "artefact", + "artist", + "artwork", + "ask", + "aspect", + "assault", + "asset", + "assist", + "assume", + "asthma", + "athlete", + "atom", + "attack", + "attend", + "attitude", + "attract", + "auction", + "audit", + "august", + "aunt", + "author", + "auto", + "autumn", + "average", + "avocado", + "avoid", + "awake", + "aware", + "away", + "awesome", + "awful", + "awkward", + "axis", + "baby", + "bachelor", + "bacon", + "badge", + "bag", + "balance", + "balcony", + "ball", + "bamboo", + "banana", + "banner", + "bar", + "barely", + "bargain", + "barrel", + "base", + "basic", + "basket", + "battle", + "beach", + "bean", + "beauty", + "because", + "become", + "beef", + "before", + "begin", + "behave", + "behind", + "believe", + "below", + "belt", + "bench", + "benefit", + "best", + "betray", + "better", + "between", + "beyond", + "bicycle", + "bid", + "bike", + "bind", + "biology", + "bird", + "birth", + "bitter", + "black", + "blade", + "blame", + "blanket", + "blast", + "bleak", + "bless", + "blind", + "blood", + "blossom", + "blouse", + "blue", + "blur", + "blush", + "board", + "boat", + "body", + "boil", + "bomb", + "bone", + "bonus", + "book", + "boost", + "border", + "boring", + "borrow", + "boss", + "bottom", + "bounce", + "box", + "boy", + "bracket", + "brain", + "brand", + "brass", + "brave", + "bread", + "breeze", + "brick", + "bridge", + "brief", + "bright", + "bring", + "brisk", + "broccoli", + "broken", + "bronze", + "broom", + "brother", + "brown", + "brush", + "bubble", + "buddy", + "budget", + "buffalo", + "build", + "bulb", + "bulk", + "bullet", + "bundle", + "bunker", + "burden", + "burger", + "burst", + "bus", + "business", + "busy", + "butter", + "buyer", + "buzz", + "cabbage", + "cabin", + "cable", + "cactus", + "cage", + "cake", + "call", + "calm", + "camera", + "camp", + "can", + "canal", + "cancel", + "candy", + "cannon", + "canoe", + "canvas", + "canyon", + "capable", + "capital", + "captain", + "car", + "carbon", + "card", + "cargo", + "carpet", + "carry", + "cart", + "case", + "cash", + "casino", + "castle", + "casual", + "cat", + "catalog", + "catch", + "category", + "cattle", + "caught", + "cause", + "caution", + "cave", + "ceiling", + "celery", + "cement", + "census", + "century", + "cereal", + "certain", + "chair", + "chalk", + "champion", + "change", + "chaos", + "chapter", + "charge", + "chase", + "chat", + "cheap", + "check", + "cheese", + "chef", + "cherry", + "chest", + "chicken", + "chief", + "child", + "chimney", + "choice", + "choose", + "chronic", + "chuckle", + "chunk", + "churn", + "cigar", + "cinnamon", + "circle", + "citizen", + "city", + "civil", + "claim", + "clap", + "clarify", + "claw", + "clay", + "clean", + "clerk", + "clever", + "click", + "client", + "cliff", + "climb", + "clinic", + "clip", + "clock", + "clog", + "close", + "cloth", + "cloud", + "clown", + "club", + "clump", + "cluster", + "clutch", + "coach", + "coast", + "coconut", + "code", + "coffee", + "coil", + "coin", + "collect", + "color", + "column", + "combine", + "come", + "comfort", + "comic", + "common", + "company", + "concert", + "conduct", + "confirm", + "congress", + "connect", + "consider", + "control", + "convince", + "cook", + "cool", + "copper", + "copy", + "coral", + "core", + "corn", + "correct", + "cost", + "cotton", + "couch", + "country", + "couple", + "course", + "cousin", + "cover", + "coyote", + "crack", + "cradle", + "craft", + "cram", + "crane", + "crash", + "crater", + "crawl", + "crazy", + "cream", + "credit", + "creek", + "crew", + "cricket", + "crime", + "crisp", + "critic", + "crop", + "cross", + "crouch", + "crowd", + "crucial", + "cruel", + "cruise", + "crumble", + "crunch", + "crush", + "cry", + "crystal", + "cube", + "culture", + "cup", + "cupboard", + "curious", + "current", + "curtain", + "curve", + "cushion", + "custom", + "cute", + "cycle", + "dad", + "damage", + "damp", + "dance", + "danger", + "daring", + "dash", + "daughter", + "dawn", + "day", + "deal", + "debate", + "debris", + "decade", + "december", + "decide", + "decline", + "decorate", + "decrease", + "deer", + "defense", + "define", + "defy", + "degree", + "delay", + "deliver", + "demand", + "demise", + "denial", + "dentist", + "deny", + "depart", + "depend", + "deposit", + "depth", + "deputy", + "derive", + "describe", + "desert", + "design", + "desk", + "despair", + "destroy", + "detail", + "detect", + "develop", + "device", + "devote", + "diagram", + "dial", + "diamond", + "diary", + "dice", + "diesel", + "diet", + "differ", + "digital", + "dignity", + "dilemma", + "dinner", + "dinosaur", + "direct", + "dirt", + "disagree", + "discover", + "disease", + "dish", + "dismiss", + "disorder", + "display", + "distance", + "divert", + "divide", + "divorce", + "dizzy", + "doctor", + "document", + "dog", + "doll", + "dolphin", + "domain", + "donate", + "donkey", + "donor", + "door", + "dose", + "double", + "dove", + "draft", + "dragon", + "drama", + "drastic", + "draw", + "dream", + "dress", + "drift", + "drill", + "drink", + "drip", + "drive", + "drop", + "drum", + "dry", + "duck", + "dumb", + "dune", + "during", + "dust", + "dutch", + "duty", + "dwarf", + "dynamic", + "eager", + "eagle", + "early", + "earn", + "earth", + "easily", + "east", + "easy", + "echo", + "ecology", + "economy", + "edge", + "edit", + "educate", + "effort", + "egg", + "eight", + "either", + "elbow", + "elder", + "electric", + "elegant", + "element", + "elephant", + "elevator", + "elite", + "else", + "embark", + "embody", + "embrace", + "emerge", + "emotion", + "employ", + "empower", + "empty", + "enable", + "enact", + "end", + "endless", + "endorse", + "enemy", + "energy", + "enforce", + "engage", + "engine", + "enhance", + "enjoy", + "enlist", + "enough", + "enrich", + "enroll", + "ensure", + "enter", + "entire", + "entry", + "envelope", + "episode", + "equal", + "equip", + "era", + "erase", + "erode", + "erosion", + "error", + "erupt", + "escape", + "essay", + "essence", + "estate", + "eternal", + "ethics", + "evidence", + "evil", + "evoke", + "evolve", + "exact", + "example", + "excess", + "exchange", + "excite", + "exclude", + "excuse", + "execute", + "exercise", + "exhaust", + "exhibit", + "exile", + "exist", + "exit", + "exotic", + "expand", + "expect", + "expire", + "explain", + "expose", + "express", + "extend", + "extra", + "eye", + "eyebrow", + "fabric", + "face", + "faculty", + "fade", + "faint", + "faith", + "fall", + "false", + "fame", + "family", + "famous", + "fan", + "fancy", + "fantasy", + "farm", + "fashion", + "fat", + "fatal", + "father", + "fatigue", + "fault", + "favorite", + "feature", + "february", + "federal", + "fee", + "feed", + "feel", + "female", + "fence", + "festival", + "fetch", + "fever", + "few", + "fiber", + "fiction", + "field", + "figure", + "file", + "film", + "filter", + "final", + "find", + "fine", + "finger", + "finish", + "fire", + "firm", + "first", + "fiscal", + "fish", + "fit", + "fitness", + "fix", + "flag", + "flame", + "flash", + "flat", + "flavor", + "flee", + "flight", + "flip", + "float", + "flock", + "floor", + "flower", + "fluid", + "flush", + "fly", + "foam", + "focus", + "fog", + "foil", + "fold", + "follow", + "food", + "foot", + "force", + "forest", + "forget", + "fork", + "fortune", + "forum", + "forward", + "fossil", + "foster", + "found", + "fox", + "fragile", + "frame", + "frequent", + "fresh", + "friend", + "fringe", + "frog", + "front", + "frost", + "frown", + "frozen", + "fruit", + "fuel", + "fun", + "funny", + "furnace", + "fury", + "future", + "gadget", + "gain", + "galaxy", + "gallery", + "game", + "gap", + "garage", + "garbage", + "garden", + "garlic", + "garment", + "gas", + "gasp", + "gate", + "gather", + "gauge", + "gaze", + "general", + "genius", + "genre", + "gentle", + "genuine", + "gesture", + "ghost", + "giant", + "gift", + "giggle", + "ginger", + "giraffe", + "girl", + "give", + "glad", + "glance", + "glare", + "glass", + "glide", + "glimpse", + "globe", + "gloom", + "glory", + "glove", + "glow", + "glue", + "goat", + "goddess", + "gold", + "good", + "goose", + "gorilla", + "gospel", + "gossip", + "govern", + "gown", + "grab", + "grace", + "grain", + "grant", + "grape", + "grass", + "gravity", + "great", + "green", + "grid", + "grief", + "grit", + "grocery", + "group", + "grow", + "grunt", + "guard", + "guess", + "guide", + "guilt", + "guitar", + "gun", + "gym", + "habit", + "hair", + "half", + "hammer", + "hamster", + "hand", + "happy", + "harbor", + "hard", + "harsh", + "harvest", + "hat", + "have", + "hawk", + "hazard", + "head", + "health", + "heart", + "heavy", + "hedgehog", + "height", + "hello", + "helmet", + "help", + "hen", + "hero", + "hidden", + "high", + "hill", + "hint", + "hip", + "hire", + "history", + "hobby", + "hockey", + "hold", + "hole", + "holiday", + "hollow", + "home", + "honey", + "hood", + "hope", + "horn", + "horror", + "horse", + "hospital", + "host", + "hotel", + "hour", + "hover", + "hub", + "huge", + "human", + "humble", + "humor", + "hundred", + "hungry", + "hunt", + "hurdle", + "hurry", + "hurt", + "husband", + "hybrid", + "ice", + "icon", + "idea", + "identify", + "idle", + "ignore", + "ill", + "illegal", + "illness", + "image", + "imitate", + "immense", + "immune", + "impact", + "impose", + "improve", + "impulse", + "inch", + "include", + "income", + "increase", + "index", + "indicate", + "indoor", + "industry", + "infant", + "inflict", + "inform", + "inhale", + "inherit", + "initial", + "inject", + "injury", + "inmate", + "inner", + "innocent", + "input", + "inquiry", + "insane", + "insect", + "inside", + "inspire", + "install", + "intact", + "interest", + "into", + "invest", + "invite", + "involve", + "iron", + "island", + "isolate", + "issue", + "item", + "ivory", + "jacket", + "jaguar", + "jar", + "jazz", + "jealous", + "jeans", + "jelly", + "jewel", + "job", + "join", + "joke", + "journey", + "joy", + "judge", + "juice", + "jump", + "jungle", + "junior", + "junk", + "just", + "kangaroo", + "keen", + "keep", + "ketchup", + "key", + "kick", + "kid", + "kidney", + "kind", + "kingdom", + "kiss", + "kit", + "kitchen", + "kite", + "kitten", + "kiwi", + "knee", + "knife", + "knock", + "know", + "lab", + "label", + "labor", + "ladder", + "lady", + "lake", + "lamp", + "language", + "laptop", + "large", + "later", + "latin", + "laugh", + "laundry", + "lava", + "law", + "lawn", + "lawsuit", + "layer", + "lazy", + "leader", + "leaf", + "learn", + "leave", + "lecture", + "left", + "leg", + "legal", + "legend", + "leisure", + "lemon", + "lend", + "length", + "lens", + "leopard", + "lesson", + "letter", + "level", + "liar", + "liberty", + "library", + "license", + "life", + "lift", + "light", + "like", + "limb", + "limit", + "link", + "lion", + "liquid", + "list", + "little", + "live", + "lizard", + "load", + "loan", + "lobster", + "local", + "lock", + "logic", + "lonely", + "long", + "loop", + "lottery", + "loud", + "lounge", + "love", + "loyal", + "lucky", + "luggage", + "lumber", + "lunar", + "lunch", + "luxury", + "lyrics", + "machine", + "mad", + "magic", + "magnet", + "maid", + "mail", + "main", + "major", + "make", + "mammal", + "man", + "manage", + "mandate", + "mango", + "mansion", + "manual", + "maple", + "marble", + "march", + "margin", + "marine", + "market", + "marriage", + "mask", + "mass", + "master", + "match", + "material", + "math", + "matrix", + "matter", + "maximum", + "maze", + "meadow", + "mean", + "measure", + "meat", + "mechanic", + "medal", + "media", + "melody", + "melt", + "member", + "memory", + "mention", + "menu", + "mercy", + "merge", + "merit", + "merry", + "mesh", + "message", + "metal", + "method", + "middle", + "midnight", + "milk", + "million", + "mimic", + "mind", + "minimum", + "minor", + "minute", + "miracle", + "mirror", + "misery", + "miss", + "mistake", + "mix", + "mixed", + "mixture", + "mobile", + "model", + "modify", + "mom", + "moment", + "monitor", + "monkey", + "monster", + "month", + "moon", + "moral", + "more", + "morning", + "mosquito", + "mother", + "motion", + "motor", + "mountain", + "mouse", + "move", + "movie", + "much", + "muffin", + "mule", + "multiply", + "muscle", + "museum", + "mushroom", + "music", + "must", + "mutual", + "myself", + "mystery", + "myth", + "naive", + "name", + "napkin", + "narrow", + "nasty", + "nation", + "nature", + "near", + "neck", + "need", + "negative", + "neglect", + "neither", + "nephew", + "nerve", + "nest", + "net", + "network", + "neutral", + "never", + "news", + "next", + "nice", + "night", + "noble", + "noise", + "nominee", + "noodle", + "normal", + "north", + "nose", + "notable", + "note", + "nothing", + "notice", + "novel", + "now", + "nuclear", + "number", + "nurse", + "nut", + "oak", + "obey", + "object", + "oblige", + "obscure", + "observe", + "obtain", + "obvious", + "occur", + "ocean", + "october", + "odor", + "off", + "offer", + "office", + "often", + "oil", + "okay", + "old", + "olive", + "olympic", + "omit", + "once", + "one", + "onion", + "online", + "only", + "open", + "opera", + "opinion", + "oppose", + "option", + "orange", + "orbit", + "orchard", + "order", + "ordinary", + "organ", + "orient", + "original", + "orphan", + "ostrich", + "other", + "outdoor", + "outer", + "output", + "outside", + "oval", + "oven", + "over", + "own", + "owner", + "oxygen", + "oyster", + "ozone", + "pact", + "paddle", + "page", + "pair", + "palace", + "palm", + "panda", + "panel", + "panic", + "panther", + "paper", + "parade", + "parent", + "park", + "parrot", + "party", + "pass", + "patch", + "path", + "patient", + "patrol", + "pattern", + "pause", + "pave", + "payment", + "peace", + "peanut", + "pear", + "peasant", + "pelican", + "pen", + "penalty", + "pencil", + "people", + "pepper", + "perfect", + "permit", + "person", + "pet", + "phone", + "photo", + "phrase", + "physical", + "piano", + "picnic", + "picture", + "piece", + "pig", + "pigeon", + "pill", + "pilot", + "pink", + "pioneer", + "pipe", + "pistol", + "pitch", + "pizza", + "place", + "planet", + "plastic", + "plate", + "play", + "please", + "pledge", + "pluck", + "plug", + "plunge", + "poem", + "poet", + "point", + "polar", + "pole", + "police", + "pond", + "pony", + "pool", + "popular", + "portion", + "position", + "possible", + "post", + "potato", + "pottery", + "poverty", + "powder", + "power", + "practice", + "praise", + "predict", + "prefer", + "prepare", + "present", + "pretty", + "prevent", + "price", + "pride", + "primary", + "print", + "priority", + "prison", + "private", + "prize", + "problem", + "process", + "produce", + "profit", + "program", + "project", + "promote", + "proof", + "property", + "prosper", + "protect", + "proud", + "provide", + "public", + "pudding", + "pull", + "pulp", + "pulse", + "pumpkin", + "punch", + "pupil", + "puppy", + "purchase", + "purity", + "purpose", + "purse", + "push", + "put", + "puzzle", + "pyramid", + "quality", + "quantum", + "quarter", + "question", + "quick", + "quit", + "quiz", + "quote", + "rabbit", + "raccoon", + "race", + "rack", + "radar", + "radio", + "rail", + "rain", + "raise", + "rally", + "ramp", + "ranch", + "random", + "range", + "rapid", + "rare", + "rate", + "rather", + "raven", + "raw", + "razor", + "ready", + "real", + "reason", + "rebel", + "rebuild", + "recall", + "receive", + "recipe", + "record", + "recycle", + "reduce", + "reflect", + "reform", + "refuse", + "region", + "regret", + "regular", + "reject", + "relax", + "release", + "relief", + "rely", + "remain", + "remember", + "remind", + "remove", + "render", + "renew", + "rent", + "reopen", + "repair", + "repeat", + "replace", + "report", + "require", + "rescue", + "resemble", + "resist", + "resource", + "response", + "result", + "retire", + "retreat", + "return", + "reunion", + "reveal", + "review", + "reward", + "rhythm", + "rib", + "ribbon", + "rice", + "rich", + "ride", + "ridge", + "rifle", + "right", + "rigid", + "ring", + "riot", + "ripple", + "risk", + "ritual", + "rival", + "river", + "road", + "roast", + "robot", + "robust", + "rocket", + "romance", + "roof", + "rookie", + "room", + "rose", + "rotate", + "rough", + "round", + "route", + "royal", + "rubber", + "rude", + "rug", + "rule", + "run", + "runway", + "rural", + "sad", + "saddle", + "sadness", + "safe", + "sail", + "salad", + "salmon", + "salon", + "salt", + "salute", + "same", + "sample", + "sand", + "satisfy", + "satoshi", + "sauce", + "sausage", + "save", + "say", + "scale", + "scan", + "scare", + "scatter", + "scene", + "scheme", + "school", + "science", + "scissors", + "scorpion", + "scout", + "scrap", + "screen", + "script", + "scrub", + "sea", + "search", + "season", + "seat", + "second", + "secret", + "section", + "security", + "seed", + "seek", + "segment", + "select", + "sell", + "seminar", + "senior", + "sense", + "sentence", + "series", + "service", + "session", + "settle", + "setup", + "seven", + "shadow", + "shaft", + "shallow", + "share", + "shed", + "shell", + "sheriff", + "shield", + "shift", + "shine", + "ship", + "shiver", + "shock", + "shoe", + "shoot", + "shop", + "short", + "shoulder", + "shove", + "shrimp", + "shrug", + "shuffle", + "shy", + "sibling", + "sick", + "side", + "siege", + "sight", + "sign", + "silent", + "silk", + "silly", + "silver", + "similar", + "simple", + "since", + "sing", + "siren", + "sister", + "situate", + "six", + "size", + "skate", + "sketch", + "ski", + "skill", + "skin", + "skirt", + "skull", + "slab", + "slam", + "sleep", + "slender", + "slice", + "slide", + "slight", + "slim", + "slogan", + "slot", + "slow", + "slush", + "small", + "smart", + "smile", + "smoke", + "smooth", + "snack", + "snake", + "snap", + "sniff", + "snow", + "soap", + "soccer", + "social", + "sock", + "soda", + "soft", + "solar", + "soldier", + "solid", + "solution", + "solve", + "someone", + "song", + "soon", + "sorry", + "sort", + "soul", + "sound", + "soup", + "source", + "south", + "space", + "spare", + "spatial", + "spawn", + "speak", + "special", + "speed", + "spell", + "spend", + "sphere", + "spice", + "spider", + "spike", + "spin", + "spirit", + "split", + "spoil", + "sponsor", + "spoon", + "sport", + "spot", + "spray", + "spread", + "spring", + "spy", + "square", + "squeeze", + "squirrel", + "stable", + "stadium", + "staff", + "stage", + "stairs", + "stamp", + "stand", + "start", + "state", + "stay", + "steak", + "steel", + "stem", + "step", + "stereo", + "stick", + "still", + "sting", + "stock", + "stomach", + "stone", + "stool", + "story", + "stove", + "strategy", + "street", + "strike", + "strong", + "struggle", + "student", + "stuff", + "stumble", + "style", + "subject", + "submit", + "subway", + "success", + "such", + "sudden", + "suffer", + "sugar", + "suggest", + "suit", + "summer", + "sun", + "sunny", + "sunset", + "super", + "supply", + "supreme", + "sure", + "surface", + "surge", + "surprise", + "surround", + "survey", + "suspect", + "sustain", + "swallow", + "swamp", + "swap", + "swarm", + "swear", + "sweet", + "swift", + "swim", + "swing", + "switch", + "sword", + "symbol", + "symptom", + "syrup", + "system", + "table", + "tackle", + "tag", + "tail", + "talent", + "talk", + "tank", + "tape", + "target", + "task", + "taste", + "tattoo", + "taxi", + "teach", + "team", + "tell", + "ten", + "tenant", + "tennis", + "tent", + "term", + "test", + "text", + "thank", + "that", + "theme", + "then", + "theory", + "there", + "they", + "thing", + "this", + "thought", + "three", + "thrive", + "throw", + "thumb", + "thunder", + "ticket", + "tide", + "tiger", + "tilt", + "timber", + "time", + "tiny", + "tip", + "tired", + "tissue", + "title", + "toast", + "tobacco", + "today", + "toddler", + "toe", + "together", + "toilet", + "token", + "tomato", + "tomorrow", + "tone", + "tongue", + "tonight", + "tool", + "tooth", + "top", + "topic", + "topple", + "torch", + "tornado", + "tortoise", + "toss", + "total", + "tourist", + "toward", + "tower", + "town", + "toy", + "track", + "trade", + "traffic", + "tragic", + "train", + "transfer", + "trap", + "trash", + "travel", + "tray", + "treat", + "tree", + "trend", + "trial", + "tribe", + "trick", + "trigger", + "trim", + "trip", + "trophy", + "trouble", + "truck", + "true", + "truly", + "trumpet", + "trust", + "truth", + "try", + "tube", + "tuition", + "tumble", + "tuna", + "tunnel", + "turkey", + "turn", + "turtle", + "twelve", + "twenty", + "twice", + "twin", + "twist", + "two", + "type", + "typical", + "ugly", + "umbrella", + "unable", + "unaware", + "uncle", + "uncover", + "under", + "undo", + "unfair", + "unfold", + "unhappy", + "uniform", + "unique", + "unit", + "universe", + "unknown", + "unlock", + "until", + "unusual", + "unveil", + "update", + "upgrade", + "uphold", + "upon", + "upper", + "upset", + "urban", + "urge", + "usage", + "use", + "used", + "useful", + "useless", + "usual", + "utility", + "vacant", + "vacuum", + "vague", + "valid", + "valley", + "valve", + "van", + "vanish", + "vapor", + "various", + "vast", + "vault", + "vehicle", + "velvet", + "vendor", + "venture", + "venue", + "verb", + "verify", + "version", + "very", + "vessel", + "veteran", + "viable", + "vibrant", + "vicious", + "victory", + "video", + "view", + "village", + "vintage", + "violin", + "virtual", + "virus", + "visa", + "visit", + "visual", + "vital", + "vivid", + "vocal", + "voice", + "void", + "volcano", + "volume", + "vote", + "voyage", + "wage", + "wagon", + "wait", + "walk", + "wall", + "walnut", + "want", + "warfare", + "warm", + "warrior", + "wash", + "wasp", + "waste", + "water", + "wave", + "way", + "wealth", + "weapon", + "wear", + "weasel", + "weather", + "web", + "wedding", + "weekend", + "weird", + "welcome", + "west", + "wet", + "whale", + "what", + "wheat", + "wheel", + "when", + "where", + "whip", + "whisper", + "wide", + "width", + "wife", + "wild", + "will", + "win", + "window", + "wine", + "wing", + "wink", + "winner", + "winter", + "wire", + "wisdom", + "wise", + "wish", + "witness", + "wolf", + "woman", + "wonder", + "wood", + "wool", + "word", + "work", + "world", + "worry", + "worth", + "wrap", + "wreck", + "wrestle", + "wrist", + "write", + "wrong", + "yard", + "year", + "yellow", + "you", + "young", + "youth", + "zebra", + "zero", + "zone", + "zoo", +} + + diff --git a/resources/wordLists/wordLists.go b/resources/wordLists/wordLists.go new file mode 100644 index 0000000..3edaf15 --- /dev/null +++ b/resources/wordLists/wordLists.go @@ -0,0 +1,17 @@ + +// wordLists provides lists of words in different languages. +// These are used to create seed phrases. + +package wordLists + +import "errors" + +// This function does not return a copy of the list +// Thus, we must be careful not to edit the returned list +func GetWordListFromLanguage(languageName string)([]string, error){ + + if (languageName == "English"){ + return englishWordList, nil + } + return nil, errors.New("GetWordListFromLanguage called with unknown language: " + languageName) +} diff --git a/resources/wordLists/wordLists_test.go b/resources/wordLists/wordLists_test.go new file mode 100644 index 0000000..bf7a3cc --- /dev/null +++ b/resources/wordLists/wordLists_test.go @@ -0,0 +1,8285 @@ +package wordLists_test + +// We use this package to make sure words in the wordList are being converted to the expected bytes +// This is important because to create a seed phrase hash, we are converting the words using []byte(word) +// A user's identity key would change if the bytes conversion output ever changed for any of the words in their seed phrase + +import "seekia/resources/wordLists" + +import "testing" + +//import "seekia/internal/helpers" + +import "bytes" +import "errors" +//import "log" + + +func TestEnglishWordList(t *testing.T){ + + getExpectedWordBytes := func(word string)([]byte, error){ + + switch word { + + case "abandon":{ + result := []byte{97, 98, 97, 110, 100, 111, 110} + return result, nil + } + case "ability":{ + result := []byte{97, 98, 105, 108, 105, 116, 121} + return result, nil + } + case "able":{ + result := []byte{97, 98, 108, 101} + return result, nil + } + case "about":{ + result := []byte{97, 98, 111, 117, 116} + return result, nil + } + case "above":{ + result := []byte{97, 98, 111, 118, 101} + return result, nil + } + case "absent":{ + result := []byte{97, 98, 115, 101, 110, 116} + return result, nil + } + case "absorb":{ + result := []byte{97, 98, 115, 111, 114, 98} + return result, nil + } + case "abstract":{ + result := []byte{97, 98, 115, 116, 114, 97, 99, 116} + return result, nil + } + case "absurd":{ + result := []byte{97, 98, 115, 117, 114, 100} + return result, nil + } + case "abuse":{ + result := []byte{97, 98, 117, 115, 101} + return result, nil + } + case "access":{ + result := []byte{97, 99, 99, 101, 115, 115} + return result, nil + } + case "accident":{ + result := []byte{97, 99, 99, 105, 100, 101, 110, 116} + return result, nil + } + case "account":{ + result := []byte{97, 99, 99, 111, 117, 110, 116} + return result, nil + } + case "accuse":{ + result := []byte{97, 99, 99, 117, 115, 101} + return result, nil + } + case "achieve":{ + result := []byte{97, 99, 104, 105, 101, 118, 101} + return result, nil + } + case "acid":{ + result := []byte{97, 99, 105, 100} + return result, nil + } + case "acoustic":{ + result := []byte{97, 99, 111, 117, 115, 116, 105, 99} + return result, nil + } + case "acquire":{ + result := []byte{97, 99, 113, 117, 105, 114, 101} + return result, nil + } + case "across":{ + result := []byte{97, 99, 114, 111, 115, 115} + return result, nil + } + case "act":{ + result := []byte{97, 99, 116} + return result, nil + } + case "action":{ + result := []byte{97, 99, 116, 105, 111, 110} + return result, nil + } + case "actor":{ + result := []byte{97, 99, 116, 111, 114} + return result, nil + } + case "actress":{ + result := []byte{97, 99, 116, 114, 101, 115, 115} + return result, nil + } + case "actual":{ + result := []byte{97, 99, 116, 117, 97, 108} + return result, nil + } + case "adapt":{ + result := []byte{97, 100, 97, 112, 116} + return result, nil + } + case "add":{ + result := []byte{97, 100, 100} + return result, nil + } + case "addict":{ + result := []byte{97, 100, 100, 105, 99, 116} + return result, nil + } + case "address":{ + result := []byte{97, 100, 100, 114, 101, 115, 115} + return result, nil + } + case "adjust":{ + result := []byte{97, 100, 106, 117, 115, 116} + return result, nil + } + case "admit":{ + result := []byte{97, 100, 109, 105, 116} + return result, nil + } + case "adult":{ + result := []byte{97, 100, 117, 108, 116} + return result, nil + } + case "advance":{ + result := []byte{97, 100, 118, 97, 110, 99, 101} + return result, nil + } + case "advice":{ + result := []byte{97, 100, 118, 105, 99, 101} + return result, nil + } + case "aerobic":{ + result := []byte{97, 101, 114, 111, 98, 105, 99} + return result, nil + } + case "affair":{ + result := []byte{97, 102, 102, 97, 105, 114} + return result, nil + } + case "afford":{ + result := []byte{97, 102, 102, 111, 114, 100} + return result, nil + } + case "afraid":{ + result := []byte{97, 102, 114, 97, 105, 100} + return result, nil + } + case "again":{ + result := []byte{97, 103, 97, 105, 110} + return result, nil + } + case "age":{ + result := []byte{97, 103, 101} + return result, nil + } + case "agent":{ + result := []byte{97, 103, 101, 110, 116} + return result, nil + } + case "agree":{ + result := []byte{97, 103, 114, 101, 101} + return result, nil + } + case "ahead":{ + result := []byte{97, 104, 101, 97, 100} + return result, nil + } + case "aim":{ + result := []byte{97, 105, 109} + return result, nil + } + case "air":{ + result := []byte{97, 105, 114} + return result, nil + } + case "airport":{ + result := []byte{97, 105, 114, 112, 111, 114, 116} + return result, nil + } + case "aisle":{ + result := []byte{97, 105, 115, 108, 101} + return result, nil + } + case "alarm":{ + result := []byte{97, 108, 97, 114, 109} + return result, nil + } + case "album":{ + result := []byte{97, 108, 98, 117, 109} + return result, nil + } + case "alcohol":{ + result := []byte{97, 108, 99, 111, 104, 111, 108} + return result, nil + } + case "alert":{ + result := []byte{97, 108, 101, 114, 116} + return result, nil + } + case "alien":{ + result := []byte{97, 108, 105, 101, 110} + return result, nil + } + case "all":{ + result := []byte{97, 108, 108} + return result, nil + } + case "alley":{ + result := []byte{97, 108, 108, 101, 121} + return result, nil + } + case "allow":{ + result := []byte{97, 108, 108, 111, 119} + return result, nil + } + case "almost":{ + result := []byte{97, 108, 109, 111, 115, 116} + return result, nil + } + case "alone":{ + result := []byte{97, 108, 111, 110, 101} + return result, nil + } + case "alpha":{ + result := []byte{97, 108, 112, 104, 97} + return result, nil + } + case "already":{ + result := []byte{97, 108, 114, 101, 97, 100, 121} + return result, nil + } + case "also":{ + result := []byte{97, 108, 115, 111} + return result, nil + } + case "alter":{ + result := []byte{97, 108, 116, 101, 114} + return result, nil + } + case "always":{ + result := []byte{97, 108, 119, 97, 121, 115} + return result, nil + } + case "amateur":{ + result := []byte{97, 109, 97, 116, 101, 117, 114} + return result, nil + } + case "amazing":{ + result := []byte{97, 109, 97, 122, 105, 110, 103} + return result, nil + } + case "among":{ + result := []byte{97, 109, 111, 110, 103} + return result, nil + } + case "amount":{ + result := []byte{97, 109, 111, 117, 110, 116} + return result, nil + } + case "amused":{ + result := []byte{97, 109, 117, 115, 101, 100} + return result, nil + } + case "analyst":{ + result := []byte{97, 110, 97, 108, 121, 115, 116} + return result, nil + } + case "anchor":{ + result := []byte{97, 110, 99, 104, 111, 114} + return result, nil + } + case "ancient":{ + result := []byte{97, 110, 99, 105, 101, 110, 116} + return result, nil + } + case "anger":{ + result := []byte{97, 110, 103, 101, 114} + return result, nil + } + case "angle":{ + result := []byte{97, 110, 103, 108, 101} + return result, nil + } + case "angry":{ + result := []byte{97, 110, 103, 114, 121} + return result, nil + } + case "animal":{ + result := []byte{97, 110, 105, 109, 97, 108} + return result, nil + } + case "ankle":{ + result := []byte{97, 110, 107, 108, 101} + return result, nil + } + case "announce":{ + result := []byte{97, 110, 110, 111, 117, 110, 99, 101} + return result, nil + } + case "annual":{ + result := []byte{97, 110, 110, 117, 97, 108} + return result, nil + } + case "another":{ + result := []byte{97, 110, 111, 116, 104, 101, 114} + return result, nil + } + case "answer":{ + result := []byte{97, 110, 115, 119, 101, 114} + return result, nil + } + case "antenna":{ + result := []byte{97, 110, 116, 101, 110, 110, 97} + return result, nil + } + case "antique":{ + result := []byte{97, 110, 116, 105, 113, 117, 101} + return result, nil + } + case "anxiety":{ + result := []byte{97, 110, 120, 105, 101, 116, 121} + return result, nil + } + case "any":{ + result := []byte{97, 110, 121} + return result, nil + } + case "apart":{ + result := []byte{97, 112, 97, 114, 116} + return result, nil + } + case "apology":{ + result := []byte{97, 112, 111, 108, 111, 103, 121} + return result, nil + } + case "appear":{ + result := []byte{97, 112, 112, 101, 97, 114} + return result, nil + } + case "apple":{ + result := []byte{97, 112, 112, 108, 101} + return result, nil + } + case "approve":{ + result := []byte{97, 112, 112, 114, 111, 118, 101} + return result, nil + } + case "april":{ + result := []byte{97, 112, 114, 105, 108} + return result, nil + } + case "arch":{ + result := []byte{97, 114, 99, 104} + return result, nil + } + case "arctic":{ + result := []byte{97, 114, 99, 116, 105, 99} + return result, nil + } + case "area":{ + result := []byte{97, 114, 101, 97} + return result, nil + } + case "arena":{ + result := []byte{97, 114, 101, 110, 97} + return result, nil + } + case "argue":{ + result := []byte{97, 114, 103, 117, 101} + return result, nil + } + case "arm":{ + result := []byte{97, 114, 109} + return result, nil + } + case "armed":{ + result := []byte{97, 114, 109, 101, 100} + return result, nil + } + case "armor":{ + result := []byte{97, 114, 109, 111, 114} + return result, nil + } + case "army":{ + result := []byte{97, 114, 109, 121} + return result, nil + } + case "around":{ + result := []byte{97, 114, 111, 117, 110, 100} + return result, nil + } + case "arrange":{ + result := []byte{97, 114, 114, 97, 110, 103, 101} + return result, nil + } + case "arrest":{ + result := []byte{97, 114, 114, 101, 115, 116} + return result, nil + } + case "arrive":{ + result := []byte{97, 114, 114, 105, 118, 101} + return result, nil + } + case "arrow":{ + result := []byte{97, 114, 114, 111, 119} + return result, nil + } + case "art":{ + result := []byte{97, 114, 116} + return result, nil + } + case "artefact":{ + result := []byte{97, 114, 116, 101, 102, 97, 99, 116} + return result, nil + } + case "artist":{ + result := []byte{97, 114, 116, 105, 115, 116} + return result, nil + } + case "artwork":{ + result := []byte{97, 114, 116, 119, 111, 114, 107} + return result, nil + } + case "ask":{ + result := []byte{97, 115, 107} + return result, nil + } + case "aspect":{ + result := []byte{97, 115, 112, 101, 99, 116} + return result, nil + } + case "assault":{ + result := []byte{97, 115, 115, 97, 117, 108, 116} + return result, nil + } + case "asset":{ + result := []byte{97, 115, 115, 101, 116} + return result, nil + } + case "assist":{ + result := []byte{97, 115, 115, 105, 115, 116} + return result, nil + } + case "assume":{ + result := []byte{97, 115, 115, 117, 109, 101} + return result, nil + } + case "asthma":{ + result := []byte{97, 115, 116, 104, 109, 97} + return result, nil + } + case "athlete":{ + result := []byte{97, 116, 104, 108, 101, 116, 101} + return result, nil + } + case "atom":{ + result := []byte{97, 116, 111, 109} + return result, nil + } + case "attack":{ + result := []byte{97, 116, 116, 97, 99, 107} + return result, nil + } + case "attend":{ + result := []byte{97, 116, 116, 101, 110, 100} + return result, nil + } + case "attitude":{ + result := []byte{97, 116, 116, 105, 116, 117, 100, 101} + return result, nil + } + case "attract":{ + result := []byte{97, 116, 116, 114, 97, 99, 116} + return result, nil + } + case "auction":{ + result := []byte{97, 117, 99, 116, 105, 111, 110} + return result, nil + } + case "audit":{ + result := []byte{97, 117, 100, 105, 116} + return result, nil + } + case "august":{ + result := []byte{97, 117, 103, 117, 115, 116} + return result, nil + } + case "aunt":{ + result := []byte{97, 117, 110, 116} + return result, nil + } + case "author":{ + result := []byte{97, 117, 116, 104, 111, 114} + return result, nil + } + case "auto":{ + result := []byte{97, 117, 116, 111} + return result, nil + } + case "autumn":{ + result := []byte{97, 117, 116, 117, 109, 110} + return result, nil + } + case "average":{ + result := []byte{97, 118, 101, 114, 97, 103, 101} + return result, nil + } + case "avocado":{ + result := []byte{97, 118, 111, 99, 97, 100, 111} + return result, nil + } + case "avoid":{ + result := []byte{97, 118, 111, 105, 100} + return result, nil + } + case "awake":{ + result := []byte{97, 119, 97, 107, 101} + return result, nil + } + case "aware":{ + result := []byte{97, 119, 97, 114, 101} + return result, nil + } + case "away":{ + result := []byte{97, 119, 97, 121} + return result, nil + } + case "awesome":{ + result := []byte{97, 119, 101, 115, 111, 109, 101} + return result, nil + } + case "awful":{ + result := []byte{97, 119, 102, 117, 108} + return result, nil + } + case "awkward":{ + result := []byte{97, 119, 107, 119, 97, 114, 100} + return result, nil + } + case "axis":{ + result := []byte{97, 120, 105, 115} + return result, nil + } + case "baby":{ + result := []byte{98, 97, 98, 121} + return result, nil + } + case "bachelor":{ + result := []byte{98, 97, 99, 104, 101, 108, 111, 114} + return result, nil + } + case "bacon":{ + result := []byte{98, 97, 99, 111, 110} + return result, nil + } + case "badge":{ + result := []byte{98, 97, 100, 103, 101} + return result, nil + } + case "bag":{ + result := []byte{98, 97, 103} + return result, nil + } + case "balance":{ + result := []byte{98, 97, 108, 97, 110, 99, 101} + return result, nil + } + case "balcony":{ + result := []byte{98, 97, 108, 99, 111, 110, 121} + return result, nil + } + case "ball":{ + result := []byte{98, 97, 108, 108} + return result, nil + } + case "bamboo":{ + result := []byte{98, 97, 109, 98, 111, 111} + return result, nil + } + case "banana":{ + result := []byte{98, 97, 110, 97, 110, 97} + return result, nil + } + case "banner":{ + result := []byte{98, 97, 110, 110, 101, 114} + return result, nil + } + case "bar":{ + result := []byte{98, 97, 114} + return result, nil + } + case "barely":{ + result := []byte{98, 97, 114, 101, 108, 121} + return result, nil + } + case "bargain":{ + result := []byte{98, 97, 114, 103, 97, 105, 110} + return result, nil + } + case "barrel":{ + result := []byte{98, 97, 114, 114, 101, 108} + return result, nil + } + case "base":{ + result := []byte{98, 97, 115, 101} + return result, nil + } + case "basic":{ + result := []byte{98, 97, 115, 105, 99} + return result, nil + } + case "basket":{ + result := []byte{98, 97, 115, 107, 101, 116} + return result, nil + } + case "battle":{ + result := []byte{98, 97, 116, 116, 108, 101} + return result, nil + } + case "beach":{ + result := []byte{98, 101, 97, 99, 104} + return result, nil + } + case "bean":{ + result := []byte{98, 101, 97, 110} + return result, nil + } + case "beauty":{ + result := []byte{98, 101, 97, 117, 116, 121} + return result, nil + } + case "because":{ + result := []byte{98, 101, 99, 97, 117, 115, 101} + return result, nil + } + case "become":{ + result := []byte{98, 101, 99, 111, 109, 101} + return result, nil + } + case "beef":{ + result := []byte{98, 101, 101, 102} + return result, nil + } + case "before":{ + result := []byte{98, 101, 102, 111, 114, 101} + return result, nil + } + case "begin":{ + result := []byte{98, 101, 103, 105, 110} + return result, nil + } + case "behave":{ + result := []byte{98, 101, 104, 97, 118, 101} + return result, nil + } + case "behind":{ + result := []byte{98, 101, 104, 105, 110, 100} + return result, nil + } + case "believe":{ + result := []byte{98, 101, 108, 105, 101, 118, 101} + return result, nil + } + case "below":{ + result := []byte{98, 101, 108, 111, 119} + return result, nil + } + case "belt":{ + result := []byte{98, 101, 108, 116} + return result, nil + } + case "bench":{ + result := []byte{98, 101, 110, 99, 104} + return result, nil + } + case "benefit":{ + result := []byte{98, 101, 110, 101, 102, 105, 116} + return result, nil + } + case "best":{ + result := []byte{98, 101, 115, 116} + return result, nil + } + case "betray":{ + result := []byte{98, 101, 116, 114, 97, 121} + return result, nil + } + case "better":{ + result := []byte{98, 101, 116, 116, 101, 114} + return result, nil + } + case "between":{ + result := []byte{98, 101, 116, 119, 101, 101, 110} + return result, nil + } + case "beyond":{ + result := []byte{98, 101, 121, 111, 110, 100} + return result, nil + } + case "bicycle":{ + result := []byte{98, 105, 99, 121, 99, 108, 101} + return result, nil + } + case "bid":{ + result := []byte{98, 105, 100} + return result, nil + } + case "bike":{ + result := []byte{98, 105, 107, 101} + return result, nil + } + case "bind":{ + result := []byte{98, 105, 110, 100} + return result, nil + } + case "biology":{ + result := []byte{98, 105, 111, 108, 111, 103, 121} + return result, nil + } + case "bird":{ + result := []byte{98, 105, 114, 100} + return result, nil + } + case "birth":{ + result := []byte{98, 105, 114, 116, 104} + return result, nil + } + case "bitter":{ + result := []byte{98, 105, 116, 116, 101, 114} + return result, nil + } + case "black":{ + result := []byte{98, 108, 97, 99, 107} + return result, nil + } + case "blade":{ + result := []byte{98, 108, 97, 100, 101} + return result, nil + } + case "blame":{ + result := []byte{98, 108, 97, 109, 101} + return result, nil + } + case "blanket":{ + result := []byte{98, 108, 97, 110, 107, 101, 116} + return result, nil + } + case "blast":{ + result := []byte{98, 108, 97, 115, 116} + return result, nil + } + case "bleak":{ + result := []byte{98, 108, 101, 97, 107} + return result, nil + } + case "bless":{ + result := []byte{98, 108, 101, 115, 115} + return result, nil + } + case "blind":{ + result := []byte{98, 108, 105, 110, 100} + return result, nil + } + case "blood":{ + result := []byte{98, 108, 111, 111, 100} + return result, nil + } + case "blossom":{ + result := []byte{98, 108, 111, 115, 115, 111, 109} + return result, nil + } + case "blouse":{ + result := []byte{98, 108, 111, 117, 115, 101} + return result, nil + } + case "blue":{ + result := []byte{98, 108, 117, 101} + return result, nil + } + case "blur":{ + result := []byte{98, 108, 117, 114} + return result, nil + } + case "blush":{ + result := []byte{98, 108, 117, 115, 104} + return result, nil + } + case "board":{ + result := []byte{98, 111, 97, 114, 100} + return result, nil + } + case "boat":{ + result := []byte{98, 111, 97, 116} + return result, nil + } + case "body":{ + result := []byte{98, 111, 100, 121} + return result, nil + } + case "boil":{ + result := []byte{98, 111, 105, 108} + return result, nil + } + case "bomb":{ + result := []byte{98, 111, 109, 98} + return result, nil + } + case "bone":{ + result := []byte{98, 111, 110, 101} + return result, nil + } + case "bonus":{ + result := []byte{98, 111, 110, 117, 115} + return result, nil + } + case "book":{ + result := []byte{98, 111, 111, 107} + return result, nil + } + case "boost":{ + result := []byte{98, 111, 111, 115, 116} + return result, nil + } + case "border":{ + result := []byte{98, 111, 114, 100, 101, 114} + return result, nil + } + case "boring":{ + result := []byte{98, 111, 114, 105, 110, 103} + return result, nil + } + case "borrow":{ + result := []byte{98, 111, 114, 114, 111, 119} + return result, nil + } + case "boss":{ + result := []byte{98, 111, 115, 115} + return result, nil + } + case "bottom":{ + result := []byte{98, 111, 116, 116, 111, 109} + return result, nil + } + case "bounce":{ + result := []byte{98, 111, 117, 110, 99, 101} + return result, nil + } + case "box":{ + result := []byte{98, 111, 120} + return result, nil + } + case "boy":{ + result := []byte{98, 111, 121} + return result, nil + } + case "bracket":{ + result := []byte{98, 114, 97, 99, 107, 101, 116} + return result, nil + } + case "brain":{ + result := []byte{98, 114, 97, 105, 110} + return result, nil + } + case "brand":{ + result := []byte{98, 114, 97, 110, 100} + return result, nil + } + case "brass":{ + result := []byte{98, 114, 97, 115, 115} + return result, nil + } + case "brave":{ + result := []byte{98, 114, 97, 118, 101} + return result, nil + } + case "bread":{ + result := []byte{98, 114, 101, 97, 100} + return result, nil + } + case "breeze":{ + result := []byte{98, 114, 101, 101, 122, 101} + return result, nil + } + case "brick":{ + result := []byte{98, 114, 105, 99, 107} + return result, nil + } + case "bridge":{ + result := []byte{98, 114, 105, 100, 103, 101} + return result, nil + } + case "brief":{ + result := []byte{98, 114, 105, 101, 102} + return result, nil + } + case "bright":{ + result := []byte{98, 114, 105, 103, 104, 116} + return result, nil + } + case "bring":{ + result := []byte{98, 114, 105, 110, 103} + return result, nil + } + case "brisk":{ + result := []byte{98, 114, 105, 115, 107} + return result, nil + } + case "broccoli":{ + result := []byte{98, 114, 111, 99, 99, 111, 108, 105} + return result, nil + } + case "broken":{ + result := []byte{98, 114, 111, 107, 101, 110} + return result, nil + } + case "bronze":{ + result := []byte{98, 114, 111, 110, 122, 101} + return result, nil + } + case "broom":{ + result := []byte{98, 114, 111, 111, 109} + return result, nil + } + case "brother":{ + result := []byte{98, 114, 111, 116, 104, 101, 114} + return result, nil + } + case "brown":{ + result := []byte{98, 114, 111, 119, 110} + return result, nil + } + case "brush":{ + result := []byte{98, 114, 117, 115, 104} + return result, nil + } + case "bubble":{ + result := []byte{98, 117, 98, 98, 108, 101} + return result, nil + } + case "buddy":{ + result := []byte{98, 117, 100, 100, 121} + return result, nil + } + case "budget":{ + result := []byte{98, 117, 100, 103, 101, 116} + return result, nil + } + case "buffalo":{ + result := []byte{98, 117, 102, 102, 97, 108, 111} + return result, nil + } + case "build":{ + result := []byte{98, 117, 105, 108, 100} + return result, nil + } + case "bulb":{ + result := []byte{98, 117, 108, 98} + return result, nil + } + case "bulk":{ + result := []byte{98, 117, 108, 107} + return result, nil + } + case "bullet":{ + result := []byte{98, 117, 108, 108, 101, 116} + return result, nil + } + case "bundle":{ + result := []byte{98, 117, 110, 100, 108, 101} + return result, nil + } + case "bunker":{ + result := []byte{98, 117, 110, 107, 101, 114} + return result, nil + } + case "burden":{ + result := []byte{98, 117, 114, 100, 101, 110} + return result, nil + } + case "burger":{ + result := []byte{98, 117, 114, 103, 101, 114} + return result, nil + } + case "burst":{ + result := []byte{98, 117, 114, 115, 116} + return result, nil + } + case "bus":{ + result := []byte{98, 117, 115} + return result, nil + } + case "business":{ + result := []byte{98, 117, 115, 105, 110, 101, 115, 115} + return result, nil + } + case "busy":{ + result := []byte{98, 117, 115, 121} + return result, nil + } + case "butter":{ + result := []byte{98, 117, 116, 116, 101, 114} + return result, nil + } + case "buyer":{ + result := []byte{98, 117, 121, 101, 114} + return result, nil + } + case "buzz":{ + result := []byte{98, 117, 122, 122} + return result, nil + } + case "cabbage":{ + result := []byte{99, 97, 98, 98, 97, 103, 101} + return result, nil + } + case "cabin":{ + result := []byte{99, 97, 98, 105, 110} + return result, nil + } + case "cable":{ + result := []byte{99, 97, 98, 108, 101} + return result, nil + } + case "cactus":{ + result := []byte{99, 97, 99, 116, 117, 115} + return result, nil + } + case "cage":{ + result := []byte{99, 97, 103, 101} + return result, nil + } + case "cake":{ + result := []byte{99, 97, 107, 101} + return result, nil + } + case "call":{ + result := []byte{99, 97, 108, 108} + return result, nil + } + case "calm":{ + result := []byte{99, 97, 108, 109} + return result, nil + } + case "camera":{ + result := []byte{99, 97, 109, 101, 114, 97} + return result, nil + } + case "camp":{ + result := []byte{99, 97, 109, 112} + return result, nil + } + case "can":{ + result := []byte{99, 97, 110} + return result, nil + } + case "canal":{ + result := []byte{99, 97, 110, 97, 108} + return result, nil + } + case "cancel":{ + result := []byte{99, 97, 110, 99, 101, 108} + return result, nil + } + case "candy":{ + result := []byte{99, 97, 110, 100, 121} + return result, nil + } + case "cannon":{ + result := []byte{99, 97, 110, 110, 111, 110} + return result, nil + } + case "canoe":{ + result := []byte{99, 97, 110, 111, 101} + return result, nil + } + case "canvas":{ + result := []byte{99, 97, 110, 118, 97, 115} + return result, nil + } + case "canyon":{ + result := []byte{99, 97, 110, 121, 111, 110} + return result, nil + } + case "capable":{ + result := []byte{99, 97, 112, 97, 98, 108, 101} + return result, nil + } + case "capital":{ + result := []byte{99, 97, 112, 105, 116, 97, 108} + return result, nil + } + case "captain":{ + result := []byte{99, 97, 112, 116, 97, 105, 110} + return result, nil + } + case "car":{ + result := []byte{99, 97, 114} + return result, nil + } + case "carbon":{ + result := []byte{99, 97, 114, 98, 111, 110} + return result, nil + } + case "card":{ + result := []byte{99, 97, 114, 100} + return result, nil + } + case "cargo":{ + result := []byte{99, 97, 114, 103, 111} + return result, nil + } + case "carpet":{ + result := []byte{99, 97, 114, 112, 101, 116} + return result, nil + } + case "carry":{ + result := []byte{99, 97, 114, 114, 121} + return result, nil + } + case "cart":{ + result := []byte{99, 97, 114, 116} + return result, nil + } + case "case":{ + result := []byte{99, 97, 115, 101} + return result, nil + } + case "cash":{ + result := []byte{99, 97, 115, 104} + return result, nil + } + case "casino":{ + result := []byte{99, 97, 115, 105, 110, 111} + return result, nil + } + case "castle":{ + result := []byte{99, 97, 115, 116, 108, 101} + return result, nil + } + case "casual":{ + result := []byte{99, 97, 115, 117, 97, 108} + return result, nil + } + case "cat":{ + result := []byte{99, 97, 116} + return result, nil + } + case "catalog":{ + result := []byte{99, 97, 116, 97, 108, 111, 103} + return result, nil + } + case "catch":{ + result := []byte{99, 97, 116, 99, 104} + return result, nil + } + case "category":{ + result := []byte{99, 97, 116, 101, 103, 111, 114, 121} + return result, nil + } + case "cattle":{ + result := []byte{99, 97, 116, 116, 108, 101} + return result, nil + } + case "caught":{ + result := []byte{99, 97, 117, 103, 104, 116} + return result, nil + } + case "cause":{ + result := []byte{99, 97, 117, 115, 101} + return result, nil + } + case "caution":{ + result := []byte{99, 97, 117, 116, 105, 111, 110} + return result, nil + } + case "cave":{ + result := []byte{99, 97, 118, 101} + return result, nil + } + case "ceiling":{ + result := []byte{99, 101, 105, 108, 105, 110, 103} + return result, nil + } + case "celery":{ + result := []byte{99, 101, 108, 101, 114, 121} + return result, nil + } + case "cement":{ + result := []byte{99, 101, 109, 101, 110, 116} + return result, nil + } + case "census":{ + result := []byte{99, 101, 110, 115, 117, 115} + return result, nil + } + case "century":{ + result := []byte{99, 101, 110, 116, 117, 114, 121} + return result, nil + } + case "cereal":{ + result := []byte{99, 101, 114, 101, 97, 108} + return result, nil + } + case "certain":{ + result := []byte{99, 101, 114, 116, 97, 105, 110} + return result, nil + } + case "chair":{ + result := []byte{99, 104, 97, 105, 114} + return result, nil + } + case "chalk":{ + result := []byte{99, 104, 97, 108, 107} + return result, nil + } + case "champion":{ + result := []byte{99, 104, 97, 109, 112, 105, 111, 110} + return result, nil + } + case "change":{ + result := []byte{99, 104, 97, 110, 103, 101} + return result, nil + } + case "chaos":{ + result := []byte{99, 104, 97, 111, 115} + return result, nil + } + case "chapter":{ + result := []byte{99, 104, 97, 112, 116, 101, 114} + return result, nil + } + case "charge":{ + result := []byte{99, 104, 97, 114, 103, 101} + return result, nil + } + case "chase":{ + result := []byte{99, 104, 97, 115, 101} + return result, nil + } + case "chat":{ + result := []byte{99, 104, 97, 116} + return result, nil + } + case "cheap":{ + result := []byte{99, 104, 101, 97, 112} + return result, nil + } + case "check":{ + result := []byte{99, 104, 101, 99, 107} + return result, nil + } + case "cheese":{ + result := []byte{99, 104, 101, 101, 115, 101} + return result, nil + } + case "chef":{ + result := []byte{99, 104, 101, 102} + return result, nil + } + case "cherry":{ + result := []byte{99, 104, 101, 114, 114, 121} + return result, nil + } + case "chest":{ + result := []byte{99, 104, 101, 115, 116} + return result, nil + } + case "chicken":{ + result := []byte{99, 104, 105, 99, 107, 101, 110} + return result, nil + } + case "chief":{ + result := []byte{99, 104, 105, 101, 102} + return result, nil + } + case "child":{ + result := []byte{99, 104, 105, 108, 100} + return result, nil + } + case "chimney":{ + result := []byte{99, 104, 105, 109, 110, 101, 121} + return result, nil + } + case "choice":{ + result := []byte{99, 104, 111, 105, 99, 101} + return result, nil + } + case "choose":{ + result := []byte{99, 104, 111, 111, 115, 101} + return result, nil + } + case "chronic":{ + result := []byte{99, 104, 114, 111, 110, 105, 99} + return result, nil + } + case "chuckle":{ + result := []byte{99, 104, 117, 99, 107, 108, 101} + return result, nil + } + case "chunk":{ + result := []byte{99, 104, 117, 110, 107} + return result, nil + } + case "churn":{ + result := []byte{99, 104, 117, 114, 110} + return result, nil + } + case "cigar":{ + result := []byte{99, 105, 103, 97, 114} + return result, nil + } + case "cinnamon":{ + result := []byte{99, 105, 110, 110, 97, 109, 111, 110} + return result, nil + } + case "circle":{ + result := []byte{99, 105, 114, 99, 108, 101} + return result, nil + } + case "citizen":{ + result := []byte{99, 105, 116, 105, 122, 101, 110} + return result, nil + } + case "city":{ + result := []byte{99, 105, 116, 121} + return result, nil + } + case "civil":{ + result := []byte{99, 105, 118, 105, 108} + return result, nil + } + case "claim":{ + result := []byte{99, 108, 97, 105, 109} + return result, nil + } + case "clap":{ + result := []byte{99, 108, 97, 112} + return result, nil + } + case "clarify":{ + result := []byte{99, 108, 97, 114, 105, 102, 121} + return result, nil + } + case "claw":{ + result := []byte{99, 108, 97, 119} + return result, nil + } + case "clay":{ + result := []byte{99, 108, 97, 121} + return result, nil + } + case "clean":{ + result := []byte{99, 108, 101, 97, 110} + return result, nil + } + case "clerk":{ + result := []byte{99, 108, 101, 114, 107} + return result, nil + } + case "clever":{ + result := []byte{99, 108, 101, 118, 101, 114} + return result, nil + } + case "click":{ + result := []byte{99, 108, 105, 99, 107} + return result, nil + } + case "client":{ + result := []byte{99, 108, 105, 101, 110, 116} + return result, nil + } + case "cliff":{ + result := []byte{99, 108, 105, 102, 102} + return result, nil + } + case "climb":{ + result := []byte{99, 108, 105, 109, 98} + return result, nil + } + case "clinic":{ + result := []byte{99, 108, 105, 110, 105, 99} + return result, nil + } + case "clip":{ + result := []byte{99, 108, 105, 112} + return result, nil + } + case "clock":{ + result := []byte{99, 108, 111, 99, 107} + return result, nil + } + case "clog":{ + result := []byte{99, 108, 111, 103} + return result, nil + } + case "close":{ + result := []byte{99, 108, 111, 115, 101} + return result, nil + } + case "cloth":{ + result := []byte{99, 108, 111, 116, 104} + return result, nil + } + case "cloud":{ + result := []byte{99, 108, 111, 117, 100} + return result, nil + } + case "clown":{ + result := []byte{99, 108, 111, 119, 110} + return result, nil + } + case "club":{ + result := []byte{99, 108, 117, 98} + return result, nil + } + case "clump":{ + result := []byte{99, 108, 117, 109, 112} + return result, nil + } + case "cluster":{ + result := []byte{99, 108, 117, 115, 116, 101, 114} + return result, nil + } + case "clutch":{ + result := []byte{99, 108, 117, 116, 99, 104} + return result, nil + } + case "coach":{ + result := []byte{99, 111, 97, 99, 104} + return result, nil + } + case "coast":{ + result := []byte{99, 111, 97, 115, 116} + return result, nil + } + case "coconut":{ + result := []byte{99, 111, 99, 111, 110, 117, 116} + return result, nil + } + case "code":{ + result := []byte{99, 111, 100, 101} + return result, nil + } + case "coffee":{ + result := []byte{99, 111, 102, 102, 101, 101} + return result, nil + } + case "coil":{ + result := []byte{99, 111, 105, 108} + return result, nil + } + case "coin":{ + result := []byte{99, 111, 105, 110} + return result, nil + } + case "collect":{ + result := []byte{99, 111, 108, 108, 101, 99, 116} + return result, nil + } + case "color":{ + result := []byte{99, 111, 108, 111, 114} + return result, nil + } + case "column":{ + result := []byte{99, 111, 108, 117, 109, 110} + return result, nil + } + case "combine":{ + result := []byte{99, 111, 109, 98, 105, 110, 101} + return result, nil + } + case "come":{ + result := []byte{99, 111, 109, 101} + return result, nil + } + case "comfort":{ + result := []byte{99, 111, 109, 102, 111, 114, 116} + return result, nil + } + case "comic":{ + result := []byte{99, 111, 109, 105, 99} + return result, nil + } + case "common":{ + result := []byte{99, 111, 109, 109, 111, 110} + return result, nil + } + case "company":{ + result := []byte{99, 111, 109, 112, 97, 110, 121} + return result, nil + } + case "concert":{ + result := []byte{99, 111, 110, 99, 101, 114, 116} + return result, nil + } + case "conduct":{ + result := []byte{99, 111, 110, 100, 117, 99, 116} + return result, nil + } + case "confirm":{ + result := []byte{99, 111, 110, 102, 105, 114, 109} + return result, nil + } + case "congress":{ + result := []byte{99, 111, 110, 103, 114, 101, 115, 115} + return result, nil + } + case "connect":{ + result := []byte{99, 111, 110, 110, 101, 99, 116} + return result, nil + } + case "consider":{ + result := []byte{99, 111, 110, 115, 105, 100, 101, 114} + return result, nil + } + case "control":{ + result := []byte{99, 111, 110, 116, 114, 111, 108} + return result, nil + } + case "convince":{ + result := []byte{99, 111, 110, 118, 105, 110, 99, 101} + return result, nil + } + case "cook":{ + result := []byte{99, 111, 111, 107} + return result, nil + } + case "cool":{ + result := []byte{99, 111, 111, 108} + return result, nil + } + case "copper":{ + result := []byte{99, 111, 112, 112, 101, 114} + return result, nil + } + case "copy":{ + result := []byte{99, 111, 112, 121} + return result, nil + } + case "coral":{ + result := []byte{99, 111, 114, 97, 108} + return result, nil + } + case "core":{ + result := []byte{99, 111, 114, 101} + return result, nil + } + case "corn":{ + result := []byte{99, 111, 114, 110} + return result, nil + } + case "correct":{ + result := []byte{99, 111, 114, 114, 101, 99, 116} + return result, nil + } + case "cost":{ + result := []byte{99, 111, 115, 116} + return result, nil + } + case "cotton":{ + result := []byte{99, 111, 116, 116, 111, 110} + return result, nil + } + case "couch":{ + result := []byte{99, 111, 117, 99, 104} + return result, nil + } + case "country":{ + result := []byte{99, 111, 117, 110, 116, 114, 121} + return result, nil + } + case "couple":{ + result := []byte{99, 111, 117, 112, 108, 101} + return result, nil + } + case "course":{ + result := []byte{99, 111, 117, 114, 115, 101} + return result, nil + } + case "cousin":{ + result := []byte{99, 111, 117, 115, 105, 110} + return result, nil + } + case "cover":{ + result := []byte{99, 111, 118, 101, 114} + return result, nil + } + case "coyote":{ + result := []byte{99, 111, 121, 111, 116, 101} + return result, nil + } + case "crack":{ + result := []byte{99, 114, 97, 99, 107} + return result, nil + } + case "cradle":{ + result := []byte{99, 114, 97, 100, 108, 101} + return result, nil + } + case "craft":{ + result := []byte{99, 114, 97, 102, 116} + return result, nil + } + case "cram":{ + result := []byte{99, 114, 97, 109} + return result, nil + } + case "crane":{ + result := []byte{99, 114, 97, 110, 101} + return result, nil + } + case "crash":{ + result := []byte{99, 114, 97, 115, 104} + return result, nil + } + case "crater":{ + result := []byte{99, 114, 97, 116, 101, 114} + return result, nil + } + case "crawl":{ + result := []byte{99, 114, 97, 119, 108} + return result, nil + } + case "crazy":{ + result := []byte{99, 114, 97, 122, 121} + return result, nil + } + case "cream":{ + result := []byte{99, 114, 101, 97, 109} + return result, nil + } + case "credit":{ + result := []byte{99, 114, 101, 100, 105, 116} + return result, nil + } + case "creek":{ + result := []byte{99, 114, 101, 101, 107} + return result, nil + } + case "crew":{ + result := []byte{99, 114, 101, 119} + return result, nil + } + case "cricket":{ + result := []byte{99, 114, 105, 99, 107, 101, 116} + return result, nil + } + case "crime":{ + result := []byte{99, 114, 105, 109, 101} + return result, nil + } + case "crisp":{ + result := []byte{99, 114, 105, 115, 112} + return result, nil + } + case "critic":{ + result := []byte{99, 114, 105, 116, 105, 99} + return result, nil + } + case "crop":{ + result := []byte{99, 114, 111, 112} + return result, nil + } + case "cross":{ + result := []byte{99, 114, 111, 115, 115} + return result, nil + } + case "crouch":{ + result := []byte{99, 114, 111, 117, 99, 104} + return result, nil + } + case "crowd":{ + result := []byte{99, 114, 111, 119, 100} + return result, nil + } + case "crucial":{ + result := []byte{99, 114, 117, 99, 105, 97, 108} + return result, nil + } + case "cruel":{ + result := []byte{99, 114, 117, 101, 108} + return result, nil + } + case "cruise":{ + result := []byte{99, 114, 117, 105, 115, 101} + return result, nil + } + case "crumble":{ + result := []byte{99, 114, 117, 109, 98, 108, 101} + return result, nil + } + case "crunch":{ + result := []byte{99, 114, 117, 110, 99, 104} + return result, nil + } + case "crush":{ + result := []byte{99, 114, 117, 115, 104} + return result, nil + } + case "cry":{ + result := []byte{99, 114, 121} + return result, nil + } + case "crystal":{ + result := []byte{99, 114, 121, 115, 116, 97, 108} + return result, nil + } + case "cube":{ + result := []byte{99, 117, 98, 101} + return result, nil + } + case "culture":{ + result := []byte{99, 117, 108, 116, 117, 114, 101} + return result, nil + } + case "cup":{ + result := []byte{99, 117, 112} + return result, nil + } + case "cupboard":{ + result := []byte{99, 117, 112, 98, 111, 97, 114, 100} + return result, nil + } + case "curious":{ + result := []byte{99, 117, 114, 105, 111, 117, 115} + return result, nil + } + case "current":{ + result := []byte{99, 117, 114, 114, 101, 110, 116} + return result, nil + } + case "curtain":{ + result := []byte{99, 117, 114, 116, 97, 105, 110} + return result, nil + } + case "curve":{ + result := []byte{99, 117, 114, 118, 101} + return result, nil + } + case "cushion":{ + result := []byte{99, 117, 115, 104, 105, 111, 110} + return result, nil + } + case "custom":{ + result := []byte{99, 117, 115, 116, 111, 109} + return result, nil + } + case "cute":{ + result := []byte{99, 117, 116, 101} + return result, nil + } + case "cycle":{ + result := []byte{99, 121, 99, 108, 101} + return result, nil + } + case "dad":{ + result := []byte{100, 97, 100} + return result, nil + } + case "damage":{ + result := []byte{100, 97, 109, 97, 103, 101} + return result, nil + } + case "damp":{ + result := []byte{100, 97, 109, 112} + return result, nil + } + case "dance":{ + result := []byte{100, 97, 110, 99, 101} + return result, nil + } + case "danger":{ + result := []byte{100, 97, 110, 103, 101, 114} + return result, nil + } + case "daring":{ + result := []byte{100, 97, 114, 105, 110, 103} + return result, nil + } + case "dash":{ + result := []byte{100, 97, 115, 104} + return result, nil + } + case "daughter":{ + result := []byte{100, 97, 117, 103, 104, 116, 101, 114} + return result, nil + } + case "dawn":{ + result := []byte{100, 97, 119, 110} + return result, nil + } + case "day":{ + result := []byte{100, 97, 121} + return result, nil + } + case "deal":{ + result := []byte{100, 101, 97, 108} + return result, nil + } + case "debate":{ + result := []byte{100, 101, 98, 97, 116, 101} + return result, nil + } + case "debris":{ + result := []byte{100, 101, 98, 114, 105, 115} + return result, nil + } + case "decade":{ + result := []byte{100, 101, 99, 97, 100, 101} + return result, nil + } + case "december":{ + result := []byte{100, 101, 99, 101, 109, 98, 101, 114} + return result, nil + } + case "decide":{ + result := []byte{100, 101, 99, 105, 100, 101} + return result, nil + } + case "decline":{ + result := []byte{100, 101, 99, 108, 105, 110, 101} + return result, nil + } + case "decorate":{ + result := []byte{100, 101, 99, 111, 114, 97, 116, 101} + return result, nil + } + case "decrease":{ + result := []byte{100, 101, 99, 114, 101, 97, 115, 101} + return result, nil + } + case "deer":{ + result := []byte{100, 101, 101, 114} + return result, nil + } + case "defense":{ + result := []byte{100, 101, 102, 101, 110, 115, 101} + return result, nil + } + case "define":{ + result := []byte{100, 101, 102, 105, 110, 101} + return result, nil + } + case "defy":{ + result := []byte{100, 101, 102, 121} + return result, nil + } + case "degree":{ + result := []byte{100, 101, 103, 114, 101, 101} + return result, nil + } + case "delay":{ + result := []byte{100, 101, 108, 97, 121} + return result, nil + } + case "deliver":{ + result := []byte{100, 101, 108, 105, 118, 101, 114} + return result, nil + } + case "demand":{ + result := []byte{100, 101, 109, 97, 110, 100} + return result, nil + } + case "demise":{ + result := []byte{100, 101, 109, 105, 115, 101} + return result, nil + } + case "denial":{ + result := []byte{100, 101, 110, 105, 97, 108} + return result, nil + } + case "dentist":{ + result := []byte{100, 101, 110, 116, 105, 115, 116} + return result, nil + } + case "deny":{ + result := []byte{100, 101, 110, 121} + return result, nil + } + case "depart":{ + result := []byte{100, 101, 112, 97, 114, 116} + return result, nil + } + case "depend":{ + result := []byte{100, 101, 112, 101, 110, 100} + return result, nil + } + case "deposit":{ + result := []byte{100, 101, 112, 111, 115, 105, 116} + return result, nil + } + case "depth":{ + result := []byte{100, 101, 112, 116, 104} + return result, nil + } + case "deputy":{ + result := []byte{100, 101, 112, 117, 116, 121} + return result, nil + } + case "derive":{ + result := []byte{100, 101, 114, 105, 118, 101} + return result, nil + } + case "describe":{ + result := []byte{100, 101, 115, 99, 114, 105, 98, 101} + return result, nil + } + case "desert":{ + result := []byte{100, 101, 115, 101, 114, 116} + return result, nil + } + case "design":{ + result := []byte{100, 101, 115, 105, 103, 110} + return result, nil + } + case "desk":{ + result := []byte{100, 101, 115, 107} + return result, nil + } + case "despair":{ + result := []byte{100, 101, 115, 112, 97, 105, 114} + return result, nil + } + case "destroy":{ + result := []byte{100, 101, 115, 116, 114, 111, 121} + return result, nil + } + case "detail":{ + result := []byte{100, 101, 116, 97, 105, 108} + return result, nil + } + case "detect":{ + result := []byte{100, 101, 116, 101, 99, 116} + return result, nil + } + case "develop":{ + result := []byte{100, 101, 118, 101, 108, 111, 112} + return result, nil + } + case "device":{ + result := []byte{100, 101, 118, 105, 99, 101} + return result, nil + } + case "devote":{ + result := []byte{100, 101, 118, 111, 116, 101} + return result, nil + } + case "diagram":{ + result := []byte{100, 105, 97, 103, 114, 97, 109} + return result, nil + } + case "dial":{ + result := []byte{100, 105, 97, 108} + return result, nil + } + case "diamond":{ + result := []byte{100, 105, 97, 109, 111, 110, 100} + return result, nil + } + case "diary":{ + result := []byte{100, 105, 97, 114, 121} + return result, nil + } + case "dice":{ + result := []byte{100, 105, 99, 101} + return result, nil + } + case "diesel":{ + result := []byte{100, 105, 101, 115, 101, 108} + return result, nil + } + case "diet":{ + result := []byte{100, 105, 101, 116} + return result, nil + } + case "differ":{ + result := []byte{100, 105, 102, 102, 101, 114} + return result, nil + } + case "digital":{ + result := []byte{100, 105, 103, 105, 116, 97, 108} + return result, nil + } + case "dignity":{ + result := []byte{100, 105, 103, 110, 105, 116, 121} + return result, nil + } + case "dilemma":{ + result := []byte{100, 105, 108, 101, 109, 109, 97} + return result, nil + } + case "dinner":{ + result := []byte{100, 105, 110, 110, 101, 114} + return result, nil + } + case "dinosaur":{ + result := []byte{100, 105, 110, 111, 115, 97, 117, 114} + return result, nil + } + case "direct":{ + result := []byte{100, 105, 114, 101, 99, 116} + return result, nil + } + case "dirt":{ + result := []byte{100, 105, 114, 116} + return result, nil + } + case "disagree":{ + result := []byte{100, 105, 115, 97, 103, 114, 101, 101} + return result, nil + } + case "discover":{ + result := []byte{100, 105, 115, 99, 111, 118, 101, 114} + return result, nil + } + case "disease":{ + result := []byte{100, 105, 115, 101, 97, 115, 101} + return result, nil + } + case "dish":{ + result := []byte{100, 105, 115, 104} + return result, nil + } + case "dismiss":{ + result := []byte{100, 105, 115, 109, 105, 115, 115} + return result, nil + } + case "disorder":{ + result := []byte{100, 105, 115, 111, 114, 100, 101, 114} + return result, nil + } + case "display":{ + result := []byte{100, 105, 115, 112, 108, 97, 121} + return result, nil + } + case "distance":{ + result := []byte{100, 105, 115, 116, 97, 110, 99, 101} + return result, nil + } + case "divert":{ + result := []byte{100, 105, 118, 101, 114, 116} + return result, nil + } + case "divide":{ + result := []byte{100, 105, 118, 105, 100, 101} + return result, nil + } + case "divorce":{ + result := []byte{100, 105, 118, 111, 114, 99, 101} + return result, nil + } + case "dizzy":{ + result := []byte{100, 105, 122, 122, 121} + return result, nil + } + case "doctor":{ + result := []byte{100, 111, 99, 116, 111, 114} + return result, nil + } + case "document":{ + result := []byte{100, 111, 99, 117, 109, 101, 110, 116} + return result, nil + } + case "dog":{ + result := []byte{100, 111, 103} + return result, nil + } + case "doll":{ + result := []byte{100, 111, 108, 108} + return result, nil + } + case "dolphin":{ + result := []byte{100, 111, 108, 112, 104, 105, 110} + return result, nil + } + case "domain":{ + result := []byte{100, 111, 109, 97, 105, 110} + return result, nil + } + case "donate":{ + result := []byte{100, 111, 110, 97, 116, 101} + return result, nil + } + case "donkey":{ + result := []byte{100, 111, 110, 107, 101, 121} + return result, nil + } + case "donor":{ + result := []byte{100, 111, 110, 111, 114} + return result, nil + } + case "door":{ + result := []byte{100, 111, 111, 114} + return result, nil + } + case "dose":{ + result := []byte{100, 111, 115, 101} + return result, nil + } + case "double":{ + result := []byte{100, 111, 117, 98, 108, 101} + return result, nil + } + case "dove":{ + result := []byte{100, 111, 118, 101} + return result, nil + } + case "draft":{ + result := []byte{100, 114, 97, 102, 116} + return result, nil + } + case "dragon":{ + result := []byte{100, 114, 97, 103, 111, 110} + return result, nil + } + case "drama":{ + result := []byte{100, 114, 97, 109, 97} + return result, nil + } + case "drastic":{ + result := []byte{100, 114, 97, 115, 116, 105, 99} + return result, nil + } + case "draw":{ + result := []byte{100, 114, 97, 119} + return result, nil + } + case "dream":{ + result := []byte{100, 114, 101, 97, 109} + return result, nil + } + case "dress":{ + result := []byte{100, 114, 101, 115, 115} + return result, nil + } + case "drift":{ + result := []byte{100, 114, 105, 102, 116} + return result, nil + } + case "drill":{ + result := []byte{100, 114, 105, 108, 108} + return result, nil + } + case "drink":{ + result := []byte{100, 114, 105, 110, 107} + return result, nil + } + case "drip":{ + result := []byte{100, 114, 105, 112} + return result, nil + } + case "drive":{ + result := []byte{100, 114, 105, 118, 101} + return result, nil + } + case "drop":{ + result := []byte{100, 114, 111, 112} + return result, nil + } + case "drum":{ + result := []byte{100, 114, 117, 109} + return result, nil + } + case "dry":{ + result := []byte{100, 114, 121} + return result, nil + } + case "duck":{ + result := []byte{100, 117, 99, 107} + return result, nil + } + case "dumb":{ + result := []byte{100, 117, 109, 98} + return result, nil + } + case "dune":{ + result := []byte{100, 117, 110, 101} + return result, nil + } + case "during":{ + result := []byte{100, 117, 114, 105, 110, 103} + return result, nil + } + case "dust":{ + result := []byte{100, 117, 115, 116} + return result, nil + } + case "dutch":{ + result := []byte{100, 117, 116, 99, 104} + return result, nil + } + case "duty":{ + result := []byte{100, 117, 116, 121} + return result, nil + } + case "dwarf":{ + result := []byte{100, 119, 97, 114, 102} + return result, nil + } + case "dynamic":{ + result := []byte{100, 121, 110, 97, 109, 105, 99} + return result, nil + } + case "eager":{ + result := []byte{101, 97, 103, 101, 114} + return result, nil + } + case "eagle":{ + result := []byte{101, 97, 103, 108, 101} + return result, nil + } + case "early":{ + result := []byte{101, 97, 114, 108, 121} + return result, nil + } + case "earn":{ + result := []byte{101, 97, 114, 110} + return result, nil + } + case "earth":{ + result := []byte{101, 97, 114, 116, 104} + return result, nil + } + case "easily":{ + result := []byte{101, 97, 115, 105, 108, 121} + return result, nil + } + case "east":{ + result := []byte{101, 97, 115, 116} + return result, nil + } + case "easy":{ + result := []byte{101, 97, 115, 121} + return result, nil + } + case "echo":{ + result := []byte{101, 99, 104, 111} + return result, nil + } + case "ecology":{ + result := []byte{101, 99, 111, 108, 111, 103, 121} + return result, nil + } + case "economy":{ + result := []byte{101, 99, 111, 110, 111, 109, 121} + return result, nil + } + case "edge":{ + result := []byte{101, 100, 103, 101} + return result, nil + } + case "edit":{ + result := []byte{101, 100, 105, 116} + return result, nil + } + case "educate":{ + result := []byte{101, 100, 117, 99, 97, 116, 101} + return result, nil + } + case "effort":{ + result := []byte{101, 102, 102, 111, 114, 116} + return result, nil + } + case "egg":{ + result := []byte{101, 103, 103} + return result, nil + } + case "eight":{ + result := []byte{101, 105, 103, 104, 116} + return result, nil + } + case "either":{ + result := []byte{101, 105, 116, 104, 101, 114} + return result, nil + } + case "elbow":{ + result := []byte{101, 108, 98, 111, 119} + return result, nil + } + case "elder":{ + result := []byte{101, 108, 100, 101, 114} + return result, nil + } + case "electric":{ + result := []byte{101, 108, 101, 99, 116, 114, 105, 99} + return result, nil + } + case "elegant":{ + result := []byte{101, 108, 101, 103, 97, 110, 116} + return result, nil + } + case "element":{ + result := []byte{101, 108, 101, 109, 101, 110, 116} + return result, nil + } + case "elephant":{ + result := []byte{101, 108, 101, 112, 104, 97, 110, 116} + return result, nil + } + case "elevator":{ + result := []byte{101, 108, 101, 118, 97, 116, 111, 114} + return result, nil + } + case "elite":{ + result := []byte{101, 108, 105, 116, 101} + return result, nil + } + case "else":{ + result := []byte{101, 108, 115, 101} + return result, nil + } + case "embark":{ + result := []byte{101, 109, 98, 97, 114, 107} + return result, nil + } + case "embody":{ + result := []byte{101, 109, 98, 111, 100, 121} + return result, nil + } + case "embrace":{ + result := []byte{101, 109, 98, 114, 97, 99, 101} + return result, nil + } + case "emerge":{ + result := []byte{101, 109, 101, 114, 103, 101} + return result, nil + } + case "emotion":{ + result := []byte{101, 109, 111, 116, 105, 111, 110} + return result, nil + } + case "employ":{ + result := []byte{101, 109, 112, 108, 111, 121} + return result, nil + } + case "empower":{ + result := []byte{101, 109, 112, 111, 119, 101, 114} + return result, nil + } + case "empty":{ + result := []byte{101, 109, 112, 116, 121} + return result, nil + } + case "enable":{ + result := []byte{101, 110, 97, 98, 108, 101} + return result, nil + } + case "enact":{ + result := []byte{101, 110, 97, 99, 116} + return result, nil + } + case "end":{ + result := []byte{101, 110, 100} + return result, nil + } + case "endless":{ + result := []byte{101, 110, 100, 108, 101, 115, 115} + return result, nil + } + case "endorse":{ + result := []byte{101, 110, 100, 111, 114, 115, 101} + return result, nil + } + case "enemy":{ + result := []byte{101, 110, 101, 109, 121} + return result, nil + } + case "energy":{ + result := []byte{101, 110, 101, 114, 103, 121} + return result, nil + } + case "enforce":{ + result := []byte{101, 110, 102, 111, 114, 99, 101} + return result, nil + } + case "engage":{ + result := []byte{101, 110, 103, 97, 103, 101} + return result, nil + } + case "engine":{ + result := []byte{101, 110, 103, 105, 110, 101} + return result, nil + } + case "enhance":{ + result := []byte{101, 110, 104, 97, 110, 99, 101} + return result, nil + } + case "enjoy":{ + result := []byte{101, 110, 106, 111, 121} + return result, nil + } + case "enlist":{ + result := []byte{101, 110, 108, 105, 115, 116} + return result, nil + } + case "enough":{ + result := []byte{101, 110, 111, 117, 103, 104} + return result, nil + } + case "enrich":{ + result := []byte{101, 110, 114, 105, 99, 104} + return result, nil + } + case "enroll":{ + result := []byte{101, 110, 114, 111, 108, 108} + return result, nil + } + case "ensure":{ + result := []byte{101, 110, 115, 117, 114, 101} + return result, nil + } + case "enter":{ + result := []byte{101, 110, 116, 101, 114} + return result, nil + } + case "entire":{ + result := []byte{101, 110, 116, 105, 114, 101} + return result, nil + } + case "entry":{ + result := []byte{101, 110, 116, 114, 121} + return result, nil + } + case "envelope":{ + result := []byte{101, 110, 118, 101, 108, 111, 112, 101} + return result, nil + } + case "episode":{ + result := []byte{101, 112, 105, 115, 111, 100, 101} + return result, nil + } + case "equal":{ + result := []byte{101, 113, 117, 97, 108} + return result, nil + } + case "equip":{ + result := []byte{101, 113, 117, 105, 112} + return result, nil + } + case "era":{ + result := []byte{101, 114, 97} + return result, nil + } + case "erase":{ + result := []byte{101, 114, 97, 115, 101} + return result, nil + } + case "erode":{ + result := []byte{101, 114, 111, 100, 101} + return result, nil + } + case "erosion":{ + result := []byte{101, 114, 111, 115, 105, 111, 110} + return result, nil + } + case "error":{ + result := []byte{101, 114, 114, 111, 114} + return result, nil + } + case "erupt":{ + result := []byte{101, 114, 117, 112, 116} + return result, nil + } + case "escape":{ + result := []byte{101, 115, 99, 97, 112, 101} + return result, nil + } + case "essay":{ + result := []byte{101, 115, 115, 97, 121} + return result, nil + } + case "essence":{ + result := []byte{101, 115, 115, 101, 110, 99, 101} + return result, nil + } + case "estate":{ + result := []byte{101, 115, 116, 97, 116, 101} + return result, nil + } + case "eternal":{ + result := []byte{101, 116, 101, 114, 110, 97, 108} + return result, nil + } + case "ethics":{ + result := []byte{101, 116, 104, 105, 99, 115} + return result, nil + } + case "evidence":{ + result := []byte{101, 118, 105, 100, 101, 110, 99, 101} + return result, nil + } + case "evil":{ + result := []byte{101, 118, 105, 108} + return result, nil + } + case "evoke":{ + result := []byte{101, 118, 111, 107, 101} + return result, nil + } + case "evolve":{ + result := []byte{101, 118, 111, 108, 118, 101} + return result, nil + } + case "exact":{ + result := []byte{101, 120, 97, 99, 116} + return result, nil + } + case "example":{ + result := []byte{101, 120, 97, 109, 112, 108, 101} + return result, nil + } + case "excess":{ + result := []byte{101, 120, 99, 101, 115, 115} + return result, nil + } + case "exchange":{ + result := []byte{101, 120, 99, 104, 97, 110, 103, 101} + return result, nil + } + case "excite":{ + result := []byte{101, 120, 99, 105, 116, 101} + return result, nil + } + case "exclude":{ + result := []byte{101, 120, 99, 108, 117, 100, 101} + return result, nil + } + case "excuse":{ + result := []byte{101, 120, 99, 117, 115, 101} + return result, nil + } + case "execute":{ + result := []byte{101, 120, 101, 99, 117, 116, 101} + return result, nil + } + case "exercise":{ + result := []byte{101, 120, 101, 114, 99, 105, 115, 101} + return result, nil + } + case "exhaust":{ + result := []byte{101, 120, 104, 97, 117, 115, 116} + return result, nil + } + case "exhibit":{ + result := []byte{101, 120, 104, 105, 98, 105, 116} + return result, nil + } + case "exile":{ + result := []byte{101, 120, 105, 108, 101} + return result, nil + } + case "exist":{ + result := []byte{101, 120, 105, 115, 116} + return result, nil + } + case "exit":{ + result := []byte{101, 120, 105, 116} + return result, nil + } + case "exotic":{ + result := []byte{101, 120, 111, 116, 105, 99} + return result, nil + } + case "expand":{ + result := []byte{101, 120, 112, 97, 110, 100} + return result, nil + } + case "expect":{ + result := []byte{101, 120, 112, 101, 99, 116} + return result, nil + } + case "expire":{ + result := []byte{101, 120, 112, 105, 114, 101} + return result, nil + } + case "explain":{ + result := []byte{101, 120, 112, 108, 97, 105, 110} + return result, nil + } + case "expose":{ + result := []byte{101, 120, 112, 111, 115, 101} + return result, nil + } + case "express":{ + result := []byte{101, 120, 112, 114, 101, 115, 115} + return result, nil + } + case "extend":{ + result := []byte{101, 120, 116, 101, 110, 100} + return result, nil + } + case "extra":{ + result := []byte{101, 120, 116, 114, 97} + return result, nil + } + case "eye":{ + result := []byte{101, 121, 101} + return result, nil + } + case "eyebrow":{ + result := []byte{101, 121, 101, 98, 114, 111, 119} + return result, nil + } + case "fabric":{ + result := []byte{102, 97, 98, 114, 105, 99} + return result, nil + } + case "face":{ + result := []byte{102, 97, 99, 101} + return result, nil + } + case "faculty":{ + result := []byte{102, 97, 99, 117, 108, 116, 121} + return result, nil + } + case "fade":{ + result := []byte{102, 97, 100, 101} + return result, nil + } + case "faint":{ + result := []byte{102, 97, 105, 110, 116} + return result, nil + } + case "faith":{ + result := []byte{102, 97, 105, 116, 104} + return result, nil + } + case "fall":{ + result := []byte{102, 97, 108, 108} + return result, nil + } + case "false":{ + result := []byte{102, 97, 108, 115, 101} + return result, nil + } + case "fame":{ + result := []byte{102, 97, 109, 101} + return result, nil + } + case "family":{ + result := []byte{102, 97, 109, 105, 108, 121} + return result, nil + } + case "famous":{ + result := []byte{102, 97, 109, 111, 117, 115} + return result, nil + } + case "fan":{ + result := []byte{102, 97, 110} + return result, nil + } + case "fancy":{ + result := []byte{102, 97, 110, 99, 121} + return result, nil + } + case "fantasy":{ + result := []byte{102, 97, 110, 116, 97, 115, 121} + return result, nil + } + case "farm":{ + result := []byte{102, 97, 114, 109} + return result, nil + } + case "fashion":{ + result := []byte{102, 97, 115, 104, 105, 111, 110} + return result, nil + } + case "fat":{ + result := []byte{102, 97, 116} + return result, nil + } + case "fatal":{ + result := []byte{102, 97, 116, 97, 108} + return result, nil + } + case "father":{ + result := []byte{102, 97, 116, 104, 101, 114} + return result, nil + } + case "fatigue":{ + result := []byte{102, 97, 116, 105, 103, 117, 101} + return result, nil + } + case "fault":{ + result := []byte{102, 97, 117, 108, 116} + return result, nil + } + case "favorite":{ + result := []byte{102, 97, 118, 111, 114, 105, 116, 101} + return result, nil + } + case "feature":{ + result := []byte{102, 101, 97, 116, 117, 114, 101} + return result, nil + } + case "february":{ + result := []byte{102, 101, 98, 114, 117, 97, 114, 121} + return result, nil + } + case "federal":{ + result := []byte{102, 101, 100, 101, 114, 97, 108} + return result, nil + } + case "fee":{ + result := []byte{102, 101, 101} + return result, nil + } + case "feed":{ + result := []byte{102, 101, 101, 100} + return result, nil + } + case "feel":{ + result := []byte{102, 101, 101, 108} + return result, nil + } + case "female":{ + result := []byte{102, 101, 109, 97, 108, 101} + return result, nil + } + case "fence":{ + result := []byte{102, 101, 110, 99, 101} + return result, nil + } + case "festival":{ + result := []byte{102, 101, 115, 116, 105, 118, 97, 108} + return result, nil + } + case "fetch":{ + result := []byte{102, 101, 116, 99, 104} + return result, nil + } + case "fever":{ + result := []byte{102, 101, 118, 101, 114} + return result, nil + } + case "few":{ + result := []byte{102, 101, 119} + return result, nil + } + case "fiber":{ + result := []byte{102, 105, 98, 101, 114} + return result, nil + } + case "fiction":{ + result := []byte{102, 105, 99, 116, 105, 111, 110} + return result, nil + } + case "field":{ + result := []byte{102, 105, 101, 108, 100} + return result, nil + } + case "figure":{ + result := []byte{102, 105, 103, 117, 114, 101} + return result, nil + } + case "file":{ + result := []byte{102, 105, 108, 101} + return result, nil + } + case "film":{ + result := []byte{102, 105, 108, 109} + return result, nil + } + case "filter":{ + result := []byte{102, 105, 108, 116, 101, 114} + return result, nil + } + case "final":{ + result := []byte{102, 105, 110, 97, 108} + return result, nil + } + case "find":{ + result := []byte{102, 105, 110, 100} + return result, nil + } + case "fine":{ + result := []byte{102, 105, 110, 101} + return result, nil + } + case "finger":{ + result := []byte{102, 105, 110, 103, 101, 114} + return result, nil + } + case "finish":{ + result := []byte{102, 105, 110, 105, 115, 104} + return result, nil + } + case "fire":{ + result := []byte{102, 105, 114, 101} + return result, nil + } + case "firm":{ + result := []byte{102, 105, 114, 109} + return result, nil + } + case "first":{ + result := []byte{102, 105, 114, 115, 116} + return result, nil + } + case "fiscal":{ + result := []byte{102, 105, 115, 99, 97, 108} + return result, nil + } + case "fish":{ + result := []byte{102, 105, 115, 104} + return result, nil + } + case "fit":{ + result := []byte{102, 105, 116} + return result, nil + } + case "fitness":{ + result := []byte{102, 105, 116, 110, 101, 115, 115} + return result, nil + } + case "fix":{ + result := []byte{102, 105, 120} + return result, nil + } + case "flag":{ + result := []byte{102, 108, 97, 103} + return result, nil + } + case "flame":{ + result := []byte{102, 108, 97, 109, 101} + return result, nil + } + case "flash":{ + result := []byte{102, 108, 97, 115, 104} + return result, nil + } + case "flat":{ + result := []byte{102, 108, 97, 116} + return result, nil + } + case "flavor":{ + result := []byte{102, 108, 97, 118, 111, 114} + return result, nil + } + case "flee":{ + result := []byte{102, 108, 101, 101} + return result, nil + } + case "flight":{ + result := []byte{102, 108, 105, 103, 104, 116} + return result, nil + } + case "flip":{ + result := []byte{102, 108, 105, 112} + return result, nil + } + case "float":{ + result := []byte{102, 108, 111, 97, 116} + return result, nil + } + case "flock":{ + result := []byte{102, 108, 111, 99, 107} + return result, nil + } + case "floor":{ + result := []byte{102, 108, 111, 111, 114} + return result, nil + } + case "flower":{ + result := []byte{102, 108, 111, 119, 101, 114} + return result, nil + } + case "fluid":{ + result := []byte{102, 108, 117, 105, 100} + return result, nil + } + case "flush":{ + result := []byte{102, 108, 117, 115, 104} + return result, nil + } + case "fly":{ + result := []byte{102, 108, 121} + return result, nil + } + case "foam":{ + result := []byte{102, 111, 97, 109} + return result, nil + } + case "focus":{ + result := []byte{102, 111, 99, 117, 115} + return result, nil + } + case "fog":{ + result := []byte{102, 111, 103} + return result, nil + } + case "foil":{ + result := []byte{102, 111, 105, 108} + return result, nil + } + case "fold":{ + result := []byte{102, 111, 108, 100} + return result, nil + } + case "follow":{ + result := []byte{102, 111, 108, 108, 111, 119} + return result, nil + } + case "food":{ + result := []byte{102, 111, 111, 100} + return result, nil + } + case "foot":{ + result := []byte{102, 111, 111, 116} + return result, nil + } + case "force":{ + result := []byte{102, 111, 114, 99, 101} + return result, nil + } + case "forest":{ + result := []byte{102, 111, 114, 101, 115, 116} + return result, nil + } + case "forget":{ + result := []byte{102, 111, 114, 103, 101, 116} + return result, nil + } + case "fork":{ + result := []byte{102, 111, 114, 107} + return result, nil + } + case "fortune":{ + result := []byte{102, 111, 114, 116, 117, 110, 101} + return result, nil + } + case "forum":{ + result := []byte{102, 111, 114, 117, 109} + return result, nil + } + case "forward":{ + result := []byte{102, 111, 114, 119, 97, 114, 100} + return result, nil + } + case "fossil":{ + result := []byte{102, 111, 115, 115, 105, 108} + return result, nil + } + case "foster":{ + result := []byte{102, 111, 115, 116, 101, 114} + return result, nil + } + case "found":{ + result := []byte{102, 111, 117, 110, 100} + return result, nil + } + case "fox":{ + result := []byte{102, 111, 120} + return result, nil + } + case "fragile":{ + result := []byte{102, 114, 97, 103, 105, 108, 101} + return result, nil + } + case "frame":{ + result := []byte{102, 114, 97, 109, 101} + return result, nil + } + case "frequent":{ + result := []byte{102, 114, 101, 113, 117, 101, 110, 116} + return result, nil + } + case "fresh":{ + result := []byte{102, 114, 101, 115, 104} + return result, nil + } + case "friend":{ + result := []byte{102, 114, 105, 101, 110, 100} + return result, nil + } + case "fringe":{ + result := []byte{102, 114, 105, 110, 103, 101} + return result, nil + } + case "frog":{ + result := []byte{102, 114, 111, 103} + return result, nil + } + case "front":{ + result := []byte{102, 114, 111, 110, 116} + return result, nil + } + case "frost":{ + result := []byte{102, 114, 111, 115, 116} + return result, nil + } + case "frown":{ + result := []byte{102, 114, 111, 119, 110} + return result, nil + } + case "frozen":{ + result := []byte{102, 114, 111, 122, 101, 110} + return result, nil + } + case "fruit":{ + result := []byte{102, 114, 117, 105, 116} + return result, nil + } + case "fuel":{ + result := []byte{102, 117, 101, 108} + return result, nil + } + case "fun":{ + result := []byte{102, 117, 110} + return result, nil + } + case "funny":{ + result := []byte{102, 117, 110, 110, 121} + return result, nil + } + case "furnace":{ + result := []byte{102, 117, 114, 110, 97, 99, 101} + return result, nil + } + case "fury":{ + result := []byte{102, 117, 114, 121} + return result, nil + } + case "future":{ + result := []byte{102, 117, 116, 117, 114, 101} + return result, nil + } + case "gadget":{ + result := []byte{103, 97, 100, 103, 101, 116} + return result, nil + } + case "gain":{ + result := []byte{103, 97, 105, 110} + return result, nil + } + case "galaxy":{ + result := []byte{103, 97, 108, 97, 120, 121} + return result, nil + } + case "gallery":{ + result := []byte{103, 97, 108, 108, 101, 114, 121} + return result, nil + } + case "game":{ + result := []byte{103, 97, 109, 101} + return result, nil + } + case "gap":{ + result := []byte{103, 97, 112} + return result, nil + } + case "garage":{ + result := []byte{103, 97, 114, 97, 103, 101} + return result, nil + } + case "garbage":{ + result := []byte{103, 97, 114, 98, 97, 103, 101} + return result, nil + } + case "garden":{ + result := []byte{103, 97, 114, 100, 101, 110} + return result, nil + } + case "garlic":{ + result := []byte{103, 97, 114, 108, 105, 99} + return result, nil + } + case "garment":{ + result := []byte{103, 97, 114, 109, 101, 110, 116} + return result, nil + } + case "gas":{ + result := []byte{103, 97, 115} + return result, nil + } + case "gasp":{ + result := []byte{103, 97, 115, 112} + return result, nil + } + case "gate":{ + result := []byte{103, 97, 116, 101} + return result, nil + } + case "gather":{ + result := []byte{103, 97, 116, 104, 101, 114} + return result, nil + } + case "gauge":{ + result := []byte{103, 97, 117, 103, 101} + return result, nil + } + case "gaze":{ + result := []byte{103, 97, 122, 101} + return result, nil + } + case "general":{ + result := []byte{103, 101, 110, 101, 114, 97, 108} + return result, nil + } + case "genius":{ + result := []byte{103, 101, 110, 105, 117, 115} + return result, nil + } + case "genre":{ + result := []byte{103, 101, 110, 114, 101} + return result, nil + } + case "gentle":{ + result := []byte{103, 101, 110, 116, 108, 101} + return result, nil + } + case "genuine":{ + result := []byte{103, 101, 110, 117, 105, 110, 101} + return result, nil + } + case "gesture":{ + result := []byte{103, 101, 115, 116, 117, 114, 101} + return result, nil + } + case "ghost":{ + result := []byte{103, 104, 111, 115, 116} + return result, nil + } + case "giant":{ + result := []byte{103, 105, 97, 110, 116} + return result, nil + } + case "gift":{ + result := []byte{103, 105, 102, 116} + return result, nil + } + case "giggle":{ + result := []byte{103, 105, 103, 103, 108, 101} + return result, nil + } + case "ginger":{ + result := []byte{103, 105, 110, 103, 101, 114} + return result, nil + } + case "giraffe":{ + result := []byte{103, 105, 114, 97, 102, 102, 101} + return result, nil + } + case "girl":{ + result := []byte{103, 105, 114, 108} + return result, nil + } + case "give":{ + result := []byte{103, 105, 118, 101} + return result, nil + } + case "glad":{ + result := []byte{103, 108, 97, 100} + return result, nil + } + case "glance":{ + result := []byte{103, 108, 97, 110, 99, 101} + return result, nil + } + case "glare":{ + result := []byte{103, 108, 97, 114, 101} + return result, nil + } + case "glass":{ + result := []byte{103, 108, 97, 115, 115} + return result, nil + } + case "glide":{ + result := []byte{103, 108, 105, 100, 101} + return result, nil + } + case "glimpse":{ + result := []byte{103, 108, 105, 109, 112, 115, 101} + return result, nil + } + case "globe":{ + result := []byte{103, 108, 111, 98, 101} + return result, nil + } + case "gloom":{ + result := []byte{103, 108, 111, 111, 109} + return result, nil + } + case "glory":{ + result := []byte{103, 108, 111, 114, 121} + return result, nil + } + case "glove":{ + result := []byte{103, 108, 111, 118, 101} + return result, nil + } + case "glow":{ + result := []byte{103, 108, 111, 119} + return result, nil + } + case "glue":{ + result := []byte{103, 108, 117, 101} + return result, nil + } + case "goat":{ + result := []byte{103, 111, 97, 116} + return result, nil + } + case "goddess":{ + result := []byte{103, 111, 100, 100, 101, 115, 115} + return result, nil + } + case "gold":{ + result := []byte{103, 111, 108, 100} + return result, nil + } + case "good":{ + result := []byte{103, 111, 111, 100} + return result, nil + } + case "goose":{ + result := []byte{103, 111, 111, 115, 101} + return result, nil + } + case "gorilla":{ + result := []byte{103, 111, 114, 105, 108, 108, 97} + return result, nil + } + case "gospel":{ + result := []byte{103, 111, 115, 112, 101, 108} + return result, nil + } + case "gossip":{ + result := []byte{103, 111, 115, 115, 105, 112} + return result, nil + } + case "govern":{ + result := []byte{103, 111, 118, 101, 114, 110} + return result, nil + } + case "gown":{ + result := []byte{103, 111, 119, 110} + return result, nil + } + case "grab":{ + result := []byte{103, 114, 97, 98} + return result, nil + } + case "grace":{ + result := []byte{103, 114, 97, 99, 101} + return result, nil + } + case "grain":{ + result := []byte{103, 114, 97, 105, 110} + return result, nil + } + case "grant":{ + result := []byte{103, 114, 97, 110, 116} + return result, nil + } + case "grape":{ + result := []byte{103, 114, 97, 112, 101} + return result, nil + } + case "grass":{ + result := []byte{103, 114, 97, 115, 115} + return result, nil + } + case "gravity":{ + result := []byte{103, 114, 97, 118, 105, 116, 121} + return result, nil + } + case "great":{ + result := []byte{103, 114, 101, 97, 116} + return result, nil + } + case "green":{ + result := []byte{103, 114, 101, 101, 110} + return result, nil + } + case "grid":{ + result := []byte{103, 114, 105, 100} + return result, nil + } + case "grief":{ + result := []byte{103, 114, 105, 101, 102} + return result, nil + } + case "grit":{ + result := []byte{103, 114, 105, 116} + return result, nil + } + case "grocery":{ + result := []byte{103, 114, 111, 99, 101, 114, 121} + return result, nil + } + case "group":{ + result := []byte{103, 114, 111, 117, 112} + return result, nil + } + case "grow":{ + result := []byte{103, 114, 111, 119} + return result, nil + } + case "grunt":{ + result := []byte{103, 114, 117, 110, 116} + return result, nil + } + case "guard":{ + result := []byte{103, 117, 97, 114, 100} + return result, nil + } + case "guess":{ + result := []byte{103, 117, 101, 115, 115} + return result, nil + } + case "guide":{ + result := []byte{103, 117, 105, 100, 101} + return result, nil + } + case "guilt":{ + result := []byte{103, 117, 105, 108, 116} + return result, nil + } + case "guitar":{ + result := []byte{103, 117, 105, 116, 97, 114} + return result, nil + } + case "gun":{ + result := []byte{103, 117, 110} + return result, nil + } + case "gym":{ + result := []byte{103, 121, 109} + return result, nil + } + case "habit":{ + result := []byte{104, 97, 98, 105, 116} + return result, nil + } + case "hair":{ + result := []byte{104, 97, 105, 114} + return result, nil + } + case "half":{ + result := []byte{104, 97, 108, 102} + return result, nil + } + case "hammer":{ + result := []byte{104, 97, 109, 109, 101, 114} + return result, nil + } + case "hamster":{ + result := []byte{104, 97, 109, 115, 116, 101, 114} + return result, nil + } + case "hand":{ + result := []byte{104, 97, 110, 100} + return result, nil + } + case "happy":{ + result := []byte{104, 97, 112, 112, 121} + return result, nil + } + case "harbor":{ + result := []byte{104, 97, 114, 98, 111, 114} + return result, nil + } + case "hard":{ + result := []byte{104, 97, 114, 100} + return result, nil + } + case "harsh":{ + result := []byte{104, 97, 114, 115, 104} + return result, nil + } + case "harvest":{ + result := []byte{104, 97, 114, 118, 101, 115, 116} + return result, nil + } + case "hat":{ + result := []byte{104, 97, 116} + return result, nil + } + case "have":{ + result := []byte{104, 97, 118, 101} + return result, nil + } + case "hawk":{ + result := []byte{104, 97, 119, 107} + return result, nil + } + case "hazard":{ + result := []byte{104, 97, 122, 97, 114, 100} + return result, nil + } + case "head":{ + result := []byte{104, 101, 97, 100} + return result, nil + } + case "health":{ + result := []byte{104, 101, 97, 108, 116, 104} + return result, nil + } + case "heart":{ + result := []byte{104, 101, 97, 114, 116} + return result, nil + } + case "heavy":{ + result := []byte{104, 101, 97, 118, 121} + return result, nil + } + case "hedgehog":{ + result := []byte{104, 101, 100, 103, 101, 104, 111, 103} + return result, nil + } + case "height":{ + result := []byte{104, 101, 105, 103, 104, 116} + return result, nil + } + case "hello":{ + result := []byte{104, 101, 108, 108, 111} + return result, nil + } + case "helmet":{ + result := []byte{104, 101, 108, 109, 101, 116} + return result, nil + } + case "help":{ + result := []byte{104, 101, 108, 112} + return result, nil + } + case "hen":{ + result := []byte{104, 101, 110} + return result, nil + } + case "hero":{ + result := []byte{104, 101, 114, 111} + return result, nil + } + case "hidden":{ + result := []byte{104, 105, 100, 100, 101, 110} + return result, nil + } + case "high":{ + result := []byte{104, 105, 103, 104} + return result, nil + } + case "hill":{ + result := []byte{104, 105, 108, 108} + return result, nil + } + case "hint":{ + result := []byte{104, 105, 110, 116} + return result, nil + } + case "hip":{ + result := []byte{104, 105, 112} + return result, nil + } + case "hire":{ + result := []byte{104, 105, 114, 101} + return result, nil + } + case "history":{ + result := []byte{104, 105, 115, 116, 111, 114, 121} + return result, nil + } + case "hobby":{ + result := []byte{104, 111, 98, 98, 121} + return result, nil + } + case "hockey":{ + result := []byte{104, 111, 99, 107, 101, 121} + return result, nil + } + case "hold":{ + result := []byte{104, 111, 108, 100} + return result, nil + } + case "hole":{ + result := []byte{104, 111, 108, 101} + return result, nil + } + case "holiday":{ + result := []byte{104, 111, 108, 105, 100, 97, 121} + return result, nil + } + case "hollow":{ + result := []byte{104, 111, 108, 108, 111, 119} + return result, nil + } + case "home":{ + result := []byte{104, 111, 109, 101} + return result, nil + } + case "honey":{ + result := []byte{104, 111, 110, 101, 121} + return result, nil + } + case "hood":{ + result := []byte{104, 111, 111, 100} + return result, nil + } + case "hope":{ + result := []byte{104, 111, 112, 101} + return result, nil + } + case "horn":{ + result := []byte{104, 111, 114, 110} + return result, nil + } + case "horror":{ + result := []byte{104, 111, 114, 114, 111, 114} + return result, nil + } + case "horse":{ + result := []byte{104, 111, 114, 115, 101} + return result, nil + } + case "hospital":{ + result := []byte{104, 111, 115, 112, 105, 116, 97, 108} + return result, nil + } + case "host":{ + result := []byte{104, 111, 115, 116} + return result, nil + } + case "hotel":{ + result := []byte{104, 111, 116, 101, 108} + return result, nil + } + case "hour":{ + result := []byte{104, 111, 117, 114} + return result, nil + } + case "hover":{ + result := []byte{104, 111, 118, 101, 114} + return result, nil + } + case "hub":{ + result := []byte{104, 117, 98} + return result, nil + } + case "huge":{ + result := []byte{104, 117, 103, 101} + return result, nil + } + case "human":{ + result := []byte{104, 117, 109, 97, 110} + return result, nil + } + case "humble":{ + result := []byte{104, 117, 109, 98, 108, 101} + return result, nil + } + case "humor":{ + result := []byte{104, 117, 109, 111, 114} + return result, nil + } + case "hundred":{ + result := []byte{104, 117, 110, 100, 114, 101, 100} + return result, nil + } + case "hungry":{ + result := []byte{104, 117, 110, 103, 114, 121} + return result, nil + } + case "hunt":{ + result := []byte{104, 117, 110, 116} + return result, nil + } + case "hurdle":{ + result := []byte{104, 117, 114, 100, 108, 101} + return result, nil + } + case "hurry":{ + result := []byte{104, 117, 114, 114, 121} + return result, nil + } + case "hurt":{ + result := []byte{104, 117, 114, 116} + return result, nil + } + case "husband":{ + result := []byte{104, 117, 115, 98, 97, 110, 100} + return result, nil + } + case "hybrid":{ + result := []byte{104, 121, 98, 114, 105, 100} + return result, nil + } + case "ice":{ + result := []byte{105, 99, 101} + return result, nil + } + case "icon":{ + result := []byte{105, 99, 111, 110} + return result, nil + } + case "idea":{ + result := []byte{105, 100, 101, 97} + return result, nil + } + case "identify":{ + result := []byte{105, 100, 101, 110, 116, 105, 102, 121} + return result, nil + } + case "idle":{ + result := []byte{105, 100, 108, 101} + return result, nil + } + case "ignore":{ + result := []byte{105, 103, 110, 111, 114, 101} + return result, nil + } + case "ill":{ + result := []byte{105, 108, 108} + return result, nil + } + case "illegal":{ + result := []byte{105, 108, 108, 101, 103, 97, 108} + return result, nil + } + case "illness":{ + result := []byte{105, 108, 108, 110, 101, 115, 115} + return result, nil + } + case "image":{ + result := []byte{105, 109, 97, 103, 101} + return result, nil + } + case "imitate":{ + result := []byte{105, 109, 105, 116, 97, 116, 101} + return result, nil + } + case "immense":{ + result := []byte{105, 109, 109, 101, 110, 115, 101} + return result, nil + } + case "immune":{ + result := []byte{105, 109, 109, 117, 110, 101} + return result, nil + } + case "impact":{ + result := []byte{105, 109, 112, 97, 99, 116} + return result, nil + } + case "impose":{ + result := []byte{105, 109, 112, 111, 115, 101} + return result, nil + } + case "improve":{ + result := []byte{105, 109, 112, 114, 111, 118, 101} + return result, nil + } + case "impulse":{ + result := []byte{105, 109, 112, 117, 108, 115, 101} + return result, nil + } + case "inch":{ + result := []byte{105, 110, 99, 104} + return result, nil + } + case "include":{ + result := []byte{105, 110, 99, 108, 117, 100, 101} + return result, nil + } + case "income":{ + result := []byte{105, 110, 99, 111, 109, 101} + return result, nil + } + case "increase":{ + result := []byte{105, 110, 99, 114, 101, 97, 115, 101} + return result, nil + } + case "index":{ + result := []byte{105, 110, 100, 101, 120} + return result, nil + } + case "indicate":{ + result := []byte{105, 110, 100, 105, 99, 97, 116, 101} + return result, nil + } + case "indoor":{ + result := []byte{105, 110, 100, 111, 111, 114} + return result, nil + } + case "industry":{ + result := []byte{105, 110, 100, 117, 115, 116, 114, 121} + return result, nil + } + case "infant":{ + result := []byte{105, 110, 102, 97, 110, 116} + return result, nil + } + case "inflict":{ + result := []byte{105, 110, 102, 108, 105, 99, 116} + return result, nil + } + case "inform":{ + result := []byte{105, 110, 102, 111, 114, 109} + return result, nil + } + case "inhale":{ + result := []byte{105, 110, 104, 97, 108, 101} + return result, nil + } + case "inherit":{ + result := []byte{105, 110, 104, 101, 114, 105, 116} + return result, nil + } + case "initial":{ + result := []byte{105, 110, 105, 116, 105, 97, 108} + return result, nil + } + case "inject":{ + result := []byte{105, 110, 106, 101, 99, 116} + return result, nil + } + case "injury":{ + result := []byte{105, 110, 106, 117, 114, 121} + return result, nil + } + case "inmate":{ + result := []byte{105, 110, 109, 97, 116, 101} + return result, nil + } + case "inner":{ + result := []byte{105, 110, 110, 101, 114} + return result, nil + } + case "innocent":{ + result := []byte{105, 110, 110, 111, 99, 101, 110, 116} + return result, nil + } + case "input":{ + result := []byte{105, 110, 112, 117, 116} + return result, nil + } + case "inquiry":{ + result := []byte{105, 110, 113, 117, 105, 114, 121} + return result, nil + } + case "insane":{ + result := []byte{105, 110, 115, 97, 110, 101} + return result, nil + } + case "insect":{ + result := []byte{105, 110, 115, 101, 99, 116} + return result, nil + } + case "inside":{ + result := []byte{105, 110, 115, 105, 100, 101} + return result, nil + } + case "inspire":{ + result := []byte{105, 110, 115, 112, 105, 114, 101} + return result, nil + } + case "install":{ + result := []byte{105, 110, 115, 116, 97, 108, 108} + return result, nil + } + case "intact":{ + result := []byte{105, 110, 116, 97, 99, 116} + return result, nil + } + case "interest":{ + result := []byte{105, 110, 116, 101, 114, 101, 115, 116} + return result, nil + } + case "into":{ + result := []byte{105, 110, 116, 111} + return result, nil + } + case "invest":{ + result := []byte{105, 110, 118, 101, 115, 116} + return result, nil + } + case "invite":{ + result := []byte{105, 110, 118, 105, 116, 101} + return result, nil + } + case "involve":{ + result := []byte{105, 110, 118, 111, 108, 118, 101} + return result, nil + } + case "iron":{ + result := []byte{105, 114, 111, 110} + return result, nil + } + case "island":{ + result := []byte{105, 115, 108, 97, 110, 100} + return result, nil + } + case "isolate":{ + result := []byte{105, 115, 111, 108, 97, 116, 101} + return result, nil + } + case "issue":{ + result := []byte{105, 115, 115, 117, 101} + return result, nil + } + case "item":{ + result := []byte{105, 116, 101, 109} + return result, nil + } + case "ivory":{ + result := []byte{105, 118, 111, 114, 121} + return result, nil + } + case "jacket":{ + result := []byte{106, 97, 99, 107, 101, 116} + return result, nil + } + case "jaguar":{ + result := []byte{106, 97, 103, 117, 97, 114} + return result, nil + } + case "jar":{ + result := []byte{106, 97, 114} + return result, nil + } + case "jazz":{ + result := []byte{106, 97, 122, 122} + return result, nil + } + case "jealous":{ + result := []byte{106, 101, 97, 108, 111, 117, 115} + return result, nil + } + case "jeans":{ + result := []byte{106, 101, 97, 110, 115} + return result, nil + } + case "jelly":{ + result := []byte{106, 101, 108, 108, 121} + return result, nil + } + case "jewel":{ + result := []byte{106, 101, 119, 101, 108} + return result, nil + } + case "job":{ + result := []byte{106, 111, 98} + return result, nil + } + case "join":{ + result := []byte{106, 111, 105, 110} + return result, nil + } + case "joke":{ + result := []byte{106, 111, 107, 101} + return result, nil + } + case "journey":{ + result := []byte{106, 111, 117, 114, 110, 101, 121} + return result, nil + } + case "joy":{ + result := []byte{106, 111, 121} + return result, nil + } + case "judge":{ + result := []byte{106, 117, 100, 103, 101} + return result, nil + } + case "juice":{ + result := []byte{106, 117, 105, 99, 101} + return result, nil + } + case "jump":{ + result := []byte{106, 117, 109, 112} + return result, nil + } + case "jungle":{ + result := []byte{106, 117, 110, 103, 108, 101} + return result, nil + } + case "junior":{ + result := []byte{106, 117, 110, 105, 111, 114} + return result, nil + } + case "junk":{ + result := []byte{106, 117, 110, 107} + return result, nil + } + case "just":{ + result := []byte{106, 117, 115, 116} + return result, nil + } + case "kangaroo":{ + result := []byte{107, 97, 110, 103, 97, 114, 111, 111} + return result, nil + } + case "keen":{ + result := []byte{107, 101, 101, 110} + return result, nil + } + case "keep":{ + result := []byte{107, 101, 101, 112} + return result, nil + } + case "ketchup":{ + result := []byte{107, 101, 116, 99, 104, 117, 112} + return result, nil + } + case "key":{ + result := []byte{107, 101, 121} + return result, nil + } + case "kick":{ + result := []byte{107, 105, 99, 107} + return result, nil + } + case "kid":{ + result := []byte{107, 105, 100} + return result, nil + } + case "kidney":{ + result := []byte{107, 105, 100, 110, 101, 121} + return result, nil + } + case "kind":{ + result := []byte{107, 105, 110, 100} + return result, nil + } + case "kingdom":{ + result := []byte{107, 105, 110, 103, 100, 111, 109} + return result, nil + } + case "kiss":{ + result := []byte{107, 105, 115, 115} + return result, nil + } + case "kit":{ + result := []byte{107, 105, 116} + return result, nil + } + case "kitchen":{ + result := []byte{107, 105, 116, 99, 104, 101, 110} + return result, nil + } + case "kite":{ + result := []byte{107, 105, 116, 101} + return result, nil + } + case "kitten":{ + result := []byte{107, 105, 116, 116, 101, 110} + return result, nil + } + case "kiwi":{ + result := []byte{107, 105, 119, 105} + return result, nil + } + case "knee":{ + result := []byte{107, 110, 101, 101} + return result, nil + } + case "knife":{ + result := []byte{107, 110, 105, 102, 101} + return result, nil + } + case "knock":{ + result := []byte{107, 110, 111, 99, 107} + return result, nil + } + case "know":{ + result := []byte{107, 110, 111, 119} + return result, nil + } + case "lab":{ + result := []byte{108, 97, 98} + return result, nil + } + case "label":{ + result := []byte{108, 97, 98, 101, 108} + return result, nil + } + case "labor":{ + result := []byte{108, 97, 98, 111, 114} + return result, nil + } + case "ladder":{ + result := []byte{108, 97, 100, 100, 101, 114} + return result, nil + } + case "lady":{ + result := []byte{108, 97, 100, 121} + return result, nil + } + case "lake":{ + result := []byte{108, 97, 107, 101} + return result, nil + } + case "lamp":{ + result := []byte{108, 97, 109, 112} + return result, nil + } + case "language":{ + result := []byte{108, 97, 110, 103, 117, 97, 103, 101} + return result, nil + } + case "laptop":{ + result := []byte{108, 97, 112, 116, 111, 112} + return result, nil + } + case "large":{ + result := []byte{108, 97, 114, 103, 101} + return result, nil + } + case "later":{ + result := []byte{108, 97, 116, 101, 114} + return result, nil + } + case "latin":{ + result := []byte{108, 97, 116, 105, 110} + return result, nil + } + case "laugh":{ + result := []byte{108, 97, 117, 103, 104} + return result, nil + } + case "laundry":{ + result := []byte{108, 97, 117, 110, 100, 114, 121} + return result, nil + } + case "lava":{ + result := []byte{108, 97, 118, 97} + return result, nil + } + case "law":{ + result := []byte{108, 97, 119} + return result, nil + } + case "lawn":{ + result := []byte{108, 97, 119, 110} + return result, nil + } + case "lawsuit":{ + result := []byte{108, 97, 119, 115, 117, 105, 116} + return result, nil + } + case "layer":{ + result := []byte{108, 97, 121, 101, 114} + return result, nil + } + case "lazy":{ + result := []byte{108, 97, 122, 121} + return result, nil + } + case "leader":{ + result := []byte{108, 101, 97, 100, 101, 114} + return result, nil + } + case "leaf":{ + result := []byte{108, 101, 97, 102} + return result, nil + } + case "learn":{ + result := []byte{108, 101, 97, 114, 110} + return result, nil + } + case "leave":{ + result := []byte{108, 101, 97, 118, 101} + return result, nil + } + case "lecture":{ + result := []byte{108, 101, 99, 116, 117, 114, 101} + return result, nil + } + case "left":{ + result := []byte{108, 101, 102, 116} + return result, nil + } + case "leg":{ + result := []byte{108, 101, 103} + return result, nil + } + case "legal":{ + result := []byte{108, 101, 103, 97, 108} + return result, nil + } + case "legend":{ + result := []byte{108, 101, 103, 101, 110, 100} + return result, nil + } + case "leisure":{ + result := []byte{108, 101, 105, 115, 117, 114, 101} + return result, nil + } + case "lemon":{ + result := []byte{108, 101, 109, 111, 110} + return result, nil + } + case "lend":{ + result := []byte{108, 101, 110, 100} + return result, nil + } + case "length":{ + result := []byte{108, 101, 110, 103, 116, 104} + return result, nil + } + case "lens":{ + result := []byte{108, 101, 110, 115} + return result, nil + } + case "leopard":{ + result := []byte{108, 101, 111, 112, 97, 114, 100} + return result, nil + } + case "lesson":{ + result := []byte{108, 101, 115, 115, 111, 110} + return result, nil + } + case "letter":{ + result := []byte{108, 101, 116, 116, 101, 114} + return result, nil + } + case "level":{ + result := []byte{108, 101, 118, 101, 108} + return result, nil + } + case "liar":{ + result := []byte{108, 105, 97, 114} + return result, nil + } + case "liberty":{ + result := []byte{108, 105, 98, 101, 114, 116, 121} + return result, nil + } + case "library":{ + result := []byte{108, 105, 98, 114, 97, 114, 121} + return result, nil + } + case "license":{ + result := []byte{108, 105, 99, 101, 110, 115, 101} + return result, nil + } + case "life":{ + result := []byte{108, 105, 102, 101} + return result, nil + } + case "lift":{ + result := []byte{108, 105, 102, 116} + return result, nil + } + case "light":{ + result := []byte{108, 105, 103, 104, 116} + return result, nil + } + case "like":{ + result := []byte{108, 105, 107, 101} + return result, nil + } + case "limb":{ + result := []byte{108, 105, 109, 98} + return result, nil + } + case "limit":{ + result := []byte{108, 105, 109, 105, 116} + return result, nil + } + case "link":{ + result := []byte{108, 105, 110, 107} + return result, nil + } + case "lion":{ + result := []byte{108, 105, 111, 110} + return result, nil + } + case "liquid":{ + result := []byte{108, 105, 113, 117, 105, 100} + return result, nil + } + case "list":{ + result := []byte{108, 105, 115, 116} + return result, nil + } + case "little":{ + result := []byte{108, 105, 116, 116, 108, 101} + return result, nil + } + case "live":{ + result := []byte{108, 105, 118, 101} + return result, nil + } + case "lizard":{ + result := []byte{108, 105, 122, 97, 114, 100} + return result, nil + } + case "load":{ + result := []byte{108, 111, 97, 100} + return result, nil + } + case "loan":{ + result := []byte{108, 111, 97, 110} + return result, nil + } + case "lobster":{ + result := []byte{108, 111, 98, 115, 116, 101, 114} + return result, nil + } + case "local":{ + result := []byte{108, 111, 99, 97, 108} + return result, nil + } + case "lock":{ + result := []byte{108, 111, 99, 107} + return result, nil + } + case "logic":{ + result := []byte{108, 111, 103, 105, 99} + return result, nil + } + case "lonely":{ + result := []byte{108, 111, 110, 101, 108, 121} + return result, nil + } + case "long":{ + result := []byte{108, 111, 110, 103} + return result, nil + } + case "loop":{ + result := []byte{108, 111, 111, 112} + return result, nil + } + case "lottery":{ + result := []byte{108, 111, 116, 116, 101, 114, 121} + return result, nil + } + case "loud":{ + result := []byte{108, 111, 117, 100} + return result, nil + } + case "lounge":{ + result := []byte{108, 111, 117, 110, 103, 101} + return result, nil + } + case "love":{ + result := []byte{108, 111, 118, 101} + return result, nil + } + case "loyal":{ + result := []byte{108, 111, 121, 97, 108} + return result, nil + } + case "lucky":{ + result := []byte{108, 117, 99, 107, 121} + return result, nil + } + case "luggage":{ + result := []byte{108, 117, 103, 103, 97, 103, 101} + return result, nil + } + case "lumber":{ + result := []byte{108, 117, 109, 98, 101, 114} + return result, nil + } + case "lunar":{ + result := []byte{108, 117, 110, 97, 114} + return result, nil + } + case "lunch":{ + result := []byte{108, 117, 110, 99, 104} + return result, nil + } + case "luxury":{ + result := []byte{108, 117, 120, 117, 114, 121} + return result, nil + } + case "lyrics":{ + result := []byte{108, 121, 114, 105, 99, 115} + return result, nil + } + case "machine":{ + result := []byte{109, 97, 99, 104, 105, 110, 101} + return result, nil + } + case "mad":{ + result := []byte{109, 97, 100} + return result, nil + } + case "magic":{ + result := []byte{109, 97, 103, 105, 99} + return result, nil + } + case "magnet":{ + result := []byte{109, 97, 103, 110, 101, 116} + return result, nil + } + case "maid":{ + result := []byte{109, 97, 105, 100} + return result, nil + } + case "mail":{ + result := []byte{109, 97, 105, 108} + return result, nil + } + case "main":{ + result := []byte{109, 97, 105, 110} + return result, nil + } + case "major":{ + result := []byte{109, 97, 106, 111, 114} + return result, nil + } + case "make":{ + result := []byte{109, 97, 107, 101} + return result, nil + } + case "mammal":{ + result := []byte{109, 97, 109, 109, 97, 108} + return result, nil + } + case "man":{ + result := []byte{109, 97, 110} + return result, nil + } + case "manage":{ + result := []byte{109, 97, 110, 97, 103, 101} + return result, nil + } + case "mandate":{ + result := []byte{109, 97, 110, 100, 97, 116, 101} + return result, nil + } + case "mango":{ + result := []byte{109, 97, 110, 103, 111} + return result, nil + } + case "mansion":{ + result := []byte{109, 97, 110, 115, 105, 111, 110} + return result, nil + } + case "manual":{ + result := []byte{109, 97, 110, 117, 97, 108} + return result, nil + } + case "maple":{ + result := []byte{109, 97, 112, 108, 101} + return result, nil + } + case "marble":{ + result := []byte{109, 97, 114, 98, 108, 101} + return result, nil + } + case "march":{ + result := []byte{109, 97, 114, 99, 104} + return result, nil + } + case "margin":{ + result := []byte{109, 97, 114, 103, 105, 110} + return result, nil + } + case "marine":{ + result := []byte{109, 97, 114, 105, 110, 101} + return result, nil + } + case "market":{ + result := []byte{109, 97, 114, 107, 101, 116} + return result, nil + } + case "marriage":{ + result := []byte{109, 97, 114, 114, 105, 97, 103, 101} + return result, nil + } + case "mask":{ + result := []byte{109, 97, 115, 107} + return result, nil + } + case "mass":{ + result := []byte{109, 97, 115, 115} + return result, nil + } + case "master":{ + result := []byte{109, 97, 115, 116, 101, 114} + return result, nil + } + case "match":{ + result := []byte{109, 97, 116, 99, 104} + return result, nil + } + case "material":{ + result := []byte{109, 97, 116, 101, 114, 105, 97, 108} + return result, nil + } + case "math":{ + result := []byte{109, 97, 116, 104} + return result, nil + } + case "matrix":{ + result := []byte{109, 97, 116, 114, 105, 120} + return result, nil + } + case "matter":{ + result := []byte{109, 97, 116, 116, 101, 114} + return result, nil + } + case "maximum":{ + result := []byte{109, 97, 120, 105, 109, 117, 109} + return result, nil + } + case "maze":{ + result := []byte{109, 97, 122, 101} + return result, nil + } + case "meadow":{ + result := []byte{109, 101, 97, 100, 111, 119} + return result, nil + } + case "mean":{ + result := []byte{109, 101, 97, 110} + return result, nil + } + case "measure":{ + result := []byte{109, 101, 97, 115, 117, 114, 101} + return result, nil + } + case "meat":{ + result := []byte{109, 101, 97, 116} + return result, nil + } + case "mechanic":{ + result := []byte{109, 101, 99, 104, 97, 110, 105, 99} + return result, nil + } + case "medal":{ + result := []byte{109, 101, 100, 97, 108} + return result, nil + } + case "media":{ + result := []byte{109, 101, 100, 105, 97} + return result, nil + } + case "melody":{ + result := []byte{109, 101, 108, 111, 100, 121} + return result, nil + } + case "melt":{ + result := []byte{109, 101, 108, 116} + return result, nil + } + case "member":{ + result := []byte{109, 101, 109, 98, 101, 114} + return result, nil + } + case "memory":{ + result := []byte{109, 101, 109, 111, 114, 121} + return result, nil + } + case "mention":{ + result := []byte{109, 101, 110, 116, 105, 111, 110} + return result, nil + } + case "menu":{ + result := []byte{109, 101, 110, 117} + return result, nil + } + case "mercy":{ + result := []byte{109, 101, 114, 99, 121} + return result, nil + } + case "merge":{ + result := []byte{109, 101, 114, 103, 101} + return result, nil + } + case "merit":{ + result := []byte{109, 101, 114, 105, 116} + return result, nil + } + case "merry":{ + result := []byte{109, 101, 114, 114, 121} + return result, nil + } + case "mesh":{ + result := []byte{109, 101, 115, 104} + return result, nil + } + case "message":{ + result := []byte{109, 101, 115, 115, 97, 103, 101} + return result, nil + } + case "metal":{ + result := []byte{109, 101, 116, 97, 108} + return result, nil + } + case "method":{ + result := []byte{109, 101, 116, 104, 111, 100} + return result, nil + } + case "middle":{ + result := []byte{109, 105, 100, 100, 108, 101} + return result, nil + } + case "midnight":{ + result := []byte{109, 105, 100, 110, 105, 103, 104, 116} + return result, nil + } + case "milk":{ + result := []byte{109, 105, 108, 107} + return result, nil + } + case "million":{ + result := []byte{109, 105, 108, 108, 105, 111, 110} + return result, nil + } + case "mimic":{ + result := []byte{109, 105, 109, 105, 99} + return result, nil + } + case "mind":{ + result := []byte{109, 105, 110, 100} + return result, nil + } + case "minimum":{ + result := []byte{109, 105, 110, 105, 109, 117, 109} + return result, nil + } + case "minor":{ + result := []byte{109, 105, 110, 111, 114} + return result, nil + } + case "minute":{ + result := []byte{109, 105, 110, 117, 116, 101} + return result, nil + } + case "miracle":{ + result := []byte{109, 105, 114, 97, 99, 108, 101} + return result, nil + } + case "mirror":{ + result := []byte{109, 105, 114, 114, 111, 114} + return result, nil + } + case "misery":{ + result := []byte{109, 105, 115, 101, 114, 121} + return result, nil + } + case "miss":{ + result := []byte{109, 105, 115, 115} + return result, nil + } + case "mistake":{ + result := []byte{109, 105, 115, 116, 97, 107, 101} + return result, nil + } + case "mix":{ + result := []byte{109, 105, 120} + return result, nil + } + case "mixed":{ + result := []byte{109, 105, 120, 101, 100} + return result, nil + } + case "mixture":{ + result := []byte{109, 105, 120, 116, 117, 114, 101} + return result, nil + } + case "mobile":{ + result := []byte{109, 111, 98, 105, 108, 101} + return result, nil + } + case "model":{ + result := []byte{109, 111, 100, 101, 108} + return result, nil + } + case "modify":{ + result := []byte{109, 111, 100, 105, 102, 121} + return result, nil + } + case "mom":{ + result := []byte{109, 111, 109} + return result, nil + } + case "moment":{ + result := []byte{109, 111, 109, 101, 110, 116} + return result, nil + } + case "monitor":{ + result := []byte{109, 111, 110, 105, 116, 111, 114} + return result, nil + } + case "monkey":{ + result := []byte{109, 111, 110, 107, 101, 121} + return result, nil + } + case "monster":{ + result := []byte{109, 111, 110, 115, 116, 101, 114} + return result, nil + } + case "month":{ + result := []byte{109, 111, 110, 116, 104} + return result, nil + } + case "moon":{ + result := []byte{109, 111, 111, 110} + return result, nil + } + case "moral":{ + result := []byte{109, 111, 114, 97, 108} + return result, nil + } + case "more":{ + result := []byte{109, 111, 114, 101} + return result, nil + } + case "morning":{ + result := []byte{109, 111, 114, 110, 105, 110, 103} + return result, nil + } + case "mosquito":{ + result := []byte{109, 111, 115, 113, 117, 105, 116, 111} + return result, nil + } + case "mother":{ + result := []byte{109, 111, 116, 104, 101, 114} + return result, nil + } + case "motion":{ + result := []byte{109, 111, 116, 105, 111, 110} + return result, nil + } + case "motor":{ + result := []byte{109, 111, 116, 111, 114} + return result, nil + } + case "mountain":{ + result := []byte{109, 111, 117, 110, 116, 97, 105, 110} + return result, nil + } + case "mouse":{ + result := []byte{109, 111, 117, 115, 101} + return result, nil + } + case "move":{ + result := []byte{109, 111, 118, 101} + return result, nil + } + case "movie":{ + result := []byte{109, 111, 118, 105, 101} + return result, nil + } + case "much":{ + result := []byte{109, 117, 99, 104} + return result, nil + } + case "muffin":{ + result := []byte{109, 117, 102, 102, 105, 110} + return result, nil + } + case "mule":{ + result := []byte{109, 117, 108, 101} + return result, nil + } + case "multiply":{ + result := []byte{109, 117, 108, 116, 105, 112, 108, 121} + return result, nil + } + case "muscle":{ + result := []byte{109, 117, 115, 99, 108, 101} + return result, nil + } + case "museum":{ + result := []byte{109, 117, 115, 101, 117, 109} + return result, nil + } + case "mushroom":{ + result := []byte{109, 117, 115, 104, 114, 111, 111, 109} + return result, nil + } + case "music":{ + result := []byte{109, 117, 115, 105, 99} + return result, nil + } + case "must":{ + result := []byte{109, 117, 115, 116} + return result, nil + } + case "mutual":{ + result := []byte{109, 117, 116, 117, 97, 108} + return result, nil + } + case "myself":{ + result := []byte{109, 121, 115, 101, 108, 102} + return result, nil + } + case "mystery":{ + result := []byte{109, 121, 115, 116, 101, 114, 121} + return result, nil + } + case "myth":{ + result := []byte{109, 121, 116, 104} + return result, nil + } + case "naive":{ + result := []byte{110, 97, 105, 118, 101} + return result, nil + } + case "name":{ + result := []byte{110, 97, 109, 101} + return result, nil + } + case "napkin":{ + result := []byte{110, 97, 112, 107, 105, 110} + return result, nil + } + case "narrow":{ + result := []byte{110, 97, 114, 114, 111, 119} + return result, nil + } + case "nasty":{ + result := []byte{110, 97, 115, 116, 121} + return result, nil + } + case "nation":{ + result := []byte{110, 97, 116, 105, 111, 110} + return result, nil + } + case "nature":{ + result := []byte{110, 97, 116, 117, 114, 101} + return result, nil + } + case "near":{ + result := []byte{110, 101, 97, 114} + return result, nil + } + case "neck":{ + result := []byte{110, 101, 99, 107} + return result, nil + } + case "need":{ + result := []byte{110, 101, 101, 100} + return result, nil + } + case "negative":{ + result := []byte{110, 101, 103, 97, 116, 105, 118, 101} + return result, nil + } + case "neglect":{ + result := []byte{110, 101, 103, 108, 101, 99, 116} + return result, nil + } + case "neither":{ + result := []byte{110, 101, 105, 116, 104, 101, 114} + return result, nil + } + case "nephew":{ + result := []byte{110, 101, 112, 104, 101, 119} + return result, nil + } + case "nerve":{ + result := []byte{110, 101, 114, 118, 101} + return result, nil + } + case "nest":{ + result := []byte{110, 101, 115, 116} + return result, nil + } + case "net":{ + result := []byte{110, 101, 116} + return result, nil + } + case "network":{ + result := []byte{110, 101, 116, 119, 111, 114, 107} + return result, nil + } + case "neutral":{ + result := []byte{110, 101, 117, 116, 114, 97, 108} + return result, nil + } + case "never":{ + result := []byte{110, 101, 118, 101, 114} + return result, nil + } + case "news":{ + result := []byte{110, 101, 119, 115} + return result, nil + } + case "next":{ + result := []byte{110, 101, 120, 116} + return result, nil + } + case "nice":{ + result := []byte{110, 105, 99, 101} + return result, nil + } + case "night":{ + result := []byte{110, 105, 103, 104, 116} + return result, nil + } + case "noble":{ + result := []byte{110, 111, 98, 108, 101} + return result, nil + } + case "noise":{ + result := []byte{110, 111, 105, 115, 101} + return result, nil + } + case "nominee":{ + result := []byte{110, 111, 109, 105, 110, 101, 101} + return result, nil + } + case "noodle":{ + result := []byte{110, 111, 111, 100, 108, 101} + return result, nil + } + case "normal":{ + result := []byte{110, 111, 114, 109, 97, 108} + return result, nil + } + case "north":{ + result := []byte{110, 111, 114, 116, 104} + return result, nil + } + case "nose":{ + result := []byte{110, 111, 115, 101} + return result, nil + } + case "notable":{ + result := []byte{110, 111, 116, 97, 98, 108, 101} + return result, nil + } + case "note":{ + result := []byte{110, 111, 116, 101} + return result, nil + } + case "nothing":{ + result := []byte{110, 111, 116, 104, 105, 110, 103} + return result, nil + } + case "notice":{ + result := []byte{110, 111, 116, 105, 99, 101} + return result, nil + } + case "novel":{ + result := []byte{110, 111, 118, 101, 108} + return result, nil + } + case "now":{ + result := []byte{110, 111, 119} + return result, nil + } + case "nuclear":{ + result := []byte{110, 117, 99, 108, 101, 97, 114} + return result, nil + } + case "number":{ + result := []byte{110, 117, 109, 98, 101, 114} + return result, nil + } + case "nurse":{ + result := []byte{110, 117, 114, 115, 101} + return result, nil + } + case "nut":{ + result := []byte{110, 117, 116} + return result, nil + } + case "oak":{ + result := []byte{111, 97, 107} + return result, nil + } + case "obey":{ + result := []byte{111, 98, 101, 121} + return result, nil + } + case "object":{ + result := []byte{111, 98, 106, 101, 99, 116} + return result, nil + } + case "oblige":{ + result := []byte{111, 98, 108, 105, 103, 101} + return result, nil + } + case "obscure":{ + result := []byte{111, 98, 115, 99, 117, 114, 101} + return result, nil + } + case "observe":{ + result := []byte{111, 98, 115, 101, 114, 118, 101} + return result, nil + } + case "obtain":{ + result := []byte{111, 98, 116, 97, 105, 110} + return result, nil + } + case "obvious":{ + result := []byte{111, 98, 118, 105, 111, 117, 115} + return result, nil + } + case "occur":{ + result := []byte{111, 99, 99, 117, 114} + return result, nil + } + case "ocean":{ + result := []byte{111, 99, 101, 97, 110} + return result, nil + } + case "october":{ + result := []byte{111, 99, 116, 111, 98, 101, 114} + return result, nil + } + case "odor":{ + result := []byte{111, 100, 111, 114} + return result, nil + } + case "off":{ + result := []byte{111, 102, 102} + return result, nil + } + case "offer":{ + result := []byte{111, 102, 102, 101, 114} + return result, nil + } + case "office":{ + result := []byte{111, 102, 102, 105, 99, 101} + return result, nil + } + case "often":{ + result := []byte{111, 102, 116, 101, 110} + return result, nil + } + case "oil":{ + result := []byte{111, 105, 108} + return result, nil + } + case "okay":{ + result := []byte{111, 107, 97, 121} + return result, nil + } + case "old":{ + result := []byte{111, 108, 100} + return result, nil + } + case "olive":{ + result := []byte{111, 108, 105, 118, 101} + return result, nil + } + case "olympic":{ + result := []byte{111, 108, 121, 109, 112, 105, 99} + return result, nil + } + case "omit":{ + result := []byte{111, 109, 105, 116} + return result, nil + } + case "once":{ + result := []byte{111, 110, 99, 101} + return result, nil + } + case "one":{ + result := []byte{111, 110, 101} + return result, nil + } + case "onion":{ + result := []byte{111, 110, 105, 111, 110} + return result, nil + } + case "online":{ + result := []byte{111, 110, 108, 105, 110, 101} + return result, nil + } + case "only":{ + result := []byte{111, 110, 108, 121} + return result, nil + } + case "open":{ + result := []byte{111, 112, 101, 110} + return result, nil + } + case "opera":{ + result := []byte{111, 112, 101, 114, 97} + return result, nil + } + case "opinion":{ + result := []byte{111, 112, 105, 110, 105, 111, 110} + return result, nil + } + case "oppose":{ + result := []byte{111, 112, 112, 111, 115, 101} + return result, nil + } + case "option":{ + result := []byte{111, 112, 116, 105, 111, 110} + return result, nil + } + case "orange":{ + result := []byte{111, 114, 97, 110, 103, 101} + return result, nil + } + case "orbit":{ + result := []byte{111, 114, 98, 105, 116} + return result, nil + } + case "orchard":{ + result := []byte{111, 114, 99, 104, 97, 114, 100} + return result, nil + } + case "order":{ + result := []byte{111, 114, 100, 101, 114} + return result, nil + } + case "ordinary":{ + result := []byte{111, 114, 100, 105, 110, 97, 114, 121} + return result, nil + } + case "organ":{ + result := []byte{111, 114, 103, 97, 110} + return result, nil + } + case "orient":{ + result := []byte{111, 114, 105, 101, 110, 116} + return result, nil + } + case "original":{ + result := []byte{111, 114, 105, 103, 105, 110, 97, 108} + return result, nil + } + case "orphan":{ + result := []byte{111, 114, 112, 104, 97, 110} + return result, nil + } + case "ostrich":{ + result := []byte{111, 115, 116, 114, 105, 99, 104} + return result, nil + } + case "other":{ + result := []byte{111, 116, 104, 101, 114} + return result, nil + } + case "outdoor":{ + result := []byte{111, 117, 116, 100, 111, 111, 114} + return result, nil + } + case "outer":{ + result := []byte{111, 117, 116, 101, 114} + return result, nil + } + case "output":{ + result := []byte{111, 117, 116, 112, 117, 116} + return result, nil + } + case "outside":{ + result := []byte{111, 117, 116, 115, 105, 100, 101} + return result, nil + } + case "oval":{ + result := []byte{111, 118, 97, 108} + return result, nil + } + case "oven":{ + result := []byte{111, 118, 101, 110} + return result, nil + } + case "over":{ + result := []byte{111, 118, 101, 114} + return result, nil + } + case "own":{ + result := []byte{111, 119, 110} + return result, nil + } + case "owner":{ + result := []byte{111, 119, 110, 101, 114} + return result, nil + } + case "oxygen":{ + result := []byte{111, 120, 121, 103, 101, 110} + return result, nil + } + case "oyster":{ + result := []byte{111, 121, 115, 116, 101, 114} + return result, nil + } + case "ozone":{ + result := []byte{111, 122, 111, 110, 101} + return result, nil + } + case "pact":{ + result := []byte{112, 97, 99, 116} + return result, nil + } + case "paddle":{ + result := []byte{112, 97, 100, 100, 108, 101} + return result, nil + } + case "page":{ + result := []byte{112, 97, 103, 101} + return result, nil + } + case "pair":{ + result := []byte{112, 97, 105, 114} + return result, nil + } + case "palace":{ + result := []byte{112, 97, 108, 97, 99, 101} + return result, nil + } + case "palm":{ + result := []byte{112, 97, 108, 109} + return result, nil + } + case "panda":{ + result := []byte{112, 97, 110, 100, 97} + return result, nil + } + case "panel":{ + result := []byte{112, 97, 110, 101, 108} + return result, nil + } + case "panic":{ + result := []byte{112, 97, 110, 105, 99} + return result, nil + } + case "panther":{ + result := []byte{112, 97, 110, 116, 104, 101, 114} + return result, nil + } + case "paper":{ + result := []byte{112, 97, 112, 101, 114} + return result, nil + } + case "parade":{ + result := []byte{112, 97, 114, 97, 100, 101} + return result, nil + } + case "parent":{ + result := []byte{112, 97, 114, 101, 110, 116} + return result, nil + } + case "park":{ + result := []byte{112, 97, 114, 107} + return result, nil + } + case "parrot":{ + result := []byte{112, 97, 114, 114, 111, 116} + return result, nil + } + case "party":{ + result := []byte{112, 97, 114, 116, 121} + return result, nil + } + case "pass":{ + result := []byte{112, 97, 115, 115} + return result, nil + } + case "patch":{ + result := []byte{112, 97, 116, 99, 104} + return result, nil + } + case "path":{ + result := []byte{112, 97, 116, 104} + return result, nil + } + case "patient":{ + result := []byte{112, 97, 116, 105, 101, 110, 116} + return result, nil + } + case "patrol":{ + result := []byte{112, 97, 116, 114, 111, 108} + return result, nil + } + case "pattern":{ + result := []byte{112, 97, 116, 116, 101, 114, 110} + return result, nil + } + case "pause":{ + result := []byte{112, 97, 117, 115, 101} + return result, nil + } + case "pave":{ + result := []byte{112, 97, 118, 101} + return result, nil + } + case "payment":{ + result := []byte{112, 97, 121, 109, 101, 110, 116} + return result, nil + } + case "peace":{ + result := []byte{112, 101, 97, 99, 101} + return result, nil + } + case "peanut":{ + result := []byte{112, 101, 97, 110, 117, 116} + return result, nil + } + case "pear":{ + result := []byte{112, 101, 97, 114} + return result, nil + } + case "peasant":{ + result := []byte{112, 101, 97, 115, 97, 110, 116} + return result, nil + } + case "pelican":{ + result := []byte{112, 101, 108, 105, 99, 97, 110} + return result, nil + } + case "pen":{ + result := []byte{112, 101, 110} + return result, nil + } + case "penalty":{ + result := []byte{112, 101, 110, 97, 108, 116, 121} + return result, nil + } + case "pencil":{ + result := []byte{112, 101, 110, 99, 105, 108} + return result, nil + } + case "people":{ + result := []byte{112, 101, 111, 112, 108, 101} + return result, nil + } + case "pepper":{ + result := []byte{112, 101, 112, 112, 101, 114} + return result, nil + } + case "perfect":{ + result := []byte{112, 101, 114, 102, 101, 99, 116} + return result, nil + } + case "permit":{ + result := []byte{112, 101, 114, 109, 105, 116} + return result, nil + } + case "person":{ + result := []byte{112, 101, 114, 115, 111, 110} + return result, nil + } + case "pet":{ + result := []byte{112, 101, 116} + return result, nil + } + case "phone":{ + result := []byte{112, 104, 111, 110, 101} + return result, nil + } + case "photo":{ + result := []byte{112, 104, 111, 116, 111} + return result, nil + } + case "phrase":{ + result := []byte{112, 104, 114, 97, 115, 101} + return result, nil + } + case "physical":{ + result := []byte{112, 104, 121, 115, 105, 99, 97, 108} + return result, nil + } + case "piano":{ + result := []byte{112, 105, 97, 110, 111} + return result, nil + } + case "picnic":{ + result := []byte{112, 105, 99, 110, 105, 99} + return result, nil + } + case "picture":{ + result := []byte{112, 105, 99, 116, 117, 114, 101} + return result, nil + } + case "piece":{ + result := []byte{112, 105, 101, 99, 101} + return result, nil + } + case "pig":{ + result := []byte{112, 105, 103} + return result, nil + } + case "pigeon":{ + result := []byte{112, 105, 103, 101, 111, 110} + return result, nil + } + case "pill":{ + result := []byte{112, 105, 108, 108} + return result, nil + } + case "pilot":{ + result := []byte{112, 105, 108, 111, 116} + return result, nil + } + case "pink":{ + result := []byte{112, 105, 110, 107} + return result, nil + } + case "pioneer":{ + result := []byte{112, 105, 111, 110, 101, 101, 114} + return result, nil + } + case "pipe":{ + result := []byte{112, 105, 112, 101} + return result, nil + } + case "pistol":{ + result := []byte{112, 105, 115, 116, 111, 108} + return result, nil + } + case "pitch":{ + result := []byte{112, 105, 116, 99, 104} + return result, nil + } + case "pizza":{ + result := []byte{112, 105, 122, 122, 97} + return result, nil + } + case "place":{ + result := []byte{112, 108, 97, 99, 101} + return result, nil + } + case "planet":{ + result := []byte{112, 108, 97, 110, 101, 116} + return result, nil + } + case "plastic":{ + result := []byte{112, 108, 97, 115, 116, 105, 99} + return result, nil + } + case "plate":{ + result := []byte{112, 108, 97, 116, 101} + return result, nil + } + case "play":{ + result := []byte{112, 108, 97, 121} + return result, nil + } + case "please":{ + result := []byte{112, 108, 101, 97, 115, 101} + return result, nil + } + case "pledge":{ + result := []byte{112, 108, 101, 100, 103, 101} + return result, nil + } + case "pluck":{ + result := []byte{112, 108, 117, 99, 107} + return result, nil + } + case "plug":{ + result := []byte{112, 108, 117, 103} + return result, nil + } + case "plunge":{ + result := []byte{112, 108, 117, 110, 103, 101} + return result, nil + } + case "poem":{ + result := []byte{112, 111, 101, 109} + return result, nil + } + case "poet":{ + result := []byte{112, 111, 101, 116} + return result, nil + } + case "point":{ + result := []byte{112, 111, 105, 110, 116} + return result, nil + } + case "polar":{ + result := []byte{112, 111, 108, 97, 114} + return result, nil + } + case "pole":{ + result := []byte{112, 111, 108, 101} + return result, nil + } + case "police":{ + result := []byte{112, 111, 108, 105, 99, 101} + return result, nil + } + case "pond":{ + result := []byte{112, 111, 110, 100} + return result, nil + } + case "pony":{ + result := []byte{112, 111, 110, 121} + return result, nil + } + case "pool":{ + result := []byte{112, 111, 111, 108} + return result, nil + } + case "popular":{ + result := []byte{112, 111, 112, 117, 108, 97, 114} + return result, nil + } + case "portion":{ + result := []byte{112, 111, 114, 116, 105, 111, 110} + return result, nil + } + case "position":{ + result := []byte{112, 111, 115, 105, 116, 105, 111, 110} + return result, nil + } + case "possible":{ + result := []byte{112, 111, 115, 115, 105, 98, 108, 101} + return result, nil + } + case "post":{ + result := []byte{112, 111, 115, 116} + return result, nil + } + case "potato":{ + result := []byte{112, 111, 116, 97, 116, 111} + return result, nil + } + case "pottery":{ + result := []byte{112, 111, 116, 116, 101, 114, 121} + return result, nil + } + case "poverty":{ + result := []byte{112, 111, 118, 101, 114, 116, 121} + return result, nil + } + case "powder":{ + result := []byte{112, 111, 119, 100, 101, 114} + return result, nil + } + case "power":{ + result := []byte{112, 111, 119, 101, 114} + return result, nil + } + case "practice":{ + result := []byte{112, 114, 97, 99, 116, 105, 99, 101} + return result, nil + } + case "praise":{ + result := []byte{112, 114, 97, 105, 115, 101} + return result, nil + } + case "predict":{ + result := []byte{112, 114, 101, 100, 105, 99, 116} + return result, nil + } + case "prefer":{ + result := []byte{112, 114, 101, 102, 101, 114} + return result, nil + } + case "prepare":{ + result := []byte{112, 114, 101, 112, 97, 114, 101} + return result, nil + } + case "present":{ + result := []byte{112, 114, 101, 115, 101, 110, 116} + return result, nil + } + case "pretty":{ + result := []byte{112, 114, 101, 116, 116, 121} + return result, nil + } + case "prevent":{ + result := []byte{112, 114, 101, 118, 101, 110, 116} + return result, nil + } + case "price":{ + result := []byte{112, 114, 105, 99, 101} + return result, nil + } + case "pride":{ + result := []byte{112, 114, 105, 100, 101} + return result, nil + } + case "primary":{ + result := []byte{112, 114, 105, 109, 97, 114, 121} + return result, nil + } + case "print":{ + result := []byte{112, 114, 105, 110, 116} + return result, nil + } + case "priority":{ + result := []byte{112, 114, 105, 111, 114, 105, 116, 121} + return result, nil + } + case "prison":{ + result := []byte{112, 114, 105, 115, 111, 110} + return result, nil + } + case "private":{ + result := []byte{112, 114, 105, 118, 97, 116, 101} + return result, nil + } + case "prize":{ + result := []byte{112, 114, 105, 122, 101} + return result, nil + } + case "problem":{ + result := []byte{112, 114, 111, 98, 108, 101, 109} + return result, nil + } + case "process":{ + result := []byte{112, 114, 111, 99, 101, 115, 115} + return result, nil + } + case "produce":{ + result := []byte{112, 114, 111, 100, 117, 99, 101} + return result, nil + } + case "profit":{ + result := []byte{112, 114, 111, 102, 105, 116} + return result, nil + } + case "program":{ + result := []byte{112, 114, 111, 103, 114, 97, 109} + return result, nil + } + case "project":{ + result := []byte{112, 114, 111, 106, 101, 99, 116} + return result, nil + } + case "promote":{ + result := []byte{112, 114, 111, 109, 111, 116, 101} + return result, nil + } + case "proof":{ + result := []byte{112, 114, 111, 111, 102} + return result, nil + } + case "property":{ + result := []byte{112, 114, 111, 112, 101, 114, 116, 121} + return result, nil + } + case "prosper":{ + result := []byte{112, 114, 111, 115, 112, 101, 114} + return result, nil + } + case "protect":{ + result := []byte{112, 114, 111, 116, 101, 99, 116} + return result, nil + } + case "proud":{ + result := []byte{112, 114, 111, 117, 100} + return result, nil + } + case "provide":{ + result := []byte{112, 114, 111, 118, 105, 100, 101} + return result, nil + } + case "public":{ + result := []byte{112, 117, 98, 108, 105, 99} + return result, nil + } + case "pudding":{ + result := []byte{112, 117, 100, 100, 105, 110, 103} + return result, nil + } + case "pull":{ + result := []byte{112, 117, 108, 108} + return result, nil + } + case "pulp":{ + result := []byte{112, 117, 108, 112} + return result, nil + } + case "pulse":{ + result := []byte{112, 117, 108, 115, 101} + return result, nil + } + case "pumpkin":{ + result := []byte{112, 117, 109, 112, 107, 105, 110} + return result, nil + } + case "punch":{ + result := []byte{112, 117, 110, 99, 104} + return result, nil + } + case "pupil":{ + result := []byte{112, 117, 112, 105, 108} + return result, nil + } + case "puppy":{ + result := []byte{112, 117, 112, 112, 121} + return result, nil + } + case "purchase":{ + result := []byte{112, 117, 114, 99, 104, 97, 115, 101} + return result, nil + } + case "purity":{ + result := []byte{112, 117, 114, 105, 116, 121} + return result, nil + } + case "purpose":{ + result := []byte{112, 117, 114, 112, 111, 115, 101} + return result, nil + } + case "purse":{ + result := []byte{112, 117, 114, 115, 101} + return result, nil + } + case "push":{ + result := []byte{112, 117, 115, 104} + return result, nil + } + case "put":{ + result := []byte{112, 117, 116} + return result, nil + } + case "puzzle":{ + result := []byte{112, 117, 122, 122, 108, 101} + return result, nil + } + case "pyramid":{ + result := []byte{112, 121, 114, 97, 109, 105, 100} + return result, nil + } + case "quality":{ + result := []byte{113, 117, 97, 108, 105, 116, 121} + return result, nil + } + case "quantum":{ + result := []byte{113, 117, 97, 110, 116, 117, 109} + return result, nil + } + case "quarter":{ + result := []byte{113, 117, 97, 114, 116, 101, 114} + return result, nil + } + case "question":{ + result := []byte{113, 117, 101, 115, 116, 105, 111, 110} + return result, nil + } + case "quick":{ + result := []byte{113, 117, 105, 99, 107} + return result, nil + } + case "quit":{ + result := []byte{113, 117, 105, 116} + return result, nil + } + case "quiz":{ + result := []byte{113, 117, 105, 122} + return result, nil + } + case "quote":{ + result := []byte{113, 117, 111, 116, 101} + return result, nil + } + case "rabbit":{ + result := []byte{114, 97, 98, 98, 105, 116} + return result, nil + } + case "raccoon":{ + result := []byte{114, 97, 99, 99, 111, 111, 110} + return result, nil + } + case "race":{ + result := []byte{114, 97, 99, 101} + return result, nil + } + case "rack":{ + result := []byte{114, 97, 99, 107} + return result, nil + } + case "radar":{ + result := []byte{114, 97, 100, 97, 114} + return result, nil + } + case "radio":{ + result := []byte{114, 97, 100, 105, 111} + return result, nil + } + case "rail":{ + result := []byte{114, 97, 105, 108} + return result, nil + } + case "rain":{ + result := []byte{114, 97, 105, 110} + return result, nil + } + case "raise":{ + result := []byte{114, 97, 105, 115, 101} + return result, nil + } + case "rally":{ + result := []byte{114, 97, 108, 108, 121} + return result, nil + } + case "ramp":{ + result := []byte{114, 97, 109, 112} + return result, nil + } + case "ranch":{ + result := []byte{114, 97, 110, 99, 104} + return result, nil + } + case "random":{ + result := []byte{114, 97, 110, 100, 111, 109} + return result, nil + } + case "range":{ + result := []byte{114, 97, 110, 103, 101} + return result, nil + } + case "rapid":{ + result := []byte{114, 97, 112, 105, 100} + return result, nil + } + case "rare":{ + result := []byte{114, 97, 114, 101} + return result, nil + } + case "rate":{ + result := []byte{114, 97, 116, 101} + return result, nil + } + case "rather":{ + result := []byte{114, 97, 116, 104, 101, 114} + return result, nil + } + case "raven":{ + result := []byte{114, 97, 118, 101, 110} + return result, nil + } + case "raw":{ + result := []byte{114, 97, 119} + return result, nil + } + case "razor":{ + result := []byte{114, 97, 122, 111, 114} + return result, nil + } + case "ready":{ + result := []byte{114, 101, 97, 100, 121} + return result, nil + } + case "real":{ + result := []byte{114, 101, 97, 108} + return result, nil + } + case "reason":{ + result := []byte{114, 101, 97, 115, 111, 110} + return result, nil + } + case "rebel":{ + result := []byte{114, 101, 98, 101, 108} + return result, nil + } + case "rebuild":{ + result := []byte{114, 101, 98, 117, 105, 108, 100} + return result, nil + } + case "recall":{ + result := []byte{114, 101, 99, 97, 108, 108} + return result, nil + } + case "receive":{ + result := []byte{114, 101, 99, 101, 105, 118, 101} + return result, nil + } + case "recipe":{ + result := []byte{114, 101, 99, 105, 112, 101} + return result, nil + } + case "record":{ + result := []byte{114, 101, 99, 111, 114, 100} + return result, nil + } + case "recycle":{ + result := []byte{114, 101, 99, 121, 99, 108, 101} + return result, nil + } + case "reduce":{ + result := []byte{114, 101, 100, 117, 99, 101} + return result, nil + } + case "reflect":{ + result := []byte{114, 101, 102, 108, 101, 99, 116} + return result, nil + } + case "reform":{ + result := []byte{114, 101, 102, 111, 114, 109} + return result, nil + } + case "refuse":{ + result := []byte{114, 101, 102, 117, 115, 101} + return result, nil + } + case "region":{ + result := []byte{114, 101, 103, 105, 111, 110} + return result, nil + } + case "regret":{ + result := []byte{114, 101, 103, 114, 101, 116} + return result, nil + } + case "regular":{ + result := []byte{114, 101, 103, 117, 108, 97, 114} + return result, nil + } + case "reject":{ + result := []byte{114, 101, 106, 101, 99, 116} + return result, nil + } + case "relax":{ + result := []byte{114, 101, 108, 97, 120} + return result, nil + } + case "release":{ + result := []byte{114, 101, 108, 101, 97, 115, 101} + return result, nil + } + case "relief":{ + result := []byte{114, 101, 108, 105, 101, 102} + return result, nil + } + case "rely":{ + result := []byte{114, 101, 108, 121} + return result, nil + } + case "remain":{ + result := []byte{114, 101, 109, 97, 105, 110} + return result, nil + } + case "remember":{ + result := []byte{114, 101, 109, 101, 109, 98, 101, 114} + return result, nil + } + case "remind":{ + result := []byte{114, 101, 109, 105, 110, 100} + return result, nil + } + case "remove":{ + result := []byte{114, 101, 109, 111, 118, 101} + return result, nil + } + case "render":{ + result := []byte{114, 101, 110, 100, 101, 114} + return result, nil + } + case "renew":{ + result := []byte{114, 101, 110, 101, 119} + return result, nil + } + case "rent":{ + result := []byte{114, 101, 110, 116} + return result, nil + } + case "reopen":{ + result := []byte{114, 101, 111, 112, 101, 110} + return result, nil + } + case "repair":{ + result := []byte{114, 101, 112, 97, 105, 114} + return result, nil + } + case "repeat":{ + result := []byte{114, 101, 112, 101, 97, 116} + return result, nil + } + case "replace":{ + result := []byte{114, 101, 112, 108, 97, 99, 101} + return result, nil + } + case "report":{ + result := []byte{114, 101, 112, 111, 114, 116} + return result, nil + } + case "require":{ + result := []byte{114, 101, 113, 117, 105, 114, 101} + return result, nil + } + case "rescue":{ + result := []byte{114, 101, 115, 99, 117, 101} + return result, nil + } + case "resemble":{ + result := []byte{114, 101, 115, 101, 109, 98, 108, 101} + return result, nil + } + case "resist":{ + result := []byte{114, 101, 115, 105, 115, 116} + return result, nil + } + case "resource":{ + result := []byte{114, 101, 115, 111, 117, 114, 99, 101} + return result, nil + } + case "response":{ + result := []byte{114, 101, 115, 112, 111, 110, 115, 101} + return result, nil + } + case "result":{ + result := []byte{114, 101, 115, 117, 108, 116} + return result, nil + } + case "retire":{ + result := []byte{114, 101, 116, 105, 114, 101} + return result, nil + } + case "retreat":{ + result := []byte{114, 101, 116, 114, 101, 97, 116} + return result, nil + } + case "return":{ + result := []byte{114, 101, 116, 117, 114, 110} + return result, nil + } + case "reunion":{ + result := []byte{114, 101, 117, 110, 105, 111, 110} + return result, nil + } + case "reveal":{ + result := []byte{114, 101, 118, 101, 97, 108} + return result, nil + } + case "review":{ + result := []byte{114, 101, 118, 105, 101, 119} + return result, nil + } + case "reward":{ + result := []byte{114, 101, 119, 97, 114, 100} + return result, nil + } + case "rhythm":{ + result := []byte{114, 104, 121, 116, 104, 109} + return result, nil + } + case "rib":{ + result := []byte{114, 105, 98} + return result, nil + } + case "ribbon":{ + result := []byte{114, 105, 98, 98, 111, 110} + return result, nil + } + case "rice":{ + result := []byte{114, 105, 99, 101} + return result, nil + } + case "rich":{ + result := []byte{114, 105, 99, 104} + return result, nil + } + case "ride":{ + result := []byte{114, 105, 100, 101} + return result, nil + } + case "ridge":{ + result := []byte{114, 105, 100, 103, 101} + return result, nil + } + case "rifle":{ + result := []byte{114, 105, 102, 108, 101} + return result, nil + } + case "right":{ + result := []byte{114, 105, 103, 104, 116} + return result, nil + } + case "rigid":{ + result := []byte{114, 105, 103, 105, 100} + return result, nil + } + case "ring":{ + result := []byte{114, 105, 110, 103} + return result, nil + } + case "riot":{ + result := []byte{114, 105, 111, 116} + return result, nil + } + case "ripple":{ + result := []byte{114, 105, 112, 112, 108, 101} + return result, nil + } + case "risk":{ + result := []byte{114, 105, 115, 107} + return result, nil + } + case "ritual":{ + result := []byte{114, 105, 116, 117, 97, 108} + return result, nil + } + case "rival":{ + result := []byte{114, 105, 118, 97, 108} + return result, nil + } + case "river":{ + result := []byte{114, 105, 118, 101, 114} + return result, nil + } + case "road":{ + result := []byte{114, 111, 97, 100} + return result, nil + } + case "roast":{ + result := []byte{114, 111, 97, 115, 116} + return result, nil + } + case "robot":{ + result := []byte{114, 111, 98, 111, 116} + return result, nil + } + case "robust":{ + result := []byte{114, 111, 98, 117, 115, 116} + return result, nil + } + case "rocket":{ + result := []byte{114, 111, 99, 107, 101, 116} + return result, nil + } + case "romance":{ + result := []byte{114, 111, 109, 97, 110, 99, 101} + return result, nil + } + case "roof":{ + result := []byte{114, 111, 111, 102} + return result, nil + } + case "rookie":{ + result := []byte{114, 111, 111, 107, 105, 101} + return result, nil + } + case "room":{ + result := []byte{114, 111, 111, 109} + return result, nil + } + case "rose":{ + result := []byte{114, 111, 115, 101} + return result, nil + } + case "rotate":{ + result := []byte{114, 111, 116, 97, 116, 101} + return result, nil + } + case "rough":{ + result := []byte{114, 111, 117, 103, 104} + return result, nil + } + case "round":{ + result := []byte{114, 111, 117, 110, 100} + return result, nil + } + case "route":{ + result := []byte{114, 111, 117, 116, 101} + return result, nil + } + case "royal":{ + result := []byte{114, 111, 121, 97, 108} + return result, nil + } + case "rubber":{ + result := []byte{114, 117, 98, 98, 101, 114} + return result, nil + } + case "rude":{ + result := []byte{114, 117, 100, 101} + return result, nil + } + case "rug":{ + result := []byte{114, 117, 103} + return result, nil + } + case "rule":{ + result := []byte{114, 117, 108, 101} + return result, nil + } + case "run":{ + result := []byte{114, 117, 110} + return result, nil + } + case "runway":{ + result := []byte{114, 117, 110, 119, 97, 121} + return result, nil + } + case "rural":{ + result := []byte{114, 117, 114, 97, 108} + return result, nil + } + case "sad":{ + result := []byte{115, 97, 100} + return result, nil + } + case "saddle":{ + result := []byte{115, 97, 100, 100, 108, 101} + return result, nil + } + case "sadness":{ + result := []byte{115, 97, 100, 110, 101, 115, 115} + return result, nil + } + case "safe":{ + result := []byte{115, 97, 102, 101} + return result, nil + } + case "sail":{ + result := []byte{115, 97, 105, 108} + return result, nil + } + case "salad":{ + result := []byte{115, 97, 108, 97, 100} + return result, nil + } + case "salmon":{ + result := []byte{115, 97, 108, 109, 111, 110} + return result, nil + } + case "salon":{ + result := []byte{115, 97, 108, 111, 110} + return result, nil + } + case "salt":{ + result := []byte{115, 97, 108, 116} + return result, nil + } + case "salute":{ + result := []byte{115, 97, 108, 117, 116, 101} + return result, nil + } + case "same":{ + result := []byte{115, 97, 109, 101} + return result, nil + } + case "sample":{ + result := []byte{115, 97, 109, 112, 108, 101} + return result, nil + } + case "sand":{ + result := []byte{115, 97, 110, 100} + return result, nil + } + case "satisfy":{ + result := []byte{115, 97, 116, 105, 115, 102, 121} + return result, nil + } + case "satoshi":{ + result := []byte{115, 97, 116, 111, 115, 104, 105} + return result, nil + } + case "sauce":{ + result := []byte{115, 97, 117, 99, 101} + return result, nil + } + case "sausage":{ + result := []byte{115, 97, 117, 115, 97, 103, 101} + return result, nil + } + case "save":{ + result := []byte{115, 97, 118, 101} + return result, nil + } + case "say":{ + result := []byte{115, 97, 121} + return result, nil + } + case "scale":{ + result := []byte{115, 99, 97, 108, 101} + return result, nil + } + case "scan":{ + result := []byte{115, 99, 97, 110} + return result, nil + } + case "scare":{ + result := []byte{115, 99, 97, 114, 101} + return result, nil + } + case "scatter":{ + result := []byte{115, 99, 97, 116, 116, 101, 114} + return result, nil + } + case "scene":{ + result := []byte{115, 99, 101, 110, 101} + return result, nil + } + case "scheme":{ + result := []byte{115, 99, 104, 101, 109, 101} + return result, nil + } + case "school":{ + result := []byte{115, 99, 104, 111, 111, 108} + return result, nil + } + case "science":{ + result := []byte{115, 99, 105, 101, 110, 99, 101} + return result, nil + } + case "scissors":{ + result := []byte{115, 99, 105, 115, 115, 111, 114, 115} + return result, nil + } + case "scorpion":{ + result := []byte{115, 99, 111, 114, 112, 105, 111, 110} + return result, nil + } + case "scout":{ + result := []byte{115, 99, 111, 117, 116} + return result, nil + } + case "scrap":{ + result := []byte{115, 99, 114, 97, 112} + return result, nil + } + case "screen":{ + result := []byte{115, 99, 114, 101, 101, 110} + return result, nil + } + case "script":{ + result := []byte{115, 99, 114, 105, 112, 116} + return result, nil + } + case "scrub":{ + result := []byte{115, 99, 114, 117, 98} + return result, nil + } + case "sea":{ + result := []byte{115, 101, 97} + return result, nil + } + case "search":{ + result := []byte{115, 101, 97, 114, 99, 104} + return result, nil + } + case "season":{ + result := []byte{115, 101, 97, 115, 111, 110} + return result, nil + } + case "seat":{ + result := []byte{115, 101, 97, 116} + return result, nil + } + case "second":{ + result := []byte{115, 101, 99, 111, 110, 100} + return result, nil + } + case "secret":{ + result := []byte{115, 101, 99, 114, 101, 116} + return result, nil + } + case "section":{ + result := []byte{115, 101, 99, 116, 105, 111, 110} + return result, nil + } + case "security":{ + result := []byte{115, 101, 99, 117, 114, 105, 116, 121} + return result, nil + } + case "seed":{ + result := []byte{115, 101, 101, 100} + return result, nil + } + case "seek":{ + result := []byte{115, 101, 101, 107} + return result, nil + } + case "segment":{ + result := []byte{115, 101, 103, 109, 101, 110, 116} + return result, nil + } + case "select":{ + result := []byte{115, 101, 108, 101, 99, 116} + return result, nil + } + case "sell":{ + result := []byte{115, 101, 108, 108} + return result, nil + } + case "seminar":{ + result := []byte{115, 101, 109, 105, 110, 97, 114} + return result, nil + } + case "senior":{ + result := []byte{115, 101, 110, 105, 111, 114} + return result, nil + } + case "sense":{ + result := []byte{115, 101, 110, 115, 101} + return result, nil + } + case "sentence":{ + result := []byte{115, 101, 110, 116, 101, 110, 99, 101} + return result, nil + } + case "series":{ + result := []byte{115, 101, 114, 105, 101, 115} + return result, nil + } + case "service":{ + result := []byte{115, 101, 114, 118, 105, 99, 101} + return result, nil + } + case "session":{ + result := []byte{115, 101, 115, 115, 105, 111, 110} + return result, nil + } + case "settle":{ + result := []byte{115, 101, 116, 116, 108, 101} + return result, nil + } + case "setup":{ + result := []byte{115, 101, 116, 117, 112} + return result, nil + } + case "seven":{ + result := []byte{115, 101, 118, 101, 110} + return result, nil + } + case "shadow":{ + result := []byte{115, 104, 97, 100, 111, 119} + return result, nil + } + case "shaft":{ + result := []byte{115, 104, 97, 102, 116} + return result, nil + } + case "shallow":{ + result := []byte{115, 104, 97, 108, 108, 111, 119} + return result, nil + } + case "share":{ + result := []byte{115, 104, 97, 114, 101} + return result, nil + } + case "shed":{ + result := []byte{115, 104, 101, 100} + return result, nil + } + case "shell":{ + result := []byte{115, 104, 101, 108, 108} + return result, nil + } + case "sheriff":{ + result := []byte{115, 104, 101, 114, 105, 102, 102} + return result, nil + } + case "shield":{ + result := []byte{115, 104, 105, 101, 108, 100} + return result, nil + } + case "shift":{ + result := []byte{115, 104, 105, 102, 116} + return result, nil + } + case "shine":{ + result := []byte{115, 104, 105, 110, 101} + return result, nil + } + case "ship":{ + result := []byte{115, 104, 105, 112} + return result, nil + } + case "shiver":{ + result := []byte{115, 104, 105, 118, 101, 114} + return result, nil + } + case "shock":{ + result := []byte{115, 104, 111, 99, 107} + return result, nil + } + case "shoe":{ + result := []byte{115, 104, 111, 101} + return result, nil + } + case "shoot":{ + result := []byte{115, 104, 111, 111, 116} + return result, nil + } + case "shop":{ + result := []byte{115, 104, 111, 112} + return result, nil + } + case "short":{ + result := []byte{115, 104, 111, 114, 116} + return result, nil + } + case "shoulder":{ + result := []byte{115, 104, 111, 117, 108, 100, 101, 114} + return result, nil + } + case "shove":{ + result := []byte{115, 104, 111, 118, 101} + return result, nil + } + case "shrimp":{ + result := []byte{115, 104, 114, 105, 109, 112} + return result, nil + } + case "shrug":{ + result := []byte{115, 104, 114, 117, 103} + return result, nil + } + case "shuffle":{ + result := []byte{115, 104, 117, 102, 102, 108, 101} + return result, nil + } + case "shy":{ + result := []byte{115, 104, 121} + return result, nil + } + case "sibling":{ + result := []byte{115, 105, 98, 108, 105, 110, 103} + return result, nil + } + case "sick":{ + result := []byte{115, 105, 99, 107} + return result, nil + } + case "side":{ + result := []byte{115, 105, 100, 101} + return result, nil + } + case "siege":{ + result := []byte{115, 105, 101, 103, 101} + return result, nil + } + case "sight":{ + result := []byte{115, 105, 103, 104, 116} + return result, nil + } + case "sign":{ + result := []byte{115, 105, 103, 110} + return result, nil + } + case "silent":{ + result := []byte{115, 105, 108, 101, 110, 116} + return result, nil + } + case "silk":{ + result := []byte{115, 105, 108, 107} + return result, nil + } + case "silly":{ + result := []byte{115, 105, 108, 108, 121} + return result, nil + } + case "silver":{ + result := []byte{115, 105, 108, 118, 101, 114} + return result, nil + } + case "similar":{ + result := []byte{115, 105, 109, 105, 108, 97, 114} + return result, nil + } + case "simple":{ + result := []byte{115, 105, 109, 112, 108, 101} + return result, nil + } + case "since":{ + result := []byte{115, 105, 110, 99, 101} + return result, nil + } + case "sing":{ + result := []byte{115, 105, 110, 103} + return result, nil + } + case "siren":{ + result := []byte{115, 105, 114, 101, 110} + return result, nil + } + case "sister":{ + result := []byte{115, 105, 115, 116, 101, 114} + return result, nil + } + case "situate":{ + result := []byte{115, 105, 116, 117, 97, 116, 101} + return result, nil + } + case "six":{ + result := []byte{115, 105, 120} + return result, nil + } + case "size":{ + result := []byte{115, 105, 122, 101} + return result, nil + } + case "skate":{ + result := []byte{115, 107, 97, 116, 101} + return result, nil + } + case "sketch":{ + result := []byte{115, 107, 101, 116, 99, 104} + return result, nil + } + case "ski":{ + result := []byte{115, 107, 105} + return result, nil + } + case "skill":{ + result := []byte{115, 107, 105, 108, 108} + return result, nil + } + case "skin":{ + result := []byte{115, 107, 105, 110} + return result, nil + } + case "skirt":{ + result := []byte{115, 107, 105, 114, 116} + return result, nil + } + case "skull":{ + result := []byte{115, 107, 117, 108, 108} + return result, nil + } + case "slab":{ + result := []byte{115, 108, 97, 98} + return result, nil + } + case "slam":{ + result := []byte{115, 108, 97, 109} + return result, nil + } + case "sleep":{ + result := []byte{115, 108, 101, 101, 112} + return result, nil + } + case "slender":{ + result := []byte{115, 108, 101, 110, 100, 101, 114} + return result, nil + } + case "slice":{ + result := []byte{115, 108, 105, 99, 101} + return result, nil + } + case "slide":{ + result := []byte{115, 108, 105, 100, 101} + return result, nil + } + case "slight":{ + result := []byte{115, 108, 105, 103, 104, 116} + return result, nil + } + case "slim":{ + result := []byte{115, 108, 105, 109} + return result, nil + } + case "slogan":{ + result := []byte{115, 108, 111, 103, 97, 110} + return result, nil + } + case "slot":{ + result := []byte{115, 108, 111, 116} + return result, nil + } + case "slow":{ + result := []byte{115, 108, 111, 119} + return result, nil + } + case "slush":{ + result := []byte{115, 108, 117, 115, 104} + return result, nil + } + case "small":{ + result := []byte{115, 109, 97, 108, 108} + return result, nil + } + case "smart":{ + result := []byte{115, 109, 97, 114, 116} + return result, nil + } + case "smile":{ + result := []byte{115, 109, 105, 108, 101} + return result, nil + } + case "smoke":{ + result := []byte{115, 109, 111, 107, 101} + return result, nil + } + case "smooth":{ + result := []byte{115, 109, 111, 111, 116, 104} + return result, nil + } + case "snack":{ + result := []byte{115, 110, 97, 99, 107} + return result, nil + } + case "snake":{ + result := []byte{115, 110, 97, 107, 101} + return result, nil + } + case "snap":{ + result := []byte{115, 110, 97, 112} + return result, nil + } + case "sniff":{ + result := []byte{115, 110, 105, 102, 102} + return result, nil + } + case "snow":{ + result := []byte{115, 110, 111, 119} + return result, nil + } + case "soap":{ + result := []byte{115, 111, 97, 112} + return result, nil + } + case "soccer":{ + result := []byte{115, 111, 99, 99, 101, 114} + return result, nil + } + case "social":{ + result := []byte{115, 111, 99, 105, 97, 108} + return result, nil + } + case "sock":{ + result := []byte{115, 111, 99, 107} + return result, nil + } + case "soda":{ + result := []byte{115, 111, 100, 97} + return result, nil + } + case "soft":{ + result := []byte{115, 111, 102, 116} + return result, nil + } + case "solar":{ + result := []byte{115, 111, 108, 97, 114} + return result, nil + } + case "soldier":{ + result := []byte{115, 111, 108, 100, 105, 101, 114} + return result, nil + } + case "solid":{ + result := []byte{115, 111, 108, 105, 100} + return result, nil + } + case "solution":{ + result := []byte{115, 111, 108, 117, 116, 105, 111, 110} + return result, nil + } + case "solve":{ + result := []byte{115, 111, 108, 118, 101} + return result, nil + } + case "someone":{ + result := []byte{115, 111, 109, 101, 111, 110, 101} + return result, nil + } + case "song":{ + result := []byte{115, 111, 110, 103} + return result, nil + } + case "soon":{ + result := []byte{115, 111, 111, 110} + return result, nil + } + case "sorry":{ + result := []byte{115, 111, 114, 114, 121} + return result, nil + } + case "sort":{ + result := []byte{115, 111, 114, 116} + return result, nil + } + case "soul":{ + result := []byte{115, 111, 117, 108} + return result, nil + } + case "sound":{ + result := []byte{115, 111, 117, 110, 100} + return result, nil + } + case "soup":{ + result := []byte{115, 111, 117, 112} + return result, nil + } + case "source":{ + result := []byte{115, 111, 117, 114, 99, 101} + return result, nil + } + case "south":{ + result := []byte{115, 111, 117, 116, 104} + return result, nil + } + case "space":{ + result := []byte{115, 112, 97, 99, 101} + return result, nil + } + case "spare":{ + result := []byte{115, 112, 97, 114, 101} + return result, nil + } + case "spatial":{ + result := []byte{115, 112, 97, 116, 105, 97, 108} + return result, nil + } + case "spawn":{ + result := []byte{115, 112, 97, 119, 110} + return result, nil + } + case "speak":{ + result := []byte{115, 112, 101, 97, 107} + return result, nil + } + case "special":{ + result := []byte{115, 112, 101, 99, 105, 97, 108} + return result, nil + } + case "speed":{ + result := []byte{115, 112, 101, 101, 100} + return result, nil + } + case "spell":{ + result := []byte{115, 112, 101, 108, 108} + return result, nil + } + case "spend":{ + result := []byte{115, 112, 101, 110, 100} + return result, nil + } + case "sphere":{ + result := []byte{115, 112, 104, 101, 114, 101} + return result, nil + } + case "spice":{ + result := []byte{115, 112, 105, 99, 101} + return result, nil + } + case "spider":{ + result := []byte{115, 112, 105, 100, 101, 114} + return result, nil + } + case "spike":{ + result := []byte{115, 112, 105, 107, 101} + return result, nil + } + case "spin":{ + result := []byte{115, 112, 105, 110} + return result, nil + } + case "spirit":{ + result := []byte{115, 112, 105, 114, 105, 116} + return result, nil + } + case "split":{ + result := []byte{115, 112, 108, 105, 116} + return result, nil + } + case "spoil":{ + result := []byte{115, 112, 111, 105, 108} + return result, nil + } + case "sponsor":{ + result := []byte{115, 112, 111, 110, 115, 111, 114} + return result, nil + } + case "spoon":{ + result := []byte{115, 112, 111, 111, 110} + return result, nil + } + case "sport":{ + result := []byte{115, 112, 111, 114, 116} + return result, nil + } + case "spot":{ + result := []byte{115, 112, 111, 116} + return result, nil + } + case "spray":{ + result := []byte{115, 112, 114, 97, 121} + return result, nil + } + case "spread":{ + result := []byte{115, 112, 114, 101, 97, 100} + return result, nil + } + case "spring":{ + result := []byte{115, 112, 114, 105, 110, 103} + return result, nil + } + case "spy":{ + result := []byte{115, 112, 121} + return result, nil + } + case "square":{ + result := []byte{115, 113, 117, 97, 114, 101} + return result, nil + } + case "squeeze":{ + result := []byte{115, 113, 117, 101, 101, 122, 101} + return result, nil + } + case "squirrel":{ + result := []byte{115, 113, 117, 105, 114, 114, 101, 108} + return result, nil + } + case "stable":{ + result := []byte{115, 116, 97, 98, 108, 101} + return result, nil + } + case "stadium":{ + result := []byte{115, 116, 97, 100, 105, 117, 109} + return result, nil + } + case "staff":{ + result := []byte{115, 116, 97, 102, 102} + return result, nil + } + case "stage":{ + result := []byte{115, 116, 97, 103, 101} + return result, nil + } + case "stairs":{ + result := []byte{115, 116, 97, 105, 114, 115} + return result, nil + } + case "stamp":{ + result := []byte{115, 116, 97, 109, 112} + return result, nil + } + case "stand":{ + result := []byte{115, 116, 97, 110, 100} + return result, nil + } + case "start":{ + result := []byte{115, 116, 97, 114, 116} + return result, nil + } + case "state":{ + result := []byte{115, 116, 97, 116, 101} + return result, nil + } + case "stay":{ + result := []byte{115, 116, 97, 121} + return result, nil + } + case "steak":{ + result := []byte{115, 116, 101, 97, 107} + return result, nil + } + case "steel":{ + result := []byte{115, 116, 101, 101, 108} + return result, nil + } + case "stem":{ + result := []byte{115, 116, 101, 109} + return result, nil + } + case "step":{ + result := []byte{115, 116, 101, 112} + return result, nil + } + case "stereo":{ + result := []byte{115, 116, 101, 114, 101, 111} + return result, nil + } + case "stick":{ + result := []byte{115, 116, 105, 99, 107} + return result, nil + } + case "still":{ + result := []byte{115, 116, 105, 108, 108} + return result, nil + } + case "sting":{ + result := []byte{115, 116, 105, 110, 103} + return result, nil + } + case "stock":{ + result := []byte{115, 116, 111, 99, 107} + return result, nil + } + case "stomach":{ + result := []byte{115, 116, 111, 109, 97, 99, 104} + return result, nil + } + case "stone":{ + result := []byte{115, 116, 111, 110, 101} + return result, nil + } + case "stool":{ + result := []byte{115, 116, 111, 111, 108} + return result, nil + } + case "story":{ + result := []byte{115, 116, 111, 114, 121} + return result, nil + } + case "stove":{ + result := []byte{115, 116, 111, 118, 101} + return result, nil + } + case "strategy":{ + result := []byte{115, 116, 114, 97, 116, 101, 103, 121} + return result, nil + } + case "street":{ + result := []byte{115, 116, 114, 101, 101, 116} + return result, nil + } + case "strike":{ + result := []byte{115, 116, 114, 105, 107, 101} + return result, nil + } + case "strong":{ + result := []byte{115, 116, 114, 111, 110, 103} + return result, nil + } + case "struggle":{ + result := []byte{115, 116, 114, 117, 103, 103, 108, 101} + return result, nil + } + case "student":{ + result := []byte{115, 116, 117, 100, 101, 110, 116} + return result, nil + } + case "stuff":{ + result := []byte{115, 116, 117, 102, 102} + return result, nil + } + case "stumble":{ + result := []byte{115, 116, 117, 109, 98, 108, 101} + return result, nil + } + case "style":{ + result := []byte{115, 116, 121, 108, 101} + return result, nil + } + case "subject":{ + result := []byte{115, 117, 98, 106, 101, 99, 116} + return result, nil + } + case "submit":{ + result := []byte{115, 117, 98, 109, 105, 116} + return result, nil + } + case "subway":{ + result := []byte{115, 117, 98, 119, 97, 121} + return result, nil + } + case "success":{ + result := []byte{115, 117, 99, 99, 101, 115, 115} + return result, nil + } + case "such":{ + result := []byte{115, 117, 99, 104} + return result, nil + } + case "sudden":{ + result := []byte{115, 117, 100, 100, 101, 110} + return result, nil + } + case "suffer":{ + result := []byte{115, 117, 102, 102, 101, 114} + return result, nil + } + case "sugar":{ + result := []byte{115, 117, 103, 97, 114} + return result, nil + } + case "suggest":{ + result := []byte{115, 117, 103, 103, 101, 115, 116} + return result, nil + } + case "suit":{ + result := []byte{115, 117, 105, 116} + return result, nil + } + case "summer":{ + result := []byte{115, 117, 109, 109, 101, 114} + return result, nil + } + case "sun":{ + result := []byte{115, 117, 110} + return result, nil + } + case "sunny":{ + result := []byte{115, 117, 110, 110, 121} + return result, nil + } + case "sunset":{ + result := []byte{115, 117, 110, 115, 101, 116} + return result, nil + } + case "super":{ + result := []byte{115, 117, 112, 101, 114} + return result, nil + } + case "supply":{ + result := []byte{115, 117, 112, 112, 108, 121} + return result, nil + } + case "supreme":{ + result := []byte{115, 117, 112, 114, 101, 109, 101} + return result, nil + } + case "sure":{ + result := []byte{115, 117, 114, 101} + return result, nil + } + case "surface":{ + result := []byte{115, 117, 114, 102, 97, 99, 101} + return result, nil + } + case "surge":{ + result := []byte{115, 117, 114, 103, 101} + return result, nil + } + case "surprise":{ + result := []byte{115, 117, 114, 112, 114, 105, 115, 101} + return result, nil + } + case "surround":{ + result := []byte{115, 117, 114, 114, 111, 117, 110, 100} + return result, nil + } + case "survey":{ + result := []byte{115, 117, 114, 118, 101, 121} + return result, nil + } + case "suspect":{ + result := []byte{115, 117, 115, 112, 101, 99, 116} + return result, nil + } + case "sustain":{ + result := []byte{115, 117, 115, 116, 97, 105, 110} + return result, nil + } + case "swallow":{ + result := []byte{115, 119, 97, 108, 108, 111, 119} + return result, nil + } + case "swamp":{ + result := []byte{115, 119, 97, 109, 112} + return result, nil + } + case "swap":{ + result := []byte{115, 119, 97, 112} + return result, nil + } + case "swarm":{ + result := []byte{115, 119, 97, 114, 109} + return result, nil + } + case "swear":{ + result := []byte{115, 119, 101, 97, 114} + return result, nil + } + case "sweet":{ + result := []byte{115, 119, 101, 101, 116} + return result, nil + } + case "swift":{ + result := []byte{115, 119, 105, 102, 116} + return result, nil + } + case "swim":{ + result := []byte{115, 119, 105, 109} + return result, nil + } + case "swing":{ + result := []byte{115, 119, 105, 110, 103} + return result, nil + } + case "switch":{ + result := []byte{115, 119, 105, 116, 99, 104} + return result, nil + } + case "sword":{ + result := []byte{115, 119, 111, 114, 100} + return result, nil + } + case "symbol":{ + result := []byte{115, 121, 109, 98, 111, 108} + return result, nil + } + case "symptom":{ + result := []byte{115, 121, 109, 112, 116, 111, 109} + return result, nil + } + case "syrup":{ + result := []byte{115, 121, 114, 117, 112} + return result, nil + } + case "system":{ + result := []byte{115, 121, 115, 116, 101, 109} + return result, nil + } + case "table":{ + result := []byte{116, 97, 98, 108, 101} + return result, nil + } + case "tackle":{ + result := []byte{116, 97, 99, 107, 108, 101} + return result, nil + } + case "tag":{ + result := []byte{116, 97, 103} + return result, nil + } + case "tail":{ + result := []byte{116, 97, 105, 108} + return result, nil + } + case "talent":{ + result := []byte{116, 97, 108, 101, 110, 116} + return result, nil + } + case "talk":{ + result := []byte{116, 97, 108, 107} + return result, nil + } + case "tank":{ + result := []byte{116, 97, 110, 107} + return result, nil + } + case "tape":{ + result := []byte{116, 97, 112, 101} + return result, nil + } + case "target":{ + result := []byte{116, 97, 114, 103, 101, 116} + return result, nil + } + case "task":{ + result := []byte{116, 97, 115, 107} + return result, nil + } + case "taste":{ + result := []byte{116, 97, 115, 116, 101} + return result, nil + } + case "tattoo":{ + result := []byte{116, 97, 116, 116, 111, 111} + return result, nil + } + case "taxi":{ + result := []byte{116, 97, 120, 105} + return result, nil + } + case "teach":{ + result := []byte{116, 101, 97, 99, 104} + return result, nil + } + case "team":{ + result := []byte{116, 101, 97, 109} + return result, nil + } + case "tell":{ + result := []byte{116, 101, 108, 108} + return result, nil + } + case "ten":{ + result := []byte{116, 101, 110} + return result, nil + } + case "tenant":{ + result := []byte{116, 101, 110, 97, 110, 116} + return result, nil + } + case "tennis":{ + result := []byte{116, 101, 110, 110, 105, 115} + return result, nil + } + case "tent":{ + result := []byte{116, 101, 110, 116} + return result, nil + } + case "term":{ + result := []byte{116, 101, 114, 109} + return result, nil + } + case "test":{ + result := []byte{116, 101, 115, 116} + return result, nil + } + case "text":{ + result := []byte{116, 101, 120, 116} + return result, nil + } + case "thank":{ + result := []byte{116, 104, 97, 110, 107} + return result, nil + } + case "that":{ + result := []byte{116, 104, 97, 116} + return result, nil + } + case "theme":{ + result := []byte{116, 104, 101, 109, 101} + return result, nil + } + case "then":{ + result := []byte{116, 104, 101, 110} + return result, nil + } + case "theory":{ + result := []byte{116, 104, 101, 111, 114, 121} + return result, nil + } + case "there":{ + result := []byte{116, 104, 101, 114, 101} + return result, nil + } + case "they":{ + result := []byte{116, 104, 101, 121} + return result, nil + } + case "thing":{ + result := []byte{116, 104, 105, 110, 103} + return result, nil + } + case "this":{ + result := []byte{116, 104, 105, 115} + return result, nil + } + case "thought":{ + result := []byte{116, 104, 111, 117, 103, 104, 116} + return result, nil + } + case "three":{ + result := []byte{116, 104, 114, 101, 101} + return result, nil + } + case "thrive":{ + result := []byte{116, 104, 114, 105, 118, 101} + return result, nil + } + case "throw":{ + result := []byte{116, 104, 114, 111, 119} + return result, nil + } + case "thumb":{ + result := []byte{116, 104, 117, 109, 98} + return result, nil + } + case "thunder":{ + result := []byte{116, 104, 117, 110, 100, 101, 114} + return result, nil + } + case "ticket":{ + result := []byte{116, 105, 99, 107, 101, 116} + return result, nil + } + case "tide":{ + result := []byte{116, 105, 100, 101} + return result, nil + } + case "tiger":{ + result := []byte{116, 105, 103, 101, 114} + return result, nil + } + case "tilt":{ + result := []byte{116, 105, 108, 116} + return result, nil + } + case "timber":{ + result := []byte{116, 105, 109, 98, 101, 114} + return result, nil + } + case "time":{ + result := []byte{116, 105, 109, 101} + return result, nil + } + case "tiny":{ + result := []byte{116, 105, 110, 121} + return result, nil + } + case "tip":{ + result := []byte{116, 105, 112} + return result, nil + } + case "tired":{ + result := []byte{116, 105, 114, 101, 100} + return result, nil + } + case "tissue":{ + result := []byte{116, 105, 115, 115, 117, 101} + return result, nil + } + case "title":{ + result := []byte{116, 105, 116, 108, 101} + return result, nil + } + case "toast":{ + result := []byte{116, 111, 97, 115, 116} + return result, nil + } + case "tobacco":{ + result := []byte{116, 111, 98, 97, 99, 99, 111} + return result, nil + } + case "today":{ + result := []byte{116, 111, 100, 97, 121} + return result, nil + } + case "toddler":{ + result := []byte{116, 111, 100, 100, 108, 101, 114} + return result, nil + } + case "toe":{ + result := []byte{116, 111, 101} + return result, nil + } + case "together":{ + result := []byte{116, 111, 103, 101, 116, 104, 101, 114} + return result, nil + } + case "toilet":{ + result := []byte{116, 111, 105, 108, 101, 116} + return result, nil + } + case "token":{ + result := []byte{116, 111, 107, 101, 110} + return result, nil + } + case "tomato":{ + result := []byte{116, 111, 109, 97, 116, 111} + return result, nil + } + case "tomorrow":{ + result := []byte{116, 111, 109, 111, 114, 114, 111, 119} + return result, nil + } + case "tone":{ + result := []byte{116, 111, 110, 101} + return result, nil + } + case "tongue":{ + result := []byte{116, 111, 110, 103, 117, 101} + return result, nil + } + case "tonight":{ + result := []byte{116, 111, 110, 105, 103, 104, 116} + return result, nil + } + case "tool":{ + result := []byte{116, 111, 111, 108} + return result, nil + } + case "tooth":{ + result := []byte{116, 111, 111, 116, 104} + return result, nil + } + case "top":{ + result := []byte{116, 111, 112} + return result, nil + } + case "topic":{ + result := []byte{116, 111, 112, 105, 99} + return result, nil + } + case "topple":{ + result := []byte{116, 111, 112, 112, 108, 101} + return result, nil + } + case "torch":{ + result := []byte{116, 111, 114, 99, 104} + return result, nil + } + case "tornado":{ + result := []byte{116, 111, 114, 110, 97, 100, 111} + return result, nil + } + case "tortoise":{ + result := []byte{116, 111, 114, 116, 111, 105, 115, 101} + return result, nil + } + case "toss":{ + result := []byte{116, 111, 115, 115} + return result, nil + } + case "total":{ + result := []byte{116, 111, 116, 97, 108} + return result, nil + } + case "tourist":{ + result := []byte{116, 111, 117, 114, 105, 115, 116} + return result, nil + } + case "toward":{ + result := []byte{116, 111, 119, 97, 114, 100} + return result, nil + } + case "tower":{ + result := []byte{116, 111, 119, 101, 114} + return result, nil + } + case "town":{ + result := []byte{116, 111, 119, 110} + return result, nil + } + case "toy":{ + result := []byte{116, 111, 121} + return result, nil + } + case "track":{ + result := []byte{116, 114, 97, 99, 107} + return result, nil + } + case "trade":{ + result := []byte{116, 114, 97, 100, 101} + return result, nil + } + case "traffic":{ + result := []byte{116, 114, 97, 102, 102, 105, 99} + return result, nil + } + case "tragic":{ + result := []byte{116, 114, 97, 103, 105, 99} + return result, nil + } + case "train":{ + result := []byte{116, 114, 97, 105, 110} + return result, nil + } + case "transfer":{ + result := []byte{116, 114, 97, 110, 115, 102, 101, 114} + return result, nil + } + case "trap":{ + result := []byte{116, 114, 97, 112} + return result, nil + } + case "trash":{ + result := []byte{116, 114, 97, 115, 104} + return result, nil + } + case "travel":{ + result := []byte{116, 114, 97, 118, 101, 108} + return result, nil + } + case "tray":{ + result := []byte{116, 114, 97, 121} + return result, nil + } + case "treat":{ + result := []byte{116, 114, 101, 97, 116} + return result, nil + } + case "tree":{ + result := []byte{116, 114, 101, 101} + return result, nil + } + case "trend":{ + result := []byte{116, 114, 101, 110, 100} + return result, nil + } + case "trial":{ + result := []byte{116, 114, 105, 97, 108} + return result, nil + } + case "tribe":{ + result := []byte{116, 114, 105, 98, 101} + return result, nil + } + case "trick":{ + result := []byte{116, 114, 105, 99, 107} + return result, nil + } + case "trigger":{ + result := []byte{116, 114, 105, 103, 103, 101, 114} + return result, nil + } + case "trim":{ + result := []byte{116, 114, 105, 109} + return result, nil + } + case "trip":{ + result := []byte{116, 114, 105, 112} + return result, nil + } + case "trophy":{ + result := []byte{116, 114, 111, 112, 104, 121} + return result, nil + } + case "trouble":{ + result := []byte{116, 114, 111, 117, 98, 108, 101} + return result, nil + } + case "truck":{ + result := []byte{116, 114, 117, 99, 107} + return result, nil + } + case "true":{ + result := []byte{116, 114, 117, 101} + return result, nil + } + case "truly":{ + result := []byte{116, 114, 117, 108, 121} + return result, nil + } + case "trumpet":{ + result := []byte{116, 114, 117, 109, 112, 101, 116} + return result, nil + } + case "trust":{ + result := []byte{116, 114, 117, 115, 116} + return result, nil + } + case "truth":{ + result := []byte{116, 114, 117, 116, 104} + return result, nil + } + case "try":{ + result := []byte{116, 114, 121} + return result, nil + } + case "tube":{ + result := []byte{116, 117, 98, 101} + return result, nil + } + case "tuition":{ + result := []byte{116, 117, 105, 116, 105, 111, 110} + return result, nil + } + case "tumble":{ + result := []byte{116, 117, 109, 98, 108, 101} + return result, nil + } + case "tuna":{ + result := []byte{116, 117, 110, 97} + return result, nil + } + case "tunnel":{ + result := []byte{116, 117, 110, 110, 101, 108} + return result, nil + } + case "turkey":{ + result := []byte{116, 117, 114, 107, 101, 121} + return result, nil + } + case "turn":{ + result := []byte{116, 117, 114, 110} + return result, nil + } + case "turtle":{ + result := []byte{116, 117, 114, 116, 108, 101} + return result, nil + } + case "twelve":{ + result := []byte{116, 119, 101, 108, 118, 101} + return result, nil + } + case "twenty":{ + result := []byte{116, 119, 101, 110, 116, 121} + return result, nil + } + case "twice":{ + result := []byte{116, 119, 105, 99, 101} + return result, nil + } + case "twin":{ + result := []byte{116, 119, 105, 110} + return result, nil + } + case "twist":{ + result := []byte{116, 119, 105, 115, 116} + return result, nil + } + case "two":{ + result := []byte{116, 119, 111} + return result, nil + } + case "type":{ + result := []byte{116, 121, 112, 101} + return result, nil + } + case "typical":{ + result := []byte{116, 121, 112, 105, 99, 97, 108} + return result, nil + } + case "ugly":{ + result := []byte{117, 103, 108, 121} + return result, nil + } + case "umbrella":{ + result := []byte{117, 109, 98, 114, 101, 108, 108, 97} + return result, nil + } + case "unable":{ + result := []byte{117, 110, 97, 98, 108, 101} + return result, nil + } + case "unaware":{ + result := []byte{117, 110, 97, 119, 97, 114, 101} + return result, nil + } + case "uncle":{ + result := []byte{117, 110, 99, 108, 101} + return result, nil + } + case "uncover":{ + result := []byte{117, 110, 99, 111, 118, 101, 114} + return result, nil + } + case "under":{ + result := []byte{117, 110, 100, 101, 114} + return result, nil + } + case "undo":{ + result := []byte{117, 110, 100, 111} + return result, nil + } + case "unfair":{ + result := []byte{117, 110, 102, 97, 105, 114} + return result, nil + } + case "unfold":{ + result := []byte{117, 110, 102, 111, 108, 100} + return result, nil + } + case "unhappy":{ + result := []byte{117, 110, 104, 97, 112, 112, 121} + return result, nil + } + case "uniform":{ + result := []byte{117, 110, 105, 102, 111, 114, 109} + return result, nil + } + case "unique":{ + result := []byte{117, 110, 105, 113, 117, 101} + return result, nil + } + case "unit":{ + result := []byte{117, 110, 105, 116} + return result, nil + } + case "universe":{ + result := []byte{117, 110, 105, 118, 101, 114, 115, 101} + return result, nil + } + case "unknown":{ + result := []byte{117, 110, 107, 110, 111, 119, 110} + return result, nil + } + case "unlock":{ + result := []byte{117, 110, 108, 111, 99, 107} + return result, nil + } + case "until":{ + result := []byte{117, 110, 116, 105, 108} + return result, nil + } + case "unusual":{ + result := []byte{117, 110, 117, 115, 117, 97, 108} + return result, nil + } + case "unveil":{ + result := []byte{117, 110, 118, 101, 105, 108} + return result, nil + } + case "update":{ + result := []byte{117, 112, 100, 97, 116, 101} + return result, nil + } + case "upgrade":{ + result := []byte{117, 112, 103, 114, 97, 100, 101} + return result, nil + } + case "uphold":{ + result := []byte{117, 112, 104, 111, 108, 100} + return result, nil + } + case "upon":{ + result := []byte{117, 112, 111, 110} + return result, nil + } + case "upper":{ + result := []byte{117, 112, 112, 101, 114} + return result, nil + } + case "upset":{ + result := []byte{117, 112, 115, 101, 116} + return result, nil + } + case "urban":{ + result := []byte{117, 114, 98, 97, 110} + return result, nil + } + case "urge":{ + result := []byte{117, 114, 103, 101} + return result, nil + } + case "usage":{ + result := []byte{117, 115, 97, 103, 101} + return result, nil + } + case "use":{ + result := []byte{117, 115, 101} + return result, nil + } + case "used":{ + result := []byte{117, 115, 101, 100} + return result, nil + } + case "useful":{ + result := []byte{117, 115, 101, 102, 117, 108} + return result, nil + } + case "useless":{ + result := []byte{117, 115, 101, 108, 101, 115, 115} + return result, nil + } + case "usual":{ + result := []byte{117, 115, 117, 97, 108} + return result, nil + } + case "utility":{ + result := []byte{117, 116, 105, 108, 105, 116, 121} + return result, nil + } + case "vacant":{ + result := []byte{118, 97, 99, 97, 110, 116} + return result, nil + } + case "vacuum":{ + result := []byte{118, 97, 99, 117, 117, 109} + return result, nil + } + case "vague":{ + result := []byte{118, 97, 103, 117, 101} + return result, nil + } + case "valid":{ + result := []byte{118, 97, 108, 105, 100} + return result, nil + } + case "valley":{ + result := []byte{118, 97, 108, 108, 101, 121} + return result, nil + } + case "valve":{ + result := []byte{118, 97, 108, 118, 101} + return result, nil + } + case "van":{ + result := []byte{118, 97, 110} + return result, nil + } + case "vanish":{ + result := []byte{118, 97, 110, 105, 115, 104} + return result, nil + } + case "vapor":{ + result := []byte{118, 97, 112, 111, 114} + return result, nil + } + case "various":{ + result := []byte{118, 97, 114, 105, 111, 117, 115} + return result, nil + } + case "vast":{ + result := []byte{118, 97, 115, 116} + return result, nil + } + case "vault":{ + result := []byte{118, 97, 117, 108, 116} + return result, nil + } + case "vehicle":{ + result := []byte{118, 101, 104, 105, 99, 108, 101} + return result, nil + } + case "velvet":{ + result := []byte{118, 101, 108, 118, 101, 116} + return result, nil + } + case "vendor":{ + result := []byte{118, 101, 110, 100, 111, 114} + return result, nil + } + case "venture":{ + result := []byte{118, 101, 110, 116, 117, 114, 101} + return result, nil + } + case "venue":{ + result := []byte{118, 101, 110, 117, 101} + return result, nil + } + case "verb":{ + result := []byte{118, 101, 114, 98} + return result, nil + } + case "verify":{ + result := []byte{118, 101, 114, 105, 102, 121} + return result, nil + } + case "version":{ + result := []byte{118, 101, 114, 115, 105, 111, 110} + return result, nil + } + case "very":{ + result := []byte{118, 101, 114, 121} + return result, nil + } + case "vessel":{ + result := []byte{118, 101, 115, 115, 101, 108} + return result, nil + } + case "veteran":{ + result := []byte{118, 101, 116, 101, 114, 97, 110} + return result, nil + } + case "viable":{ + result := []byte{118, 105, 97, 98, 108, 101} + return result, nil + } + case "vibrant":{ + result := []byte{118, 105, 98, 114, 97, 110, 116} + return result, nil + } + case "vicious":{ + result := []byte{118, 105, 99, 105, 111, 117, 115} + return result, nil + } + case "victory":{ + result := []byte{118, 105, 99, 116, 111, 114, 121} + return result, nil + } + case "video":{ + result := []byte{118, 105, 100, 101, 111} + return result, nil + } + case "view":{ + result := []byte{118, 105, 101, 119} + return result, nil + } + case "village":{ + result := []byte{118, 105, 108, 108, 97, 103, 101} + return result, nil + } + case "vintage":{ + result := []byte{118, 105, 110, 116, 97, 103, 101} + return result, nil + } + case "violin":{ + result := []byte{118, 105, 111, 108, 105, 110} + return result, nil + } + case "virtual":{ + result := []byte{118, 105, 114, 116, 117, 97, 108} + return result, nil + } + case "virus":{ + result := []byte{118, 105, 114, 117, 115} + return result, nil + } + case "visa":{ + result := []byte{118, 105, 115, 97} + return result, nil + } + case "visit":{ + result := []byte{118, 105, 115, 105, 116} + return result, nil + } + case "visual":{ + result := []byte{118, 105, 115, 117, 97, 108} + return result, nil + } + case "vital":{ + result := []byte{118, 105, 116, 97, 108} + return result, nil + } + case "vivid":{ + result := []byte{118, 105, 118, 105, 100} + return result, nil + } + case "vocal":{ + result := []byte{118, 111, 99, 97, 108} + return result, nil + } + case "voice":{ + result := []byte{118, 111, 105, 99, 101} + return result, nil + } + case "void":{ + result := []byte{118, 111, 105, 100} + return result, nil + } + case "volcano":{ + result := []byte{118, 111, 108, 99, 97, 110, 111} + return result, nil + } + case "volume":{ + result := []byte{118, 111, 108, 117, 109, 101} + return result, nil + } + case "vote":{ + result := []byte{118, 111, 116, 101} + return result, nil + } + case "voyage":{ + result := []byte{118, 111, 121, 97, 103, 101} + return result, nil + } + case "wage":{ + result := []byte{119, 97, 103, 101} + return result, nil + } + case "wagon":{ + result := []byte{119, 97, 103, 111, 110} + return result, nil + } + case "wait":{ + result := []byte{119, 97, 105, 116} + return result, nil + } + case "walk":{ + result := []byte{119, 97, 108, 107} + return result, nil + } + case "wall":{ + result := []byte{119, 97, 108, 108} + return result, nil + } + case "walnut":{ + result := []byte{119, 97, 108, 110, 117, 116} + return result, nil + } + case "want":{ + result := []byte{119, 97, 110, 116} + return result, nil + } + case "warfare":{ + result := []byte{119, 97, 114, 102, 97, 114, 101} + return result, nil + } + case "warm":{ + result := []byte{119, 97, 114, 109} + return result, nil + } + case "warrior":{ + result := []byte{119, 97, 114, 114, 105, 111, 114} + return result, nil + } + case "wash":{ + result := []byte{119, 97, 115, 104} + return result, nil + } + case "wasp":{ + result := []byte{119, 97, 115, 112} + return result, nil + } + case "waste":{ + result := []byte{119, 97, 115, 116, 101} + return result, nil + } + case "water":{ + result := []byte{119, 97, 116, 101, 114} + return result, nil + } + case "wave":{ + result := []byte{119, 97, 118, 101} + return result, nil + } + case "way":{ + result := []byte{119, 97, 121} + return result, nil + } + case "wealth":{ + result := []byte{119, 101, 97, 108, 116, 104} + return result, nil + } + case "weapon":{ + result := []byte{119, 101, 97, 112, 111, 110} + return result, nil + } + case "wear":{ + result := []byte{119, 101, 97, 114} + return result, nil + } + case "weasel":{ + result := []byte{119, 101, 97, 115, 101, 108} + return result, nil + } + case "weather":{ + result := []byte{119, 101, 97, 116, 104, 101, 114} + return result, nil + } + case "web":{ + result := []byte{119, 101, 98} + return result, nil + } + case "wedding":{ + result := []byte{119, 101, 100, 100, 105, 110, 103} + return result, nil + } + case "weekend":{ + result := []byte{119, 101, 101, 107, 101, 110, 100} + return result, nil + } + case "weird":{ + result := []byte{119, 101, 105, 114, 100} + return result, nil + } + case "welcome":{ + result := []byte{119, 101, 108, 99, 111, 109, 101} + return result, nil + } + case "west":{ + result := []byte{119, 101, 115, 116} + return result, nil + } + case "wet":{ + result := []byte{119, 101, 116} + return result, nil + } + case "whale":{ + result := []byte{119, 104, 97, 108, 101} + return result, nil + } + case "what":{ + result := []byte{119, 104, 97, 116} + return result, nil + } + case "wheat":{ + result := []byte{119, 104, 101, 97, 116} + return result, nil + } + case "wheel":{ + result := []byte{119, 104, 101, 101, 108} + return result, nil + } + case "when":{ + result := []byte{119, 104, 101, 110} + return result, nil + } + case "where":{ + result := []byte{119, 104, 101, 114, 101} + return result, nil + } + case "whip":{ + result := []byte{119, 104, 105, 112} + return result, nil + } + case "whisper":{ + result := []byte{119, 104, 105, 115, 112, 101, 114} + return result, nil + } + case "wide":{ + result := []byte{119, 105, 100, 101} + return result, nil + } + case "width":{ + result := []byte{119, 105, 100, 116, 104} + return result, nil + } + case "wife":{ + result := []byte{119, 105, 102, 101} + return result, nil + } + case "wild":{ + result := []byte{119, 105, 108, 100} + return result, nil + } + case "will":{ + result := []byte{119, 105, 108, 108} + return result, nil + } + case "win":{ + result := []byte{119, 105, 110} + return result, nil + } + case "window":{ + result := []byte{119, 105, 110, 100, 111, 119} + return result, nil + } + case "wine":{ + result := []byte{119, 105, 110, 101} + return result, nil + } + case "wing":{ + result := []byte{119, 105, 110, 103} + return result, nil + } + case "wink":{ + result := []byte{119, 105, 110, 107} + return result, nil + } + case "winner":{ + result := []byte{119, 105, 110, 110, 101, 114} + return result, nil + } + case "winter":{ + result := []byte{119, 105, 110, 116, 101, 114} + return result, nil + } + case "wire":{ + result := []byte{119, 105, 114, 101} + return result, nil + } + case "wisdom":{ + result := []byte{119, 105, 115, 100, 111, 109} + return result, nil + } + case "wise":{ + result := []byte{119, 105, 115, 101} + return result, nil + } + case "wish":{ + result := []byte{119, 105, 115, 104} + return result, nil + } + case "witness":{ + result := []byte{119, 105, 116, 110, 101, 115, 115} + return result, nil + } + case "wolf":{ + result := []byte{119, 111, 108, 102} + return result, nil + } + case "woman":{ + result := []byte{119, 111, 109, 97, 110} + return result, nil + } + case "wonder":{ + result := []byte{119, 111, 110, 100, 101, 114} + return result, nil + } + case "wood":{ + result := []byte{119, 111, 111, 100} + return result, nil + } + case "wool":{ + result := []byte{119, 111, 111, 108} + return result, nil + } + case "word":{ + result := []byte{119, 111, 114, 100} + return result, nil + } + case "work":{ + result := []byte{119, 111, 114, 107} + return result, nil + } + case "world":{ + result := []byte{119, 111, 114, 108, 100} + return result, nil + } + case "worry":{ + result := []byte{119, 111, 114, 114, 121} + return result, nil + } + case "worth":{ + result := []byte{119, 111, 114, 116, 104} + return result, nil + } + case "wrap":{ + result := []byte{119, 114, 97, 112} + return result, nil + } + case "wreck":{ + result := []byte{119, 114, 101, 99, 107} + return result, nil + } + case "wrestle":{ + result := []byte{119, 114, 101, 115, 116, 108, 101} + return result, nil + } + case "wrist":{ + result := []byte{119, 114, 105, 115, 116} + return result, nil + } + case "write":{ + result := []byte{119, 114, 105, 116, 101} + return result, nil + } + case "wrong":{ + result := []byte{119, 114, 111, 110, 103} + return result, nil + } + case "yard":{ + result := []byte{121, 97, 114, 100} + return result, nil + } + case "year":{ + result := []byte{121, 101, 97, 114} + return result, nil + } + case "yellow":{ + result := []byte{121, 101, 108, 108, 111, 119} + return result, nil + } + case "you":{ + result := []byte{121, 111, 117} + return result, nil + } + case "young":{ + result := []byte{121, 111, 117, 110, 103} + return result, nil + } + case "youth":{ + result := []byte{121, 111, 117, 116, 104} + return result, nil + } + case "zebra":{ + result := []byte{122, 101, 98, 114, 97} + return result, nil + } + case "zero":{ + result := []byte{122, 101, 114, 111} + return result, nil + } + case "zone":{ + result := []byte{122, 111, 110, 101} + return result, nil + } + case "zoo":{ + result := []byte{122, 111, 111} + return result, nil + } + } + + return nil, errors.New("Unknown word exists in English word list: " + word) + } + + englishWordList, err := wordLists.GetWordListFromLanguage("English") + if (err != nil){ + t.Fatalf("GetWordListFromLanguage failed: " + err.Error()) + } + + for _, word := range englishWordList{ + + expectedWordBytes, err := getExpectedWordBytes(word) + if (err != nil){ + t.Fatalf("getExpectedWordBytes failed: " + err.Error()) + } + + wordBytes := []byte(word) + + areEqual := bytes.Equal(expectedWordBytes, wordBytes) + if (areEqual == false){ + t.Fatalf("Word bytes do not match expected bytes for word: " + word) + } + } +} + + +/* +// We use this function to generate the code for this page. +// We can copy the code from the test output +func TestGenerateTestCode(t *testing.T){ + + englishWordList, err := wordLists.GetWordListFromLanguage("English") + if (err != nil){ + t.Fatalf("GetWordListFromLanguage failed: " + err.Error()) + } + + result := "" + + for _, word := range englishWordList{ + + result += `case "` + word + `":{` + "\n" + result += "\tresult := []byte{" + + wordBytes := []byte(word) + + finalIndex := len(wordBytes)-1 + + for index, element := range wordBytes{ + + byteString := helpers.ConvertByteToString(element) + + result += byteString + + if (index != finalIndex){ + result += ", " + } + } + + result += "}\n" + result += "\treturn result, nil\n" + result += "}\n" + } + + log.Println(result) +} +*/ + + + diff --git a/resources/worldLanguages/worldLanguages.go b/resources/worldLanguages/worldLanguages.go new file mode 100644 index 0000000..ca37014 --- /dev/null +++ b/resources/worldLanguages/worldLanguages.go @@ -0,0 +1,1231 @@ + +// worldLanguages provides a list of languages. +// Each language has an identifier and at least 1 name. + +package worldLanguages + +// This is not a list of languages supported by the Seekia client. That list is located in translation.go +// This is a list of languages that mate/moderator profiles can indicate they speak/understand on their profile. +// Updating this list requires updating the profile version. +// Users can also submit a custom language if their language is not listed. + +import "errors" + + +type LanguageObject struct{ + + Identifier int + NamesList []string +} + +func VerifyLanguageIdentifier(inputIdentifier int)bool{ + + if (inputIdentifier >= 1 && inputIdentifier <= 182){ + return true + } + return false +} + +var worldLanguageObjectsList []LanguageObject +var worldLanguageObjectsMap map[string]LanguageObject +var worldLanguagePrimaryNamesList []string + +func InitializeWorldLanguageVariables()error{ + + initializeWorldLanguageObjectsList() + + err := initializeWorldLanguageObjectsMap() + if (err != nil) { return err } + + err = initializeWorldLanguagePrimaryNamesList() + if (err != nil) { return err } + + return nil +} + +func GetLanguageObjectFromLanguageIdentifier(inputLanguageIdentifier int)(LanguageObject, error){ + + worldLanguageObjectsList, err := GetWorldLanguageObjectsList() + if (err != nil) { + + emptyObject := LanguageObject{} + return emptyObject, err + } + + for _, languageObject := range worldLanguageObjectsList{ + + languageIdentifier := languageObject.Identifier + + if (inputLanguageIdentifier == languageIdentifier){ + return languageObject, nil + } + } + emptyObject := LanguageObject{} + return emptyObject, errors.New("GetLanguageObjectFromLanguageIdentifier called with unknown language identifier.") +} + +func GetWorldLanguageObjectsList()([]LanguageObject, error){ + + if (worldLanguageObjectsList == nil){ + return nil, errors.New("GetWorldLanguageObjectsList called when list is not initialized.") + } + + return worldLanguageObjectsList, nil +} + +//Outputs: +// -map[string]LanguageObject -> Language Primary Name -> Language Object +// -error +func GetWorldLanguageObjectsMap()(map[string]LanguageObject, error){ + + if (worldLanguageObjectsMap == nil){ + return nil, errors.New("GetWorldLanguageObjectsMap called when map is not initialized.") + } + + return worldLanguageObjectsMap, nil +} + +func GetWorldLanguagePrimaryNamesList()([]string, error){ + + if (worldLanguagePrimaryNamesList == nil){ + return nil, errors.New("GetWorldLanguagePrimaryNamesList called when list is not initialized.") + } + + return worldLanguagePrimaryNamesList, nil +} + + +func initializeWorldLanguageObjectsMap()error{ + + worldLanguageObjectsList, err := GetWorldLanguageObjectsList() + if (err != nil) { return err } + + worldLanguageObjectsMap = make(map[string]LanguageObject) + + for _, languageObject := range worldLanguageObjectsList{ + + languageNamesList := languageObject.NamesList + languagePrimaryName := languageNamesList[0] + + worldLanguageObjectsMap[languagePrimaryName] = languageObject + } + + return nil +} + +func initializeWorldLanguagePrimaryNamesList()error{ + + worldLanguageObjectsList, err := GetWorldLanguageObjectsList() + if (err != nil) { return err } + + worldLanguagePrimaryNamesList = make([]string, 0, len(worldLanguageObjectsList)) + + for _, languageObject := range worldLanguageObjectsList{ + + languageNamesList := languageObject.NamesList + languagePrimaryName := languageNamesList[0] + worldLanguagePrimaryNamesList = append(worldLanguagePrimaryNamesList, languagePrimaryName) + } + + return nil +} + + +func initializeWorldLanguageObjectsList(){ + + languageObject_1 := LanguageObject{ + + Identifier: 1, + NamesList: []string{"Abkhaz"}, + } + + languageObject_2 := LanguageObject{ + + Identifier: 2, + NamesList: []string{"Afar"}, + } + + languageObject_3 := LanguageObject{ + + Identifier: 3, + NamesList: []string{"Afrikaans"}, + } + + languageObject_4 := LanguageObject{ + + Identifier: 4, + NamesList: []string{"Akan"}, + } + + languageObject_5 := LanguageObject{ + + Identifier: 5, + NamesList: []string{"Albanian"}, + } + + languageObject_6 := LanguageObject{ + + Identifier: 6, + NamesList: []string{"Amharic"}, + } + + languageObject_7 := LanguageObject{ + + Identifier: 7, + NamesList: []string{"Arabic"}, + } + + languageObject_8 := LanguageObject{ + + Identifier: 8, + NamesList: []string{"Aragonese"}, + } + + languageObject_9 := LanguageObject{ + + Identifier: 9, + NamesList: []string{"Armenian"}, + } + + languageObject_10 := LanguageObject{ + + Identifier: 10, + NamesList: []string{"Assamese"}, + } + + languageObject_11 := LanguageObject{ + + Identifier: 11, + NamesList: []string{"Avaric"}, + } + + languageObject_12 := LanguageObject{ + + Identifier: 12, + NamesList: []string{"Avestan"}, + } + + languageObject_13 := LanguageObject{ + + Identifier: 13, + NamesList: []string{"Aymara"}, + } + + languageObject_14 := LanguageObject{ + + Identifier: 14, + NamesList: []string{"Azerbaijani"}, + } + + languageObject_15 := LanguageObject{ + + Identifier: 15, + NamesList: []string{"Bambara"}, + } + + languageObject_16 := LanguageObject{ + + Identifier: 16, + NamesList: []string{"Bashkir"}, + } + + languageObject_17 := LanguageObject{ + + Identifier: 17, + NamesList: []string{"Basque"}, + } + + languageObject_18 := LanguageObject{ + + Identifier: 18, + NamesList: []string{"Belarusian"}, + } + + languageObject_19 := LanguageObject{ + + Identifier: 19, + NamesList: []string{"Bengali"}, + } + languageObject_20 := LanguageObject{ + + Identifier: 20, + NamesList: []string{"Bihari"}, + } + + languageObject_21 := LanguageObject{ + + Identifier: 21, + NamesList: []string{"Bislama"}, + } + + languageObject_22 := LanguageObject{ + + Identifier: 22, + NamesList: []string{"Bosnian"}, + } + + languageObject_23 := LanguageObject{ + + Identifier: 23, + NamesList: []string{"Breton"}, + } + + languageObject_24 := LanguageObject{ + + Identifier: 24, + NamesList: []string{"Bulgarian"}, + } + + languageObject_25 := LanguageObject{ + + Identifier: 25, + NamesList: []string{"Burmese"}, + } + + languageObject_26 := LanguageObject{ + + Identifier: 26, + NamesList: []string{"Catalan"}, + } + + languageObject_27 := LanguageObject{ + + Identifier: 27, + NamesList: []string{"Chamorro"}, + } + + languageObject_28 := LanguageObject{ + + Identifier: 28, + NamesList: []string{"Chechen"}, + } + + languageObject_29 := LanguageObject{ + + Identifier: 29, + NamesList: []string{"Chichewa", "Chewa"}, + } + + languageObject_30 := LanguageObject{ + + Identifier: 30, + NamesList: []string{"Chinese"}, + } + + languageObject_31 := LanguageObject{ + + Identifier: 31, + NamesList: []string{"Chuvash"}, + } + + languageObject_32 := LanguageObject{ + + Identifier: 32, + NamesList: []string{"Cornish"}, + } + + languageObject_33 := LanguageObject{ + + Identifier: 33, + NamesList: []string{"Corsican"}, + } + + languageObject_34 := LanguageObject{ + + Identifier: 34, + NamesList: []string{"Cree"}, + } + + languageObject_35 := LanguageObject{ + + Identifier: 35, + NamesList: []string{"Croatian"}, + } + + languageObject_36 := LanguageObject{ + + Identifier: 36, + NamesList: []string{"Czech"}, + } + + languageObject_37 := LanguageObject{ + + Identifier: 37, + NamesList: []string{"Danish"}, + } + + languageObject_38 := LanguageObject{ + + Identifier: 38, + NamesList: []string{"Divehi", "Dhivehi", "Maldivian"}, + } + + languageObject_39 := LanguageObject{ + + Identifier: 39, + NamesList: []string{"Dutch"}, + } + + languageObject_40 := LanguageObject{ + + Identifier: 40, + NamesList: []string{"English"}, + } + + languageObject_41 := LanguageObject{ + + Identifier: 41, + NamesList: []string{"Esperanto"}, + } + + languageObject_42 := LanguageObject{ + + Identifier: 42, + NamesList: []string{"Estonian"}, + } + + languageObject_43 := LanguageObject{ + + Identifier: 43, + NamesList: []string{"Ewe"}, + } + + languageObject_44 := LanguageObject{ + + Identifier: 44, + NamesList: []string{"Faroese"}, + } + + languageObject_45 := LanguageObject{ + + Identifier: 45, + NamesList: []string{"Fijian"}, + } + + languageObject_46 := LanguageObject{ + + Identifier: 46, + NamesList: []string{"Finnish"}, + } + + languageObject_47 := LanguageObject{ + + Identifier: 47, + NamesList: []string{"French"}, + } + + languageObject_48 := LanguageObject{ + + Identifier: 48, + NamesList: []string{"Fulah", "Fula"}, + } + + languageObject_49 := LanguageObject{ + + Identifier: 49, + NamesList: []string{"Gaelic"}, + } + + languageObject_50 := LanguageObject{ + + Identifier: 50, + NamesList: []string{"Galician"}, + } + + languageObject_51 := LanguageObject{ + + Identifier: 51, + NamesList: []string{"Georgian"}, + } + + languageObject_52 := LanguageObject{ + + Identifier: 52, + NamesList: []string{"German"}, + } + + languageObject_53 := LanguageObject{ + + Identifier: 53, + NamesList: []string{"Greek"}, + } + + languageObject_54 := LanguageObject{ + + Identifier: 54, + NamesList: []string{"Guaraní"}, + } + + languageObject_55 := LanguageObject{ + + Identifier: 55, + NamesList: []string{"Gujarati"}, + } + + languageObject_56 := LanguageObject{ + + Identifier: 56, + NamesList: []string{"Haitian Creole"}, + } + + languageObject_57 := LanguageObject{ + + Identifier: 57, + NamesList: []string{"Hausa"}, + } + + languageObject_58 := LanguageObject{ + + Identifier: 58, + NamesList: []string{"Hebrew"}, + } + + languageObject_59 := LanguageObject{ + + Identifier: 181, + NamesList: []string{"Yiddish"}, + } + + languageObject_60 := LanguageObject{ + + Identifier: 60, + NamesList: []string{"Herero"}, + } + + languageObject_61 := LanguageObject{ + + Identifier: 61, + NamesList: []string{"Hindi"}, + } + + languageObject_62 := LanguageObject{ + + Identifier: 62, + NamesList: []string{"Hiri Motu"}, + } + + languageObject_63 := LanguageObject{ + + Identifier: 63, + NamesList: []string{"Hungarian"}, + } + + languageObject_64 := LanguageObject{ + + Identifier: 64, + NamesList: []string{"Interlingua"}, + } + + languageObject_65 := LanguageObject{ + + Identifier: 65, + NamesList: []string{"Indonesian"}, + } + + languageObject_66 := LanguageObject{ + + Identifier: 66, + NamesList: []string{"Interlingue"}, + } + + languageObject_67 := LanguageObject{ + + Identifier: 67, + NamesList: []string{"Irish"}, + } + + languageObject_68 := LanguageObject{ + + Identifier: 68, + NamesList: []string{"Igbo"}, + } + + languageObject_69 := LanguageObject{ + + Identifier: 69, + NamesList: []string{"Inupiaq"}, + } + + languageObject_70 := LanguageObject{ + + Identifier: 70, + NamesList: []string{"Ido"}, + } + + languageObject_71 := LanguageObject{ + + Identifier: 71, + NamesList: []string{"Icelandic"}, + } + + languageObject_72 := LanguageObject{ + + Identifier: 72, + NamesList: []string{"Italian"}, + } + + languageObject_73 := LanguageObject{ + + Identifier: 73, + NamesList: []string{"Inuktitut"}, + } + + languageObject_74 := LanguageObject{ + + Identifier: 74, + NamesList: []string{"Japanese"}, + } + + languageObject_75 := LanguageObject{ + + Identifier: 75, + NamesList: []string{"Javanese"}, + } + + languageObject_76 := LanguageObject{ + + Identifier: 76, + NamesList: []string{"Greenlandic"}, + } + + languageObject_77 := LanguageObject{ + + Identifier: 77, + NamesList: []string{"Kannada"}, + } + + languageObject_78 := LanguageObject{ + + Identifier: 78, + NamesList: []string{"Kanuri"}, + } + + languageObject_79 := LanguageObject{ + + Identifier: 79, + NamesList: []string{"Kashmiri"}, + } + + languageObject_80 := LanguageObject{ + + Identifier: 80, + NamesList: []string{"Kazakh"}, + } + + languageObject_81 := LanguageObject{ + + Identifier: 81, + NamesList: []string{"Khmer"}, + } + + languageObject_82 := LanguageObject{ + + Identifier: 82, + NamesList: []string{"Kikuyu", "Gikuyu"}, + } + + languageObject_83 := LanguageObject{ + + Identifier: 83, + NamesList: []string{"Kinyarwanda"}, + } + + languageObject_84 := LanguageObject{ + + Identifier: 84, + NamesList: []string{"Kyrgyz", "Kirghiz"}, + } + + languageObject_85 := LanguageObject{ + + Identifier: 85, + NamesList: []string{"Komi"}, + } + + languageObject_86 := LanguageObject{ + + Identifier: 86, + NamesList: []string{"Kongo"}, + } + + languageObject_87 := LanguageObject{ + + Identifier: 87, + NamesList: []string{"Korean"}, + } + + languageObject_88 := LanguageObject{ + + Identifier: 88, + NamesList: []string{"Kurdish"}, + } + + languageObject_89 := LanguageObject{ + + Identifier: 89, + NamesList: []string{"Kwanyama"}, + } + + languageObject_90 := LanguageObject{ + + Identifier: 90, + NamesList: []string{"Latin"}, + } + + languageObject_91 := LanguageObject{ + + Identifier: 91, + NamesList: []string{"Luxembourgish"}, + } + + languageObject_92 := LanguageObject{ + + Identifier: 92, + NamesList: []string{"Luganda"}, + } + + languageObject_93 := LanguageObject{ + + Identifier: 93, + NamesList: []string{"Limburgish", "Limburgan", "Limburger"}, + } + + languageObject_94 := LanguageObject{ + + Identifier: 94, + NamesList: []string{"Lingala"}, + } + + languageObject_95 := LanguageObject{ + + Identifier: 95, + NamesList: []string{"Lao"}, + } + + languageObject_96 := LanguageObject{ + + Identifier: 96, + NamesList: []string{"Lithuanian"}, + } + + languageObject_97 := LanguageObject{ + + Identifier: 97, + NamesList: []string{"Luba-Katanga"}, + } + + languageObject_98 := LanguageObject{ + + Identifier: 98, + NamesList: []string{"Latvian"}, + } + + languageObject_99 := LanguageObject{ + + Identifier: 99, + NamesList: []string{"Manx"}, + } + + languageObject_100 := LanguageObject{ + + Identifier: 100, + NamesList: []string{"Macedonian"}, + } + + languageObject_101 := LanguageObject{ + + Identifier: 101, + NamesList: []string{"Malagasy"}, + } + + languageObject_102 := LanguageObject{ + + Identifier: 102, + NamesList: []string{"Malay"}, + } + + languageObject_103 := LanguageObject{ + + Identifier: 103, + NamesList: []string{"Malayalam"}, + } + + languageObject_104 := LanguageObject{ + + Identifier: 104, + NamesList: []string{"Maltese"}, + } + + languageObject_105 := LanguageObject{ + + Identifier: 105, + NamesList: []string{"Māori"}, + } + + languageObject_106 := LanguageObject{ + + Identifier: 106, + NamesList: []string{"Marathi"}, + } + + languageObject_107 := LanguageObject{ + + Identifier: 107, + NamesList: []string{"Marshallese"}, + } + + languageObject_108 := LanguageObject{ + + Identifier: 108, + NamesList: []string{"Mongolian"}, + } + + languageObject_109 := LanguageObject{ + + Identifier: 109, + NamesList: []string{"Nauru"}, + } + + languageObject_110 := LanguageObject{ + + Identifier: 110, + NamesList: []string{"Navajo", "Navaho"}, + } + + languageObject_111 := LanguageObject{ + + Identifier: 111, + NamesList: []string{"Norwegian Bokmål"}, + } + + languageObject_112 := LanguageObject{ + + Identifier: 112, + NamesList: []string{"North Ndebele"}, + } + + languageObject_113 := LanguageObject{ + + Identifier: 113, + NamesList: []string{"Nepali"}, + } + + languageObject_114 := LanguageObject{ + + Identifier: 114, + NamesList: []string{"Ndonga"}, + } + + languageObject_115 := LanguageObject{ + + Identifier: 115, + NamesList: []string{"Norwegian Nynorsk"}, + } + + languageObject_116 := LanguageObject{ + + Identifier: 116, + NamesList: []string{"Norwegian"}, + } + + languageObject_117 := LanguageObject{ + + Identifier: 117, + NamesList: []string{"Nuosu"}, + } + + languageObject_118 := LanguageObject{ + + Identifier: 118, + NamesList: []string{"South Ndebele"}, + } + + languageObject_119 := LanguageObject{ + + Identifier: 119, + NamesList: []string{"Occitan"}, + } + + languageObject_120 := LanguageObject{ + + Identifier: 120, + NamesList: []string{"Ojibwe", "Ojibwa"}, + } + + languageObject_121 := LanguageObject{ + + Identifier: 121, + NamesList: []string{"Old Church Slavonic", "Old Bulgarian", "Old Slavonic"}, + } + + languageObject_122 := LanguageObject{ + + Identifier: 122, + NamesList: []string{"Oromo"}, + } + + languageObject_123 := LanguageObject{ + + Identifier: 123, + NamesList: []string{"Oriya"}, + } + + languageObject_124 := LanguageObject{ + + Identifier: 124, + NamesList: []string{"Ossetian", "Ossetic"}, + } + + languageObject_125 := LanguageObject{ + + Identifier: 125, + NamesList: []string{"Punjabi", "Panjabi"}, + } + + languageObject_126 := LanguageObject{ + + Identifier: 126, + NamesList: []string{"Pāli"}, + } + + languageObject_127 := LanguageObject{ + + Identifier: 127, + NamesList: []string{"Persian"}, + } + + languageObject_128 := LanguageObject{ + + Identifier: 128, + NamesList: []string{"Polish"}, + } + + languageObject_129 := LanguageObject{ + + Identifier: 129, + NamesList: []string{"Pashto", "Pushto"}, + } + + languageObject_130 := LanguageObject{ + + Identifier: 130, + NamesList: []string{"Portuguese"}, + } + + languageObject_131 := LanguageObject{ + + Identifier: 131, + NamesList: []string{"Quechua"}, + } + + languageObject_132 := LanguageObject{ + + Identifier: 132, + NamesList: []string{"Romansh"}, + } + + languageObject_133 := LanguageObject{ + + Identifier: 133, + NamesList: []string{"Kirundi"}, + } + + languageObject_134 := LanguageObject{ + + Identifier: 134, + NamesList: []string{"Romanian", "Moldovan", "Moldavian"}, + } + + languageObject_135 := LanguageObject{ + + Identifier: 135, + NamesList: []string{"Russian"}, + } + + languageObject_136 := LanguageObject{ + + Identifier: 136, + NamesList: []string{"Sanskrit"}, + } + + languageObject_137 := LanguageObject{ + + Identifier: 137, + NamesList: []string{"Sardinian"}, + } + + languageObject_138 := LanguageObject{ + + Identifier: 138, + NamesList: []string{"Sindhi"}, + } + + languageObject_139 := LanguageObject{ + + Identifier: 139, + NamesList: []string{"Northern Sami"}, + } + + languageObject_140 := LanguageObject{ + + Identifier: 140, + NamesList: []string{"Samoan"}, + } + + languageObject_141 := LanguageObject{ + + Identifier: 141, + NamesList: []string{"Sango"}, + } + + languageObject_142 := LanguageObject{ + + Identifier: 142, + NamesList: []string{"Serbian"}, + } + + languageObject_143 := LanguageObject{ + + Identifier: 143, + NamesList: []string{"Shona"}, + } + + languageObject_144 := LanguageObject{ + + Identifier: 144, + NamesList: []string{"Sinhala", "Sinhalese"}, + } + + languageObject_145 := LanguageObject{ + + Identifier: 145, + NamesList: []string{"Slovak"}, + } + + languageObject_146 := LanguageObject{ + + Identifier: 146, + NamesList: []string{"Slovene"}, + } + + languageObject_147 := LanguageObject{ + + Identifier: 147, + NamesList: []string{"Somali"}, + } + + languageObject_148 := LanguageObject{ + + Identifier: 148, + NamesList: []string{"Southern Sotho"}, + } + + languageObject_149 := LanguageObject{ + + Identifier: 149, + NamesList: []string{"Spanish", "Castilian"}, + } + + languageObject_150 := LanguageObject{ + + Identifier: 150, + NamesList: []string{"Sundanese"}, + } + + languageObject_151 := LanguageObject{ + + Identifier: 151, + NamesList: []string{"Swahili"}, + } + + languageObject_152 := LanguageObject{ + + Identifier: 152, + NamesList: []string{"Swati"}, + } + + languageObject_153 := LanguageObject{ + + Identifier: 153, + NamesList: []string{"Swedish"}, + } + + languageObject_154 := LanguageObject{ + + Identifier: 154, + NamesList: []string{"Tamil"}, + } + + languageObject_155 := LanguageObject{ + + Identifier: 155, + NamesList: []string{"Telugu"}, + } + + languageObject_156 := LanguageObject{ + + Identifier: 156, + NamesList: []string{"Tajik"}, + } + + languageObject_157 := LanguageObject{ + + Identifier: 157, + NamesList: []string{"Thai"}, + } + + languageObject_158 := LanguageObject{ + + Identifier: 158, + NamesList: []string{"Tigrinya"}, + } + + languageObject_159 := LanguageObject{ + + Identifier: 159, + NamesList: []string{"Tibetan"}, + } + + languageObject_160 := LanguageObject{ + + Identifier: 160, + NamesList: []string{"Turkmen"}, + } + + languageObject_161 := LanguageObject{ + + Identifier: 161, + NamesList: []string{"Tagalog"}, + } + + languageObject_162 := LanguageObject{ + + Identifier: 162, + NamesList: []string{"Tswana"}, + } + + languageObject_163 := LanguageObject{ + + Identifier: 163, + NamesList: []string{"Tonga"}, + } + + languageObject_164 := LanguageObject{ + + Identifier: 164, + NamesList: []string{"Turkish"}, + } + + languageObject_165 := LanguageObject{ + + Identifier: 165, + NamesList: []string{"Tsonga"}, + } + + languageObject_166 := LanguageObject{ + + Identifier: 166, + NamesList: []string{"Tatar"}, + } + + languageObject_167 := LanguageObject{ + + Identifier: 167, + NamesList: []string{"Twi"}, + } + + languageObject_168 := LanguageObject{ + + Identifier: 168, + NamesList: []string{"Tahitian"}, + } + + languageObject_169 := LanguageObject{ + + Identifier: 169, + NamesList: []string{"Uyghur", "Uighur"}, + } + + languageObject_170 := LanguageObject{ + + Identifier: 170, + NamesList: []string{"Ukrainian"}, + } + + languageObject_171 := LanguageObject{ + + Identifier: 171, + NamesList: []string{"Urdu"}, + } + + languageObject_172 := LanguageObject{ + + Identifier: 172, + NamesList: []string{"Uzbek"}, + } + + languageObject_173 := LanguageObject{ + + Identifier: 173, + NamesList: []string{"Venda"}, + } + + languageObject_174 := LanguageObject{ + + Identifier: 174, + NamesList: []string{"Vietnamese"}, + } + + languageObject_175 := LanguageObject{ + + Identifier: 175, + NamesList: []string{"Volapük"}, + } + + languageObject_176 := LanguageObject{ + + Identifier: 176, + NamesList: []string{"Walloon"}, + } + + languageObject_177 := LanguageObject{ + + Identifier: 177, + NamesList: []string{"Welsh"}, + } + + languageObject_178 := LanguageObject{ + + Identifier: 178, + NamesList: []string{"Wolof"}, + } + + languageObject_179 := LanguageObject{ + + Identifier: 179, + NamesList: []string{"Western Frisian"}, + } + + languageObject_180 := LanguageObject{ + + Identifier: 180, + NamesList: []string{"Xhosa"}, + } + + languageObject_181 := LanguageObject{ + + Identifier: 182, + NamesList: []string{"Yoruba"}, + } + + languageObject_182 := LanguageObject{ + + Identifier: 183, + NamesList: []string{"Zhuang", "Chuang"}, + } + + worldLanguageObjectsList = []LanguageObject{languageObject_1, languageObject_2, languageObject_3, languageObject_4, languageObject_5, languageObject_6, languageObject_7, languageObject_8, languageObject_9, languageObject_10, languageObject_11, languageObject_12, languageObject_13, languageObject_14, languageObject_15, languageObject_16, languageObject_17, languageObject_18, languageObject_19, languageObject_20, languageObject_21, languageObject_22, languageObject_23, languageObject_24, languageObject_25, languageObject_26, languageObject_27, languageObject_28, languageObject_29, languageObject_30, languageObject_31, languageObject_32, languageObject_33, languageObject_34, languageObject_35, languageObject_36, languageObject_37, languageObject_38, languageObject_39, languageObject_40, languageObject_41, languageObject_42, languageObject_43, languageObject_44, languageObject_45, languageObject_46, languageObject_47, languageObject_48, languageObject_49, languageObject_50, languageObject_51, languageObject_52, languageObject_53, languageObject_54, languageObject_55, languageObject_56, languageObject_57, languageObject_58, languageObject_59, languageObject_60, languageObject_61, languageObject_62, languageObject_63, languageObject_64, languageObject_65, languageObject_66, languageObject_67, languageObject_68, languageObject_69, languageObject_70, languageObject_71, languageObject_72, languageObject_73, languageObject_74, languageObject_75, languageObject_76, languageObject_77, languageObject_78, languageObject_79, languageObject_80, languageObject_81, languageObject_82, languageObject_83, languageObject_84, languageObject_85, languageObject_86, languageObject_87, languageObject_88, languageObject_89, languageObject_90, languageObject_91, languageObject_92, languageObject_93, languageObject_94, languageObject_95, languageObject_96, languageObject_97, languageObject_98, languageObject_99, languageObject_100, languageObject_101, languageObject_102, languageObject_103, languageObject_104, languageObject_105, languageObject_106, languageObject_107, languageObject_108, languageObject_109, languageObject_110, languageObject_111, languageObject_112, languageObject_113, languageObject_114, languageObject_115, languageObject_116, languageObject_117, languageObject_118, languageObject_119, languageObject_120, languageObject_121, languageObject_122, languageObject_123, languageObject_124, languageObject_125, languageObject_126, languageObject_127, languageObject_128, languageObject_129, languageObject_130, languageObject_131, languageObject_132, languageObject_133, languageObject_134, languageObject_135, languageObject_136, languageObject_137, languageObject_138, languageObject_139, languageObject_140, languageObject_141, languageObject_142, languageObject_143, languageObject_144, languageObject_145, languageObject_146, languageObject_147, languageObject_148, languageObject_149, languageObject_150, languageObject_151, languageObject_152, languageObject_153, languageObject_154, languageObject_155, languageObject_156, languageObject_157, languageObject_158, languageObject_159, languageObject_160, languageObject_161, languageObject_162, languageObject_163, languageObject_164, languageObject_165, languageObject_166, languageObject_167, languageObject_168, languageObject_169, languageObject_170, languageObject_171, languageObject_172, languageObject_173, languageObject_174, languageObject_175, languageObject_176, languageObject_177, languageObject_178, languageObject_179, languageObject_180, languageObject_181, languageObject_182} + +} + + diff --git a/resources/worldLanguages/worldLanguages_test.go b/resources/worldLanguages/worldLanguages_test.go new file mode 100644 index 0000000..eeaf92a --- /dev/null +++ b/resources/worldLanguages/worldLanguages_test.go @@ -0,0 +1,75 @@ +package worldLanguages_test + +import "testing" + +import "seekia/resources/worldLanguages" + +import "seekia/internal/helpers" + +import "strings" + +func TestWorldLanguages(t *testing.T){ + + err := worldLanguages.InitializeWorldLanguageVariables() + if (err != nil){ + t.Fatalf("Failed to initialize worldLanguageVariables: " + err.Error()) + } + + // We must check to make sure no identifier or name collisions exist + + languageIdentifiersMap := make(map[int]struct{}) + + languageNamesMap := make(map[string]struct{}) + + worldLanguageObjectsList, err := worldLanguages.GetWorldLanguageObjectsList() + if (err != nil){ + t.Fatalf("Failed to get WorldLanguageObjectsList: " + err.Error()) + } + + for _, languageObject := range worldLanguageObjectsList{ + + languageIdentifier := languageObject.Identifier + languageNamesList := languageObject.NamesList + + _, exists := languageIdentifiersMap[languageIdentifier] + if (exists == true){ + languageIdentifierString := helpers.ConvertIntToString(languageIdentifier) + t.Fatalf("Language identifier duplicate exists: " + languageIdentifierString) + } + + languageIdentifiersMap[languageIdentifier] = struct{}{} + + if (len(languageNamesList) == 0){ + t.Fatalf("Language contains empty names list.") + } + + for _, languageName := range languageNamesList{ + + // Each name cannot contain "+&" or ":" + // Each name cannot be greater than 30 bytes + + if (len(languageName) > 30){ + t.Fatalf("Language name is too long: " + languageName) + } + + stringFound := strings.Contains(languageName, "+&") + if (stringFound == true){ + t.Fatalf("Invalid language name found: " + languageName) + } + + stringFound = strings.Contains(languageName, "$") + if (stringFound == true){ + t.Fatalf("Invalid language name found: " + languageName) + } + + _, exists := languageNamesMap[languageName] + if (exists == true){ + t.Fatalf("Language name duplicate exists: " + languageName) + } + languageNamesMap[languageName] = struct{}{} + } + } +} + + + diff --git a/resources/worldLocations/worldCities.messagepack b/resources/worldLocations/worldCities.messagepack new file mode 100644 index 0000000..b8f6be1 Binary files /dev/null and b/resources/worldLocations/worldCities.messagepack differ diff --git a/resources/worldLocations/worldLocations.go b/resources/worldLocations/worldLocations.go new file mode 100644 index 0000000..edc1fd1 --- /dev/null +++ b/resources/worldLocations/worldLocations.go @@ -0,0 +1,1640 @@ + +// worldLocations provides functions to retrieve information about the world's countries and cities. +// All location data is taken from https://github.com/dr5hn/countries-states-cities-database/ + +package worldLocations + +import "seekia/imported/geodist" + +import "seekia/internal/encoding" +import "seekia/internal/helpers" + +import _ "embed" + +import "slices" +import "errors" + + +//go:embed worldCities.messagepack +var worldCitiesFileBytes []byte + +func init(){ + + initializeAllCountryObjectsList() +} + + +// This must be called on startup once +func InitializeWorldLocationsVariables()error{ + + var allCityObjectsListForMessagepack []CityObjectForMessagepack + + err := encoding.DecodeMessagePackBytes(false, worldCitiesFileBytes, &allCityObjectsListForMessagepack) + if (err != nil) { return err } + + allCityObjectsList = make([]CityObject, 0, len(allCityObjectsListForMessagepack)) + + for _, cityObjectForMessagepack := range allCityObjectsListForMessagepack { + + cityName := cityObjectForMessagepack.Name + cityState := cityObjectForMessagepack.State + cityCountryIdentifier := cityObjectForMessagepack.Country + cityLatitude := cityObjectForMessagepack.Latitude + cityLongitude := cityObjectForMessagepack.Longitude + + countryObject, err := GetCountryObjectFromCountryIdentifier(cityCountryIdentifier) + if (err != nil) { return err } + + cityCountryPrimaryName := countryObject.NamesList[0] + + newCityObject := CityObject{ + Name: cityName, + State: cityState, + Country: cityCountryPrimaryName, + Latitude: cityLatitude, + Longitude: cityLongitude, + } + + allCityObjectsList = append(allCityObjectsList, newCityObject) + } + + allCityObjectsMap = make(map[CoordinatesObject]CityObject) + + for _, cityObject := range allCityObjectsList{ + + cityLatitude := cityObject.Latitude + cityLongitude := cityObject.Longitude + + coordinatesObject := CoordinatesObject{ + Latitude: cityLatitude, + Longitude: cityLongitude, + } + + allCityObjectsMap[coordinatesObject] = cityObject + } + + return nil +} + +var allCityObjectsList []CityObject + +func getAllCityObjectsList()([]CityObject, error){ + + if (allCityObjectsList == nil){ + return nil, errors.New("allCityObjectsList is not initialized.") + } + + // The list only has to be established once upon startup + + return allCityObjectsList, nil +} + +var allCityObjectsMap map[CoordinatesObject]CityObject + +func getAllCityObjectsMap()(map[CoordinatesObject]CityObject, error){ + + if (allCityObjectsMap == nil){ + return nil, errors.New("getAllCityObjectsMap called when allCityObjectsMap is not initialized.") + } + + return allCityObjectsMap, nil +} + +// This is the format that cities are stored in in the worldCitites.messagepack file +// We store it in this format to save space +type CityObjectForMessagepack struct{ + Name string + State string + + // This is the country's identifier + Country int + + Latitude float64 + Longitude float64 +} + +type CityObject struct{ + Name string + State string + Country string + Latitude float64 + Longitude float64 +} + +type CoordinatesObject struct{ + Latitude float64 + Longitude float64 +} + +type CountryObject struct{ + + Identifier int + // The first name in this list is the country's primary name + NamesList []string +} + + +//Outputs: +// -bool: City found (Typically during use, if this is false, the user has entered coordinates manually, which is fine) +// -string: City name +// -string: City state +// -string: City country +// -error +func GetCityFromCoordinates(locationLatitude float64, locationLongitude float64)(bool, string, string, string, error){ + + allCityObjectsMap, err := getAllCityObjectsMap() + if (err != nil) { return false, "", "", "", err } + + coordinatesObject := CoordinatesObject{ + + Latitude: locationLatitude, + Longitude: locationLongitude, + } + + cityObject, exists := allCityObjectsMap[coordinatesObject] + if (exists == false){ + return false, "", "", "", nil + } + + cityName := cityObject.Name + cityState := cityObject.State + cityCountry := cityObject.Country + + return true, cityName, cityState, cityCountry, nil +} + + +//Outputs: +// -string: City Name +// -string: City State +// -int: City Country Identifier +// -float64: Kilometers distance from city +// -error +func GetClosestCityFromCoordinates(locationLatitude float64, locationLongitude float64)(string, string, int, float64, error){ + + cityFound, cityName, cityState, cityCountry, err := GetCityFromCoordinates(locationLatitude, locationLongitude) + if (err != nil) { return "", "", 0, 0, err } + if (cityFound == true){ + + countryIdentifier, err := GetCountryIdentifierFromCountryName(cityCountry) + if (err != nil){ return "", "", 0, 0, err } + + return cityName, cityState, countryIdentifier, 0, nil + } + + allCityObjectList, err := getAllCityObjectsList() + if (err != nil) { return "", "", 0, 0, err } + + closestCityName := "" + closestCityState := "" + closestCityCountry := "" + closestCityKilometersDistance := float64(0) + + for index, cityObject := range allCityObjectList{ + + cityName := cityObject.Name + cityStateName := cityObject.State + cityCountryName := cityObject.Country + + cityLatitude := cityObject.Latitude + cityLongitude := cityObject.Longitude + + cityKilometersDistance, err := geodist.GetDistanceBetweenCoordinates(locationLatitude, locationLongitude, cityLatitude, cityLongitude) + if (err != nil) { return "", "", 0, 0, err } + + if (index == 0 || cityKilometersDistance < closestCityKilometersDistance){ + closestCityName = cityName + closestCityState = cityStateName + closestCityCountry = cityCountryName + closestCityKilometersDistance = cityKilometersDistance + } + } + + closestCountryIdentifier, err := GetCountryIdentifierFromCountryName(closestCityCountry) + if (err != nil){ return "", "", 0, 0, err } + + return closestCityName, closestCityState, closestCountryIdentifier, closestCityKilometersDistance, nil +} + +func GetAllCityObjectsInCountry(countryIdentifier int)([]CityObject, error){ + + countryObject, err := GetCountryObjectFromCountryIdentifier(countryIdentifier) + if (err != nil){ + return nil, errors.New("GetAllCityObjectsInCountry failed: " + err.Error()) + } + + countryNamesList := countryObject.NamesList + + allCityObjectsList, err := getAllCityObjectsList() + if (err != nil) { return nil, err } + + countryCityObjectsList := make([]CityObject, 0) + + for _, cityObject := range allCityObjectsList{ + + cityCountryName := cityObject.Country + + isWithinCountry := slices.Contains(countryNamesList, cityCountryName) + if (isWithinCountry == true){ + countryCityObjectsList = append(countryCityObjectsList, cityObject) + } + } + + return countryCityObjectsList, nil +} + + +// Name should be the primary name +func GetCountryIdentifierFromCountryName(countryName string)(int, error){ + + allCountryObjectsList, err := GetAllCountryObjectsList() + if (err != nil) { return 0, err } + + for _, countryObject := range allCountryObjectsList{ + + countryIdentifier := countryObject.Identifier + countryPrimaryName := countryObject.NamesList[0] + + if (countryName == countryPrimaryName){ + return countryIdentifier, nil + } + } + + return 0, errors.New("GetCountryIdentifierFromCountryName called with unknown country name: " + countryName) +} + + +func GetCountryObjectFromCountryIdentifier(countryIdentifier int)(CountryObject, error){ + + allCountryObjectsList, err := GetAllCountryObjectsList() + if (err != nil) { + emptyObject := CountryObject{} + return emptyObject, err + } + + for _, countryObject := range allCountryObjectsList{ + + currentCountryIdentifier := countryObject.Identifier + + if (countryIdentifier == currentCountryIdentifier){ + return countryObject, nil + } + } + + emptyObject := CountryObject{} + countryIdentifierString := helpers.ConvertIntToString(countryIdentifier) + return emptyObject, errors.New("GetCountryObjectFromCountryIdentifier called with unknown country identifier: " + countryIdentifierString) +} + +func VerifyCountryIdentifier(inputIdentifier int)bool{ + + if (inputIdentifier >= 1 && inputIdentifier <= 219){ + return true + } + + return false +} + + +// The countries list does not contain all of the country names from the original cities.json file from which worldCities.messagepack is created +// All countries within the worldCities.messagepack file must exist within the country objects list. + +// Countries are defined as being within the jurisdiction of a particular government. +// Users can use the countries filters to filter out other users who may live nearby, but are not in the country they are in. +// This is useful because there may be restrictions on travel and living that would make mating more difficult. +// Most users who do not live near a border should only rely on the Distance filter to find users who are nearby. + +// Any time the countries list is updated, a new profile version must be created. + +var allCountryObjectsList []CountryObject + +func GetAllCountryObjectsList()([]CountryObject, error){ + + if (allCountryObjectsList == nil){ + return nil, errors.New("allCountryObjectsList is not initialized.") + } + + return allCountryObjectsList, nil +} + +func initializeAllCountryObjectsList(){ + + countryObject_1 := CountryObject{ + + Identifier: 1, + NamesList: []string{"Afghanistan"}, + } + + countryObject_2 := CountryObject{ + + Identifier: 2, + NamesList: []string{"Aland Islands"}, + } + + countryObject_3 := CountryObject{ + + Identifier: 3, + NamesList: []string{"Albania"}, + } + + countryObject_4 := CountryObject{ + + Identifier: 4, + NamesList: []string{"Algeria"}, + } + + countryObject_5 := CountryObject{ + + Identifier: 5, + NamesList: []string{"Andorra"}, + } + + countryObject_6 := CountryObject{ + + Identifier: 6, + NamesList: []string{"Angola"}, + } + + countryObject_7 := CountryObject{ + + Identifier: 7, + NamesList: []string{"Anguilla"}, + } + + countryObject_8 := CountryObject{ + + Identifier: 8, + NamesList: []string{"Antarctica"}, + } + + countryObject_9 := CountryObject{ + + Identifier: 9, + NamesList: []string{"Antigua And Barbuda"}, + } + + countryObject_10 := CountryObject{ + + Identifier: 10, + NamesList: []string{"Argentina"}, + } + + countryObject_11 := CountryObject{ + + Identifier: 11, + NamesList: []string{"Armenia"}, + } + + countryObject_12 := CountryObject{ + + Identifier: 12, + NamesList: []string{"Aruba"}, + } + + countryObject_13 := CountryObject{ + + Identifier: 13, + NamesList: []string{"Australia"}, + } + + countryObject_14 := CountryObject{ + + Identifier: 14, + NamesList: []string{"Austria"}, + } + + countryObject_15 := CountryObject{ + + Identifier: 15, + NamesList: []string{"Azerbaijan"}, + } + + countryObject_16 := CountryObject{ + + Identifier: 16, + NamesList: []string{"Bangladesh"}, + } + + countryObject_17 := CountryObject{ + + Identifier: 17, + NamesList: []string{"Bahrain"}, + } + + countryObject_18 := CountryObject{ + + Identifier: 18, + NamesList: []string{"Barbados"}, + } + + countryObject_19 := CountryObject{ + + Identifier: 19, + NamesList: []string{"Belarus"}, + } + + countryObject_20 := CountryObject{ + + Identifier: 20, + NamesList: []string{"Belgium"}, + } + + countryObject_21 := CountryObject{ + + Identifier: 21, + NamesList: []string{"Belize"}, + } + + countryObject_22 := CountryObject{ + + Identifier: 22, + NamesList: []string{"Benin"}, + } + + countryObject_23 := CountryObject{ + + Identifier: 23, + NamesList: []string{"Bhutan"}, + } + + countryObject_24 := CountryObject{ + + Identifier: 24, + NamesList: []string{"Bolivia"}, + } + + countryObject_25 := CountryObject{ + + Identifier: 25, + NamesList: []string{"Bonaire"}, + } + + countryObject_26 := CountryObject{ + + Identifier: 26, + NamesList: []string{"Bosnia and Herzegovina"}, + } + + countryObject_27 := CountryObject{ + + Identifier: 27, + NamesList: []string{"Botswana"}, + } + + countryObject_28 := CountryObject{ + + Identifier: 28, + NamesList: []string{"Brazil"}, + } + + countryObject_29 := CountryObject{ + + Identifier: 29, + NamesList: []string{"British Indian Ocean Territory"}, + } + + countryObject_30 := CountryObject{ + + Identifier: 30, + NamesList: []string{"Brunei"}, + } + + countryObject_31 := CountryObject{ + + Identifier: 31, + NamesList: []string{"Bulgaria"}, + } + + countryObject_32 := CountryObject{ + + Identifier: 32, + NamesList: []string{"Burkina Faso"}, + } + + countryObject_33 := CountryObject{ + + Identifier: 33, + NamesList: []string{"Burundi"}, + } + + countryObject_34 := CountryObject{ + + Identifier: 34, + NamesList: []string{"Cambodia"}, + } + + countryObject_35 := CountryObject{ + + Identifier: 35, + NamesList: []string{"Cameroon"}, + } + + countryObject_36 := CountryObject{ + + Identifier: 36, + NamesList: []string{"Canada"}, + } + + countryObject_37 := CountryObject{ + + Identifier: 37, + NamesList: []string{"Cape Verde"}, + } + + countryObject_38 := CountryObject{ + + Identifier: 38, + NamesList: []string{"Cayman Islands"}, + } + + countryObject_39 := CountryObject{ + + Identifier: 39, + NamesList: []string{"Central African Republic"}, + } + + countryObject_40 := CountryObject{ + + Identifier: 40, + NamesList: []string{"Chad"}, + } + + countryObject_41 := CountryObject{ + + Identifier: 41, + NamesList: []string{"Chile"}, + } + + countryObject_42 := CountryObject{ + + Identifier: 42, + NamesList: []string{"China"}, + } + + countryObject_43 := CountryObject{ + + Identifier: 43, + NamesList: []string{"Christmas Island"}, + } + + countryObject_44 := CountryObject{ + + Identifier: 44, + NamesList: []string{"Colombia"}, + } + + countryObject_45 := CountryObject{ + + Identifier: 45, + NamesList: []string{"Comoros"}, + } + + countryObject_46 := CountryObject{ + + Identifier: 46, + NamesList: []string{"Congo"}, + } + + countryObject_47 := CountryObject{ + + Identifier: 47, + NamesList: []string{"Costa Rica"}, + } + + countryObject_48 := CountryObject{ + + Identifier: 48, + NamesList: []string{"Cote D'Ivoire"}, + } + + countryObject_49 := CountryObject{ + + Identifier: 49, + NamesList: []string{"Croatia"}, + } + + countryObject_50 := CountryObject{ + + Identifier: 50, + NamesList: []string{"Cuba"}, + } + + countryObject_51 := CountryObject{ + + Identifier: 51, + NamesList: []string{"Curaçao"}, + } + + countryObject_52 := CountryObject{ + + Identifier: 52, + NamesList: []string{"Cyprus"}, + } + + countryObject_53 := CountryObject{ + + Identifier: 53, + NamesList: []string{"Czech Republic"}, + } + + countryObject_54 := CountryObject{ + + Identifier: 54, + NamesList: []string{"Democratic Republic of the Congo"}, + } + + countryObject_55 := CountryObject{ + + Identifier: 55, + NamesList: []string{"Denmark"}, + } + + countryObject_56 := CountryObject{ + + Identifier: 56, + NamesList: []string{"Djibouti"}, + } + + countryObject_57 := CountryObject{ + + Identifier: 57, + NamesList: []string{"Dominica"}, + } + + countryObject_58 := CountryObject{ + + Identifier: 58, + NamesList: []string{"Dominican Republic"}, + } + + countryObject_59 := CountryObject{ + + Identifier: 59, + NamesList: []string{"East Timor", "Timor-Leste"}, + } + + countryObject_60 := CountryObject{ + + Identifier: 60, + NamesList: []string{"Ecuador"}, + } + + countryObject_61 := CountryObject{ + + Identifier: 61, + NamesList: []string{"Egypt"}, + } + + countryObject_62 := CountryObject{ + + Identifier: 62, + NamesList: []string{"El Salvador"}, + } + + countryObject_63 := CountryObject{ + + Identifier: 63, + NamesList: []string{"Equatorial Guinea"}, + } + + countryObject_64 := CountryObject{ + + Identifier: 64, + NamesList: []string{"Eritrea"}, + } + + countryObject_65 := CountryObject{ + + Identifier: 65, + NamesList: []string{"Estonia"}, + } + + countryObject_66 := CountryObject{ + + Identifier: 66, + NamesList: []string{"Ethiopia"}, + } + + countryObject_67 := CountryObject{ + + Identifier: 67, + NamesList: []string{"Falkland Islands"}, + } + + countryObject_68 := CountryObject{ + + Identifier: 68, + NamesList: []string{"Fiji Islands"}, + } + + countryObject_69 := CountryObject{ + + Identifier: 69, + NamesList: []string{"Finland"}, + } + + countryObject_70 := CountryObject{ + + Identifier: 70, + NamesList: []string{"France"}, + } + + countryObject_71 := CountryObject{ + + Identifier: 71, + NamesList: []string{"Gabon"}, + } + + countryObject_72 := CountryObject{ + + Identifier: 72, + NamesList: []string{"Gambia"}, + } + + countryObject_73 := CountryObject{ + + Identifier: 73, + NamesList: []string{"Georgia"}, + } + + countryObject_74 := CountryObject{ + + Identifier: 74, + NamesList: []string{"Germany"}, + } + + countryObject_75 := CountryObject{ + + Identifier: 75, + NamesList: []string{"Ghana"}, + } + + countryObject_76 := CountryObject{ + + Identifier: 76, + NamesList: []string{"Greece"}, + } + + countryObject_77 := CountryObject{ + + Identifier: 77, + NamesList: []string{"Greenland"}, + } + + countryObject_78 := CountryObject{ + + Identifier: 78, + NamesList: []string{"Grenada"}, + } + + countryObject_79 := CountryObject{ + + Identifier: 79, + NamesList: []string{"Guatemala"}, + } + + countryObject_80 := CountryObject{ + + Identifier: 80, + NamesList: []string{"Guinea"}, + } + + countryObject_81 := CountryObject{ + + Identifier: 81, + NamesList: []string{"Guinea-Bissau"}, + } + + countryObject_82 := CountryObject{ + + Identifier: 82, + NamesList: []string{"Guyana"}, + } + + countryObject_83 := CountryObject{ + + Identifier: 83, + NamesList: []string{"Haiti"}, + } + + countryObject_84 := CountryObject{ + + Identifier: 84, + NamesList: []string{"Honduras"}, + } + + countryObject_85 := CountryObject{ + + Identifier: 85, + NamesList: []string{"Hungary"}, + } + + countryObject_86 := CountryObject{ + + Identifier: 86, + NamesList: []string{"Iceland"}, + } + + countryObject_87 := CountryObject{ + + Identifier: 87, + NamesList: []string{"India"}, + } + + countryObject_88 := CountryObject{ + + Identifier: 88, + NamesList: []string{"Indonesia"}, + } + + countryObject_89 := CountryObject{ + + Identifier: 89, + NamesList: []string{"Iran"}, + } + + countryObject_90 := CountryObject{ + + Identifier: 90, + NamesList: []string{"Iraq"}, + } + + countryObject_91 := CountryObject{ + + Identifier: 91, + NamesList: []string{"Ireland"}, + } + + countryObject_92 := CountryObject{ + + Identifier: 92, + NamesList: []string{"Israel"}, + } + + countryObject_93 := CountryObject{ + + Identifier: 93, + NamesList: []string{"Italy"}, + } + + countryObject_94 := CountryObject{ + + Identifier: 94, + NamesList: []string{"Jamaica"}, + } + + countryObject_95 := CountryObject{ + + Identifier: 95, + NamesList: []string{"Japan"}, + } + + countryObject_96 := CountryObject{ + + Identifier: 96, + NamesList: []string{"Jersey"}, + } + + countryObject_97 := CountryObject{ + + Identifier: 97, + NamesList: []string{"Jordan"}, + } + + countryObject_98 := CountryObject{ + + Identifier: 98, + NamesList: []string{"Kazakhstan"}, + } + + countryObject_99 := CountryObject{ + + Identifier: 99, + NamesList: []string{"Kenya"}, + } + + countryObject_100 := CountryObject{ + + Identifier: 100, + NamesList: []string{"Kiribati"}, + } + + countryObject_101 := CountryObject{ + + Identifier: 101, + NamesList: []string{"Kosovo"}, + } + + countryObject_102 := CountryObject{ + + Identifier: 102, + NamesList: []string{"Kuwait"}, + } + + countryObject_103 := CountryObject{ + + Identifier: 103, + NamesList: []string{"Kyrgyzstan"}, + } + + countryObject_104 := CountryObject{ + + Identifier: 104, + NamesList: []string{"Laos"}, + } + + countryObject_105 := CountryObject{ + + Identifier: 105, + NamesList: []string{"Latvia"}, + } + + countryObject_106 := CountryObject{ + + Identifier: 106, + NamesList: []string{"Lebanon"}, + } + + countryObject_107 := CountryObject{ + + Identifier: 107, + NamesList: []string{"Lesotho"}, + } + + countryObject_108 := CountryObject{ + + Identifier: 108, + NamesList: []string{"Liberia"}, + } + + countryObject_109 := CountryObject{ + + Identifier: 109, + NamesList: []string{"Libya"}, + } + + countryObject_110 := CountryObject{ + + Identifier: 110, + NamesList: []string{"Liechtenstein"}, + } + + countryObject_111 := CountryObject{ + + Identifier: 111, + NamesList: []string{"Lithuania"}, + } + + countryObject_112 := CountryObject{ + + Identifier: 112, + NamesList: []string{"Luxembourg"}, + } + + countryObject_113 := CountryObject{ + + Identifier: 113, + NamesList: []string{"North Macedonia"}, + } + + countryObject_114 := CountryObject{ + + Identifier: 114, + NamesList: []string{"Madagascar"}, + } + + countryObject_115 := CountryObject{ + + Identifier: 115, + NamesList: []string{"Malawi"}, + } + + countryObject_116 := CountryObject{ + + Identifier: 116, + NamesList: []string{"Malaysia"}, + } + + countryObject_117 := CountryObject{ + + Identifier: 117, + NamesList: []string{"Maldives"}, + } + + countryObject_118 := CountryObject{ + + Identifier: 118, + NamesList: []string{"Mali"}, + } + + countryObject_119 := CountryObject{ + + Identifier: 119, + NamesList: []string{"Malta"}, + } + + countryObject_120 := CountryObject{ + + Identifier: 120, + NamesList: []string{"Isle of Man"}, + } + + countryObject_121 := CountryObject{ + + Identifier: 121, + NamesList: []string{"Marshall Islands"}, + } + + countryObject_122 := CountryObject{ + + Identifier: 122, + NamesList: []string{"Mauritania"}, + } + + countryObject_123 := CountryObject{ + + Identifier: 123, + NamesList: []string{"Mauritius"}, + } + + countryObject_124 := CountryObject{ + + Identifier: 124, + NamesList: []string{"Mexico"}, + } + + countryObject_125 := CountryObject{ + + Identifier: 125, + NamesList: []string{"Micronesia"}, + } + + countryObject_126 := CountryObject{ + + Identifier: 126, + NamesList: []string{"Moldova"}, + } + + countryObject_127 := CountryObject{ + + Identifier: 127, + NamesList: []string{"Monaco"}, + } + + countryObject_128 := CountryObject{ + + Identifier: 128, + NamesList: []string{"Mongolia"}, + } + + countryObject_129 := CountryObject{ + + Identifier: 129, + NamesList: []string{"Montenegro"}, + } + + countryObject_130 := CountryObject{ + + Identifier: 130, + NamesList: []string{"Morocco"}, + } + + countryObject_131 := CountryObject{ + + Identifier: 131, + NamesList: []string{"Mozambique"}, + } + + countryObject_132 := CountryObject{ + + Identifier: 132, + NamesList: []string{"Myanmar"}, + } + + countryObject_133 := CountryObject{ + + Identifier: 133, + NamesList: []string{"Namibia"}, + } + + countryObject_134 := CountryObject{ + + Identifier: 134, + NamesList: []string{"Nauru"}, + } + + countryObject_135 := CountryObject{ + + Identifier: 135, + NamesList: []string{"Nepal"}, + } + + countryObject_136 := CountryObject{ + + Identifier: 136, + NamesList: []string{"Netherlands"}, + } + + countryObject_137 := CountryObject{ + + Identifier: 137, + NamesList: []string{"New Caledonia"}, + } + + countryObject_138 := CountryObject{ + + Identifier: 138, + NamesList: []string{"New Zealand"}, + } + + countryObject_139 := CountryObject{ + + Identifier: 139, + NamesList: []string{"Nicaragua"}, + } + + countryObject_140 := CountryObject{ + + Identifier: 140, + NamesList: []string{"Niger"}, + } + + countryObject_141 := CountryObject{ + + Identifier: 141, + NamesList: []string{"Nigeria"}, + } + + countryObject_142 := CountryObject{ + + Identifier: 142, + NamesList: []string{"Niue"}, + } + + countryObject_143 := CountryObject{ + + Identifier: 143, + NamesList: []string{"North Korea"}, + } + + countryObject_144 := CountryObject{ + + Identifier: 144, + NamesList: []string{"Norway"}, + } + + countryObject_145 := CountryObject{ + + Identifier: 145, + NamesList: []string{"Oman"}, + } + + countryObject_146 := CountryObject{ + + Identifier: 146, + NamesList: []string{"Pakistan"}, + } + + countryObject_147 := CountryObject{ + + Identifier: 147, + NamesList: []string{"Palau"}, + } + + countryObject_148 := CountryObject{ + + Identifier: 148, + NamesList: []string{"Panama"}, + } + + countryObject_149 := CountryObject{ + + Identifier: 149, + NamesList: []string{"Papua New Guinea"}, + } + + countryObject_150 := CountryObject{ + + Identifier: 150, + NamesList: []string{"Paraguay"}, + } + + countryObject_151 := CountryObject{ + + Identifier: 151, + NamesList: []string{"Peru"}, + } + + countryObject_152 := CountryObject{ + + Identifier: 152, + NamesList: []string{"Philippines"}, + } + + countryObject_153 := CountryObject{ + + Identifier: 153, + NamesList: []string{"Poland"}, + } + + countryObject_154 := CountryObject{ + + Identifier: 154, + NamesList: []string{"Portugal"}, + } + + countryObject_155 := CountryObject{ + + Identifier: 155, + NamesList: []string{"Puerto Rico"}, + } + + countryObject_156 := CountryObject{ + + Identifier: 156, + NamesList: []string{"Qatar"}, + } + + countryObject_157 := CountryObject{ + + Identifier: 157, + NamesList: []string{"Romania"}, + } + + countryObject_158 := CountryObject{ + + Identifier: 158, + NamesList: []string{"Russia"}, + } + + countryObject_159 := CountryObject{ + + Identifier: 159, + NamesList: []string{"Rwanda"}, + } + + countryObject_160 := CountryObject{ + + Identifier: 160, + NamesList: []string{"Saint Kitts And Nevis"}, + } + + countryObject_161 := CountryObject{ + + Identifier: 161, + NamesList: []string{"Saint Lucia"}, + } + + countryObject_162 := CountryObject{ + + Identifier: 162, + NamesList: []string{"Saint Pierre and Miquelon"}, + } + + countryObject_163 := CountryObject{ + + Identifier: 163, + NamesList: []string{"Saint Vincent And The Grenadines"}, + } + + countryObject_164 := CountryObject{ + + Identifier: 164, + NamesList: []string{"Saint-Martin"}, + } + + countryObject_165 := CountryObject{ + + Identifier: 165, + NamesList: []string{"Samoa"}, + } + + countryObject_166 := CountryObject{ + + Identifier: 166, + NamesList: []string{"San Marino"}, + } + + countryObject_167 := CountryObject{ + + Identifier: 167, + NamesList: []string{"Sao Tome and Principe"}, + } + + countryObject_168 := CountryObject{ + + Identifier: 168, + NamesList: []string{"Saudi Arabia"}, + } + + countryObject_169 := CountryObject{ + + Identifier: 169, + NamesList: []string{"Senegal"}, + } + + countryObject_170 := CountryObject{ + + Identifier: 170, + NamesList: []string{"Serbia"}, + } + + countryObject_171 := CountryObject{ + + Identifier: 171, + NamesList: []string{"Seychelles"}, + } + + countryObject_172 := CountryObject{ + + Identifier: 172, + NamesList: []string{"Sierra Leone"}, + } + + countryObject_173 := CountryObject{ + + Identifier: 173, + NamesList: []string{"Singapore"}, + } + + countryObject_174 := CountryObject{ + + Identifier: 174, + NamesList: []string{"Sint Maarten"}, + } + + countryObject_175 := CountryObject{ + + Identifier: 175, + NamesList: []string{"Slovakia"}, + } + + countryObject_176 := CountryObject{ + + Identifier: 176, + NamesList: []string{"Slovenia"}, + } + + countryObject_177 := CountryObject{ + + Identifier: 177, + NamesList: []string{"Solomon Islands"}, + } + + countryObject_178 := CountryObject{ + + Identifier: 178, + NamesList: []string{"Somalia"}, + } + + countryObject_179 := CountryObject{ + + Identifier: 179, + NamesList: []string{"South Africa"}, + } + + countryObject_180 := CountryObject{ + + Identifier: 180, + NamesList: []string{"South Korea"}, + } + + countryObject_181 := CountryObject{ + + Identifier: 181, + NamesList: []string{"South Sudan"}, + } + + countryObject_182 := CountryObject{ + + Identifier: 182, + NamesList: []string{"Spain"}, + } + + countryObject_183 := CountryObject{ + + Identifier: 183, + NamesList: []string{"Sri Lanka"}, + } + + countryObject_184 := CountryObject{ + + Identifier: 184, + NamesList: []string{"Sudan"}, + } + + countryObject_185 := CountryObject{ + + Identifier: 185, + NamesList: []string{"Suriname"}, + } + + countryObject_186 := CountryObject{ + + Identifier: 186, + NamesList: []string{"Eswatini", "Swaziland"}, + } + + countryObject_187 := CountryObject{ + + Identifier: 187, + NamesList: []string{"Sweden"}, + } + + countryObject_188 := CountryObject{ + + Identifier: 188, + NamesList: []string{"Switzerland"}, + } + + countryObject_189 := CountryObject{ + + Identifier: 189, + NamesList: []string{"Syria"}, + } + + countryObject_190 := CountryObject{ + + Identifier: 190, + NamesList: []string{"Taiwan"}, + } + + countryObject_191 := CountryObject{ + + Identifier: 191, + NamesList: []string{"Tajikistan"}, + } + + countryObject_192 := CountryObject{ + + Identifier: 192, + NamesList: []string{"Tanzania"}, + } + + countryObject_193 := CountryObject{ + + Identifier: 193, + NamesList: []string{"Thailand"}, + } + + countryObject_194 := CountryObject{ + + Identifier: 194, + NamesList: []string{"The Bahamas"}, + } + + countryObject_195 := CountryObject{ + + Identifier: 195, + NamesList: []string{"Togo"}, + } + + countryObject_196 := CountryObject{ + + Identifier: 196, + NamesList: []string{"Tonga"}, + } + + countryObject_197 := CountryObject{ + + Identifier: 197, + NamesList: []string{"Trinidad And Tobago"}, + } + + countryObject_198 := CountryObject{ + + Identifier: 198, + NamesList: []string{"Tunisia"}, + } + + countryObject_199 := CountryObject{ + + Identifier: 199, + NamesList: []string{"Turkey"}, + } + + countryObject_200 := CountryObject{ + + Identifier: 200, + NamesList: []string{"Turkmenistan"}, + } + + countryObject_201 := CountryObject{ + + Identifier: 201, + NamesList: []string{"Turks And Caicos Islands"}, + } + + countryObject_202 := CountryObject{ + + Identifier: 202, + NamesList: []string{"Tuvalu"}, + } + + countryObject_203 := CountryObject{ + + Identifier: 203, + NamesList: []string{"Uganda"}, + } + + countryObject_204 := CountryObject{ + + Identifier: 204, + NamesList: []string{"Ukraine"}, + } + + countryObject_205 := CountryObject{ + + Identifier: 205, + NamesList: []string{"United Arab Emirates"}, + } + + countryObject_206 := CountryObject{ + + Identifier: 206, + NamesList: []string{"United Kingdom"}, + } + + countryObject_207 := CountryObject{ + + Identifier: 207, + NamesList: []string{"United States"}, + } + + countryObject_208 := CountryObject{ + + Identifier: 208, + NamesList: []string{"Uruguay"}, + } + + countryObject_209 := CountryObject{ + + Identifier: 209, + NamesList: []string{"Uzbekistan"}, + } + + countryObject_210 := CountryObject{ + + Identifier: 210, + NamesList: []string{"Vanuatu"}, + } + + countryObject_211 := CountryObject{ + + Identifier: 211, + NamesList: []string{"Vatican City State"}, + } + + countryObject_212 := CountryObject{ + + Identifier: 212, + NamesList: []string{"Venezuela"}, + } + + countryObject_213 := CountryObject{ + + Identifier: 213, + NamesList: []string{"Vietnam"}, + } + + countryObject_214 := CountryObject{ + + Identifier: 214, + NamesList: []string{"British Virgin Islands"}, + } + + countryObject_215 := CountryObject{ + + Identifier: 215, + NamesList: []string{"Wallis And Futuna Islands"}, + } + + countryObject_216 := CountryObject{ + + Identifier: 216, + NamesList: []string{"Yemen"}, + } + + countryObject_217 := CountryObject{ + + Identifier: 217, + NamesList: []string{"Zambia"}, + } + + countryObject_218 := CountryObject{ + + Identifier: 218, + NamesList: []string{"Zimbabwe"}, + } + + countryObject_219 := CountryObject{ + + Identifier: 219, + NamesList: []string{"Bermuda"}, + } + + allCountryObjectsList = []CountryObject{ countryObject_1, countryObject_2, countryObject_3, countryObject_4, countryObject_5, countryObject_6, countryObject_7, countryObject_8, countryObject_9, countryObject_10, countryObject_11, countryObject_12, countryObject_13, countryObject_14, countryObject_15, countryObject_16, countryObject_17, countryObject_18, countryObject_19, countryObject_20, countryObject_21, countryObject_22, countryObject_23, countryObject_24, countryObject_25, countryObject_26, countryObject_27, countryObject_28, countryObject_29, countryObject_30, countryObject_31, countryObject_32, countryObject_33, countryObject_34, countryObject_35, countryObject_36, countryObject_37, countryObject_38, countryObject_39, countryObject_40, countryObject_41, countryObject_42, countryObject_43, countryObject_44, countryObject_45, countryObject_46, countryObject_47, countryObject_48, countryObject_49, countryObject_50, countryObject_51, countryObject_52, countryObject_53, countryObject_54, countryObject_55, countryObject_56, countryObject_57, countryObject_58, countryObject_59, countryObject_60, countryObject_61, countryObject_62, countryObject_63, countryObject_64, countryObject_65, countryObject_66, countryObject_67, countryObject_68, countryObject_69, countryObject_70, countryObject_71, countryObject_72, countryObject_73, countryObject_74, countryObject_75, countryObject_76, countryObject_77, countryObject_78, countryObject_79, countryObject_80, countryObject_81, countryObject_82, countryObject_83, countryObject_84, countryObject_85, countryObject_86, countryObject_87, countryObject_88, countryObject_89, countryObject_90, countryObject_91, countryObject_92, countryObject_93, countryObject_94, countryObject_95, countryObject_96, countryObject_97, countryObject_98, countryObject_99, countryObject_100, countryObject_101, countryObject_102, countryObject_103, countryObject_104, countryObject_105, countryObject_106, countryObject_107, countryObject_108, countryObject_109, countryObject_110, countryObject_111, countryObject_112, countryObject_113, countryObject_114, countryObject_115, countryObject_116, countryObject_117, countryObject_118, countryObject_119, countryObject_120, countryObject_121, countryObject_122, countryObject_123, countryObject_124, countryObject_125, countryObject_126, countryObject_127, countryObject_128, countryObject_129, countryObject_130, countryObject_131, countryObject_132, countryObject_133, countryObject_134, countryObject_135, countryObject_136, countryObject_137, countryObject_138, countryObject_139, countryObject_140, countryObject_141, countryObject_142, countryObject_143, countryObject_144, countryObject_145, countryObject_146, countryObject_147, countryObject_148, countryObject_149, countryObject_150, countryObject_151, countryObject_152, countryObject_153, countryObject_154, countryObject_155, countryObject_156, countryObject_157, countryObject_158, countryObject_159, countryObject_160, countryObject_161, countryObject_162, countryObject_163, countryObject_164, countryObject_165, countryObject_166, countryObject_167, countryObject_168, countryObject_169, countryObject_170, countryObject_171, countryObject_172, countryObject_173, countryObject_174, countryObject_175, countryObject_176, countryObject_177, countryObject_178, countryObject_179, countryObject_180, countryObject_181, countryObject_182, countryObject_183, countryObject_184, countryObject_185, countryObject_186, countryObject_187, countryObject_188, countryObject_189, countryObject_190, countryObject_191, countryObject_192, countryObject_193, countryObject_194, countryObject_195, countryObject_196, countryObject_197, countryObject_198, countryObject_199, countryObject_200, countryObject_201, countryObject_202, countryObject_203, countryObject_204, countryObject_205, countryObject_206, countryObject_207, countryObject_208, countryObject_209, countryObject_210, countryObject_211, countryObject_212, countryObject_213, countryObject_214, countryObject_215, countryObject_216, countryObject_217, countryObject_218, countryObject_219} +} + + + diff --git a/resources/worldLocations/worldLocations_test.go b/resources/worldLocations/worldLocations_test.go new file mode 100644 index 0000000..02b2b6e --- /dev/null +++ b/resources/worldLocations/worldLocations_test.go @@ -0,0 +1,102 @@ +package worldLocations + +import "testing" + +import "seekia/internal/helpers" + + +func TestLocations(t *testing.T){ + + err := InitializeWorldLocationsVariables() + if (err != nil){ + t.Fatalf("Failed to initialize world locations variables: " + err.Error()) + } + + allCountryObjectsList, err := GetAllCountryObjectsList() + if (err != nil){ + t.Fatalf("Failed to get all country objects list: " + err.Error()) + } + + // We make sure there are no duplicates in the countries list + allCountryIdentifiersMap := make(map[int]struct{}) + allCountryNamesMap := make(map[string]struct{}) + + for _, countryObject := range allCountryObjectsList{ + + countryIdentifier := countryObject.Identifier + + _, exists := allCountryIdentifiersMap[countryIdentifier] + if (exists == true){ + + countryIdentifierString := helpers.ConvertIntToString(countryIdentifier) + + t.Fatalf("Duplicate country identifier found: " + countryIdentifierString) + } + + allCountryIdentifiersMap[countryIdentifier] = struct{}{} + + isValid := VerifyCountryIdentifier(countryIdentifier) + if (isValid == false){ + t.Fatalf("VerifyCountryIdentifier returning invalid for existing countryIdentifier.") + } + + countryNamesList := countryObject.NamesList + + for _, countryName := range countryNamesList{ + + _, exists := allCountryNamesMap[countryName] + if (exists == true){ + t.Fatalf("Duplicate country name found: " + countryName) + } + allCountryNamesMap[countryName] = struct{}{} + } + } + + // We use this map to make sure no cities share the same coordinates + allCityObjectsMap := make(map[CoordinatesObject]struct{}) + + allCityObjectsList, err := getAllCityObjectsList() + if (err != nil){ + t.Fatalf("Failed to get all city objects list: " + err.Error()) + } + + for _, cityObject := range allCityObjectsList{ + + cityName := cityObject.Name + cityState := cityObject.State + cityCountry := cityObject.Country + cityLatitude := cityObject.Latitude + cityLongitude := cityObject.Longitude + + // We make sure that all locations correspond to the countries we have + _, exists := allCountryNamesMap[cityCountry] + if (exists == false){ + t.Fatalf("City objects contain a country not in the countries list: " + cityCountry) + } + + // We make sure state/city fields are not empty + if (cityState == ""){ + t.Fatalf("City object contains a city with an empty State.") + } + if (cityName == ""){ + t.Fatalf("City object contains a city with an empty Name.") + } + + // We make sure all cities contain unique coordinates + coordinatesObject := CoordinatesObject{ + Latitude: cityLatitude, + Longitude: cityLongitude, + } + + _, exists = allCityObjectsMap[coordinatesObject] + if (exists == true){ + + t.Fatalf("Two cities share the same coordinates:" + cityName) + } + + allCityObjectsMap[coordinatesObject] = struct{}{} + } +} + + + diff --git a/timestamps/Miscellaneous/OpenSNPDataArchive.tar.gz.ots b/timestamps/Miscellaneous/OpenSNPDataArchive.tar.gz.ots new file mode 100644 index 0000000..f43f201 Binary files /dev/null and b/timestamps/Miscellaneous/OpenSNPDataArchive.tar.gz.ots differ diff --git a/timestamps/ReadMe.md b/timestamps/ReadMe.md new file mode 100644 index 0000000..b071b3b --- /dev/null +++ b/timestamps/ReadMe.md @@ -0,0 +1,57 @@ +# Timestamps + +OpenTimestamps is used to timestamp Seekia commits, releases, and other files. + +Visit [OpenTimestamps.org](https://www.opentimestamps.org) to learn more. + +Timestamps are useful to provide evidence that a commit was authored by a specific person. + +Timestamps can defend Seekia against patent trolls by proving that the patent troll was not the first inventor of an idea. + +Timestamps can also protect developers from wrongful accusations of plagiarism. + +Timestamping commits is optional. + +When someone timestamps a commit on a branch in which your commit has already been merged, your commit will be timestamped as well. + +## How to Timestamp Commits + +##### 1. Install the OpenTimestamps client. +Link: [Github.com/opentimestamps/opentimestamps-client](https://github.com/opentimestamps/opentimestamps-client) + +##### 2. Create a file containing the hex encoded commit hash. + +The name of this file should be the commit hash. + +Example: 34169c1384cd725d8ab580b6569fcd7276cb93e8.txt + +You can view recent commit hashes with the command `git log`. + +##### 3. Timestamp the file + +Use the command `ots stamp filename.txt`. + +##### 4. Wait for the timestamp to be included in the Bitcoin blockchain. + +This usually takes several hours. + +##### 5. Upgrade the timestamp + +Use the command `ots upgrade filename.txt.ots`. + +This command upgrades the timestamp to be locally verifiable. + +This command will tell you if the timestamp has been confirmed in the Bitcoin blockchain. + +##### 6. Push your commit to the Seekia repository. + +You don't have to wait for the timestamp to confirm before committing unless you are paranoid about bad actors. + +##### 7. Push another commit which adds your .txt and .txt.ots file to the timestamps/Commits folder. + +You can do this at a later date. You can upload timestamps in batches. + +## How To Verify Commit Timestamps + +*TODO* + diff --git a/timestamps/Releases/0.1/Seekia-v0.1.zip.ots b/timestamps/Releases/0.1/Seekia-v0.1.zip.ots new file mode 100644 index 0000000..29601b6 Binary files /dev/null and b/timestamps/Releases/0.1/Seekia-v0.1.zip.ots differ diff --git a/timestamps/Releases/0.11/Seekia-v0.11.zip.ots b/timestamps/Releases/0.11/Seekia-v0.11.zip.ots new file mode 100644 index 0000000..fe7a337 Binary files /dev/null and b/timestamps/Releases/0.11/Seekia-v0.11.zip.ots differ diff --git a/timestamps/Releases/0.20/Seekia-v0.20.zip.ots b/timestamps/Releases/0.20/Seekia-v0.20.zip.ots new file mode 100644 index 0000000..2f0f200 Binary files /dev/null and b/timestamps/Releases/0.20/Seekia-v0.20.zip.ots differ diff --git a/timestamps/Releases/0.30/Seekia-v0.30.zip.ots b/timestamps/Releases/0.30/Seekia-v0.30.zip.ots new file mode 100644 index 0000000..2da4283 Binary files /dev/null and b/timestamps/Releases/0.30/Seekia-v0.30.zip.ots differ diff --git a/timestamps/Releases/0.40/Seekia-v0.40.zip.ots b/timestamps/Releases/0.40/Seekia-v0.40.zip.ots new file mode 100644 index 0000000..9c7fa18 Binary files /dev/null and b/timestamps/Releases/0.40/Seekia-v0.40.zip.ots differ diff --git a/timestamps/Releases/0.50/Seekia-v0.50.tgz.asc.ots b/timestamps/Releases/0.50/Seekia-v0.50.tgz.asc.ots new file mode 100644 index 0000000..5e5fad7 Binary files /dev/null and b/timestamps/Releases/0.50/Seekia-v0.50.tgz.asc.ots differ diff --git a/timestamps/Releases/0.50/Seekia-v0.50.tgz.ots b/timestamps/Releases/0.50/Seekia-v0.50.tgz.ots new file mode 100644 index 0000000..562fb50 Binary files /dev/null and b/timestamps/Releases/0.50/Seekia-v0.50.tgz.ots differ diff --git a/timestamps/Releases/0.50/SeekiaGitHistory-v0.50.bundle.asc.ots b/timestamps/Releases/0.50/SeekiaGitHistory-v0.50.bundle.asc.ots new file mode 100644 index 0000000..d085722 Binary files /dev/null and b/timestamps/Releases/0.50/SeekiaGitHistory-v0.50.bundle.asc.ots differ diff --git a/timestamps/Releases/0.50/SeekiaGitHistory-v0.50.bundle.ots b/timestamps/Releases/0.50/SeekiaGitHistory-v0.50.bundle.ots new file mode 100644 index 0000000..2d11233 Binary files /dev/null and b/timestamps/Releases/0.50/SeekiaGitHistory-v0.50.bundle.ots differ diff --git a/timestamps/Releases/ReadMe.md b/timestamps/Releases/ReadMe.md new file mode 100644 index 0000000..ff14d22 --- /dev/null +++ b/timestamps/Releases/ReadMe.md @@ -0,0 +1,6 @@ + +# Seekia Release Timestamps + +These OpenTimestamps timestamps are not the earliest timestamps for at least some of these releases. + +Simon Sarasova's Memo announcement for each release is usually the earliest timestamp for each release. \ No newline at end of file diff --git a/utilities/addLocusMetadata/addLocusMetadata.go b/utilities/addLocusMetadata/addLocusMetadata.go new file mode 100644 index 0000000..20f9e38 --- /dev/null +++ b/utilities/addLocusMetadata/addLocusMetadata.go @@ -0,0 +1,124 @@ + +// addLocusMetadata.go provides a function to manually add locus metadata to the .json files. + +package main + +import "seekia/resources/geneticReferences/locusMetadata" + +import "seekia/internal/helpers" +import "seekia/internal/localFilesystem" + +import "encoding/json" + +import "slices" + +import "log" + +func main(){ + + /* + + //TODO: Add these loci + // They are on the X chromosome, and we haven't defined the syntax to deal with X chromosome loci yet. + + newLocusMetadataObject_1 := locusMetadata.LocusMetadata{ + RSIDsList: []int64{5957354}, + Chromosome: X, + Position: 120305480, + GeneNamesList: []string{"TMEM255A"}, + CompanyAliases: make(map[locusMetadata.GeneticsCompany][]string), + References: make(map[string]string), + } + + newLocusMetadataObject_2 := locusMetadata.LocusMetadata{ + RSIDsList: []int64{78542430}, + Chromosome: X, + Position: 48259397, + GeneNamesList: []string{"SSX1"}, + CompanyAliases: make(map[locusMetadata.GeneticsCompany][]string), + References: make(map[string]string), + } + + */ + + // This is a list of metadata objects to add to the locus metadata + lociToAddList := []locusMetadata.LocusMetadata{ + //newLocusMetadataObject_1, + //newLocusMetadataObject_2, + } + + numberOfLociToAdd := len(lociToAddList) + + err := locusMetadata.InitializeLocusMetadataVariables() + if (err != nil){ + log.Println("ERROR: " + err.Error()) + return + } + + // Map Structure: Chromosome -> List of locus metadata objects to add + lociToAddMap := make(map[int][]locusMetadata.LocusMetadata) + + for _, locusObject := range lociToAddList{ + + // First we check to see if locus metadata already exists + + locusRSIDsList := locusObject.RSIDsList + + for _, rsID := range locusRSIDsList{ + + exists, _, err := locusMetadata.GetLocusMetadata(rsID) + if (err != nil){ + log.Println("ERROR: " + err.Error()) + return + } + if (exists == true){ + rsIDString := helpers.ConvertInt64ToString(rsID) + log.Println("lociToAddList contains locus whose metadata already exists: " + rsIDString) + return + } + } + + locusChromosome := locusObject.Chromosome + + existingList, exists := lociToAddMap[locusChromosome] + if (exists == false){ + lociToAddMap[locusChromosome] = []locusMetadata.LocusMetadata{locusObject} + } else { + existingList = append(existingList, locusObject) + lociToAddMap[locusChromosome] = existingList + } + } + + for chromosomeInt, locusMetadataObjectsToAddList := range lociToAddMap{ + + existingLocusMetadataObjectsList, err := locusMetadata.GetLocusMetadataObjectsListByChromosome(chromosomeInt) + if (err != nil) { + log.Println(err) + return + } + + newLocusMetadataObjectsList := slices.Concat(existingLocusMetadataObjectsList, locusMetadataObjectsToAddList) + + newChromosomeFileBytes, err := json.MarshalIndent(newLocusMetadataObjectsList, "", "\t") + if (err != nil){ + log.Println("ERROR: " + err.Error()) + return + } + + currentChromosomeString := helpers.ConvertIntToString(chromosomeInt) + + locusMetadataFilepath := "../../resources/geneticReferences/locusMetadata/" + + err = localFilesystem.CreateOrOverwriteFile(newChromosomeFileBytes, locusMetadataFilepath, "LocusMetadata_Chromosome" + currentChromosomeString + ".json") + if (err != nil){ + log.Println("ERROR: " + err.Error()) + return + } + } + + numberOfAddedLociString := helpers.ConvertIntToString(numberOfLociToAdd) + + log.Println("Successfully added " + numberOfAddedLociString + " locus metadatas!") +} + + diff --git a/utilities/createCitiesFile/.gitignore b/utilities/createCitiesFile/.gitignore new file mode 100644 index 0000000..7aecf9f --- /dev/null +++ b/utilities/createCitiesFile/.gitignore @@ -0,0 +1,2 @@ +worldCities.messagepack +cities.json \ No newline at end of file diff --git a/utilities/createCitiesFile/createCitiesFile.go b/utilities/createCitiesFile/createCitiesFile.go new file mode 100644 index 0000000..9c2d490 --- /dev/null +++ b/utilities/createCitiesFile/createCitiesFile.go @@ -0,0 +1,242 @@ + +// This utility will create a .messagepack file containing the information for all cities from the Countries States Cities database +// We remove fields that we don't need and remove cities without latitude and longitude data. +// We do this to make the file smaller, and thus make Seekia a smaller download for users. +// We also remove duplicate coordinates from the data. +// This input file is 50.8 MB, the output file is 6.4 MB +// The output file should be placed in /resources/worldLocations. + +// You have to download the database file to generate the output worldCities.messagepack file. +// We are not including the input file in the source code so that the source code download is smaller + +// The database repository: https://github.com/dr5hn/countries-states-cities-database/ +// +// We are using version 2.2 of the database +// File to download: v2.2.tar.gz +// Download Link: https://github.com/dr5hn/countries-states-cities-database/archive/refs/tags/v2.2.tar.gz +// v2.2.tar.gz SHA256 checksum: fc9ed9642906d8629059c06094ed1ce53ad4fa510991a6485ff3422962cff1b9 +// +// First, you must extract the downloaded .tar.gz file. +// +// We use the file cities.json as the input to the createCitiesFile utility. +// cities.json SHA256 checksum: b82a21f2c8402041c00787131f206eb06d931d12d062f4a8a0f2b07075263c99 +// +// Output file: worldCities.messagepack +// worldCities.messagepack SHA256 checksum: 0c42a4c51db7f42dcc71035e375f010a5f80c41cd89d87f581a93f3226bf650d + +package main + +import "seekia/resources/worldLocations" + +import "seekia/internal/encoding" +import "seekia/internal/helpers" +import "seekia/internal/localFilesystem" + +import messagepack "github.com/vmihailenco/msgpack/v5" + +import "errors" +import "encoding/json" +import "log" + +func main(){ + + createCitiesFileFunction := func()error{ + + type cityItem struct{ + + // Name of the city + Name string + + // Name of state city is in + State_name string + + // Name of country city is in + Country_name string + + Latitude string + Longitude string + } + + fileExists, fileContents, err := localFilesystem.GetFileContents("./cities.json") + if (err != nil){ return err } + if (fileExists == false){ + return errors.New("cities.json file was not found.") + } + + var citiesList []cityItem + + err = json.Unmarshal(fileContents, &citiesList) + if (err != nil){ return err } + + //Map Structure: Name in input file -> Name we replace it with in our new file + countryNamesToReplaceMap := map[string]string{ + "Bonaire, Sint Eustatius and Saba": "Bonaire", + "Cote D'Ivoire (Ivory Coast)": "Cote D'Ivoire", + "Gambia The": "Gambia", + "Virgin Islands (US)": "United States", + "Papua new Guinea": "Papua New Guinea", + "Antigua and Barbuda": "Antigua And Barbuda", + "Hong Kong S.A.R.": "China", + "Saint Kitts and Nevis": "Saint Kitts And Nevis", + "Saint Vincent and the Grenadines": "Saint Vincent And The Grenadines", + "Trinidad and Tobago": "Trinidad And Tobago", + } + + allCountryObjectsList, err := worldLocations.GetAllCountryObjectsList() + if (err != nil) { return err } + + // Map Structure: Country Name -> Country Identifier + countryIdentifiersMap := make(map[string]int) + + for _, countryObject := range allCountryObjectsList{ + + countryIdentifier := countryObject.Identifier + countryNamesList := countryObject.NamesList + + for _, countryName := range countryNamesList{ + + countryIdentifiersMap[countryName] = countryIdentifier + } + } + + // We encode a messagepack file with the cities: + // It is a slice of messagepack-encoded slices + // Each slice represents a city object + // []{Name (string), State (string), Country Identifier (int), Latitude (float64), Longitude (float64)} + + newLocationsSlice := make([]messagepack.RawMessage, 0, 148402) + + type coordinatesObject struct{ + Latitude float64 + Longitude float64 + } + // We use this map to make sure no duplicate coordinate pairs are added + existingCoordinatesMap := make(map[coordinatesObject]struct{}) + + for _, cityItem := range citiesList{ + + cityName := cityItem.Name + cityState := cityItem.State_name + cityCountry := cityItem.Country_name + + //Outputs: + // -bool: Coordinates exist + // -float64: Latitude + // -float64: Longitude + // -error + getCityCoordinates := func()(bool, float64, float64, error){ + + cityLatitude := cityItem.Latitude + cityLongitude := cityItem.Longitude + + cityLatitudeFloat64, err := helpers.ConvertStringToFloat64(cityLatitude) + if (err != nil){ return false, 0, 0, err } + + cityLongitudeFloat64, err := helpers.ConvertStringToFloat64(cityLongitude) + if (err != nil){ return false, 0, 0, err } + + if (cityLatitudeFloat64 == 0 && cityLongitudeFloat64 == 0){ + return false, 0, 0, nil + } + + cityCoordinatesObject := coordinatesObject{ + Latitude: cityLatitudeFloat64, + Longitude: cityLongitudeFloat64, + } + + _, exists := existingCoordinatesMap[cityCoordinatesObject] + if (exists == false){ + return true, cityLatitudeFloat64, cityLongitudeFloat64, nil + } + + // The database has a duplicate. + // We will slightly shift the coordinate until it is not a duplicate + + for i := 1; i < 1000; i++{ + + newCityLatitude := cityLatitudeFloat64 + (.00000001 * float64(i)) + + newCityCoordinatesObject := coordinatesObject{ + Latitude: newCityLatitude, + Longitude: cityLongitudeFloat64, + } + + _, exists := existingCoordinatesMap[newCityCoordinatesObject] + if (exists == false){ + return true, newCityLatitude, cityLongitudeFloat64, nil + } + } + + return false, 0, 0, errors.New("Too many coordinate collisions: " + cityName) + } + + coordinatesExist, cityLatitude, cityLongitude, err := getCityCoordinates() + if (err != nil) { return err } + if (coordinatesExist == false){ + // This city has no coordinate info. Skip it. + continue + } + + getCountryName := func()string{ + + newName, exists := countryNamesToReplaceMap[cityCountry] + if (exists == false){ + return cityCountry + } + return newName + } + + countryName := getCountryName() + + countryIdentifier, exists := countryIdentifiersMap[countryName] + if (exists == false){ + return errors.New("countryIdentifiersMap missing countryName: " + countryName) + } + + cityNameMessagepack, err := encoding.EncodeMessagePackBytes(cityName) + if (err != nil) { return err } + + cityStateMessagepack, err := encoding.EncodeMessagePackBytes(cityState) + if (err != nil) { return err } + + cityCountryIdentifierMessagepack, err := encoding.EncodeMessagePackBytes(countryIdentifier) + if (err != nil) { return err } + + cityLatitudeMessagepack, err := encoding.EncodeMessagePackBytes(cityLatitude) + if (err != nil) { return err } + + cityLongitudeMessagepack, err := encoding.EncodeMessagePackBytes(cityLongitude) + if (err != nil) { return err } + + cityMessagepackSlice := []messagepack.RawMessage{cityNameMessagepack, cityStateMessagepack, cityCountryIdentifierMessagepack, cityLatitudeMessagepack, cityLongitudeMessagepack} + + cityEncodedMessagepack, err := encoding.EncodeMessagePackBytes(cityMessagepackSlice) + if (err != nil){ return err } + + newLocationsSlice = append(newLocationsSlice, cityEncodedMessagepack) + + newCityCoordinatesObject := coordinatesObject{ + Latitude: cityLatitude, + Longitude: cityLongitude, + } + + existingCoordinatesMap[newCityCoordinatesObject] = struct{}{} + } + + newFileBytes, err := encoding.EncodeMessagePackBytes(newLocationsSlice) + if (err != nil){ return err } + + err = localFilesystem.CreateOrOverwriteFile(newFileBytes, "./", "worldCities.messagepack") + if (err != nil){ return err } + + return nil + } + + err := createCitiesFileFunction() + if (err != nil){ + log.Println("Failed to create cities file. Reason: " + err.Error()) + } else { + log.Println("Successfully created cities file!") + } +} + diff --git a/utilities/createGeneticModels/.gitignore b/utilities/createGeneticModels/.gitignore new file mode 100644 index 0000000..fe5b4d2 --- /dev/null +++ b/utilities/createGeneticModels/.gitignore @@ -0,0 +1,3 @@ +OpenSNPDataArchiveFolderpath.txt +TrainingData +TrainedModels \ No newline at end of file diff --git a/utilities/createGeneticModels/createGeneticModels.go b/utilities/createGeneticModels/createGeneticModels.go new file mode 100644 index 0000000..98dad38 --- /dev/null +++ b/utilities/createGeneticModels/createGeneticModels.go @@ -0,0 +1,1568 @@ + +// createGeneticModels.go provides an interface to create genetic prediction models +// These are neural networks which predict traits such as eye color from raw genome files +// The OpenSNP.org dataset is used, and more datasets will be added in the future. +// You must download the dataset and extract it. The instructions are described in the utility. + +package main + +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/theme" +import "fyne.io/fyne/v2/layout" +import "fyne.io/fyne/v2/dialog" +import "fyne.io/fyne/v2/data/binding" + +import "seekia/resources/geneticReferences/traits" +import "seekia/resources/geneticReferences/locusMetadata" + +import "seekia/internal/encoding" +import "seekia/internal/genetics/locusValue" +import "seekia/internal/genetics/prepareRawGenomes" +import "seekia/internal/genetics/readRawGenomes" +import "seekia/internal/genetics/geneticPrediction" +import "seekia/internal/helpers" +import "seekia/internal/imagery" +import "seekia/internal/localFilesystem" +import "seekia/internal/genetics/readBiobankData" + +import "errors" +import "crypto/sha256" +import "bytes" +import "image/color" +import "io" +import "os" +import "strings" +import "sync" +import "slices" +import mathRand "math/rand" +import goFilepath "path/filepath" + + +func main(){ + + app := app.New() + + customTheme := getCustomFyneTheme() + + app.Settings().SetTheme(customTheme) + + window := app.NewWindow("Seekia - Create Genetic Models Utility") + + windowSize := fyne.NewSize(600, 600) + window.Resize(windowSize) + + window.CenterOnScreen() + + setHomePage(window) + + window.ShowAndRun() +} + + +func getWidgetCentered(widget fyne.Widget)*fyne.Container{ + + widgetCentered := container.NewHBox(layout.NewSpacer(), widget, layout.NewSpacer()) + + return widgetCentered +} + +func getLabelCentered(text string) *fyne.Container{ + + label := widget.NewLabel(text) + labelCentered := container.NewHBox(layout.NewSpacer(), label, layout.NewSpacer()) + + return labelCentered +} + +func getBoldLabel(text string) fyne.Widget{ + + titleStyle := fyne.TextStyle{ + Bold: true, + Italic: false, + Monospace: false, + } + + boldLabel := widget.NewLabelWithStyle(text, fyne.TextAlign(fyne.TextAlignCenter), titleStyle) + + return boldLabel +} + +func getItalicLabel(text string) fyne.Widget{ + + italicTextStyle := fyne.TextStyle{ + Bold: false, + Italic: true, + Monospace: false, + } + + italicLabel := widget.NewLabelWithStyle(text, fyne.TextAlign(fyne.TextAlignCenter), italicTextStyle) + + return italicLabel +} + +func getBoldLabelCentered(inputText string)*fyne.Container{ + + boldLabel := getBoldLabel(inputText) + + boldLabelCentered := container.NewHBox(layout.NewSpacer(), boldLabel, layout.NewSpacer()) + + return boldLabelCentered +} + +func getItalicLabelCentered(inputText string)*fyne.Container{ + + italicLabel := getItalicLabel(inputText) + + italicLabelCentered := container.NewHBox(layout.NewSpacer(), italicLabel, layout.NewSpacer()) + + return italicLabelCentered +} + + +func showUnderConstructionDialog(window fyne.Window){ + + dialogTitle := "Under Construction" + dialogMessageA := getLabelCentered("Seekia is under construction.") + dialogMessageB := getLabelCentered("This page/feature needs to be built.") + dialogContent := container.NewVBox(dialogMessageA, dialogMessageB) + dialog.ShowCustom(dialogTitle, "Close", dialogContent, window) +} + + +func getBackButtonCentered(previousPage func())*fyne.Container{ + + backButton := getWidgetCentered(widget.NewButtonWithIcon("Go Back", theme.NavigateBackIcon(), previousPage)) + + return backButton +} + +func setErrorEncounteredPage(window fyne.Window, err error, previousPage func()){ + + title := getBoldLabelCentered("Error Encountered") + + backButton := getBackButtonCentered(previousPage) + + description1 := getLabelCentered("Something went wrong. Report this error to Seekia developers.") + + header := container.NewVBox(title, backButton, widget.NewSeparator(), description1, widget.NewSeparator()) + + getErrorString := func()string{ + if (err == nil){ + return "No nav bar error encountered page called with nil error." + } + errorString := err.Error() + return errorString + } + + errorString := getErrorString() + + errorLabel := widget.NewLabel(errorString) + errorLabel.Wrapping = 3 + errorLabel.Alignment = 1 + errorLabel.TextStyle = fyne.TextStyle{ + Bold: true, + Italic: false, + Monospace: false, + } + + + //TODO: Add copyable toggle + + page := container.NewBorder(header, nil, nil, nil, errorLabel) + + window.SetContent(page) +} + + +// This loading screen shows no progress, so it should only be used when retrieving progress is impossible +func setLoadingScreen(window fyne.Window, pageTitle string, loadingText string){ + + title := getBoldLabelCentered(pageTitle) + + loadingLabel := getWidgetCentered(getItalicLabel(loadingText)) + progressBar := getWidgetCentered(widget.NewProgressBarInfinite()) + + pageContent := container.NewVBox(title, loadingLabel, progressBar) + + page := container.NewCenter(pageContent) + + window.SetContent(page) +} + + +func setHomePage(window fyne.Window){ + + currentPage := func(){setHomePage(window)} + + title := getBoldLabelCentered("Create Genetic Models Utility") + + description1 := getLabelCentered("This utility is used to create the genetic prediction models.") + description2 := getLabelCentered("These models are used to predict traits such as eye color from raw genome files.") + description3 := getLabelCentered("Seekia aims to have open source and reproducible genetic prediction technology.") + + step1Label := getLabelCentered("Step 1:") + + downloadTrainingDataButton := getWidgetCentered(widget.NewButton("Download Training Data", func(){ + setDownloadTrainingDataPage(window, currentPage) + })) + + step2Label := getLabelCentered("Step 2:") + + extractTrainingDataButton := getWidgetCentered(widget.NewButton("Extract Training Data", func(){ + setExtractTrainingDataPage(window, currentPage) + })) + + step3Label := getLabelCentered("Step 3:") + + createTrainingDataButton := getWidgetCentered(widget.NewButton("Create Training Data", func(){ + setCreateTrainingDataPage(window, currentPage) + })) + + step4Label := getLabelCentered("Step 4:") + + trainModelsButton := getWidgetCentered(widget.NewButton("Train Models", func(){ + setTrainModelsPage(window, currentPage) + })) + + step5Label := getLabelCentered("Step 5:") + + testModelsButton := getWidgetCentered(widget.NewButton("Test Models", func(){ + setTestModelsPage(window, currentPage) + })) + + //TODO: A page to verify the checksums of the generated .gob models + + page := container.NewVBox(title, widget.NewSeparator(), description1, description2, description3, widget.NewSeparator(), step1Label, downloadTrainingDataButton, widget.NewSeparator(), step2Label, extractTrainingDataButton, widget.NewSeparator(), step3Label, createTrainingDataButton, widget.NewSeparator(), step4Label, trainModelsButton, widget.NewSeparator(), step5Label, testModelsButton) + + window.SetContent(page) +} + + +func setDownloadTrainingDataPage(window fyne.Window, previousPage func()){ + + currentPage := func(){setDownloadTrainingDataPage(window, previousPage)} + + title := getBoldLabelCentered("Download Training Data") + + backButton := getBackButtonCentered(previousPage) + + description1 := getLabelCentered("You must download the OpenSNP.org data dump file.") + description2 := getLabelCentered("This is a .tar.gz file which was created in August of 2023.") + description3 := getLabelCentered("It will be hosted on IPFS, a decentralized data sharing network.") + description4 := getLabelCentered("You must use an IPFS client to download the file.") + description5 := getLabelCentered("You can also download it via a torrent or web server if someone shares it elsewhere.") + + currentClipboard := window.Clipboard() + + ipfsIdentifierTitle := getLabelCentered("IPFS Content Identifier:") + + ipfsIdentifierLabel := getBoldLabelCentered("Qme64v7Go941s3psokZ7aDngQR6Tdv55jDhUDdLZXsRiRh") + + ipfsIdentifierCopyToClipboardButton := getWidgetCentered(widget.NewButtonWithIcon("Copy", theme.ContentCopyIcon(), func(){ + + currentClipboard.SetContent("Qme64v7Go941s3psokZ7aDngQR6Tdv55jDhUDdLZXsRiRh") + })) + + fileNameTitle := getLabelCentered("File Name:") + + fileNameLabel := getBoldLabelCentered("OpenSNPDataArchive.tar.gz") + + fileHashTitle := getLabelCentered("File SHA256 Checksum Hash:") + + fileHashLabel := getBoldLabelCentered("49f84fb71cb12df718a80c1ce25f6370ba758cbee8f24bd8a6d4f0da2e3c51ee") + + fileSizeTitle := getLabelCentered("File Size:") + + fileSizeLabel := getBoldLabelCentered("48,961,240 bytes (50.1 GB)") + + fileExtractedSizeTitle := getLabelCentered("File Extracted Size:") + + fileExtractedSizeLabel := getBoldLabelCentered("128,533,341,751 bytes (119.7 GB)") + + verifyFileTitle := getBoldLabelCentered("Verify File") + + verifyFileDescription1 := getLabelCentered("You can use the Seekia client to verify your downloaded file.") + verifyFileDescription2 := getLabelCentered("Press the button below and select your file.") + verifyFileDescription3 := getLabelCentered("This will take a while, because the file contents must be hashed.") + + selectFileCallbackFunction := func(fyneFileObject fyne.URIReadCloser, err error){ + + if (err != nil){ + setErrorEncounteredPage(window, err, currentPage) + return + } + + if (fyneFileObject == nil){ + return + } + + setLoadingScreen(window, "Hashing File", "Calculating file hash...") + + filePath := fyneFileObject.URI().String() + filePath = strings.TrimPrefix(filePath, "file://") + + fileObject, err := os.Open(filePath) + if (err != nil){ + setErrorEncounteredPage(window, err, currentPage) + return + } + defer fileObject.Close() + + //TODO: Use Blake3 instead of sha256 for faster hashing + + hasher := sha256.New() + _, err = io.Copy(hasher, fileObject) + if (err != nil){ + setErrorEncounteredPage(window, err, currentPage) + return + } + + hashResultBytes := hasher.Sum(nil) + + expectedResult := "49f84fb71cb12df718a80c1ce25f6370ba758cbee8f24bd8a6d4f0da2e3c51ee" + + expectedResultBytes, err := encoding.DecodeHexStringToBytes(expectedResult) + if (err != nil){ + setErrorEncounteredPage(window, err, currentPage) + return + } + + currentPage() + + bytesAreEqual := bytes.Equal(hashResultBytes, expectedResultBytes) + if (bytesAreEqual == false){ + title := "File Is Invalid" + dialogMessage1 := getLabelCentered("The file you downloaded is not valid.") + dialogMessage2 := getLabelCentered("The SHA256 Checksum does not match the expected checksum.") + dialogContent := container.NewVBox(dialogMessage1, dialogMessage2) + dialog.ShowCustom(title, "Close", dialogContent, window) + } else { + title := "File Is Valid" + dialogMessage1 := getLabelCentered("The file you downloaded is valid!") + dialogMessage2 := getLabelCentered("The SHA256 Checksum matches the expected checksum.") + dialogContent := container.NewVBox(dialogMessage1, dialogMessage2) + dialog.ShowCustom(title, "Close", dialogContent, window) + } + } + + verifyFileButton := getWidgetCentered(widget.NewButton("Verify File", func(){ + dialog.ShowFileOpen(selectFileCallbackFunction, window) + })) + + page := container.NewVBox(title, backButton, widget.NewSeparator(), description1, description2, description3, description4, description5, widget.NewSeparator(), ipfsIdentifierTitle, ipfsIdentifierLabel, ipfsIdentifierCopyToClipboardButton, widget.NewSeparator(), fileNameTitle, fileNameLabel, widget.NewSeparator(), fileHashTitle, fileHashLabel, widget.NewSeparator(), fileSizeTitle, fileSizeLabel, widget.NewSeparator(), fileExtractedSizeTitle, fileExtractedSizeLabel, widget.NewSeparator(), verifyFileTitle, verifyFileDescription1, verifyFileDescription2, verifyFileDescription3, verifyFileButton) + + scrollablePage := container.NewVScroll(page) + + window.SetContent(scrollablePage) +} + + +func setExtractTrainingDataPage(window fyne.Window, previousPage func()){ + + currentPage := func(){setExtractTrainingDataPage(window, previousPage)} + + title := getBoldLabelCentered("Extract Training Data") + + backButton := getBackButtonCentered(previousPage) + + description1 := getLabelCentered("You must extract the downloaded OpenSNPDataArchive.tar.gz to a folder.") + description2 := getLabelCentered("Once you have extracted the file, select the extracted folder using the page below.") + + currentLocationTitle := getLabelCentered("Current Folder Location:") + + getCurrentLocationLabel := func()(*fyne.Container, error){ + + fileExists, fileContents, err := localFilesystem.GetFileContents("./OpenSNPDataArchiveFolderpath.txt") + if (err != nil) { return nil, err } + if (fileExists == false){ + noneLabel := getItalicLabelCentered("None") + return noneLabel, nil + } + + folderpathLabel := getBoldLabelCentered(string(fileContents)) + + return folderpathLabel, nil + } + + currentLocationLabel, err := getCurrentLocationLabel() + if (err != nil) { + setErrorEncounteredPage(window, err, previousPage) + return + } + + selectFolderCallbackFunction := func(folderObject fyne.ListableURI, err error){ + + if (err != nil){ + title := "Failed to open folder." + dialogMessage := getLabelCentered("Report this error to Seekia developers: " + err.Error()) + dialogContent := container.NewVBox(dialogMessage) + dialog.ShowCustom(title, "Close", dialogContent, window) + return + } + + if (folderObject == nil) { + return + } + + folderPath := folderObject.Path() + + fileContents := []byte(folderPath) + + err = localFilesystem.CreateOrOverwriteFile(fileContents, "./", "OpenSNPDataArchiveFolderpath.txt") + if (err != nil){ + title := "Failed to save file." + dialogMessage := getLabelCentered("Report this error to Seekia developers: " + err.Error()) + dialogContent := container.NewVBox(dialogMessage) + dialog.ShowCustom(title, "Close", dialogContent, window) + return + } + + currentPage() + } + + selectFolderLocationButton := getWidgetCentered(widget.NewButtonWithIcon("Select Folder Location", theme.FolderIcon(), func(){ + dialog.ShowFolderOpen(selectFolderCallbackFunction, window) + })) + + page := container.NewVBox(title, backButton, widget.NewSeparator(), description1, description2, widget.NewSeparator(), currentLocationTitle, currentLocationLabel, widget.NewSeparator(), selectFolderLocationButton) + + scrollablePage := container.NewVScroll(page) + + window.SetContent(scrollablePage) +} + + +func setCreateTrainingDataPage(window fyne.Window, previousPage func()){ + + currentPage := func(){setCreateTrainingDataPage(window, previousPage)} + + title := getBoldLabelCentered("Create Training Data") + + backButton := getBackButtonCentered(previousPage) + + description1 := getLabelCentered("Press the button below to begin creating the training data.") + description2 := getLabelCentered("This will prepare each user's genome into a file to use to train each neural network.") + description3 := getLabelCentered("This will take a while.") + + beginCreatingButton := getWidgetCentered(widget.NewButtonWithIcon("Begin Creating Data", theme.MediaPlayIcon(), func(){ + setStartAndMonitorCreateTrainingDataPage(window, currentPage) + })) + + page := container.NewVBox(title, backButton, widget.NewSeparator(), description1, description2, description3, beginCreatingButton) + + window.SetContent(page) +} + +func setStartAndMonitorCreateTrainingDataPage(window fyne.Window, previousPage func()){ + + traits.InitializeTraitVariables() + + err := locusMetadata.InitializeLocusMetadataVariables() + if (err != nil){ + setErrorEncounteredPage(window, err, previousPage) + return + } + + title := getBoldLabelCentered("Creating Training Data") + + fileExists, fileContents, err := localFilesystem.GetFileContents("./OpenSNPDataArchiveFolderpath.txt") + if (err != nil) { + setErrorEncounteredPage(window, err, previousPage) + return + } + if (fileExists == false){ + + backButton := getBackButtonCentered(previousPage) + + description1 := getBoldLabelCentered("You have not selected your OpenSNP data archive folderpath.") + description2 := getLabelCentered("Go back to step 2 and follow the instructions.") + + page := container.NewVBox(title, backButton, widget.NewSeparator(), description1, description2) + + window.SetContent(page) + return + } + + dataArchiveFolderpath := string(fileContents) + + progressDetailsBinding := binding.NewString() + progressPercentageBinding := binding.NewFloat() + + loadingBar := getWidgetCentered(widget.NewProgressBarWithData(progressPercentageBinding)) + + progressDetailsTitle := getBoldLabelCentered("Progress Details:") + + progressDetailsLabel := widget.NewLabelWithData(progressDetailsBinding) + progressDetailsLabel.TextStyle = fyne.TextStyle{ + Bold: false, + Italic: true, + Monospace: false, + } + + progressDetailsLabelCentered := getWidgetCentered(progressDetailsLabel) + + // We set this bool to true to stop the createData process + var createDataIsStoppedBoolMutex sync.RWMutex + createDataIsStoppedBool := false + + cancelButton := getWidgetCentered(widget.NewButtonWithIcon("Cancel", theme.CancelIcon(), func(){ + + createDataIsStoppedBoolMutex.Lock() + createDataIsStoppedBool = true + createDataIsStoppedBoolMutex.Unlock() + + previousPage() + })) + + page := container.NewVBox(title, widget.NewSeparator(), loadingBar, progressDetailsTitle, progressDetailsLabelCentered, widget.NewSeparator(), cancelButton) + + window.SetContent(page) + + createTrainingDataFunction := func(){ + + //Outputs: + // -bool: Process completed (true == was not stopped mid-way) + // -bool: Data archive is well formed + // -error + createTrainingData := func()(bool, bool, error){ + + phenotypesFilepath := goFilepath.Join(dataArchiveFolderpath, "OpenSNPData", "phenotypes_202308230100.csv") + + fileObject, err := os.Open(phenotypesFilepath) + if (err != nil){ + + fileDoesNotExist := os.IsNotExist(err) + if (fileDoesNotExist == true){ + // Archive is corrupt + return true, false, nil + } + + return false, false, err + } + defer fileObject.Close() + + fileIsWellFormed, userPhenotypesList_OpenSNP := readBiobankData.ReadOpenSNPPhenotypesFile(fileObject) + if (fileIsWellFormed == false){ + // Archive is corrupt + return true, false, nil + } + + // This is the folderpath for the folder which contains all of the user raw genomes + openSNPRawGenomesFolderpath := goFilepath.Join(dataArchiveFolderpath, "OpenSNPData") + + filesList, err := os.ReadDir(openSNPRawGenomesFolderpath) + if (err != nil) { return false, false, err } + + // Map Structure: User ID -> List of user raw genome filepaths + userRawGenomeFilepathsMap := make(map[int][]string) + + for _, filesystemObject := range filesList{ + + filepathIsFolder := filesystemObject.IsDir() + if (filepathIsFolder == true){ + // Archive is corrupt + return true, false, nil + } + + fileName := filesystemObject.Name() + + // Example of a raw genome filename: "user1_file9_yearofbirth_1985_sex_XY.23andme" + + userIDWithRawGenomeInfo, fileIsUserGenome := strings.CutPrefix(fileName, "user") + if (fileIsUserGenome == false){ + // File is not a user genome, skip it. + continue + } + + userIDString, rawGenomeInfo, separatorFound := strings.Cut(userIDWithRawGenomeInfo, "_") + if (separatorFound == false){ + // Archive is corrupt + return true, false, nil + } + + userID, err := helpers.ConvertStringToInt(userIDString) + if (err != nil){ + // Archive is corrupt + return true, false, nil + } + + getFileIsReadableStatus := func()bool{ + + is23andMe := strings.HasSuffix(rawGenomeInfo, ".23andme.txt") + if (is23andMe == true){ + // We can read this file + return true + } + + isAncestry := strings.HasSuffix(rawGenomeInfo, ".ancestry.txt") + if (isAncestry == true){ + // We can read this file + return true + } + + // We cannot read this raw genome file + //TODO: Add ability to read more raw genome files + + return false + } + + fileIsReadable := getFileIsReadableStatus() + if (fileIsReadable == true){ + + rawGenomeFilepath := goFilepath.Join(openSNPRawGenomesFolderpath, fileName) + + existingList, exists := userRawGenomeFilepathsMap[userID] + if (exists == false){ + userRawGenomeFilepathsMap[userID] = []string{rawGenomeFilepath} + } else { + existingList = append(existingList, rawGenomeFilepath) + userRawGenomeFilepathsMap[userID] = existingList + } + } + } + + // We create folder to store training data + + _, err = localFilesystem.CreateFolder("./TrainingData") + if (err != nil) { return false, false, err } + + _, err = localFilesystem.CreateFolder("./TrainingData/EyeColor") + if (err != nil) { return false, false, err } + + numberOfUserPhenotypeDataObjects := len(userPhenotypesList_OpenSNP) + maximumIndex := numberOfUserPhenotypeDataObjects-1 + + numberOfUsersString := helpers.ConvertIntToString(numberOfUserPhenotypeDataObjects) + + for index, userPhenotypeDataObject := range userPhenotypesList_OpenSNP{ + + trainingProgressPercentage, err := helpers.ScaleNumberProportionally(true, index, 0, maximumIndex, 0, 100) + if (err != nil) { return false, false, err } + + trainingProgressFloat64 := float64(trainingProgressPercentage)/100 + + err = progressPercentageBinding.Set(trainingProgressFloat64) + if (err != nil) { return false, false, err } + + + createDataIsStoppedBoolMutex.RLock() + createDataIsStopped := createDataIsStoppedBool + createDataIsStoppedBoolMutex.RUnlock() + + if (createDataIsStopped == true){ + // User exited the process + return false, false, nil + } + + userIndexString := helpers.ConvertIntToString(index + 1) + + progressDetailsStatus := "Processing User " + userIndexString + "/" + numberOfUsersString + + err = progressDetailsBinding.Set(progressDetailsStatus) + if (err != nil) { return false, false, err } + + userID := userPhenotypeDataObject.UserID + + userRawGenomeFilepathsList, exists := userRawGenomeFilepathsMap[userID] + if (exists == false){ + // User has no genomes + continue + } + + // We read all of the user's raw genomes and combine them into a single genomeMap which excludes conflicting loci values + + userRawGenomesWithMetadataList := make([]prepareRawGenomes.RawGenomeWithMetadata, 0) + + for _, userRawGenomeFilepath := range userRawGenomeFilepathsList{ + + //Outputs: + // -bool: Able to read raw genome file + // -bool: Genome is phased + // -map[int64]readRawGenomes.RawGenomeLocusValue + // -error + readRawGenomeMap := func()(bool, bool, map[int64]readRawGenomes.RawGenomeLocusValue, error){ + + fileObject, err := os.Open(userRawGenomeFilepath) + if (err != nil) { return false, false, nil, err } + defer fileObject.Close() + + _, _, _, _, genomeIsPhased, rawGenomeMap, err := readRawGenomes.ReadRawGenomeFile(fileObject) + if (err != nil) { + //log.Println("Raw genome file is malformed: " + userRawGenomeFilepath + ". Reason: " + err.Error()) + return false, false, nil, nil + } + + return true, genomeIsPhased, rawGenomeMap, nil + } + + ableToReadRawGenome, rawGenomeIsPhased, rawGenomeMap, err := readRawGenomeMap() + if (err != nil){ return false, false, err } + if (ableToReadRawGenome == false){ + // We cannot read this genome file + // Many of the genome files are unreadable. + //TODO: Improve ability to read slightly corrupted genome files + continue + } + + newGenomeIdentifier, err := helpers.GetNewRandomHexString(16) + if (err != nil) { return false, false, err } + + rawGenomeWithMetadata := prepareRawGenomes.RawGenomeWithMetadata{ + + GenomeIdentifier: newGenomeIdentifier, + GenomeIsPhased: rawGenomeIsPhased, + RawGenomeMap: rawGenomeMap, + } + + userRawGenomesWithMetadataList = append(userRawGenomesWithMetadataList, rawGenomeWithMetadata) + } + + if (len(userRawGenomesWithMetadataList) == 0){ + // None of the user's genome files are readable + continue + } + + getUserLociValuesMap := func()(map[int64]locusValue.LocusValue, error){ + + updatePercentageCompleteFunction := func(_ int)error{ + + return nil + } + + genomesWithMetadataList, _, combinedGenomesExist, onlyExcludeConflictsGenomeIdentifier, _, err := prepareRawGenomes.GetGenomesWithMetadataListFromRawGenomesList(userRawGenomesWithMetadataList, updatePercentageCompleteFunction) + if (err != nil) { return nil, err } + if (combinedGenomesExist == false){ + + if (len(genomesWithMetadataList) != 1){ + return nil, errors.New("GetGenomesWithMetadataListFromRawGenomesList returning non-1 length genomesWithMetadataList when combinedGenomesExist == false") + } + + // Only 1 genome exists + + genomeWithMetadataObject := genomesWithMetadataList[0] + + genomeMap := genomeWithMetadataObject.GenomeMap + + return genomeMap, nil + } + + for _, genomeWithMetadataObject := range genomesWithMetadataList{ + + genomeIdentifier := genomeWithMetadataObject.GenomeIdentifier + + if (genomeIdentifier == onlyExcludeConflictsGenomeIdentifier){ + + genomeMap := genomeWithMetadataObject.GenomeMap + + return genomeMap, nil + } + } + + return nil, errors.New("OnlyExcludeConflicts genome not found from GetGenomesWithMetadataListFromRawGenomesList's returned list.") + } + + userLociValuesMap, err := getUserLociValuesMap() + if (err != nil) { return false, false, err } + + //TODO: Add more traits + traitNamesList := []string{"Eye Color"} + + for _, traitName := range traitNamesList{ + + traitNameWithoutWhitespace := strings.ReplaceAll(traitName, " ", "") + + trainingDataFolderpath := goFilepath.Join("./TrainingData", traitNameWithoutWhitespace) + + userDataExists, userTrainingDataList, err := geneticPrediction.CreateGeneticPredictionTrainingData_OpenSNP(traitName, userPhenotypeDataObject, userLociValuesMap) + if (err != nil) { return false, false, err } + if (userDataExists == false){ + // User cannot be used for training + continue + } + + for index, trainingData := range userTrainingDataList{ + + userTrainingDataBytes, err := geneticPrediction.EncodeTrainingDataObjectToBytes(trainingData) + if (err != nil) { return false, false, err } + + trainingDataIndexString := helpers.ConvertIntToString(index+1) + + userIDString := helpers.ConvertIntToString(userID) + + trainingDataFilename := "User" + userIDString + "_TrainingData_" + trainingDataIndexString + ".gob" + + err = localFilesystem.CreateOrOverwriteFile(userTrainingDataBytes, trainingDataFolderpath, trainingDataFilename) + if (err != nil) { return false, false, err } + } + } + } + + return true, false, nil + } + + processIsComplete, archiveIsCorrupt, err := createTrainingData() + if (err != nil){ + setErrorEncounteredPage(window, err, previousPage) + return + } + if (processIsComplete == false){ + // User exited the page + return + } + if (archiveIsCorrupt == true){ + + title := getBoldLabelCentered("OpenSNP Archive Is Corrupt") + + description1 := getBoldLabelCentered("Your downloaded OpenSNP data archive is corrupt.") + description2 := getLabelCentered("The extracted folder contents do not match what the archive should contain.") + description3 := getLabelCentered("You should re-extract the contents of the archive.") + + exitButton := getWidgetCentered(widget.NewButtonWithIcon("Exit", theme.CancelIcon(), previousPage)) + + page := container.NewVBox(title, widget.NewSeparator(), description1, description2, description3, exitButton) + + window.SetContent(page) + return + } + + setCreateTrainingDataIsCompletePage(window) + } + + go createTrainingDataFunction() +} + +func setCreateTrainingDataIsCompletePage(window fyne.Window){ + + title := getBoldLabelCentered("Creating Data Is Complete") + + description1 := getLabelCentered("Creating training data is complete!") + description2 := getLabelCentered("The data have been saved in the TrainingData folder.") + + exitButton := getWidgetCentered(widget.NewButtonWithIcon("Exit", theme.CancelIcon(), func(){ + setHomePage(window) + })) + + page := container.NewVBox(title, widget.NewSeparator(), description1, description2, exitButton) + + window.SetContent(page) +} + + + +func setTrainModelsPage(window fyne.Window, previousPage func()){ + + currentPage := func(){setTrainModelsPage(window, previousPage)} + + title := getBoldLabelCentered("Train Models") + + backButton := getBackButtonCentered(previousPage) + + description1 := getLabelCentered("Press the button below to begin training the genetic models.") + description2 := getLabelCentered("This will train each neural network using the user training data.") + description3 := getLabelCentered("This will take a while.") + + beginTrainingButton := getWidgetCentered(widget.NewButtonWithIcon("Begin Training Models", theme.MediaPlayIcon(), func(){ + setStartAndMonitorTrainModelsPage(window, currentPage) + })) + + page := container.NewVBox(title, backButton, widget.NewSeparator(), description1, description2, description3, beginTrainingButton) + + window.SetContent(page) +} + + + +func setStartAndMonitorTrainModelsPage(window fyne.Window, previousPage func()){ + + title := getBoldLabelCentered("Train Models") + + //TODO: Verify TrainingData folder integrity + + progressDetailsBinding := binding.NewString() + progressPercentageBinding := binding.NewFloat() + + loadingBar := getWidgetCentered(widget.NewProgressBarWithData(progressPercentageBinding)) + + progressDetailsTitle := getBoldLabelCentered("Progress Details:") + + progressDetailsLabel := widget.NewLabelWithData(progressDetailsBinding) + progressDetailsLabel.TextStyle = fyne.TextStyle{ + Bold: false, + Italic: true, + Monospace: false, + } + + progressDetailsLabelCentered := getWidgetCentered(progressDetailsLabel) + + // We set this bool to true to stop the trainModels process + var trainModelsIsStoppedBoolMutex sync.RWMutex + trainModelsIsStoppedBool := false + + cancelButton := getWidgetCentered(widget.NewButtonWithIcon("Cancel", theme.CancelIcon(), func(){ + + trainModelsIsStoppedBoolMutex.Lock() + trainModelsIsStoppedBool = true + trainModelsIsStoppedBoolMutex.Unlock() + + previousPage() + })) + + page := container.NewVBox(title, widget.NewSeparator(), loadingBar, progressDetailsTitle, progressDetailsLabelCentered, widget.NewSeparator(), cancelButton) + + window.SetContent(page) + + trainModelsFunction := func(){ + + //Outputs: + // -bool: Process completed (true == was not stopped mid-way) + // -error + trainModels := func()(bool, error){ + + _, err := localFilesystem.CreateFolder("./TrainedModels") + if (err != nil) { return false, err } + + traitNamesList := []string{"Eye Color"} + + for _, traitName := range traitNamesList{ + + trainingSetFilepathsList, _, err := getTrainingAndTestingDataFilepathLists(traitName) + if (err != nil) { return false, err } + + // We create a new neural network object to train + neuralNetworkObject, err := geneticPrediction.GetNewUntrainedNeuralNetworkObject(traitName) + if (err != nil) { return false, err } + + numberOfTrainingDatas := len(trainingSetFilepathsList) + numberOfTrainingDatasString := helpers.ConvertIntToString(numberOfTrainingDatas) + + finalIndex := numberOfTrainingDatas - 1 + + for index, filePath := range trainingSetFilepathsList{ + + trainModelsIsStoppedBoolMutex.RLock() + trainModelsIsStopped := trainModelsIsStoppedBool + trainModelsIsStoppedBoolMutex.RUnlock() + + if (trainModelsIsStopped == true){ + // User exited the process + return false, nil + } + + fileExists, fileContents, err := localFilesystem.GetFileContents(filePath) + if (err != nil) { return false, err } + if (fileExists == false){ + return false, errors.New("TrainingData file not found: " + filePath) + } + + trainingDataObject, err := geneticPrediction.DecodeBytesToTrainingDataObject(fileContents) + if (err != nil) { return false, err } + + err = geneticPrediction.TrainNeuralNetwork(traitName, neuralNetworkObject, trainingDataObject) + if (err != nil) { return false, err } + + exampleIndexString := helpers.ConvertIntToString(index+1) + numberOfExamplesProgress := "Trained " + exampleIndexString + "/" + numberOfTrainingDatasString + " Examples" + + progressDetailsBinding.Set(numberOfExamplesProgress) + + percentageProgressInt, err := helpers.ScaleNumberProportionally(true, index, 0, finalIndex, 0, 100) + if (err != nil) { return false, err } + + newProgressFloat64 := float64(percentageProgressInt)/100 + + progressPercentageBinding.Set(newProgressFloat64) + } + + // Network training is complete. + // We now save the neural network as a .gob file + + neuralNetworkBytes, err := geneticPrediction.EncodeNeuralNetworkObjectToBytes(*neuralNetworkObject) + if (err != nil) { return false, err } + + traitNameWithoutWhitespaces := strings.ReplaceAll(traitName, " ", "") + + neuralNetworkFilename := traitNameWithoutWhitespaces + "Model.gob" + + err = localFilesystem.CreateOrOverwriteFile(neuralNetworkBytes, "./TrainedModels/", neuralNetworkFilename) + if (err != nil) { return false, err } + } + + progressPercentageBinding.Set(1) + + return true, nil + } + + processIsComplete, err := trainModels() + if (err != nil){ + setErrorEncounteredPage(window, err, previousPage) + return + } + if (processIsComplete == false){ + // User exited the page + return + } + + setTrainModelsIsCompletePage(window) + } + + go trainModelsFunction() +} + + +func setTrainModelsIsCompletePage(window fyne.Window){ + + title := getBoldLabelCentered("Training Models Is Complete") + + description1 := getLabelCentered("Model training is complete!") + description2 := getLabelCentered("The models have been saved in the TrainedModels folder.") + + exitButton := getWidgetCentered(widget.NewButtonWithIcon("Exit", theme.CancelIcon(), func(){ + setHomePage(window) + })) + + page := container.NewVBox(title, widget.NewSeparator(), description1, description2, exitButton) + + window.SetContent(page) +} + + +func setTestModelsPage(window fyne.Window, previousPage func()){ + + currentPage := func(){setTestModelsPage(window, previousPage)} + + title := getBoldLabelCentered("Test Models") + + backButton := getBackButtonCentered(previousPage) + + description1 := getLabelCentered("Press the button below to begin testing the genetic models.") + description2 := getLabelCentered("This will test each neural network using user training data examples.") + description3 := getLabelCentered("The testing data is not used to train the models.") + description4 := getLabelCentered("The results of the testing will be displayed at the end.") + + beginTestingButton := getWidgetCentered(widget.NewButtonWithIcon("Begin Testing Models", theme.MediaPlayIcon(), func(){ + setStartAndMonitorTestModelsPage(window, currentPage) + })) + + page := container.NewVBox(title, backButton, widget.NewSeparator(), description1, description2, description3, description4, beginTestingButton) + + window.SetContent(page) +} + + +func setStartAndMonitorTestModelsPage(window fyne.Window, previousPage func()){ + + title := getBoldLabelCentered("Test Models") + + progressDetailsBinding := binding.NewString() + progressPercentageBinding := binding.NewFloat() + + loadingBar := getWidgetCentered(widget.NewProgressBarWithData(progressPercentageBinding)) + + progressDetailsTitle := getBoldLabelCentered("Progress Details:") + + progressDetailsLabel := widget.NewLabelWithData(progressDetailsBinding) + progressDetailsLabel.TextStyle = fyne.TextStyle{ + Bold: false, + Italic: true, + Monospace: false, + } + + progressDetailsLabelCentered := getWidgetCentered(progressDetailsLabel) + + // We set this bool to true to stop the testModels process + var testModelsIsStoppedBoolMutex sync.RWMutex + testModelsIsStoppedBool := false + + cancelButton := getWidgetCentered(widget.NewButtonWithIcon("Cancel", theme.CancelIcon(), func(){ + + testModelsIsStoppedBoolMutex.Lock() + testModelsIsStoppedBool = true + testModelsIsStoppedBoolMutex.Unlock() + + previousPage() + })) + + page := container.NewVBox(title, widget.NewSeparator(), loadingBar, progressDetailsTitle, progressDetailsLabelCentered, widget.NewSeparator(), cancelButton) + + window.SetContent(page) + + testModelsFunction := func(){ + + // This map stores the accuracy for each model + // Map Structure: Trait Name -> Accuracy (A value between 0 and 1, 1 is fully accurate, 0 is fully inaccurate) + traitAverageAccuracyMap := make(map[string]float32) + + //Outputs: + // -bool: Process completed (true == was not stopped mid-way) + // -error + testModels := func()(bool, error){ + + traitNamesList := []string{"Eye Color"} + + for _, traitName := range traitNamesList{ + + _, testingSetFilepathsList, err := getTrainingAndTestingDataFilepathLists(traitName) + if (err != nil) { return false, err } + + traitNameWithoutWhitespaces := strings.ReplaceAll(traitName, " ", "") + + // We read the trained model for this trait + modelFilename := traitNameWithoutWhitespaces + "Model.gob" + + trainedModelFilepath := goFilepath.Join("./TrainedModels/", modelFilename) + + fileExists, fileContents, err := localFilesystem.GetFileContents(trainedModelFilepath) + if (err != nil) { return false, err } + if (fileExists == false){ + return false, errors.New("TrainedModel not found: " + trainedModelFilepath) + } + + neuralNetworkObject, err := geneticPrediction.DecodeBytesToNeuralNetworkObject(fileContents) + if (err != nil) { return false, err } + + numberOfTrainingDatas := len(testingSetFilepathsList) + numberOfTrainingDatasString := helpers.ConvertIntToString(numberOfTrainingDatas) + + finalIndex := numberOfTrainingDatas - 1 + + // This is the sum of accuracy for each training data + accuracySum := float32(0) + + for index, filePath := range testingSetFilepathsList{ + + testModelsIsStoppedBoolMutex.RLock() + testModelsIsStopped := testModelsIsStoppedBool + testModelsIsStoppedBoolMutex.RUnlock() + + if (testModelsIsStopped == true){ + // User exited the process + return false, nil + } + + fileExists, fileContents, err := localFilesystem.GetFileContents(filePath) + if (err != nil) { return false, err } + if (fileExists == false){ + return false, errors.New("TrainingData file not found: " + filePath) + } + + trainingDataObject, err := geneticPrediction.DecodeBytesToTrainingDataObject(fileContents) + if (err != nil) { return false, err } + + trainingDataInputLayer := trainingDataObject.InputLayer + trainingDataExpectedOutputLayer := trainingDataObject.OutputLayer + + predictionLayer, err := geneticPrediction.GetNeuralNetworkRawPrediction(&neuralNetworkObject, trainingDataInputLayer) + if (err != nil) { return false, err } + + numberOfPredictionNeurons := len(predictionLayer) + + if (len(trainingDataExpectedOutputLayer) != numberOfPredictionNeurons){ + return false, errors.New("Neural network prediction output length does not match expected output length.") + } + + // TODO: Improve how we calculate the accuracy + // We should take into account the number of loci that were provided by the user's genome, + // and display an accuracy for each number of loci provided. + // For example, if 90% of loci values were provided, accuracy is 80%. If only 10% were provided, accuracy is 20%. + + // This is the sum of the distance between the expected values and the predicted values + totalDistance := float32(0) + + for index, element := range predictionLayer{ + + // Each element is a neuron value between 0 and 1 + // We see how far away the answer is from the expected value + + expectedValue := trainingDataExpectedOutputLayer[index] + + distance := element - expectedValue + + // We make distance positive + if (distance < 0){ + distance = -distance + } + + totalDistance += distance + } + + averageDistance := totalDistance/float32(numberOfPredictionNeurons) + + accuracy := 1 - averageDistance + + accuracySum += accuracy + + exampleIndexString := helpers.ConvertIntToString(index+1) + numberOfExamplesProgress := "Tested " + exampleIndexString + "/" + numberOfTrainingDatasString + " Examples" + + progressDetailsBinding.Set(numberOfExamplesProgress) + + percentageProgressInt, err := helpers.ScaleNumberProportionally(true, index, 0, finalIndex, 0, 100) + if (err != nil) { return false, err } + + newProgressFloat64 := float64(percentageProgressInt)/100 + + progressPercentageBinding.Set(newProgressFloat64) + } + + averageAccuracy := accuracySum/float32(numberOfTrainingDatas) + + traitAverageAccuracyMap[traitName] = averageAccuracy + } + + // Testing is complete. + + progressPercentageBinding.Set(1) + + return true, nil + } + + processIsComplete, err := testModels() + if (err != nil){ + setErrorEncounteredPage(window, err, previousPage) + return + } + if (processIsComplete == false){ + // User exited the page + return + } + + setTestModelsIsCompletePage(window, traitAverageAccuracyMap) + } + + go testModelsFunction() +} + + +// This function returns a list of training data and testing data filepaths for a trait. +//Outputs: +// -[]string: Sorted list of training data filepaths +// -[]string: Unsorted list of testing data filepaths +// -error +func getTrainingAndTestingDataFilepathLists(traitName string)([]string, []string, error){ + + if (traitName != "Eye Color"){ + return nil, nil, errors.New("getTrainingAndTestingDataFilepathLists called with invalid traitName: " + traitName) + } + + traitNameWithoutWhitespaces := strings.ReplaceAll(traitName, " ", "") + + trainingDataFolderpath := goFilepath.Join("./TrainingData/", traitNameWithoutWhitespaces) + + filesList, err := os.ReadDir(trainingDataFolderpath) + if (err != nil) { return nil, nil, err } + + // This stores the filepath for each training data + trainingDataFilepathsList := make([]string, 0, len(filesList)) + + for _, filesystemObject := range filesList{ + + filepathIsFolder := filesystemObject.IsDir() + if (filepathIsFolder == true){ + // Folder is corrupt + return nil, nil, errors.New("Training data is corrupt for trait: " + traitName) + } + + fileName := filesystemObject.Name() + + filepath := goFilepath.Join(trainingDataFolderpath, fileName) + + trainingDataFilepathsList = append(trainingDataFilepathsList, filepath) + } + + numberOfTrainingDataFiles := len(trainingDataFilepathsList) + + if (numberOfTrainingDataFiles == 0){ + return nil, nil, errors.New("No training data exists for trait: " + traitName) + } + + if ((numberOfTrainingDataFiles % 110) != 0){ + // There are 110 examples for each user. + return nil, nil, errors.New(traitName + " training data has an invalid number of examples.") + } + + getNumberOfExpectedTrainingDatas := func()(int, error){ + + if (traitName == "Eye Color"){ + + return 113190, nil + } + + return 0, errors.New("Unknown traitName: " + traitName) + } + + numberOfExpectedTrainingDatas, err := getNumberOfExpectedTrainingDatas() + if (err != nil){ return nil, nil, err } + + if (numberOfTrainingDataFiles != numberOfExpectedTrainingDatas){ + + numberOfTrainingDataFilesString := helpers.ConvertIntToString(numberOfTrainingDataFiles) + + return nil, nil, errors.New(traitName + " number of training datas is unexpected: " + numberOfTrainingDataFilesString) + } + + // We sort the training data to be in a deterministically random order + // This allows us to train the neural network in the same order each time + // We do this so we can generate deterministic models which are identical byte-for-byte + + // We have to set aside 200 user's training datas for testing the neural network + // + // We have to remove them per-user because each user has 110 training datas. + // Otherwise, we would be training and testing on data from the same users. + // We need to test with users that the models were never trained upon. + + // First we extract the user identifiers from the data + + userIdentifiersMap := make(map[int]struct{}) + + for _, trainingDataFilepath := range trainingDataFilepathsList{ + + // We have to extract the filename from the filepath + + trainingDataFilename := goFilepath.Base(trainingDataFilepath) + + // Example filepath format: "User4680_TrainingData_89.gob" + + trimmedFilename := strings.TrimPrefix(trainingDataFilename, "User") + + userIdentifierString, _, underscoreExists := strings.Cut(trimmedFilename, "_") + if (underscoreExists == false){ + return nil, nil, errors.New("Invalid trainingData filename: " + trainingDataFilename) + } + + userIdentifier, err := helpers.ConvertStringToInt(userIdentifierString) + if (err != nil){ + return nil, nil, errors.New("Invalid trainingData filename: " + trainingDataFilename) + } + + userIdentifiersMap[userIdentifier] = struct{}{} + } + + userIdentifiersList := helpers.GetListOfMapKeys(userIdentifiersMap) + + // We sort the user identifiers list in ascending order + + slices.Sort(userIdentifiersList) + + // Now we deterministically randomize the order of the user identifiers list + pseudorandomNumberGenerator := mathRand.New(mathRand.NewSource(1)) + + pseudorandomNumberGenerator.Shuffle(len(userIdentifiersList), func(i int, j int){ + userIdentifiersList[i], userIdentifiersList[j] = userIdentifiersList[j], userIdentifiersList[i] + }) + + trainingSetFilepathsList := make([]string, 0) + testingSetFilepathsList := make([]string, 0) + + numberOfUsers := len(userIdentifiersList) + + if (numberOfUsers < 250){ + return nil, nil, errors.New("Too few training data examples for trait: " + traitName) + } + + // We use 200 users for testing (validation), so we don't train using them + numberOfTrainingUsers := numberOfUsers - 200 + + for index, userIdentifier := range userIdentifiersList{ + + // Example filepath format: "User4680_TrainingData_89.gob" + + userIdentifierString := helpers.ConvertIntToString(userIdentifier) + + trainingDataFilenamePrefix := "User" + userIdentifierString + "_TrainingData_" + + for k:=1; k <= 110; k++{ + + kString := helpers.ConvertIntToString(k) + + trainingDataFilename := trainingDataFilenamePrefix + kString + ".gob" + + trainingDataFilepath := goFilepath.Join(trainingDataFolderpath, trainingDataFilename) + + if (index < numberOfTrainingUsers){ + trainingSetFilepathsList = append(trainingSetFilepathsList, trainingDataFilepath) + } else { + testingSetFilepathsList = append(testingSetFilepathsList, trainingDataFilepath) + } + } + } + + return trainingSetFilepathsList, testingSetFilepathsList, nil +} + +// +func setTestModelsIsCompletePage(window fyne.Window, traitPredictionAccuracyMap map[string]float32){ + + title := getBoldLabelCentered("Testing Models Is Complete") + + description1 := getLabelCentered("Model testing is complete!") + description2 := getLabelCentered("The results of the testing are below.") + + getResultsGrid := func()(*fyne.Container, error){ + + traitNameTitle := getItalicLabelCentered("Trait Name") + + predictionAccuracyTitle := getItalicLabelCentered("Prediction Accuracy") + + traitNameColumn := container.NewVBox(traitNameTitle, widget.NewSeparator()) + predictionAccuracyColumn := container.NewVBox(predictionAccuracyTitle, widget.NewSeparator()) + + traitNamesList := helpers.GetListOfMapKeys(traitPredictionAccuracyMap) + + for _, traitName := range traitNamesList{ + + traitNameLabel := getBoldLabelCentered(traitName) + + traitPredictionAccuracy, exists := traitPredictionAccuracyMap[traitName] + if (exists == false){ + return nil, errors.New("traitPredictionAccuracyMap missing traitName: " + traitName) + } + + traitPredictionAccuracyString := helpers.ConvertFloat64ToStringRounded(float64(traitPredictionAccuracy)*100, 2) + + traitPredictionAccuracyFormatted := traitPredictionAccuracyString + "%" + + traitPredictionAccuracyLabel := getBoldLabelCentered(traitPredictionAccuracyFormatted) + + traitNameColumn.Add(traitNameLabel) + predictionAccuracyColumn.Add(traitPredictionAccuracyLabel) + + traitNameColumn.Add(widget.NewSeparator()) + predictionAccuracyColumn.Add(widget.NewSeparator()) + } + + resultsGrid := container.NewHBox(layout.NewSpacer(), traitNameColumn, predictionAccuracyColumn, layout.NewSpacer()) + + return resultsGrid, nil + } + + resultsGrid, err := getResultsGrid() + if (err != nil){ + setErrorEncounteredPage(window, err, func(){setHomePage(window)}) + return + } + + exitButton := getWidgetCentered(widget.NewButtonWithIcon("Exit", theme.CancelIcon(), func(){ + setHomePage(window) + })) + + page := container.NewVBox(title, widget.NewSeparator(), description1, description2, exitButton, widget.NewSeparator(), resultsGrid) + + window.SetContent(page) +} + +// We use this to define a custom fyne theme +// We are only overriding the foreground color to pure black +type customTheme struct{ + defaultTheme fyne.Theme +} + +func getCustomFyneTheme()fyne.Theme{ + + standardThemeObject := theme.LightTheme() + + newTheme := customTheme{ + defaultTheme: standardThemeObject, + } + + return newTheme +} + + +// 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{ + + switch colorName{ + + case theme.ColorNameForeground:{ + + newColor := color.Black + return newColor + } + case theme.ColorNameSeparator:{ + + // This is the color used for separators + + newColor := color.Black + return newColor + } + case theme.ColorNameInputBackground:{ + + // This color is used for the background of input elements such as text entries + newColor, err := imagery.GetColorObjectFromColorCode("b3b3b3") + if (err == nil){ + return newColor + } + } + case theme.ColorNameButton:{ + + // This is the color used for buttons + newColor, err := imagery.GetColorObjectFromColorCode("d8d8d8") + 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 + } + } + + } + + // 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 +} + + + diff --git a/utilities/createSampleGeneticAnalyses/.gitignore b/utilities/createSampleGeneticAnalyses/.gitignore new file mode 100644 index 0000000..f296b3a --- /dev/null +++ b/utilities/createSampleGeneticAnalyses/.gitignore @@ -0,0 +1,5 @@ +Person1RawGenome.txt +Person2RawGenome.txt +SamplePerson1Analysis.json +SamplePerson2Analysis.json +SampleCoupleAnalysis.json \ No newline at end of file diff --git a/utilities/createSampleGeneticAnalyses/createSampleGeneticAnalyses.go b/utilities/createSampleGeneticAnalyses/createSampleGeneticAnalyses.go new file mode 100644 index 0000000..e303db6 --- /dev/null +++ b/utilities/createSampleGeneticAnalyses/createSampleGeneticAnalyses.go @@ -0,0 +1,137 @@ + +// createSampleGeneticAnalyses.go is a utility used to create the sample genetic analyses. +// These are analyses which are shown in the GUI to demonstrate what genetic analyses look like. +// These analyses are stored in the /internal/genetics/sampleAnalyses folder. +// You must add 2 raw genome files to this folder before running the utility: Person1RawGenome.txt and Person2RawGenome.txt +// I added some cystic fibrosis variants to my raw genome files so that the sample analysis would show a non-zero offspring risk. + +package main + +import "seekia/resources/geneticReferences/locusMetadata" +import "seekia/resources/geneticReferences/monogenicDiseases" +import "seekia/resources/geneticReferences/polygenicDiseases" +import "seekia/resources/geneticReferences/traits" + +import "seekia/internal/localFilesystem" +import "seekia/internal/genetics/createGeneticAnalysis" +import "seekia/internal/genetics/prepareRawGenomes" + +import "errors" +import "log" + + +func main(){ + + err := locusMetadata.InitializeLocusMetadataVariables() + if (err != nil) { + log.Println("InitializeLocusMetadataVariables failed: " + err.Error()) + return + } + + monogenicDiseases.InitializeMonogenicDiseaseVariables() + polygenicDiseases.InitializePolygenicDiseaseVariables() + traits.InitializeTraitVariables() + + //Outputs: + // -bool: File exists + // -[]prepareRawGenomes.RawGenomeWithMetadata: Person genomes list + // -string: Analysis string + // -error + getPersonRawGenomeListAndAnalysis := func(personRawGenomeFilepath string, genomeIdentifier string)(bool, []prepareRawGenomes.RawGenomeWithMetadata, string, error){ + + fileExists, personRawGenomeFileBytes, err := localFilesystem.GetFileContents(personRawGenomeFilepath) + if (err != nil){ return false, nil, "", err } + if (fileExists == false){ + return false, nil, "", nil + } + + personRawGenomeFileString := string(personRawGenomeFileBytes) + + genomeIsValid, rawGenomeWithMetadata, err := prepareRawGenomes.CreateRawGenomeWithMetadataObject(genomeIdentifier, personRawGenomeFileString) + if (err != nil){ return false, nil, "", err } + if (genomeIsValid == false){ + return false, nil, "", errors.New("Raw genome is invalid: " + personRawGenomeFilepath) + } + + personGenomesList := []prepareRawGenomes.RawGenomeWithMetadata{rawGenomeWithMetadata} + + updateProgressFunction := func(_ int)error{ + return nil + } + + checkIfProcessIsStoppedFunction := func()bool{ + return false + } + + processCompleted, personGeneticAnalysis, err := createGeneticAnalysis.CreatePersonGeneticAnalysis(personGenomesList, updateProgressFunction, checkIfProcessIsStoppedFunction) + if (err != nil){ + return false, nil, "", errors.New("Failed to create person genetic analysis: " + err.Error()) + } + if (processCompleted == false){ + return false, nil, "", errors.New("Failed to create person genetic analysis: Process did not complete.") + } + + return true, personGenomesList, personGeneticAnalysis, nil + } + + fileExists, person1GenomeList, person1GeneticAnalysis, err := getPersonRawGenomeListAndAnalysis("./Person1RawGenome.txt", "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa") + if (err != nil){ + log.Println(err.Error()) + return + } + if (fileExists == false){ + log.Println("Error: Person1RawGenome.txt does not exist.") + log.Println("You must add this raw genome file to the createSampleGeneticAnalyses folder.") + return + } + + fileExists, person2GenomeList, person2GeneticAnalysis, err := getPersonRawGenomeListAndAnalysis("./Person2RawGenome.txt", "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb") + if (err != nil){ + log.Println(err.Error()) + return + } + if (fileExists == false){ + log.Println("Error: Person2RawGenome.txt does not exist.") + log.Println("You must add this raw genome file to the createSampleGeneticAnalyses folder.") + return + } + + updateProgressFunction := func(_ int)error{ + return nil + } + + checkIfProcessIsStoppedFunction := func()bool{ + return false + } + + processCompleted, coupleGeneticAnalysis, err := createGeneticAnalysis.CreateCoupleGeneticAnalysis(person1GenomeList, person2GenomeList, updateProgressFunction, checkIfProcessIsStoppedFunction) + if (err != nil){ + log.Println("Failed to create couple genetic analysis: " + err.Error()) + return + } + if (processCompleted == false){ + log.Println("Failed to create couple genetic analysis: Process did not complete.") + return + } + + err = localFilesystem.CreateOrOverwriteFile([]byte(person1GeneticAnalysis), "./", "SamplePerson1Analysis.json") + if (err != nil){ + log.Println(err.Error()) + return + } + err = localFilesystem.CreateOrOverwriteFile([]byte(person2GeneticAnalysis), "./", "SamplePerson2Analysis.json") + if (err != nil){ + log.Println(err.Error()) + return + } + + err = localFilesystem.CreateOrOverwriteFile([]byte(coupleGeneticAnalysis), "./", "SampleCoupleAnalysis.json") + if (err != nil){ + log.Println(err.Error()) + return + } + + log.Println("Successfully created sample genetic analyses!") +} + + diff --git a/utilities/editEmojiFiles/editEmojiFiles.go b/utilities/editEmojiFiles/editEmojiFiles.go new file mode 100644 index 0000000..5a11411 --- /dev/null +++ b/utilities/editEmojiFiles/editEmojiFiles.go @@ -0,0 +1,55 @@ + +// editEmojiFiles.go provides a utility to edit the emoji files in bulk +// I've already changed some colors, and we might want to change more + +package main + +import "seekia/internal/localFilesystem" + +import goFilepath "path/filepath" + +import "os" +import "log" +import "strings" + + +func main(){ + + emojisFolderPath := "../../resources/imageFiles/emojis/" + + fileList, err := os.ReadDir(emojisFolderPath) + if (err != nil) { + log.Println(err) + return + } + + for _, fileObject := range fileList{ + + fileName := fileObject.Name() + filePath := goFilepath.Join(emojisFolderPath, fileName) + + fileBytes, err := os.ReadFile(filePath) + if (err != nil){ + log.Println(err) + return + } + + fileString := string(fileBytes) + + containsAny := strings.Contains(fileString, "#7C0000") + if (containsAny == false){ + continue + } + + // Change color here: + newFileString := strings.ReplaceAll(fileString, "#7C0000", "#7C0000") + + err = localFilesystem.CreateOrOverwriteFile([]byte(newFileString), emojisFolderPath, fileName) + if (err != nil){ + log.Println(err) + return + } + } +} + + diff --git a/utilities/generateContent/generateContent.go b/utilities/generateContent/generateContent.go new file mode 100644 index 0000000..81fac41 --- /dev/null +++ b/utilities/generateContent/generateContent.go @@ -0,0 +1,94 @@ + +// generateContent.go is used to generate fake messages, profiles, reviews, and reports. +// This is useful for simulating what use of Seekia is like, debugging, and building new features. + +package main + +import "seekia/internal/appUsers" +import "seekia/internal/localFilesystem" +import "seekia/internal/network/appNetworkType/getAppNetworkType" +import "seekia/internal/generate" + +import "errors" +import "log" + +func main(){ + + generateContentFunction := func()error{ + + err := localFilesystem.InitializeAppDatastores() + if (err != nil){ + return errors.New("Failed to initialize app datastores: " + err.Error()) + } + + appUsersList, err := appUsers.GetAppUsersList() + if (err != nil){ + return errors.New("Failed to GetAppUsersList: " + err.Error()) + } + + if (len(appUsersList) == 0){ + // We cannot generate fake content if there is no app user. + return errors.New("No app users exist.") + } + + // We always generate content for the first app user. + appUserName := appUsersList[0] + + err = appUsers.SignInToAppUser(appUserName, false) + if (err != nil){ return err } + + appNetworkType, err := getAppNetworkType.GetAppNetworkType() + if (err != nil) { return err } + + err = generate.GenerateFakeProfiles("Mate", appNetworkType, 10) + if (err != nil){ return err } + + //TODO: We will uncomment this once we can generate valid host profiles + //err = generate.GenerateFakeProfiles("Host", 5) + //if (err != nil){ return err } + + err = generate.GenerateFakeProfiles("Moderator", appNetworkType, 5) + if (err != nil){ return err } + + err = generate.GenerateDisabledProfiles("Mate", appNetworkType, 3) + if (err != nil){ return err } + err = generate.GenerateDisabledProfiles("Host", appNetworkType, 3) + if (err != nil){ return err } + err = generate.GenerateDisabledProfiles("Moderator", appNetworkType, 3) + if (err != nil){ return err } + + err = generate.GenerateFakeProfilesWithMessages("Mate", appNetworkType, 50) + if (err != nil){ return err } + + err = generate.GenerateFakeReviews("Profile", appNetworkType, 30) + if (err != nil){ return err } + + err = generate.GenerateFakeReviews("Identity", appNetworkType, 20) + if (err != nil){ return err } + err = generate.GenerateFakeReviews("Message", appNetworkType, 20) + if (err != nil){ return err } + + err = generate.GenerateFakeReports("Identity", appNetworkType, 20) + if (err != nil){ return err } + err = generate.GenerateFakeReports("Profile", appNetworkType, 20) + if (err != nil){ return err } + err = generate.GenerateFakeReports("Message", appNetworkType, 20) + if (err != nil){ return err } + + err = appUsers.SignOutOfAppUser() + if (err != nil){ return err } + + return nil + } + + err := generateContentFunction() + if (err != nil){ + log.Println("Failed to generate fake content. Error: " + err.Error()) + return + } + + log.Println("Done generating fake content!") +} + + + diff --git a/utilities/importLocusMetadata/.gitignore b/utilities/importLocusMetadata/.gitignore new file mode 100644 index 0000000..9da13f0 --- /dev/null +++ b/utilities/importLocusMetadata/.gitignore @@ -0,0 +1 @@ +23andMeRawGenome.txt \ No newline at end of file diff --git a/utilities/importLocusMetadata/importLocusMetadata.go b/utilities/importLocusMetadata/importLocusMetadata.go new file mode 100644 index 0000000..5186c12 --- /dev/null +++ b/utilities/importLocusMetadata/importLocusMetadata.go @@ -0,0 +1,279 @@ + +// importLocusMetadata.go provides a function to import locus metadata from raw genome files. +// It uses a 23andMe raw genome file to find the chromosomes and positions for new rsIDs. +// The imported loci will be missing the GeneNames list and any references. +// The imported loci may be missing locus aliases +// TODO: Instead of using 23andMe files, use a better full-genome reference which has gene names. + +package main + +import "seekia/resources/geneticReferences/locusMetadata" + +import "seekia/internal/helpers" +import "seekia/internal/localFilesystem" + +import "encoding/json" + +import "slices" +import "strings" +import "bufio" +import "bytes" + +import "log" + +func main(){ + + fileExists, fileBytes, err := localFilesystem.GetFileContents("./23andMeRawGenome.txt") + if (err != nil){ + log.Println(err.Error()) + return + } + if (fileExists == false){ + log.Println("Error: 23AndMeRawGenome.txt does not exist.") + log.Println("You must add a 23andMe raw genome file to the addLocusMetadata folder so we can retrieve locus metadata from the file.") + return + } + + fileReader := bytes.NewReader(fileBytes) + + fileBufioReader := bufio.NewReader(fileReader) + + firstLine, err := fileBufioReader.ReadString('\n') + if (err != nil){ + // File does not have another line + log.Println("Malformed 23andMe genome file: Too short.") + return + } + + fileIs23andMe := strings.HasPrefix(firstLine, "# This data file generated by 23andMe at:") + if (fileIs23andMe == false){ + log.Println("Malformed 23andMe genome file: Missing header.") + return + } + + // Now we advance bufio reader to the snp rows + for { + fileLineString, err := fileBufioReader.ReadString('\n') + if (err != nil){ + // File does not have another line + log.Println("Malformed 23andMe genome file: Too short.") + return + } + + // All SNP rows come after this line: + // "# rsid chromosome position genotype" + lineReached := strings.HasPrefix(fileLineString, "# rsid") + if (lineReached == true){ + break + } + } + + type LocusInfoObject struct{ + Chromosome int + Position int + } + + // Map structure: Locus rsID -> Info Object + lociInfoMap := make(map[int64]LocusInfoObject) + + for { + + fileLineString, err := fileBufioReader.ReadString('\n') + if (err != nil){ + // File does not have another line + break + } + if (fileLineString == "\n"){ + // This is the final line + break + } + + fileLineWithoutNewline := strings.TrimSuffix(fileLineString, "\n") + + // Rows look like this + // "rs4477212 1 82154 GG" + // "rs571313759 1 1181945 --" (-- means no entry) + // "i3001920 MT 16470 G" (one base is possible) + + rowSlice := strings.Split(fileLineWithoutNewline, "\t") + + if (len(rowSlice) != 4){ + log.Println("Malformed 23andMe genome data: Invalid SNP row: " + fileLineString) + return + } + + locusIdentifierString := rowSlice[0] + locusChromosomeString := rowSlice[1] + locusPositionString := rowSlice[2] + + //Outputs: + // -bool: rsID found + // -int64: rsID value + getRSIDIdentifier := func()(bool, int64){ + + stringWithoutPrefix, prefixExists := strings.CutPrefix(locusIdentifierString, "rs") + if (prefixExists == false){ + return false, 0 + } + + rsidInt64, err := helpers.ConvertStringToInt64(stringWithoutPrefix) + if (err != nil){ + return false, 0 + } + + return true, rsidInt64 + } + + isRSID, locusRSID := getRSIDIdentifier() + if (isRSID == false){ + // RSID is unknown. + // It is probably a custom identifier (Example: i713426) + continue + } + + locusChromosome, err := helpers.ConvertStringToInt(locusChromosomeString) + if (err != nil){ + // It is probably "MT" or "X" chromosome + continue + } + + locusPosition, err := helpers.ConvertStringToInt(locusPositionString) + if (err != nil){ + log.Println("23andMe file is malformed: Contains invalid locusPosition: " + locusPositionString) + return + } + + locusInfoObject := LocusInfoObject{ + Chromosome: locusChromosome, + Position: locusPosition, + } + + lociInfoMap[locusRSID] = locusInfoObject + } + + // This is a list of rsIDs whose metadata we should add to the locus metadata + lociToAddList := []int64{} + + containsDuplicates, _ := helpers.CheckIfListContainsDuplicates(lociToAddList) + if (containsDuplicates == true){ + log.Println("lociToAddList contains duplicates.") + return + } + + err = locusMetadata.InitializeLocusMetadataVariables() + if (err != nil){ + log.Println("ERROR: " + err.Error()) + return + } + + // This list will store the loci for which no metadata existed + missingLociList := make([]int64, 0) + + // Map Structure: Chromosome -> List of locus metadata objects to add + lociToAddMap := make(map[int][]locusMetadata.LocusMetadata) + + numberOfImportedLoci := 0 + + for _, rsID := range lociToAddList{ + + // First we check to see if locus metadata already exists + + exists, _, err := locusMetadata.GetLocusMetadata(rsID) + if (err != nil){ + log.Println("ERROR: " + err.Error()) + return + } + if (exists == true){ + log.Println("lociToAddList contains locus whose metadata already exists.") + return + } + + locusInfoObject, exists := lociInfoMap[rsID] + if (exists == false){ + // The 23andMe file does not contain metadata for this locus + missingLociList = append(missingLociList, rsID) + continue + } + + numberOfImportedLoci += 1 + + locusChromosome := locusInfoObject.Chromosome + locusPosition := locusInfoObject.Position + + newLocusMetadataObject := locusMetadata.LocusMetadata{ + RSIDsList: []int64{rsID}, + Chromosome: locusChromosome, + Position: locusPosition, + GeneNamesList: []string{"MISSING"}, + CompanyAliases: make(map[locusMetadata.GeneticsCompany][]string), + References: make(map[string]string), + } + + existingList, exists := lociToAddMap[locusChromosome] + if (exists == false){ + lociToAddMap[locusChromosome] = []locusMetadata.LocusMetadata{newLocusMetadataObject} + } else { + existingList = append(existingList, newLocusMetadataObject) + lociToAddMap[locusChromosome] = existingList + } + } + + for chromosomeInt, locusMetadataObjectsToAddList := range lociToAddMap{ + + existingLocusMetadataObjectsList, err := locusMetadata.GetLocusMetadataObjectsListByChromosome(chromosomeInt) + if (err != nil) { + log.Println(err) + return + } + + newLocusMetadataObjectsList := slices.Concat(existingLocusMetadataObjectsList, locusMetadataObjectsToAddList) + + newChromosomeFileBytes, err := json.MarshalIndent(newLocusMetadataObjectsList, "", "\t") + if (err != nil){ + log.Println("ERROR: " + err.Error()) + return + } + + currentChromosomeString := helpers.ConvertIntToString(chromosomeInt) + + locusMetadataFilepath := "../../resources/geneticReferences/locusMetadata/" + + err = localFilesystem.CreateOrOverwriteFile(newChromosomeFileBytes, locusMetadataFilepath, "LocusMetadata_Chromosome" + currentChromosomeString + ".json") + if (err != nil){ + log.Println("ERROR: " + err.Error()) + return + } + } + + totalLociToAdd := len(lociToAddList) + totalLociToAddString := helpers.ConvertIntToString(totalLociToAdd) + + numberOfImportedLociString := helpers.ConvertIntToString(numberOfImportedLoci) + + log.Println("Successfully imported " + numberOfImportedLociString + "/" + totalLociToAddString + " locus metadatas!") + + numberOfMissingLoci := len(missingLociList) + + numberOfMissingLociString := helpers.ConvertIntToString(numberOfMissingLoci) + + log.Println(numberOfMissingLociString + " loci contained no metadata in the 23andMe genome file.") + + if (len(missingLociList) == 0){ + return + } + + missingLociStringsList := make([]string, 0, len(missingLociList)) + + for _, rsID := range missingLociList{ + + rsIDString := helpers.ConvertInt64ToString(rsID) + + missingLociStringsList = append(missingLociStringsList, rsIDString) + } + + missingLociListFormatted := strings.Join(missingLociStringsList, ", ") + + log.Println("Missing loci list: " + missingLociListFormatted) +} + +